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.
Files changed (69) hide show
  1. {qbraid_cli-0.15.0/qbraid_cli.egg-info → qbraid_cli-0.15.2}/PKG-INFO +1 -1
  2. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/pyproject.toml +1 -1
  3. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/agents/app.py +55 -13
  4. qbraid_cli-0.15.2/qbraid_cli/agents/permission_response.py +49 -0
  5. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/skills/app.py +158 -17
  6. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2/qbraid_cli.egg-info}/PKG-INFO +1 -1
  7. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/SOURCES.txt +1 -0
  8. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/LICENSE +0 -0
  9. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/README.md +0 -0
  10. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/__init__.py +0 -0
  11. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/_build_info.py +0 -0
  12. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/_version.py +0 -0
  13. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/account/__init__.py +0 -0
  14. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/account/app.py +0 -0
  15. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/__init__.py +0 -0
  16. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/app.py +0 -0
  17. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/headers.py +0 -0
  18. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/admin/validation.py +0 -0
  19. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/agents/__init__.py +0 -0
  20. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/ai/__init__.py +0 -0
  21. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/ai/app.py +0 -0
  22. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/ai/token.py +0 -0
  23. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/__init__.py +0 -0
  24. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/app.py +0 -0
  25. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/instances.py +0 -0
  26. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/profiles.py +0 -0
  27. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/server.py +0 -0
  28. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/compute/ssh.py +0 -0
  29. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/configure/__init__.py +0 -0
  30. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/configure/actions.py +0 -0
  31. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/configure/app.py +0 -0
  32. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/devices/__init__.py +0 -0
  33. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/devices/app.py +0 -0
  34. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/devices/validation.py +0 -0
  35. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/__init__.py +0 -0
  36. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/activate.py +0 -0
  37. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/app.py +0 -0
  38. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/create.py +0 -0
  39. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/data_handling.py +0 -0
  40. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/groups.py +0 -0
  41. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/publish.py +0 -0
  42. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/registry.py +0 -0
  43. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/envs/share.py +0 -0
  44. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/exceptions.py +0 -0
  45. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/handlers.py +0 -0
  46. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/jobs/__init__.py +0 -0
  47. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/jobs/app.py +0 -0
  48. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/jobs/groups.py +0 -0
  49. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/kernels/__init__.py +0 -0
  50. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/kernels/app.py +0 -0
  51. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/lab/__init__.py +0 -0
  52. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/lab/app.py +0 -0
  53. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/main.py +0 -0
  54. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/pip/__init__.py +0 -0
  55. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/pip/app.py +0 -0
  56. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/pip/hooks.py +0 -0
  57. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/projects/__init__.py +0 -0
  58. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/projects/app.py +0 -0
  59. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/py.typed +0 -0
  60. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/skills/__init__.py +0 -0
  61. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/troubleshoot/__init__.py +0 -0
  62. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli/troubleshoot/app.py +0 -0
  63. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  64. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/entry_points.txt +0 -0
  65. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/requires.txt +0 -0
  66. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/qbraid_cli.egg-info/top_level.txt +0 -0
  67. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/setup.cfg +0 -0
  68. {qbraid_cli-0.15.0 → qbraid_cli-0.15.2}/tests/test_cli_e2e.py +0 -0
  69. {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.0
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.0"
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, respond_to_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
- console.print(" [bold]1.[/bold] Allow")
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
- # Claude: reply via file
1957
- respond_to_request(perm_session_id, decision, reason)
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
- _send_response("ask")
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
- # Delete the request file to prevent re-detection (Claude only)
1979
- if not oc_req_id:
1980
- req_file = (
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
- Use --target all to install to all supported tools.
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
- return client.install_skill(slug, target=target.value)
228
-
229
- console = Console()
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
- installed_paths = run_progress_task(install_skill)
274
+ results = run_progress_task(install_skill, include_error_traceback=False)
234
275
 
235
- console.print(f"\n[green]✓[/green] Skill '{slug}' installed successfully!\n")
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 target_name, path in installed_paths.items():
238
- console.print(f" [cyan]{target_name}[/cyan]: {path}")
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 target:
304
- return {target.value: client.list_installed_skills(target=target.value)}
305
- return client.list_all_installed_skills()
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 skill_slug in skills:
324
- console.print(f" • {skill_slug}")
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.0
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
@@ -22,6 +22,7 @@ qbraid_cli/admin/headers.py
22
22
  qbraid_cli/admin/validation.py
23
23
  qbraid_cli/agents/__init__.py
24
24
  qbraid_cli/agents/app.py
25
+ qbraid_cli/agents/permission_response.py
25
26
  qbraid_cli/ai/__init__.py
26
27
  qbraid_cli/ai/app.py
27
28
  qbraid_cli/ai/token.py
File without changes
File without changes
File without changes