Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# cmd.exe requires CRLF: an LF-only .cmd mis-parses `if`/`goto` blocks.
*.cmd text eol=crlf
*.ps1 text eol=crlf

# Everything a POSIX shell runs must stay LF, whatever the checkout platform.
*.sh text eol=lf
install.sh text eol=lf
qbraid-code text eol=lf
77 changes: 77 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: ci
on: [push, pull_request]

jobs:
shell:
# Both platforms on purpose. Every portability defect found in review broke
# on exactly one of them: BSD vs GNU `stat`, and GNU `sed` rejecting line 0.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Install shellcheck (macOS)
if: runner.os == 'macOS'
run: brew install shellcheck

- name: shellcheck
run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh

- name: syntax
run: |
bash -n install.sh
bash -n qbraid-code
bash -n statusline.sh

- name: statusline tests
run: tests/statusline.sh

- name: installer --help does not touch the machine
run: bash install.sh --help

powershell:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- name: PSScriptAnalyzer
shell: pwsh
run: |
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
$r = Invoke-ScriptAnalyzer -Path . -Severity Error,Warning -Recurse
$r | Format-Table -AutoSize
if ($r | Where-Object { $_.Severity -eq 'Error' }) { exit 1 }

- name: parse every script
shell: pwsh
run: |
foreach ($f in Get-ChildItem *.ps1) {
$errs = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$f.FullName, [ref]$null, [ref]$errs) | Out-Null
if ($errs) { $errs; exit 1 }
"parsed $($f.Name)"
}

- name: batch launcher is CRLF and BOM-free
shell: pwsh
run: |
$bytes = [IO.File]::ReadAllBytes('qbraid-code.cmd')
if ($bytes[0] -eq 0xEF) { throw 'qbraid-code.cmd must not have a UTF-8 BOM' }
$text = [Text.Encoding]::UTF8.GetString($bytes)
if ($text -notmatch "`r`n") { throw 'qbraid-code.cmd must use CRLF line endings' }
'CRLF ok, no BOM'

- name: launcher reports a missing install instead of crashing
shell: cmd
run: |
set QBRAID_CODE_HOME=%TEMP%\qc-does-not-exist
call qbraid-code.cmd --help
if not errorlevel 1 exit /b 1
echo missing-install path reports an error
rem `echo` does not reset ERRORLEVEL in cmd, so the expected 1 would
rem otherwise leak out and fail the step.
exit /b 0
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
credits.cache
*.tmp.*
169 changes: 168 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,169 @@
# qbraid-code
Claude Code, powered by the qBraid AI gateway — one-line installer for macOS, Linux and Windows

Claude Code, powered by the **qBraid AI gateway**. One command installs everything
and leaves you in a working session: Claude Code itself, your qBraid credentials,
a live credit statusline, and the qBraid MCP tools.

Your own `claude` command stays untouched unless you ask for `--global`.

## Install

**macOS and Linux**

```bash
curl -fsSL https://qbraid.com/code.sh | bash
```

**Windows** (PowerShell)

```powershell
irm https://qbraid.com/code.ps1 | iex
```

That is all. The installer:

- installs **Claude Code** if you do not have it — a native binary, no Node.js and no administrator rights
- finds your **qBraid API key**, or opens the page where you can copy one
- shows you which **organization** the key belongs to and asks you to confirm it
- lets you **choose a default model** from the list the gateway is actually serving
- registers the **qBraid MCP** and signs you in
- installs a **statusline** showing your remaining qBraid credits
- runs a **real request** to prove it works before it says "ready"

Re-running it is safe.

> **While this repository is private**, neither `qbraid.com/code.sh` nor
> `qbraid.com/code.ps1` is live yet. Use the GitHub CLI instead (`gh auth login`
> first, and you must be in the `qBraid` org):
>
> ```bash
> # macOS and Linux
> gh api -H "Accept: application/vnd.github.raw" \
> /repos/qBraid/qbraid-code/contents/install.sh | bash
> ```
>
> ```powershell
> # Windows
> gh api -H "Accept: application/vnd.github.raw" /repos/qBraid/qbraid-code/contents/install.ps1 | Out-String | iex
> ```

## Use

```bash
qbraid-code # start a session
qbraid-code -p "explain this" # ask one question and exit
qbraid-code --doctor # check your setup
```

Every other flag goes straight through to `claude`, so `-c`, `--model`,
`--allowedTools` and the rest behave normally.

A session looks like this:

```
qbraid-code ⎇ main │ Claude Opus 5 │ C13 █░░░░░ │ 4281 credits
```

Folder and branch, the model you are talking to, how much of the context window
is used, and what you have left to spend.

### Take over the plain `claude` command

If qBraid is the only backend you use, install with `--global` and plain `claude`
will use it too:

```bash
curl -fsSL https://qbraid.com/code.sh | bash -s -- --global
```

```powershell
& ([scriptblock]::Create((irm https://qbraid.com/code.ps1))) -Global
```

## How it works

Claude Code speaks the Anthropic Messages API. So does the qBraid gateway — it
exposes an Anthropic-compatible surface at `/api/v1/ai/v1/messages` (the double
`v1` is deliberate, so `ANTHROPIC_BASE_URL` works with no client changes).

```
qbraid-code
│ ANTHROPIC_BASE_URL=https://api-v2.qbraid.com/api/v1/ai
│ ANTHROPIC_AUTH_TOKEN=qbr_...
Claude Code ──Anthropic Messages──► qBraid AI gateway ──► Claude on Bedrock
```

**There is no proxy and no daemon.** Nothing runs in the background, nothing
listens on a port. The launcher sets four environment variables and execs
`claude`.

Requests are billed against your qBraid credits at the usual rate
(100 credits = $1).

## The qBraid MCP

The MCP endpoint at `mcp.qbraid.com/mcp` uses OAuth, not API keys, so it needs a
browser sign-in that your API key cannot do for you. The installer runs it during
setup, while you are still there. If you skipped it:

```bash
claude mcp login qbraid
```

## Layout

| Path | What |
|---|---|
| `~/.qbraid-code/env` | your key, gateway URL and default model (mode `600`) |
| `~/.qbraid-code/statusline.sh` | statusline script (`statusline.ps1` on Windows) |
| `~/.qbraid-code/credits.cache` | last known credit balance, refreshed every 60s |
| `~/.qbraid-code/credits.attempt` | when a refresh was last tried, so failures back off |
| `~/.local/bin/qbraid-code` | the launcher (`qbraid-code.cmd` on Windows) |
| `~/.claude/settings.json` | statusline wiring, plus gateway env with `--global` |

## Troubleshooting

Start with `qbraid-code --doctor`. It reports whether Claude Code is installed,
whether your key still works, your credit balance, whether the gateway is
reachable, and whether the MCP is registered.

**`key: REJECTED`** — your API key was deleted or expired. Make a new one at
[account.qbraid.com/account/api-keys](https://account.qbraid.com/account/api-keys)
and re-run the installer.

**`qbraid-code: command not found`** — `~/.local/bin` is not on your `PATH`. The
installer prints the line to add. On Windows, open a new terminal first.

**Wrong organization** — credits come from the organization the key belongs to.
Create a key under the organization you want, then re-run the installer.

**Statusline did not appear** — when `python3` is unavailable (no Xcode Command
Line Tools on macOS, or no `python3` on Linux) and `~/.claude/settings.json`
already exists, the installer cannot merge JSON safely and skips this step. It
prints the snippet to add by hand.

## Trust

The installer is served from `qbraid.com` but its source of truth is the `main`
branch of this repository, fetched at request time with no pin and no signature.
Anyone who can push here can change what a `curl | bash` runs, which is the
normal trade-off for a one-line installer that must stay current. Two things
follow from that: branch protection on `main` is load-bearing, and if you would
rather read before you run, fetch the script and inspect it first:

```bash
curl -fsSL https://qbraid.com/code.sh -o install.sh
less install.sh
bash install.sh
```

## Uninstall

```bash
rm -rf ~/.qbraid-code ~/.local/bin/qbraid-code
claude mcp remove qbraid
```

Then remove the `statusLine` entry (and the `env` block, if you used `--global`)
from `~/.claude/settings.json`.
74 changes: 74 additions & 0 deletions doctor.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<#
.SYNOPSIS
Health check for qbraid-code on Windows. Invoked by `qbraid-code --doctor`.
#>
# No Set-StrictMode: this command exists to report a broken setup, so a missing
# field must print a diagnosis rather than a PowerShell exception.
$ErrorActionPreference = 'Continue'

$HomeDir = if ($env:QBRAID_CODE_HOME) { $env:QBRAID_CODE_HOME } else { Join-Path $env:USERPROFILE '.qbraid-code' }
$envPath = Join-Path $HomeDir 'env'

if (-not (Test-Path $envPath)) {
Write-Host "qbraid-code: not installed - no $envPath"
exit 1
}

$settings = @{}
foreach ($line in Get-Content $envPath) {
if ($line -match '^\s*([A-Z_]+)\s*=\s*(.*)$') { $settings[$Matches[1]] = $Matches[2] }
}

$apiBase = $settings['QBRAID_CODE_API_BASE']
$baseUrl = $settings['QBRAID_CODE_BASE_URL']
$token = $settings['QBRAID_CODE_TOKEN']
$model = $settings['QBRAID_CODE_MODEL']

if (Get-Command claude -ErrorAction SilentlyContinue) {
$version = (& claude --version 2>$null)
if (-not $version) { $version = 'present' }
Write-Host "claude: $version"
} else {
Write-Host 'claude: NOT INSTALLED'
}

# Separate transport failure from rejection. Reporting "REJECTED" for a dropped
# connection sent people off to make a new key for no reason.
try {
$balance = Invoke-RestMethod -Uri "$apiBase/billing/credits/balance" `
-Headers @{ 'X-API-Key' = $token } -TimeoutSec 20
Write-Host 'key: valid'
$raw = $balance.data.qbraidCredits
if ($null -ne $raw) {
Write-Host "credits: $([Math]::Round([double]$raw))"
} else {
Write-Host 'credits: unknown'
}
} catch {
$status = $null
if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
if ($status -eq 401 -or $status -eq 403) {
Write-Host 'key: REJECTED - make a new one at https://account.qbraid.com/account/api-keys'
} elseif ($status) {
Write-Host "key: UNKNOWN - qBraid returned HTTP $status"
} else {
Write-Host 'key: UNKNOWN - could not reach qBraid (check your connection)'
}
Write-Host 'credits: unknown'
}

try {
Invoke-RestMethod -Uri "$baseUrl/v1/models" -Headers @{ 'X-API-Key' = $token } -TimeoutSec 20 | Out-Null
Write-Host 'gateway: reachable'
} catch {
Write-Host 'gateway: UNREACHABLE'
}

& claude mcp get qbraid *> $null
if ($LASTEXITCODE -eq 0) {
Write-Host "mcp: registered (run 'claude mcp login qbraid' if tools are missing)"
} else {
Write-Host 'mcp: NOT REGISTERED'
}

Write-Host "model: $model"
Loading
Loading