qbraid-cli 0.13.2__tar.gz → 0.13.3__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 (67) hide show
  1. {qbraid_cli-0.13.2/qbraid_cli.egg-info → qbraid_cli-0.13.3}/PKG-INFO +2 -2
  2. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/pyproject.toml +5 -2
  3. qbraid_cli-0.13.3/qbraid_cli/ai/__init__.py +11 -0
  4. qbraid_cli-0.13.3/qbraid_cli/ai/app.py +390 -0
  5. qbraid_cli-0.13.3/qbraid_cli/ai/token.py +66 -0
  6. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/main.py +3 -1
  7. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3/qbraid_cli.egg-info}/PKG-INFO +2 -2
  8. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/SOURCES.txt +3 -0
  9. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/entry_points.txt +1 -0
  10. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/requires.txt +1 -1
  11. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/LICENSE +0 -0
  12. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/README.md +0 -0
  13. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/__init__.py +0 -0
  14. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/_build_info.py +0 -0
  15. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/_version.py +0 -0
  16. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/account/__init__.py +0 -0
  17. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/account/app.py +0 -0
  18. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/__init__.py +0 -0
  19. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/app.py +0 -0
  20. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/headers.py +0 -0
  21. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/admin/validation.py +0 -0
  22. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/agents/__init__.py +0 -0
  23. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/agents/app.py +0 -0
  24. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/__init__.py +0 -0
  25. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/app.py +0 -0
  26. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/profiles.py +0 -0
  27. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/server.py +0 -0
  28. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/compute/ssh.py +0 -0
  29. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/configure/__init__.py +0 -0
  30. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/configure/actions.py +0 -0
  31. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/configure/app.py +0 -0
  32. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/devices/__init__.py +0 -0
  33. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/devices/app.py +0 -0
  34. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/devices/validation.py +0 -0
  35. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/__init__.py +0 -0
  36. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/activate.py +0 -0
  37. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/app.py +0 -0
  38. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/create.py +0 -0
  39. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/data_handling.py +0 -0
  40. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/groups.py +0 -0
  41. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/publish.py +0 -0
  42. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/registry.py +0 -0
  43. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/envs/share.py +0 -0
  44. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/exceptions.py +0 -0
  45. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/handlers.py +0 -0
  46. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/jobs/__init__.py +0 -0
  47. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/jobs/app.py +0 -0
  48. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/jobs/groups.py +0 -0
  49. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/kernels/__init__.py +0 -0
  50. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/kernels/app.py +0 -0
  51. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/lab/__init__.py +0 -0
  52. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/lab/app.py +0 -0
  53. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/pip/__init__.py +0 -0
  54. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/pip/app.py +0 -0
  55. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/pip/hooks.py +0 -0
  56. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/projects/__init__.py +0 -0
  57. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/projects/app.py +0 -0
  58. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/py.typed +0 -0
  59. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/skills/__init__.py +0 -0
  60. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/skills/app.py +0 -0
  61. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/troubleshoot/__init__.py +0 -0
  62. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli/troubleshoot/app.py +0 -0
  63. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  64. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/qbraid_cli.egg-info/top_level.txt +0 -0
  65. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/setup.cfg +0 -0
  66. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/tests/test_cli_e2e.py +0 -0
  67. {qbraid_cli-0.13.2 → qbraid_cli-0.13.3}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.13.2
3
+ Version: 0.13.3
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,7 +30,7 @@ Requires-Dist: typer>=0.12.1
30
30
  Requires-Dist: rich>=10.11.0
31
31
  Requires-Dist: click<=8.3.3
32
32
  Requires-Dist: questionary>=2.0.0
33
- Requires-Dist: qbraid-core>=0.3.3
33
+ Requires-Dist: qbraid-core>=0.3.11
34
34
  Provides-Extra: compute
35
35
  Requires-Dist: qbraid-core[compute]>=0.3.3; extra == "compute"
36
36
  Provides-Extra: envs
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qbraid-cli"
7
- version = "0.13.2"
7
+ version = "0.13.3"
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.3.3",
34
34
  "questionary>=2.0.0",
35
- "qbraid-core>=0.3.3",
35
+ "qbraid-core>=0.3.11",
36
36
  ]
37
37
  requires-python = ">= 3.10"
38
38
 
@@ -49,6 +49,9 @@ dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
49
49
 
50
50
  [project.scripts]
51
51
  qbraid = "qbraid_cli.main:app"
52
+ # Separate entry point on purpose: agents re-run this on a timer, and routing it
53
+ # through the full CLI would import every command group to print one string.
54
+ qbraid-ai-token = "qbraid_cli.ai.token:main"
52
55
 
53
56
  [tool.black]
54
57
  line-length = 110
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2026, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module for the 'qbraid ai' namespace.
6
+
7
+ Deliberately empty. ``qbraid_cli.ai.token`` is the console script the agents
8
+ re-run on a timer, and re-exporting the command module here would drag typer
9
+ and rich into the one process whose stdout is a credential — the cost the
10
+ separate entry point exists to avoid. Import ``qbraid_cli.ai.app`` directly.
11
+ """
@@ -0,0 +1,390 @@
1
+ # Copyright (c) 2026, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining commands in the 'qbraid ai' namespace.
6
+
7
+ """
8
+
9
+ from typing import Optional
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from qbraid_cli.handlers import handle_error
16
+
17
+ ai_app = typer.Typer(
18
+ help=(
19
+ "Use qBraid as your AI provider.\n\n"
20
+ "Point Claude Code or Codex at the qBraid AI proxy so requests run on "
21
+ "your qBraid plan instead of a separate vendor subscription, and switch "
22
+ "back whenever you want.\n\n"
23
+ "Quick start: qbraid ai connect claude"
24
+ ),
25
+ no_args_is_help=True,
26
+ )
27
+
28
+ console = Console()
29
+
30
+ _AGENT_LABELS = {"claude": "Claude Code", "codex": "Codex"}
31
+
32
+ _AUTH_LABELS = {
33
+ "api_key_helper": "token command (auto-refreshing)",
34
+ "auth_command": "token command (auto-refreshing)",
35
+ "api_key": "API key written into the config",
36
+ "env_key": "API key read from an environment variable",
37
+ }
38
+
39
+
40
+ def _warn_if_token_command_missing() -> None:
41
+ """Warn when the agents won't be able to run the token command.
42
+
43
+ They invoke it by bare name, so a `pip install --user` whose bin directory
44
+ isn't on PATH — or a venv the agent doesn't inherit — means both agents
45
+ 401 forever with nothing in our own output suggesting why.
46
+ """
47
+ import shutil
48
+
49
+ executable = _ai().token_command()[0]
50
+ if shutil.which(executable):
51
+ return
52
+
53
+ console.print()
54
+ console.print(
55
+ f"[yellow]⚠ '{executable}' is not on PATH.[/yellow] The agents run it to fetch "
56
+ "a credential, so they will fail to authenticate until it resolves — "
57
+ "check that the directory pip installed it into is on your PATH."
58
+ )
59
+
60
+
61
+ def _ai():
62
+ """Import the core AI gateway, or explain why it isn't there.
63
+
64
+ The module landed in qbraid-core 0.3.11. An older core is a realistic
65
+ install state (the pin only takes effect on a fresh resolve), and the bare
66
+ ImportError surfaces as a full traceback — the convention elsewhere in this
67
+ CLI is a single actionable line.
68
+ """
69
+ try:
70
+ from qbraid_core.services import ai
71
+ except ImportError as err: # pragma: no cover - depends on installed core
72
+ handle_error(
73
+ message=(
74
+ "This command needs qbraid-core>=0.3.11 (the AI gateway module "
75
+ "is missing from the installed version). Upgrade with: "
76
+ "pip install -U qbraid-core"
77
+ ),
78
+ include_traceback=False,
79
+ )
80
+ raise typer.Exit(code=1) from err
81
+ return ai
82
+
83
+
84
+ def _agents_from(agent: str) -> list[str]:
85
+ """Expand the 'all' shorthand into concrete agent names."""
86
+ supported = _ai().SUPPORTED_AGENTS
87
+
88
+ if agent == "all":
89
+ return list(supported)
90
+ if agent not in supported:
91
+ console.print(
92
+ f"[red]Unknown agent '{agent}'.[/red] Expected one of: " f"{', '.join(supported)}, or 'all'."
93
+ )
94
+ raise typer.Exit(code=2)
95
+ return [agent]
96
+
97
+
98
+ def _print_claude_caveats() -> None:
99
+ """Consequences of pointing Claude Code at a non-Anthropic host.
100
+
101
+ All three are documented Claude Code behaviours, and all three surprise
102
+ people who weren't told up front.
103
+ """
104
+ console.print()
105
+ console.print("[bold]Worth knowing:[/bold]")
106
+ console.print(" • This overrides a Claude Pro/Max/Team subscription — requests now bill to qBraid.")
107
+ console.print(
108
+ " • MCP tool search is disabled for non-Anthropic hosts. Set "
109
+ "ENABLE_TOOL_SEARCH=true if the proxy forwards tool_reference blocks."
110
+ )
111
+ console.print(" • Remote Control is disabled while pointed off api.anthropic.com.")
112
+
113
+
114
+ @ai_app.command(name="connect")
115
+ def ai_connect( # pylint: disable=too-many-branches
116
+ agent: str = typer.Argument("all", help="Agent to configure: claude, codex, or all"),
117
+ model: Optional[str] = typer.Option(
118
+ None, "--model", "-m", help="Model id to pin (see `qbraid ai models`)"
119
+ ),
120
+ refresh_interval: Optional[int] = typer.Option(
121
+ None,
122
+ "--refresh-interval",
123
+ help="Seconds between credential refreshes (default: 300)",
124
+ ),
125
+ api_url: Optional[str] = typer.Option(None, "--api-url", help="Override the qBraid API base URL"),
126
+ plaintext_key: bool = typer.Option(
127
+ False,
128
+ "--plaintext-key",
129
+ help="Write the API key into the config instead of using a token command",
130
+ ),
131
+ ):
132
+ """Route an agent through the qBraid AI proxy.
133
+
134
+ Writes to the agent's own config — Claude Code's settings.json, Codex's
135
+ config.toml — and records whatever it replaces so `qbraid ai disconnect`
136
+ can put it back.
137
+
138
+ The credential is not written into either config by default. Both agents
139
+ are pointed at a command that prints a token, so the key stays in your
140
+ qBraid config and a rotated token is picked up automatically.
141
+
142
+ Example:
143
+ qbraid ai connect claude
144
+ qbraid ai connect all --model claude-sonnet-4-6
145
+ """
146
+ ai = _ai()
147
+
148
+ agents = _agents_from(agent)
149
+
150
+ try:
151
+ _, kind = ai.resolve_credential()
152
+ except ai.AiGatewayError as err:
153
+ handle_error(message=str(err), include_traceback=False)
154
+ return
155
+
156
+ api_key = None
157
+ if plaintext_key:
158
+ if kind == "access_token":
159
+ console.print(
160
+ "[yellow]--plaintext-key is only meaningful for API keys; "
161
+ "access tokens rotate and must use the token command.[/yellow]"
162
+ )
163
+ else:
164
+ api_key, _ = ai.resolve_credential()
165
+ if "codex" in agents:
166
+ console.print(
167
+ "[yellow]--plaintext-key applies to Claude Code only; "
168
+ "Codex keeps using the token command.[/yellow]"
169
+ )
170
+
171
+ for name in agents:
172
+ try:
173
+ result = ai.connect_agent(
174
+ name,
175
+ api_url=api_url,
176
+ api_key=api_key,
177
+ model=model,
178
+ refresh_seconds=refresh_interval,
179
+ )
180
+ except ai.AiGatewayError as err:
181
+ handle_error(message=str(err), include_traceback=False)
182
+ return
183
+
184
+ label = _AGENT_LABELS.get(name, name)
185
+ console.print(f"[green]✓[/green] {label} → [cyan]{result['gateway_url']}[/cyan]")
186
+ console.print(f" config: [dim]{result['path']}[/dim]")
187
+ if result.get("model"):
188
+ console.print(f" model: [dim]{result['model']}[/dim]")
189
+ cred = "access token" if kind == "access_token" else "API key"
190
+ how = _AUTH_LABELS.get(result.get("auth_mode"), result.get("auth_mode", "?"))
191
+ console.print(f" auth: [dim]qBraid {cred} via {how}[/dim]")
192
+
193
+ if not plaintext_key:
194
+ _warn_if_token_command_missing()
195
+
196
+ if "claude" in agents:
197
+ _print_claude_caveats()
198
+
199
+ console.print()
200
+ console.print("[dim]Restart the agent to pick up the new configuration.[/dim]")
201
+ console.print("[dim]Revert any time with: qbraid ai disconnect[/dim]")
202
+
203
+
204
+ @ai_app.command(name="disconnect")
205
+ def ai_disconnect(
206
+ agent: str = typer.Argument("all", help="Agent to restore: claude, codex, or all"),
207
+ ):
208
+ """Restore an agent's previous configuration.
209
+
210
+ Puts back exactly what was there before connecting — including removing
211
+ settings that weren't present to begin with.
212
+
213
+ Example:
214
+ qbraid ai disconnect claude
215
+ """
216
+ ai = _ai()
217
+
218
+ for name in _agents_from(agent):
219
+ try:
220
+ result = ai.disconnect_agent(name)
221
+ except ai.AiGatewayError as err:
222
+ handle_error(message=str(err), include_traceback=False)
223
+ return
224
+
225
+ label = _AGENT_LABELS.get(name, name)
226
+ if result["status"] == "not_configured":
227
+ console.print(f"[dim]○[/dim] {label} was not configured.")
228
+ else:
229
+ restored = "restored previous settings" if result.get("restored") else "cleared"
230
+ console.print(f"[green]✓[/green] {label} — {restored}")
231
+
232
+ console.print()
233
+ # A running agent keeps its settings in memory and rewrites the file on
234
+ # exit, which silently puts our config back. Observed with both Claude Code
235
+ # and Codex, so this is guidance the user genuinely needs, not boilerplate.
236
+ console.print(
237
+ "[yellow]If the agent is running, stop it and re-run this command.[/yellow] "
238
+ "A running agent can rewrite its config from memory and undo the change."
239
+ )
240
+ console.print("[dim]Confirm with: qbraid ai status[/dim]")
241
+
242
+
243
+ @ai_app.command(name="status")
244
+ def ai_status(
245
+ check: bool = typer.Option(False, "--check", help="Also verify the credential against the proxy"),
246
+ ):
247
+ """Show which agents are routed through qBraid.
248
+
249
+ Pass --check to confirm the credential is actually accepted — useful when
250
+ a session has started failing and you want to know whether it expired.
251
+
252
+ Example:
253
+ qbraid ai status --check
254
+ """
255
+ ai = _ai()
256
+
257
+ status = ai.gateway_status()
258
+
259
+ table = Table(title="qBraid AI gateway")
260
+ table.add_column("Agent")
261
+ table.add_column("Routed")
262
+ table.add_column("Endpoint")
263
+ table.add_column("Model")
264
+
265
+ for name, info in status.items():
266
+ table.add_row(
267
+ _AGENT_LABELS.get(name, name),
268
+ "[green]yes[/green]" if info.get("connected") else "[dim]no[/dim]",
269
+ info.get("base_url") or "-",
270
+ info.get("model") or "-",
271
+ )
272
+
273
+ console.print(table)
274
+
275
+ if not check:
276
+ return
277
+
278
+ try:
279
+ _, kind = ai.resolve_credential()
280
+ except ai.AiGatewayError as err:
281
+ console.print(f"[red]✗[/red] {err}")
282
+ raise typer.Exit(code=1) from err
283
+
284
+ console.print()
285
+ try:
286
+ ai.list_models()
287
+ except Exception as err: # pylint: disable=broad-exception-caught
288
+ status_code = getattr(err, "status_code", None)
289
+ if status_code in (401, 403):
290
+ # The one case where the credential is actually the problem.
291
+ console.print(f"[red]✗[/red] credential rejected ({status_code})")
292
+ console.print()
293
+ console.print(ai.credential_expired_hint(kind))
294
+ elif status_code is not None:
295
+ console.print(f"[red]✗[/red] proxy returned {status_code}: {err}")
296
+ console.print("[dim]Your credential may be fine — this looks server-side.[/dim]")
297
+ else:
298
+ console.print(f"[red]✗[/red] could not reach the proxy: {err}")
299
+ console.print("[dim]Check your connection and `qbraid configure` URL.[/dim]")
300
+ raise typer.Exit(code=1) from err
301
+
302
+ console.print(
303
+ "[green]✓[/green] credential accepted "
304
+ f"[dim]({'access token' if kind == 'access_token' else 'API key'})[/dim]"
305
+ )
306
+
307
+
308
+ @ai_app.command(name="models")
309
+ def ai_models():
310
+ """List models available on the qBraid proxy.
311
+
312
+ The proxy matches model ids exactly, so pick one of these when setting
313
+ `--model` — an id the proxy doesn't know is rejected on the first request.
314
+
315
+ Example:
316
+ qbraid ai models
317
+ """
318
+ try:
319
+ models = _ai().list_models()
320
+ except Exception as err: # pylint: disable=broad-exception-caught
321
+ handle_error(message=f"Could not list models: {err}", include_traceback=False)
322
+ return
323
+
324
+ if not models:
325
+ console.print("[yellow]No models returned.[/yellow]")
326
+ return
327
+
328
+ table = Table(title="Available models")
329
+ table.add_column("ID")
330
+ table.add_column("Name")
331
+ table.add_column("Backend")
332
+ table.add_column("Max tokens", justify="right")
333
+
334
+ for item in models:
335
+ if not isinstance(item, dict):
336
+ table.add_row(str(item), "-", "-", "-")
337
+ continue
338
+ # The listing is OpenAI-compatible, so the qBraid metadata (name,
339
+ # provider, limits) is nested under `_qbraid` rather than top level.
340
+ meta = item.get("_qbraid") or {}
341
+ max_tokens = meta.get("maxTokens")
342
+ table.add_row(
343
+ item.get("id", "-"),
344
+ meta.get("name", "-"),
345
+ meta.get("provider", "-"),
346
+ f"{max_tokens:,}" if isinstance(max_tokens, int) else "-",
347
+ )
348
+
349
+ console.print(table)
350
+
351
+
352
+ @ai_app.command(name="quota")
353
+ def ai_quota():
354
+ """Show remaining qBraid AI quota.
355
+
356
+ Example:
357
+ qbraid ai quota
358
+ """
359
+ try:
360
+ quota = _ai().get_quota()
361
+ except Exception as err: # pylint: disable=broad-exception-caught
362
+ handle_error(message=f"Could not fetch quota: {err}", include_traceback=False)
363
+ return
364
+
365
+ if not quota:
366
+ console.print("[yellow]No quota information returned.[/yellow]")
367
+ return
368
+
369
+ for key, value in quota.items():
370
+ console.print(f" {key}: [cyan]{value}[/cyan]")
371
+
372
+
373
+ @ai_app.command(name="token", hidden=True)
374
+ def ai_token():
375
+ """Print the qBraid credential for agents.
376
+
377
+ Hidden because it exists for Claude Code's `apiKeyHelper` and Codex's
378
+ `[model_providers.qbraid.auth]` command rather than for people. Prints the
379
+ credential and nothing else, so the agents can read it from stdout.
380
+ """
381
+ ai = _ai()
382
+
383
+ try:
384
+ credential, _ = ai.resolve_credential()
385
+ except ai.AiGatewayError as err:
386
+ # stderr, so a failure never looks like a token on stdout.
387
+ typer.echo(str(err), err=True)
388
+ raise typer.Exit(code=1) from err
389
+
390
+ typer.echo(credential)
@@ -0,0 +1,66 @@
1
+ # Copyright (c) 2026, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Standalone entry point that prints the qBraid credential to stdout.
6
+
7
+ This exists as its own console script rather than as `qbraid ai token` because
8
+ it is the one command that runs unattended, forever, on every configured agent
9
+ — Claude Code re-invokes it via ``apiKeyHelper`` and Codex via
10
+ ``[model_providers.qbraid.auth]``, both on a timer.
11
+
12
+ Going through ``qbraid_cli.main`` would import every command group in the CLI
13
+ (~0.9s here) to print one string. Importing only what's needed costs ~0.2s, and
14
+ — more to the point — keeps this path from getting slower every time a new
15
+ command group is added.
16
+
17
+ Deliberately has no argument parsing: it prints the credential and nothing else,
18
+ so an agent reading stdout can never pick up a stray banner or prompt.
19
+
20
+ The credential is written to the binary stdout buffer rather than via ``print``
21
+ so the bytes are identical on every platform. Text-mode stdout on Windows
22
+ translates ``\n`` into ``\r\n``, which would append a stray carriage return
23
+ to the credential an agent reads.
24
+ """
25
+
26
+ import sys
27
+
28
+
29
+ def _emit(credential: str) -> None:
30
+ """Write the credential and exactly one newline, byte-for-byte.
31
+
32
+ Goes through ``sys.stdout.buffer`` to bypass the text layer: on Windows
33
+ that layer turns the newline into ``\r\n``, and an agent that does not
34
+ strip the carriage return sends it as part of the credential and 401s.
35
+ Falls back to ``print`` when stdout has been replaced by an object with no
36
+ binary buffer (pytest's capture, for one).
37
+ """
38
+ buffer = getattr(sys.stdout, "buffer", None)
39
+ if buffer is None:
40
+ print(credential)
41
+ return
42
+ buffer.write(credential.encode("utf-8") + b"\n")
43
+ buffer.flush()
44
+
45
+
46
+ def main() -> int:
47
+ """Print the credential, or explain on stderr why there isn't one."""
48
+ try:
49
+ from qbraid_core.services.ai import AiGatewayError, resolve_credential
50
+ except ImportError as err: # pragma: no cover - broken install
51
+ print(f"qbraid-core is not importable: {err}", file=sys.stderr)
52
+ return 1
53
+
54
+ try:
55
+ credential, _ = resolve_credential()
56
+ except AiGatewayError as err:
57
+ # stderr only — a failure must never look like a token on stdout.
58
+ print(str(err), file=sys.stderr)
59
+ return 1
60
+
61
+ _emit(credential)
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__": # pragma: no cover
66
+ sys.exit(main())
@@ -12,6 +12,7 @@ import typer
12
12
  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
+ from qbraid_cli.ai.app import ai_app
15
16
  from qbraid_cli.compute import compute_app
16
17
  from qbraid_cli.configure import configure_app
17
18
  from qbraid_cli.devices import devices_app
@@ -32,7 +33,7 @@ except ImportError:
32
33
 
33
34
  app = typer.Typer(
34
35
  help=(
35
- "The qBraid CLI — your unified interface for AI agents, quantum computing,\n"
36
+ "The qBraid CLI — A unified interface for AI agents, quantum computing,\n"
36
37
  "Python environments, and cloud compute.\n\n"
37
38
  "Whether you're running AI coding agents locally or on cloud servers,\n"
38
39
  "managing quantum experiments across providers, or setting up reproducible\n"
@@ -43,6 +44,7 @@ app = typer.Typer(
43
44
 
44
45
  app.add_typer(admin_app, name="admin")
45
46
  app.add_typer(agents_app, name="agents")
47
+ app.add_typer(ai_app, name="ai")
46
48
  app.add_typer(compute_app, name="compute")
47
49
  app.add_typer(lab_app, name="lab")
48
50
  app.add_typer(configure_app, name="configure")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.13.2
3
+ Version: 0.13.3
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,7 +30,7 @@ Requires-Dist: typer>=0.12.1
30
30
  Requires-Dist: rich>=10.11.0
31
31
  Requires-Dist: click<=8.3.3
32
32
  Requires-Dist: questionary>=2.0.0
33
- Requires-Dist: qbraid-core>=0.3.3
33
+ Requires-Dist: qbraid-core>=0.3.11
34
34
  Provides-Extra: compute
35
35
  Requires-Dist: qbraid-core[compute]>=0.3.3; extra == "compute"
36
36
  Provides-Extra: envs
@@ -22,6 +22,9 @@ 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/ai/__init__.py
26
+ qbraid_cli/ai/app.py
27
+ qbraid_cli/ai/token.py
25
28
  qbraid_cli/compute/__init__.py
26
29
  qbraid_cli/compute/app.py
27
30
  qbraid_cli/compute/profiles.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  qbraid = qbraid_cli.main:app
3
+ qbraid-ai-token = qbraid_cli.ai.token:main
@@ -2,7 +2,7 @@ typer>=0.12.1
2
2
  rich>=10.11.0
3
3
  click<=8.3.3
4
4
  questionary>=2.0.0
5
- qbraid-core>=0.3.3
5
+ qbraid-core>=0.3.11
6
6
 
7
7
  [compute]
8
8
  qbraid-core[compute]>=0.3.3
File without changes
File without changes
File without changes