qbraid-cli 0.15.0__tar.gz → 0.15.2__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.15.0/qbraid_cli.egg-info → qbraid_cli-0.15.2}/PKG-INFO +1 -1
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/pyproject.toml +1 -1
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/agents/app.py +55 -13
- qbraid_cli-0.15.2/qbraid_cli/agents/permission_response.py +49 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/skills/app.py +158 -17
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2/qbraid_cli.egg-info}/PKG-INFO +1 -1
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/SOURCES.txt +1 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/LICENSE +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/README.md +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/main.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/requires.txt +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/setup.cfg +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/tests/test_cli_e2e.py +0 -0
- {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.2
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qbraid-cli"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.2"
|
|
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" }]
|
|
@@ -1780,6 +1780,11 @@ def agents_watch(
|
|
|
1780
1780
|
can_prompt = interactive and not stream
|
|
1781
1781
|
can_notify = stream
|
|
1782
1782
|
|
|
1783
|
+
if can_prompt or can_notify:
|
|
1784
|
+
from .permission_response import require_bound_permission_support
|
|
1785
|
+
|
|
1786
|
+
require_bound_permission_support()
|
|
1787
|
+
|
|
1783
1788
|
# Resolve the session ID used for permission files
|
|
1784
1789
|
perm_session_id = session.session_id
|
|
1785
1790
|
|
|
@@ -1840,9 +1845,17 @@ def agents_watch(
|
|
|
1840
1845
|
# (stdin can't be read from multiple threads safely).
|
|
1841
1846
|
import threading
|
|
1842
1847
|
|
|
1843
|
-
from qbraid_core.services.agents.permissions import get_pending_request
|
|
1848
|
+
from qbraid_core.services.agents.permissions import get_pending_request
|
|
1849
|
+
|
|
1850
|
+
from .permission_response import (
|
|
1851
|
+
UnsupportedPermissionApproval,
|
|
1852
|
+
request_identity,
|
|
1853
|
+
respond_to_displayed_request,
|
|
1854
|
+
supports_bound_approval,
|
|
1855
|
+
)
|
|
1844
1856
|
|
|
1845
1857
|
perm_stop_event = threading.Event()
|
|
1858
|
+
perm_retry = threading.Event()
|
|
1846
1859
|
perm_pending = threading.Event() # Set when a permission request is detected
|
|
1847
1860
|
perm_data: dict = {} # Stores the current request
|
|
1848
1861
|
|
|
@@ -1850,8 +1863,12 @@ def agents_watch(
|
|
|
1850
1863
|
|
|
1851
1864
|
def _permission_detector():
|
|
1852
1865
|
"""Background thread that detects permission requests."""
|
|
1866
|
+
last_request_id = None
|
|
1853
1867
|
while not perm_stop_event.is_set():
|
|
1854
1868
|
if not perm_pending.is_set():
|
|
1869
|
+
if perm_retry.is_set():
|
|
1870
|
+
last_request_id = None
|
|
1871
|
+
perm_retry.clear()
|
|
1855
1872
|
request = None
|
|
1856
1873
|
|
|
1857
1874
|
if oc_port:
|
|
@@ -1877,7 +1894,12 @@ def agents_watch(
|
|
|
1877
1894
|
# Claude/Codex: check file-based permission request
|
|
1878
1895
|
request = get_pending_request(perm_session_id)
|
|
1879
1896
|
|
|
1880
|
-
if request:
|
|
1897
|
+
if not isinstance(request, dict):
|
|
1898
|
+
request = None
|
|
1899
|
+
identity = request_identity(perm_session_id, request) if request else None
|
|
1900
|
+
if request and identity != last_request_id:
|
|
1901
|
+
last_request_id = identity
|
|
1902
|
+
perm_data.clear()
|
|
1881
1903
|
perm_data.update(request)
|
|
1882
1904
|
perm_pending.set()
|
|
1883
1905
|
|
|
@@ -1886,6 +1908,8 @@ def agents_watch(
|
|
|
1886
1908
|
|
|
1887
1909
|
notification = {
|
|
1888
1910
|
"type": "permission_required",
|
|
1911
|
+
"request_id": identity,
|
|
1912
|
+
"protocol_version": request.get("protocol_version", 1),
|
|
1889
1913
|
"session_id": perm_session_id,
|
|
1890
1914
|
"tool_name": request.get("tool_name", "?"),
|
|
1891
1915
|
"summary": request.get("summary", ""),
|
|
@@ -1896,6 +1920,10 @@ def agents_watch(
|
|
|
1896
1920
|
notification["opencode_port"] = oc_port
|
|
1897
1921
|
print(_json.dumps(notification), flush=True)
|
|
1898
1922
|
|
|
1923
|
+
if not can_prompt:
|
|
1924
|
+
perm_data.clear()
|
|
1925
|
+
perm_pending.clear()
|
|
1926
|
+
|
|
1899
1927
|
# Wait for main loop to clear the flag
|
|
1900
1928
|
while perm_pending.is_set() and not perm_stop_event.is_set():
|
|
1901
1929
|
time.sleep(0.5)
|
|
@@ -1937,7 +1965,13 @@ def agents_watch(
|
|
|
1937
1965
|
console.print()
|
|
1938
1966
|
console.print(f"[bold yellow]Permission required:[/bold yellow] [bold]{tool_name}[/bold]")
|
|
1939
1967
|
console.print(f"[dim]{summary}[/dim]")
|
|
1940
|
-
|
|
1968
|
+
if supports_bound_approval(perm_data):
|
|
1969
|
+
console.print(" [bold]1.[/bold] Allow")
|
|
1970
|
+
else:
|
|
1971
|
+
console.print(
|
|
1972
|
+
"[dim]Automatic Allow unavailable for this legacy gate; "
|
|
1973
|
+
"restart the agent or use its terminal.[/dim]"
|
|
1974
|
+
)
|
|
1941
1975
|
console.print(" [bold]2.[/bold] Deny")
|
|
1942
1976
|
console.print(" [bold]3.[/bold] Pass to agent (show Claude's prompt)")
|
|
1943
1977
|
|
|
@@ -1953,8 +1987,8 @@ def agents_watch(
|
|
|
1953
1987
|
oc_reply_map = {"allow": "once", "deny": "reject", "ask": "once"}
|
|
1954
1988
|
reply_to_permission(oc_req_port, oc_req_id, oc_reply_map.get(decision, "once"))
|
|
1955
1989
|
else:
|
|
1956
|
-
#
|
|
1957
|
-
|
|
1990
|
+
# Bind the reply to the request actually shown to the user.
|
|
1991
|
+
respond_to_displayed_request(perm_session_id, perm_data, decision, reason)
|
|
1958
1992
|
|
|
1959
1993
|
try:
|
|
1960
1994
|
choice = console.input("[dim]> [/dim]").strip()
|
|
@@ -1971,16 +2005,21 @@ def agents_watch(
|
|
|
1971
2005
|
_send_response("ask")
|
|
1972
2006
|
console.print("[dim]Invalid — passed to agent's terminal.[/dim]")
|
|
1973
2007
|
except (KeyboardInterrupt, EOFError):
|
|
1974
|
-
|
|
2008
|
+
try:
|
|
2009
|
+
_send_response("ask")
|
|
2010
|
+
except ValueError as exc:
|
|
2011
|
+
perm_retry.set()
|
|
2012
|
+
console.print(f"[yellow]{exc}[/yellow]")
|
|
2013
|
+
except UnsupportedPermissionApproval as exc:
|
|
2014
|
+
console.print(f"[yellow]{exc}[/yellow]")
|
|
2015
|
+
except ValueError as exc:
|
|
2016
|
+
perm_retry.set()
|
|
2017
|
+
console.print(f"[yellow]{exc}[/yellow]")
|
|
1975
2018
|
|
|
1976
|
-
perm_pending.clear()
|
|
1977
2019
|
perm_data.clear()
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
Path(os.path.expanduser("~/.qbraid/permissions")) / f"{perm_session_id}.request.json"
|
|
1982
|
-
)
|
|
1983
|
-
req_file.unlink(missing_ok=True)
|
|
2020
|
+
perm_pending.clear()
|
|
2021
|
+
# Only the tool-side gate owns request-file cleanup. Removing it
|
|
2022
|
+
# here can erase the next request after the tool consumes our reply.
|
|
1984
2023
|
continue
|
|
1985
2024
|
|
|
1986
2025
|
# Drain available transcript entries
|
|
@@ -2030,6 +2069,9 @@ def agents_watch(
|
|
|
2030
2069
|
console.print("\n[dim]Stopped watching.[/dim]")
|
|
2031
2070
|
except (typer.Exit, SystemExit):
|
|
2032
2071
|
raise
|
|
2072
|
+
except ValueError as e:
|
|
2073
|
+
handle_error(message=str(e), include_traceback=False)
|
|
2074
|
+
raise typer.Exit(code=1)
|
|
2033
2075
|
except Exception as e:
|
|
2034
2076
|
handle_error(message=str(e))
|
|
2035
2077
|
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (c) 2026, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""Bind watch-mode decisions to the permission request displayed to the user."""
|
|
5
|
+
|
|
6
|
+
from qbraid_core.services.agents import permissions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UnsupportedPermissionApproval(ValueError):
|
|
10
|
+
"""The displayed gate cannot safely accept an automatic approval."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def supports_bound_approval(request: dict) -> bool:
|
|
14
|
+
"""Only explicit v2-or-newer gates can bind automatic approvals."""
|
|
15
|
+
try:
|
|
16
|
+
return int(request.get("protocol_version", 1)) >= 2
|
|
17
|
+
except (TypeError, ValueError, OverflowError):
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require_bound_permission_support() -> None:
|
|
22
|
+
"""Refuse permission handling on core versions that ignore request identity."""
|
|
23
|
+
if not getattr(permissions, "SUPPORTS_BOUND_PERMISSION_RESPONSES", False):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"Permission handling requires a qbraid-core release with bound-response support; "
|
|
26
|
+
"upgrade core first, or watch read-only with --no-interactive."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def request_identity(session_id: str, request: dict) -> str:
|
|
31
|
+
"""Support nonce requests and the timestamp identity of older integrations."""
|
|
32
|
+
return (
|
|
33
|
+
request.get("request_id")
|
|
34
|
+
or request.get("opencode_request_id")
|
|
35
|
+
or (f"{session_id}:{request['timestamp']}" if request.get("timestamp") is not None else session_id)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def respond_to_displayed_request(session_id: str, request: dict, decision: str, reason: str = "") -> None:
|
|
40
|
+
"""Reject an expired prompt without reporting success or deleting gate state."""
|
|
41
|
+
require_bound_permission_support()
|
|
42
|
+
if decision == "allow" and not supports_bound_approval(request):
|
|
43
|
+
raise UnsupportedPermissionApproval(
|
|
44
|
+
"This legacy gate cannot bind approvals. Restart the agent or use its terminal."
|
|
45
|
+
)
|
|
46
|
+
if not permissions.respond_to_permission(
|
|
47
|
+
session_id, request_identity(session_id, request), decision, reason
|
|
48
|
+
):
|
|
49
|
+
raise ValueError("Permission request expired or was already answered")
|
|
@@ -40,11 +40,21 @@ class SkillTargetOption(str, Enum):
|
|
|
40
40
|
|
|
41
41
|
CLAUDE_CODE = "claude-code"
|
|
42
42
|
CODEX = "codex"
|
|
43
|
+
PI = "pi"
|
|
43
44
|
CURSOR = "cursor"
|
|
44
45
|
COPILOT = "copilot"
|
|
45
46
|
ALL = "all"
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
_ACTION_STYLE = {
|
|
50
|
+
"installed": "[green]installed[/green]",
|
|
51
|
+
"updated": "[green]updated[/green]",
|
|
52
|
+
"unchanged": "[dim]unchanged[/dim]",
|
|
53
|
+
"failed": "[red]failed[/red]",
|
|
54
|
+
"unknown": "[dim]done (older qbraid-core)[/dim]",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
class VisibilityOption(str, Enum):
|
|
49
59
|
"""Skill visibility level."""
|
|
50
60
|
|
|
@@ -213,55 +223,134 @@ def skills_install(
|
|
|
213
223
|
"-t",
|
|
214
224
|
help="Target AI coding tool(s) to install for",
|
|
215
225
|
),
|
|
226
|
+
path: Optional[str] = typer.Option(
|
|
227
|
+
None,
|
|
228
|
+
"--path",
|
|
229
|
+
"-p",
|
|
230
|
+
help="Install into this directory instead of a tool's skills directory",
|
|
231
|
+
),
|
|
232
|
+
force: bool = typer.Option(
|
|
233
|
+
False,
|
|
234
|
+
"--force",
|
|
235
|
+
"-f",
|
|
236
|
+
help="Replace the installed copy even if it is unchanged or was not installed by qbraid",
|
|
237
|
+
),
|
|
216
238
|
) -> None:
|
|
217
239
|
"""Install a skill for AI coding tools.
|
|
218
240
|
|
|
219
|
-
By default, installs to Claude Code (~/.claude/skills/).
|
|
220
|
-
|
|
241
|
+
By default, installs to Claude Code (~/.claude/skills/). Use --target all
|
|
242
|
+
for every supported tool; --target codex or --target pi both land in
|
|
243
|
+
~/.agents/skills/, which Codex and CodeQ (pi) read.
|
|
244
|
+
|
|
245
|
+
In a tool's skills directory the skill is placed under its own name (not
|
|
246
|
+
its slug); with --path it is placed under its slug at that path. The
|
|
247
|
+
download is checked against the registry's checksum, and installing an
|
|
248
|
+
unchanged skill again is a no-op.
|
|
221
249
|
"""
|
|
250
|
+
from pathlib import Path
|
|
251
|
+
|
|
252
|
+
install_path = Path(path).expanduser() if path else None
|
|
253
|
+
console = Console()
|
|
254
|
+
legacy_core = False
|
|
222
255
|
|
|
223
256
|
def install_skill():
|
|
257
|
+
nonlocal legacy_core
|
|
224
258
|
from qbraid_core.services.skills import SkillsClient
|
|
225
259
|
|
|
226
260
|
client = SkillsClient()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
if hasattr(client, "install_skill_ex"):
|
|
262
|
+
return client.install_skill_ex(slug, target=target.value, install_path=install_path, force=force)
|
|
263
|
+
# older qbraid-core: slug-named directories, no checksum or manifest, no --force
|
|
264
|
+
legacy_core = True
|
|
265
|
+
return {
|
|
266
|
+
name: _LegacyInstall(slug, installed_path)
|
|
267
|
+
for name, installed_path in client.install_skill(
|
|
268
|
+
slug, target=target.value, install_path=install_path
|
|
269
|
+
).items()
|
|
270
|
+
}
|
|
230
271
|
|
|
231
272
|
try:
|
|
232
273
|
console.print(f"[dim]Installing skill '{slug}'...[/dim]")
|
|
233
|
-
|
|
274
|
+
results = run_progress_task(install_skill, include_error_traceback=False)
|
|
234
275
|
|
|
235
|
-
|
|
276
|
+
if legacy_core and force:
|
|
277
|
+
console.print(
|
|
278
|
+
"[yellow]Note:[/yellow] the installed qbraid-core does not support --force; "
|
|
279
|
+
"the flag was ignored. Upgrade qbraid-core for --force, checksum "
|
|
280
|
+
"verification and name-based skill directories."
|
|
281
|
+
)
|
|
236
282
|
|
|
237
|
-
for
|
|
238
|
-
|
|
283
|
+
failed = [name for name, result in results.items() if result.action == "failed"]
|
|
284
|
+
mark = "[yellow]![/yellow]" if failed else "[green]✓[/green]"
|
|
285
|
+
console.print(f"\n{mark} Skill '{slug}':\n")
|
|
286
|
+
for target_name, result in results.items():
|
|
287
|
+
version = f" v{result.version}" if result.version else ""
|
|
288
|
+
line = (
|
|
289
|
+
f" [cyan]{target_name}[/cyan]: "
|
|
290
|
+
f"{_ACTION_STYLE.get(result.action, result.action)} "
|
|
291
|
+
f"[bold]{result.name}[/bold]{version} → {result.path}"
|
|
292
|
+
)
|
|
293
|
+
if result.action == "failed" and getattr(result, "error", None):
|
|
294
|
+
line += f"\n [red]{result.error}[/red]"
|
|
295
|
+
console.print(line)
|
|
239
296
|
|
|
297
|
+
if failed:
|
|
298
|
+
handle_error(message=f"Skill '{slug}' failed for: {', '.join(failed)}")
|
|
240
299
|
console.print("\n[dim]The skill is now available in your AI coding tool(s).[/dim]")
|
|
241
300
|
|
|
301
|
+
except typer.Exit:
|
|
302
|
+
raise
|
|
242
303
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
243
304
|
handle_error(message=f"Failed to install skill: {err}")
|
|
244
305
|
|
|
245
306
|
|
|
307
|
+
class _LegacyInstall: # pylint: disable=too-few-public-methods
|
|
308
|
+
"""Install result shape for a qbraid-core without install_skill_ex.
|
|
309
|
+
|
|
310
|
+
That core cannot tell whether anything changed, so the action is reported
|
|
311
|
+
as ``unknown`` rather than claiming ``installed``.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def __init__(self, slug: str, path) -> None:
|
|
315
|
+
self.slug = slug
|
|
316
|
+
self.name = slug
|
|
317
|
+
self.path = path
|
|
318
|
+
self.version = None
|
|
319
|
+
self.action = "unknown"
|
|
320
|
+
self.error = None
|
|
321
|
+
|
|
322
|
+
|
|
246
323
|
@skills_app.command(name="uninstall")
|
|
247
324
|
def skills_uninstall(
|
|
248
|
-
slug: str = typer.Argument(..., help="The skill slug to uninstall"),
|
|
325
|
+
slug: str = typer.Argument(..., help="The skill slug (or installed name) to uninstall"),
|
|
249
326
|
target: SkillTargetOption = typer.Option(
|
|
250
327
|
SkillTargetOption.ALL,
|
|
251
328
|
"--target",
|
|
252
329
|
"-t",
|
|
253
330
|
help="Target AI coding tool(s) to uninstall from",
|
|
254
331
|
),
|
|
332
|
+
path: Optional[str] = typer.Option(
|
|
333
|
+
None,
|
|
334
|
+
"--path",
|
|
335
|
+
help="Uninstall from this directory instead of the tool directories",
|
|
336
|
+
),
|
|
255
337
|
) -> None:
|
|
256
338
|
"""Uninstall a skill from AI coding tools.
|
|
257
339
|
|
|
258
|
-
By default, uninstalls from all supported tools.
|
|
340
|
+
By default, uninstalls from all supported tools. Accepts the registry slug
|
|
341
|
+
or the installed directory name.
|
|
259
342
|
"""
|
|
260
343
|
|
|
261
344
|
def uninstall_skill():
|
|
345
|
+
from pathlib import Path as _Path
|
|
346
|
+
|
|
262
347
|
from qbraid_core.services.skills import SkillsClient
|
|
263
348
|
|
|
264
349
|
client = SkillsClient()
|
|
350
|
+
if path:
|
|
351
|
+
# `install --path` had no counterpart, so such an install could
|
|
352
|
+
# only be removed with `rm -rf`.
|
|
353
|
+
return client.uninstall_skill(slug, install_path=_Path(path).expanduser())
|
|
265
354
|
return client.uninstall_skill(slug, target=target.value)
|
|
266
355
|
|
|
267
356
|
console = Console()
|
|
@@ -281,6 +370,8 @@ def skills_uninstall(
|
|
|
281
370
|
else:
|
|
282
371
|
console.print(f"[yellow]Skill '{slug}' was not installed in any location.[/yellow]")
|
|
283
372
|
|
|
373
|
+
except typer.Exit:
|
|
374
|
+
raise
|
|
284
375
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
285
376
|
handle_error(message=f"Failed to uninstall skill: {err}")
|
|
286
377
|
|
|
@@ -293,16 +384,45 @@ def skills_installed(
|
|
|
293
384
|
"-t",
|
|
294
385
|
help="Show skills for specific tool only",
|
|
295
386
|
),
|
|
387
|
+
path: Optional[str] = typer.Option(
|
|
388
|
+
None,
|
|
389
|
+
"--path",
|
|
390
|
+
help="List skills installed in this directory",
|
|
391
|
+
),
|
|
296
392
|
) -> None:
|
|
297
|
-
"""List locally installed skills.
|
|
393
|
+
"""List locally installed skills.
|
|
394
|
+
|
|
395
|
+
Shows each skill's name, registry slug and version per tool.
|
|
396
|
+
"""
|
|
397
|
+
# `all` is in the enum for `install`/`uninstall`, where it means every
|
|
398
|
+
# tool. There is no directory called "all", so passing it here reached
|
|
399
|
+
# get_skills_install_path and raised.
|
|
400
|
+
one_target = target if target and target is not SkillTargetOption.ALL else None
|
|
298
401
|
|
|
299
402
|
def list_installed():
|
|
403
|
+
from pathlib import Path as _Path
|
|
404
|
+
|
|
300
405
|
from qbraid_core.services.skills import SkillsClient
|
|
301
406
|
|
|
302
407
|
client = SkillsClient()
|
|
303
|
-
if
|
|
304
|
-
|
|
305
|
-
|
|
408
|
+
install_path = _Path(path).expanduser() if path else None
|
|
409
|
+
if hasattr(client, "list_all_installed_skills_detailed"):
|
|
410
|
+
if install_path is not None:
|
|
411
|
+
return {"custom": client.list_installed_skills_detailed(install_path=install_path)}
|
|
412
|
+
if one_target:
|
|
413
|
+
return {one_target.value: client.list_installed_skills_detailed(target=one_target.value)}
|
|
414
|
+
return client.list_all_installed_skills_detailed()
|
|
415
|
+
# older qbraid-core: directory names only
|
|
416
|
+
if install_path is not None:
|
|
417
|
+
listed = {"custom": client.list_installed_skills(install_path=install_path)}
|
|
418
|
+
elif one_target:
|
|
419
|
+
listed = {one_target.value: client.list_installed_skills(target=one_target.value)}
|
|
420
|
+
else:
|
|
421
|
+
listed = client.list_all_installed_skills()
|
|
422
|
+
return {
|
|
423
|
+
name: [{"name": skill, "slug": skill, "version": None} for skill in skills]
|
|
424
|
+
for name, skills in listed.items()
|
|
425
|
+
}
|
|
306
426
|
|
|
307
427
|
console = Console()
|
|
308
428
|
|
|
@@ -320,13 +440,23 @@ def skills_installed(
|
|
|
320
440
|
for target_name, skills in results.items():
|
|
321
441
|
if skills:
|
|
322
442
|
console.print(f"[cyan]{target_name}[/cyan]:")
|
|
323
|
-
for
|
|
324
|
-
|
|
443
|
+
for entry in skills:
|
|
444
|
+
slug_note = (
|
|
445
|
+
f" [dim]({entry['slug']})[/dim]"
|
|
446
|
+
if entry.get("slug") and entry["slug"] != entry["name"]
|
|
447
|
+
else ""
|
|
448
|
+
)
|
|
449
|
+
version = f" v{entry['version']}" if entry.get("version") else ""
|
|
450
|
+
console.print(f" • {entry['name']}{slug_note}{version}")
|
|
325
451
|
else:
|
|
326
452
|
console.print(f"[dim]{target_name}: (none)[/dim]")
|
|
327
453
|
|
|
328
454
|
console.print("")
|
|
329
455
|
|
|
456
|
+
except typer.Exit:
|
|
457
|
+
# handle_error already reported and chose the exit code; catching it
|
|
458
|
+
# again below printed a second, empty "Error:" block.
|
|
459
|
+
raise
|
|
330
460
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
331
461
|
handle_error(message=f"Failed to list installed skills: {err}")
|
|
332
462
|
|
|
@@ -334,13 +464,22 @@ def skills_installed(
|
|
|
334
464
|
@skills_app.command(name="check")
|
|
335
465
|
def skills_check(
|
|
336
466
|
slug: str = typer.Argument(..., help="The skill slug to check"),
|
|
467
|
+
path: Optional[str] = typer.Option(
|
|
468
|
+
None,
|
|
469
|
+
"--path",
|
|
470
|
+
help="Check this directory instead of the tool directories",
|
|
471
|
+
),
|
|
337
472
|
) -> None:
|
|
338
473
|
"""Check if a skill is installed and where."""
|
|
339
474
|
|
|
340
475
|
def check_skill():
|
|
476
|
+
from pathlib import Path as _Path
|
|
477
|
+
|
|
341
478
|
from qbraid_core.services.skills import SkillsClient
|
|
342
479
|
|
|
343
480
|
client = SkillsClient()
|
|
481
|
+
if path:
|
|
482
|
+
return {"custom": client.is_skill_installed(slug, install_path=_Path(path).expanduser())}
|
|
344
483
|
return client.is_skill_installed_anywhere(slug)
|
|
345
484
|
|
|
346
485
|
console = Console()
|
|
@@ -361,6 +500,8 @@ def skills_check(
|
|
|
361
500
|
console.print(f"\n[yellow]Skill '{slug}' is not installed anywhere.[/yellow]")
|
|
362
501
|
console.print(f"\n[dim]Install it with: qbraid skills install {slug}[/dim]")
|
|
363
502
|
|
|
503
|
+
except typer.Exit:
|
|
504
|
+
raise
|
|
364
505
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
365
506
|
handle_error(message=f"Failed to check skill: {err}")
|
|
366
507
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.2
|
|
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
|
|
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
|