From e2a3c358b228038698034316a2d61ad954b7f636 Mon Sep 17 00:00:00 2001 From: Kenny-Heitritter Date: Fri, 21 Aug 2026 09:58:39 -0500 Subject: [PATCH 1/4] Handle Claude Code version compatibility --- .github/workflows/ci.yml | 36 ++++- README.md | 30 +++++ doctor.ps1 | 70 +++++++++- install.ps1 | 218 +++++++++++++++++++++++++++---- install.sh | 198 +++++++++++++++++++++++++--- qbraid-code | 83 +++++++++++- tests/claude-compat.Tests.ps1 | 158 ++++++++++++++++++++++ tests/claude-compat.sh | 102 +++++++++++++++ tests/claude-installed-compat.sh | 26 ++++ tests/claude-policy-flow.sh | 105 +++++++++++++++ tests/doctor.sh | 62 +++++++++ 11 files changed, 1041 insertions(+), 47 deletions(-) create mode 100644 tests/claude-compat.Tests.ps1 create mode 100755 tests/claude-compat.sh create mode 100755 tests/claude-installed-compat.sh create mode 100755 tests/claude-policy-flow.sh create mode 100755 tests/doctor.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da26946..0bf85f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: run: brew install shellcheck - name: shellcheck - run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh tests/extractors.sh tests/model-routing.sh + run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh tests/extractors.sh tests/model-routing.sh tests/claude-compat.sh tests/claude-policy-flow.sh tests/claude-installed-compat.sh tests/doctor.sh - name: syntax run: | @@ -35,6 +35,14 @@ jobs: - name: model routing tests run: tests/model-routing.sh + - name: Claude compatibility policy tests + run: | + tests/claude-compat.sh + tests/claude-policy-flow.sh + + - name: doctor compatibility tests + run: tests/doctor.sh + - name: installer --help does not touch the machine run: bash install.sh --help @@ -62,6 +70,10 @@ jobs: "parsed $($f.Name)" } + - name: Claude compatibility policy tests + shell: pwsh + run: ./tests/claude-compat.Tests.ps1 + - name: batch launcher is CRLF and BOM-free shell: pwsh run: | @@ -81,3 +93,25 @@ jobs: rem `echo` does not reset ERRORLEVEL in cmd, so the expected 1 would rem otherwise leak out and fail the step. exit /b 0 + + claude-compat: + strategy: + fail-fast: false + matrix: + include: + - release: '2.1.186' + experimental: false + - release: stable + experimental: false + - release: latest + experimental: true + continue-on-error: ${{ matrix.experimental }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Claude Code ${{ matrix.release }} + run: curl -fsSL https://claude.ai/install.sh | bash -s "${{ matrix.release }}" + - name: Check installed CLI capabilities + run: | + export PATH="$HOME/.local/bin:$PATH" + tests/claude-installed-compat.sh diff --git a/README.md b/README.md index 642ce04..20e1351 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,29 @@ That is all. The installer: Re-running it is safe. +### Claude Code compatibility + +qbraid-code currently supports Claude Code **2.1.186 or newer** and is tested +through **2.1.238**. A newer version is never blocked or downgraded; the +installer only notes that it is newer than the latest version covered by CI. + +If an existing Claude Code is too old or is missing the HTTP MCP commands that +qbraid-code needs, the installer offers to move it to Anthropic's `stable` +channel. It always asks before changing an existing installation. Set +`QBRAID_CODE_CLAUDE_POLICY` to choose the behavior explicitly: + +| Value | Behavior | +|---|---| +| `prompt` | Ask before installing or upgrading (default with a terminal) | +| `upgrade` | Install or upgrade to the stable channel without prompting | +| `fail` | Stop rather than changing Claude Code (default without a terminal) | +| `continue` | Keep an unsupported version and skip or degrade unavailable features | + +The installer checks the actual CLI capabilities as well as its version. In +particular, an older release without `claude mcp login` can still finish setup: +qbraid-code registers the server when possible and directs you to Claude Code's +interactive `/mcp` menu for authentication. + ## Use ```bash @@ -118,6 +141,9 @@ setup, while you are still there. If you skipped it: claude mcp login qbraid ``` +On Claude Code versions without that command, start Claude Code, run `/mcp`, +select `qbraid`, and choose **Authenticate**. + ## Layout | Path | What | @@ -145,6 +171,10 @@ 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. +**`unknown command 'login'`** — your Claude Code predates the MCP login command. +Run `claude update`, re-run the qbraid-code installer and accept its stable-channel +upgrade, or authenticate from Claude Code's `/mcp` menu. + **Wrong organization** — credits come from the organization the key belongs to. Create a key under the organization you want, then re-run the installer. diff --git a/doctor.ps1 b/doctor.ps1 index c91489a..856db13 100644 --- a/doctor.ps1 +++ b/doctor.ps1 @@ -23,14 +23,68 @@ $apiBase = $settings['QBRAID_CODE_API_BASE'] $baseUrl = $settings['QBRAID_CODE_BASE_URL'] $token = $settings['QBRAID_CODE_TOKEN'] $model = $settings['QBRAID_CODE_MODEL'] +$claudeMinVersion = '2.1.186' +$claudeTestedMax = '2.1.238' +function ConvertFrom-ClaudeVersionString { + param([string]$Text) + if ($Text -match '(\d+\.\d+\.\d+)') { return $Matches[1] } + return $null +} + +function Test-ClaudeMcpCommand { + param([string]$Command) + $help = (& claude mcp --help 2>$null | Out-String) + return $help -match "(?m)^\s+$([regex]::Escape($Command))(?:\s|$)" +} + +function Test-ClaudeMcpHttp { + $help = (& claude mcp add --help 2>$null | Out-String) + return $help -match '(?s)--transport.*\bhttp\b' +} + +function Test-ClaudeMcpUserScope { + $help = (& claude mcp add --help 2>$null | Out-String) + return $help -match '(?s)--scope.*\buser\b' +} + +$claudePresent = $false +$claudeVersion = $null +$mcpAdd = $false +$mcpGet = $false +$mcpLogin = $false +$mcpHttp = $false +$mcpUserScope = $false if (Get-Command claude -ErrorAction SilentlyContinue) { + $claudePresent = $true $version = (& claude --version 2>$null) if (-not $version) { $version = 'present' } Write-Host "claude: $version" + $claudeVersion = ConvertFrom-ClaudeVersionString ($version | Out-String) + $mcpAdd = Test-ClaudeMcpCommand 'add' + $mcpGet = Test-ClaudeMcpCommand 'get' + $mcpLogin = Test-ClaudeMcpCommand 'login' + $mcpHttp = Test-ClaudeMcpHttp + $mcpUserScope = Test-ClaudeMcpUserScope } else { Write-Host 'claude: NOT INSTALLED' } +if (-not $claudeVersion) { + Write-Host "claude-min: UNKNOWN (requires $claudeMinVersion+)" + Write-Host 'claude-tested: unknown' +} elseif ([version]$claudeVersion -lt [version]$claudeMinVersion) { + Write-Host "claude-min: FAIL (requires $claudeMinVersion+, found $claudeVersion)" + Write-Host 'claude-tested: unsupported' +} elseif ([version]$claudeVersion -gt [version]$claudeTestedMax) { + Write-Host "claude-min: PASS (requires $claudeMinVersion+)" + Write-Host "claude-tested: NEWER than tested $claudeTestedMax (not blocked)" +} else { + Write-Host "claude-min: PASS (requires $claudeMinVersion+)" + Write-Host "claude-tested: within tested range through $claudeTestedMax" +} +$claudePolicy = if ($env:QBRAID_CODE_CLAUDE_POLICY) { $env:QBRAID_CODE_CLAUDE_POLICY } else { 'prompt' } +Write-Host "claude-policy: $claudePolicy" +Write-Host "capabilities: mcp-add=$($mcpAdd.ToString().ToLower()) mcp-get=$($mcpGet.ToString().ToLower()) mcp-login=$($mcpLogin.ToString().ToLower()) mcp-http=$($mcpHttp.ToString().ToLower()) mcp-user-scope=$($mcpUserScope.ToString().ToLower())" # Separate transport failure from rejection. Reporting "REJECTED" for a dropped # connection sent people off to make a new key for no reason. @@ -64,11 +118,19 @@ try { 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)" +if (-not $claudePresent -or -not $mcpGet) { + Write-Host 'mcp: UNAVAILABLE - upgrade Claude Code or configure it through /mcp' } else { - Write-Host 'mcp: NOT REGISTERED' + & claude mcp get qbraid *> $null + if ($LASTEXITCODE -eq 0) { + if ($mcpLogin) { + Write-Host "mcp: registered (run 'claude mcp login qbraid' if tools are missing)" + } else { + Write-Host 'mcp: registered (run /mcp inside Claude Code to authenticate)' + } + } else { + Write-Host 'mcp: NOT REGISTERED' + } } Write-Host "model: $model" diff --git a/install.ps1 b/install.ps1 index c3d073c..00deab7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -30,6 +30,8 @@ $GatewayUrl = "$ApiBase/ai" $McpName = 'qbraid' $McpUrl = 'https://mcp.qbraid.com/mcp' $KeysUrl = 'https://account.qbraid.com/account/api-keys' +$script:ClaudeMinVersion = '2.1.186' +$script:ClaudeTestedMax = '2.1.238' $SiteBase = 'https://qbraid.com/code' $RawBase = 'https://raw.githubusercontent.com/qBraid/qbraid-code/main' $GhContents = '/repos/qBraid/qbraid-code/contents' @@ -43,12 +45,17 @@ $ClaudeJson = Join-Path $env:USERPROFILE '.claude.json' function Say { param($m) Write-Host "==> $m" -ForegroundColor White } function Ok { param($m) Write-Host " + $m" -ForegroundColor Green } function Warn { param($m) Write-Host " ! $m" -ForegroundColor Yellow } +function Test-InteractiveConsole { + try { return -not [Console]::IsInputRedirected } catch { return $false } +} function Die { param($m) Write-Host "`nerror: $m" -ForegroundColor Red # Under `irm | iex` in a fresh window, exiting closes the window with the # message still on screen for a fraction of a second. Hold it open. - if ($Host.UI.RawUI) { try { Read-Host 'Press Enter to close' | Out-Null } catch { } } + if ((Test-InteractiveConsole) -and $Host.UI.RawUI) { + try { Read-Host 'Press Enter to close' | Out-Null } catch { } + } exit 1 } @@ -70,37 +77,191 @@ function Confirm-Step { return ($reply -eq 'y' -or $reply -eq 'yes') } -# ---------------------------------------------------------------- 1. platform +# ------------------------------------------------------ Claude compatibility -if (-not [Environment]::Is64BitOperatingSystem) { - Die '32-bit Windows is not supported.' +function ConvertFrom-ClaudeVersionString { + param([string]$Text) + if ($Text -match '(\d+\.\d+\.\d+)') { return $Matches[1] } + return $null } -Say "Platform: windows/$($env:PROCESSOR_ARCHITECTURE.ToLower())" -New-Item -ItemType Directory -Force -Path $HomeDir, $BinDir, $ClaudeDir | Out-Null +function Compare-ClaudeVersion { + param([string]$Left, [string]$Right) + return ([version]$Left).CompareTo([version]$Right) +} -# ------------------------------------------------------------ 2. claude code +function Get-ClaudeVersionStatus { + param([string]$Version) + if (-not $Version) { return 'unknown' } + if ((Compare-ClaudeVersion $Version $script:ClaudeMinVersion) -lt 0) { + return 'below-minimum' + } + if ((Compare-ClaudeVersion $Version $script:ClaudeTestedMax) -gt 0) { + return 'newer-than-tested' + } + return 'tested' +} -Say 'Claude Code' -if (Get-Command claude -ErrorAction SilentlyContinue) { - Ok 'already installed' -} else { - Warn 'not installed — installing' - # Anthropic's official installer: a native binary, no Node.js, no admin rights. +function Get-ClaudePolicyAction { + param([string]$Policy, [bool]$Interactive) + switch ($Policy) { + 'upgrade' { return 'upgrade' } + 'fail' { return 'fail' } + 'continue' { return 'continue' } + 'prompt' { if ($Interactive) { return 'prompt' } else { return 'fail' } } + default { return 'invalid' } + } +} + +function Test-ClaudeMcpCommand { + param([string]$Command) + $help = (& claude mcp --help 2>$null | Out-String) + return $help -match "(?m)^\s+$([regex]::Escape($Command))(?:\s|$)" +} + +function Test-ClaudeMcpHttp { + $help = (& claude mcp add --help 2>$null | Out-String) + return $help -match '(?s)--transport.*\bhttp\b' +} + +function Test-ClaudeMcpUserScope { + $help = (& claude mcp add --help 2>$null | Out-String) + return $help -match '(?s)--scope.*\buser\b' +} + +function Install-ClaudeStable { + Warn "installing Claude Code from Anthropic's stable channel" try { - & ([scriptblock]::Create((Invoke-RestMethod -Uri 'https://claude.ai/install.ps1'))) + $installer = [scriptblock]::Create( + (Invoke-RestMethod -Uri 'https://claude.ai/install.ps1')) + & $installer stable } catch { - Die "Claude Code install failed: $_" + Die "Claude Code stable-channel install failed: $_" } - # Anthropic's installer always writes to %USERPROFILE%\.local\bin; $BinDir is - # overridable and may be somewhere else entirely. $env:Path = "$(Join-Path $env:USERPROFILE '.local\bin');$BinDir;$env:Path" if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { Die 'Claude Code installed but `claude` is not on PATH. Open a new terminal and re-run.' } - Ok 'installed' } +function Update-ClaudeState { + $output = (& claude --version 2>$null | Out-String).Trim() + $script:ClaudeVersion = ConvertFrom-ClaudeVersionString $output + $script:ClaudeVersionStatus = Get-ClaudeVersionStatus $script:ClaudeVersion + $script:ClaudeMcpAdd = Test-ClaudeMcpCommand 'add' + $script:ClaudeMcpGet = Test-ClaudeMcpCommand 'get' + $script:ClaudeMcpLogin = Test-ClaudeMcpCommand 'login' + $script:ClaudeMcpHttp = Test-ClaudeMcpHttp + $script:ClaudeMcpUserScope = Test-ClaudeMcpUserScope +} + +function Test-ClaudeRequiredCapabilities { + return $script:ClaudeMcpAdd -and $script:ClaudeMcpGet -and + $script:ClaudeMcpHttp -and $script:ClaudeMcpUserScope +} + +function Confirm-ClaudeCompatibility { + $policy = if ($env:QBRAID_CODE_CLAUDE_POLICY) { + $env:QBRAID_CODE_CLAUDE_POLICY.ToLower() + } else { + 'prompt' + } + if ($policy -notin @('prompt', 'upgrade', 'fail', 'continue')) { + Die 'QBRAID_CODE_CLAUDE_POLICY must be prompt, upgrade, fail, or continue.' + } + $interactive = Test-InteractiveConsole + + if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { + $action = Get-ClaudePolicyAction $policy $interactive + if ($action -eq 'upgrade') { + Install-ClaudeStable + } elseif ($action -eq 'prompt') { + if (Confirm-Step 'Claude Code is not installed. Install the stable channel now?' 'y') { + Install-ClaudeStable + } else { + Die 'Claude Code is required. Re-run with QBRAID_CODE_CLAUDE_POLICY=upgrade to install it.' + } + } else { + Die 'Claude Code is not installed. Re-run with QBRAID_CODE_CLAUDE_POLICY=upgrade.' + } + } + + Update-ClaudeState + $issue = $null + if ($script:ClaudeVersionStatus -eq 'unknown') { + $issue = 'could not determine its version' + } elseif ($script:ClaudeVersionStatus -eq 'below-minimum') { + $issue = "version $($script:ClaudeVersion) is below the supported minimum $script:ClaudeMinVersion" + } + if (-not (Test-ClaudeRequiredCapabilities)) { + if ($issue) { $issue += '; ' } + $issue += 'required HTTP MCP commands are unavailable' + } + + $upgraded = $false + if ($issue) { + $action = Get-ClaudePolicyAction $policy $interactive + if ($script:ClaudeVersionStatus -eq 'newer-than-tested') { + if ($action -in @('continue', 'prompt')) { + Warn "Claude Code $($script:ClaudeVersion) is newer than tested and lacks required capabilities; refusing to downgrade it and continuing with reduced compatibility." + $action = 'handled' + } else { + Die "Claude Code $($script:ClaudeVersion) is newer than tested and lacks required capabilities. Refusing to downgrade it; set QBRAID_CODE_CLAUDE_POLICY=continue to skip unavailable features." + } + } + if ($action -eq 'upgrade') { + Warn "the installed Claude Code is incompatible: $issue" + Install-ClaudeStable + $upgraded = $true + } elseif ($action -eq 'prompt') { + Warn "the installed Claude Code is incompatible: $issue" + if (Confirm-Step 'Upgrade Claude Code to the stable channel now?' 'y') { + Install-ClaudeStable + $upgraded = $true + } else { + Warn 'continuing with reduced compatibility at your request' + } + } elseif ($action -eq 'continue') { + Warn "continuing with an unsupported Claude Code: $issue" + } elseif ($action -eq 'handled') { + # The newer-version branch above already reported the safe fallback. + } else { + Die "the installed Claude Code is incompatible: $issue. Upgrade it, or explicitly set QBRAID_CODE_CLAUDE_POLICY=continue." + } + } + + if ($upgraded) { + Update-ClaudeState + if ($script:ClaudeVersionStatus -in @('unknown', 'below-minimum') -or + -not (Test-ClaudeRequiredCapabilities)) { + Die "Claude Code was upgraded, but version $script:ClaudeMinVersion+ with HTTP MCP support is still unavailable." + } + } + + if ($script:ClaudeVersionStatus -eq 'newer-than-tested') { + Warn "Claude Code $($script:ClaudeVersion) is newer than the latest tested version ($script:ClaudeTestedMax); continuing without downgrading." + } elseif ($script:ClaudeVersionStatus -eq 'tested') { + Ok "Claude Code $($script:ClaudeVersion)" + } else { + $displayVersion = if ($script:ClaudeVersion) { $script:ClaudeVersion } else { 'present' } + Warn "Claude Code $displayVersion remains outside the supported range; unavailable features will be skipped." + } +} + +# ---------------------------------------------------------------- 1. platform + +if (-not [Environment]::Is64BitOperatingSystem) { + Die '32-bit Windows is not supported.' +} +Say "Platform: windows/$($env:PROCESSOR_ARCHITECTURE.ToLower())" + +New-Item -ItemType Directory -Force -Path $HomeDir, $BinDir, $ClaudeDir | Out-Null + +# ------------------------------------------------------------ 2. claude code + +Say 'Claude Code' +Confirm-ClaudeCompatibility + # ------------------------------------------------------------- 3. credential function Get-Balance { @@ -485,19 +646,32 @@ if ($Global) { # ------------------------------------------------------------------- 10. mcp Say 'qBraid MCP' -claude mcp get $McpName *> $null -if ($LASTEXITCODE -eq 0) { - Ok 'already registered' +$mcpRegistered = $false +if (-not (Test-ClaudeRequiredCapabilities)) { + Warn 'this Claude Code version cannot register an HTTP MCP server from the command line.' + Warn "Start Claude Code, run /mcp, and add $McpUrl manually; or upgrade Claude Code." } else { + claude mcp get $McpName *> $null +} +if ((Test-ClaudeRequiredCapabilities) -and $LASTEXITCODE -eq 0) { + $mcpRegistered = $true + Ok 'already registered' +} elseif (Test-ClaudeRequiredCapabilities) { claude mcp add --transport http $McpName $McpUrl --scope user *> $null if ($LASTEXITCODE -ne 0) { Die 'could not register the qBraid MCP server.' } + $mcpRegistered = $true Ok "registered $McpUrl" } # The MCP endpoint is JWT-only (OAuth + dynamic client registration): the API # key above cannot authorize it. Do the browser sign-in now, while the user is # still here, rather than surprising them mid-session. -if (Confirm-Step 'Sign in to the qBraid MCP now? (opens a browser)' 'y') { +if (-not $mcpRegistered) { + Warn 'MCP sign-in was skipped because registration is incomplete.' +} elseif (-not $script:ClaudeMcpLogin) { + Warn 'this Claude Code version authenticates MCP servers through its interactive menu.' + Warn "Start Claude Code, run /mcp, select '$McpName', and choose Authenticate." +} elseif (Confirm-Step 'Sign in to the qBraid MCP now? (opens a browser)' 'y') { # Unlike the piped bash path, `iex` keeps the console attached, so the # OAuth prompt can read the redirect URL directly. claude mcp login $McpName diff --git a/install.sh b/install.sh index 2a8ee69..fbbd867 100755 --- a/install.sh +++ b/install.sh @@ -26,6 +26,8 @@ PROXY_PORT="${QBRAID_CODE_PROXY_PORT:-8320}" PROXY_REPO="router-for-me/CLIProxyAPI" MCP_URL="https://mcp.qbraid.com/mcp" KEYS_URL="https://account.qbraid.com/account/api-keys" +CLAUDE_MIN_VERSION="2.1.186" +CLAUDE_TESTED_MAX="2.1.238" # Companion files are fetched from qbraid.com first. That is the whole point # of the proxy: on a campus network that blocks raw.githubusercontent.com, an @@ -57,6 +59,8 @@ Environment: QBRAID_CODE_MODEL use this model instead of prompting QBRAID_CODE_HOME config directory (default ~/.qbraid-code) QBRAID_CODE_BIN_DIR install directory (default ~/.local/bin) + QBRAID_CODE_CLAUDE_POLICY + prompt, upgrade, fail, or continue EOF exit 0 ;; *) ;; @@ -121,6 +125,171 @@ confirm() { # confirm -> 0 if yes [ "$reply" = y ] || [ "$reply" = yes ] } +# ------------------------------------------------------ Claude compatibility + +parse_claude_version() { # parse_claude_version + ( + set +o pipefail + printf '%s' "$1" | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' | head -1 + ) || true +} + +compare_versions() { # compare_versions -> -1, 0, or 1 + awk -v left="$1" -v right="$2" 'BEGIN { + split(left, l, "."); split(right, r, ".") + for (i = 1; i <= 3; i++) { + if ((l[i] + 0) < (r[i] + 0)) { print -1; exit } + if ((l[i] + 0) > (r[i] + 0)) { print 1; exit } + } + print 0 + }' +} + +claude_version_status() { # claude_version_status + local version="$1" compared + [ -n "$version" ] || { printf 'unknown'; return; } + compared=$(compare_versions "$version" "$CLAUDE_MIN_VERSION") + [ "$compared" -ge 0 ] || { printf 'below-minimum'; return; } + compared=$(compare_versions "$version" "$CLAUDE_TESTED_MAX") + [ "$compared" -le 0 ] && printf 'tested' || printf 'newer-than-tested' +} + +claude_policy_action() { # claude_policy_action + case "$1" in + upgrade|fail|continue) printf '%s' "$1" ;; + prompt) [ "$2" = yes ] && printf 'prompt' || printf 'fail' ;; + *) printf 'invalid' ;; + esac +} + +claude_supports_mcp_command() { # claude_supports_mcp_command + local help + help=$(claude mcp --help 2>/dev/null || true) + printf '%s\n' "$help" | grep -Eq "^[[:space:]]+$1([[:space:]]|$)" +} + +claude_supports_mcp_http() { + local help + help=$(claude mcp add --help 2>/dev/null || true) + printf '%s\n' "$help" | grep -Eq -- '--transport.*http' +} + +claude_supports_mcp_user_scope() { + local help + help=$(claude mcp add --help 2>/dev/null || true) + printf '%s\n' "$help" | grep -Eq -- '--scope.*user' +} + +install_claude_stable() { + warn "installing Claude Code from Anthropic's stable channel" + curl -fsSL https://claude.ai/install.sh | bash -s stable \ + || die "Claude Code stable-channel install failed. See https://claude.com/product/claude-code" + export PATH="$HOME/.local/bin:$PATH" + hash -r + command -v claude >/dev/null 2>&1 \ + || die "Claude Code installed but \`claude\` is not on PATH." +} + +refresh_claude_state() { + local output + output=$(claude --version 2>/dev/null || true) + CLAUDE_VERSION=$(parse_claude_version "$output") + CLAUDE_VERSION_STATUS=$(claude_version_status "$CLAUDE_VERSION") + CLAUDE_MCP_ADD=0; CLAUDE_MCP_GET=0; CLAUDE_MCP_LOGIN=0 + CLAUDE_MCP_HTTP=0; CLAUDE_MCP_USER_SCOPE=0 + claude_supports_mcp_command add && CLAUDE_MCP_ADD=1 + claude_supports_mcp_command get && CLAUDE_MCP_GET=1 + claude_supports_mcp_command login && CLAUDE_MCP_LOGIN=1 + claude_supports_mcp_http && CLAUDE_MCP_HTTP=1 + claude_supports_mcp_user_scope && CLAUDE_MCP_USER_SCOPE=1 + return 0 +} + +claude_required_capabilities_present() { + [ "$CLAUDE_MCP_ADD" = 1 ] && [ "$CLAUDE_MCP_GET" = 1 ] \ + && [ "$CLAUDE_MCP_HTTP" = 1 ] && [ "$CLAUDE_MCP_USER_SCOPE" = 1 ] +} + +ensure_claude_compatible() { + local policy="${QBRAID_CODE_CLAUDE_POLICY:-prompt}" interactive=no action issue="" upgraded=0 + [ -n "$TTY" ] && interactive=yes + case "$policy" in + prompt|upgrade|fail|continue) ;; + *) die "QBRAID_CODE_CLAUDE_POLICY must be prompt, upgrade, fail, or continue." ;; + esac + + if ! command -v claude >/dev/null 2>&1; then + action=$(claude_policy_action "$policy" "$interactive") + case "$action" in + upgrade) install_claude_stable ;; + prompt) + confirm "Claude Code is not installed. Install the stable channel now?" y \ + && install_claude_stable \ + || die "Claude Code is required. Re-run with QBRAID_CODE_CLAUDE_POLICY=upgrade to install it." + ;; + *) die "Claude Code is not installed. Re-run with QBRAID_CODE_CLAUDE_POLICY=upgrade." ;; + esac + fi + + refresh_claude_state + case "$CLAUDE_VERSION_STATUS" in + unknown) issue="could not determine its version" ;; + below-minimum) issue="version $CLAUDE_VERSION is below the supported minimum $CLAUDE_MIN_VERSION" ;; + esac + if ! claude_required_capabilities_present; then + [ -n "$issue" ] && issue="$issue; " + issue="${issue}required HTTP MCP commands are unavailable" + fi + + if [ -n "$issue" ]; then + action=$(claude_policy_action "$policy" "$interactive") + if [ "$CLAUDE_VERSION_STATUS" = newer-than-tested ]; then + case "$action" in + continue|prompt) + warn "Claude Code $CLAUDE_VERSION is newer than tested and lacks required capabilities; refusing to downgrade it and continuing with reduced compatibility." + ;; + *) + die "Claude Code $CLAUDE_VERSION is newer than tested and lacks required capabilities. Refusing to downgrade it; set QBRAID_CODE_CLAUDE_POLICY=continue to skip unavailable features." + ;; + esac + action=handled + fi + case "$action" in + upgrade) + warn "the installed Claude Code is incompatible: $issue" + install_claude_stable; upgraded=1 + ;; + prompt) + warn "the installed Claude Code is incompatible: $issue" + if confirm "Upgrade Claude Code to the stable channel now?" y; then + install_claude_stable; upgraded=1 + else + warn "continuing with reduced compatibility at your request" + fi + ;; + continue) warn "continuing with an unsupported Claude Code: $issue" ;; + fail) + die "the installed Claude Code is incompatible: $issue. Upgrade it, or explicitly set QBRAID_CODE_CLAUDE_POLICY=continue." + ;; + handled) ;; + esac + fi + + if [ "$upgraded" = 1 ]; then + refresh_claude_state + [ "$CLAUDE_VERSION_STATUS" != unknown ] \ + && [ "$CLAUDE_VERSION_STATUS" != below-minimum ] \ + && claude_required_capabilities_present \ + || die "Claude Code was upgraded, but version $CLAUDE_MIN_VERSION+ with HTTP MCP support is still unavailable." + fi + + case "$CLAUDE_VERSION_STATUS" in + tested) ok "Claude Code $CLAUDE_VERSION" ;; + newer-than-tested) warn "Claude Code $CLAUDE_VERSION is newer than the latest tested version ($CLAUDE_TESTED_MAX); continuing without downgrading." ;; + *) warn "Claude Code ${CLAUDE_VERSION:-present} remains outside the supported range; unavailable features will be skipped." ;; + esac +} + # ---------------------------------------------------------------- 1. platform case "$(uname -s)" in @@ -151,19 +320,7 @@ have_python() { # ------------------------------------------------------------ 2. claude code say "Claude Code" -if command -v claude >/dev/null 2>&1; then - ok "already installed ($(claude --version 2>/dev/null || echo present))" -else - warn "not installed — installing" - # Anthropic's official installer: a native binary into ~/.local/bin. - # No Node.js and no administrator rights required. - curl -fsSL https://claude.ai/install.sh | bash \ - || die "Claude Code install failed. See https://claude.com/product/claude-code" - export PATH="$HOME/.local/bin:$PATH" - command -v claude >/dev/null 2>&1 \ - || die "Claude Code installed but \`claude\` is not on PATH." - ok "installed" -fi +ensure_claude_compatible # ------------------------------------------------------------- 3. credential @@ -715,18 +872,29 @@ fi # ------------------------------------------------------------------- 10. mcp say "qBraid MCP" -if claude mcp get "$MCP_NAME" >/dev/null 2>&1; then +MCP_REGISTERED=0 +if ! claude_required_capabilities_present; then + warn "this Claude Code version cannot register an HTTP MCP server from the command line." + warn "Start Claude Code, run /mcp, and add $MCP_URL manually; or upgrade Claude Code." +elif claude mcp get "$MCP_NAME" >/dev/null 2>&1; then + MCP_REGISTERED=1 ok "already registered" else claude mcp add --transport http "$MCP_NAME" "$MCP_URL" --scope user >/dev/null \ || die "could not register the qBraid MCP server." + MCP_REGISTERED=1 ok "registered $MCP_URL" fi # The MCP endpoint is JWT-only (OAuth + dynamic client registration): the API # key above cannot authorize it. Do the browser sign-in now, while the user is # still here, rather than surprising them mid-session. -if [ -n "$TTY" ]; then +if [ "$MCP_REGISTERED" != 1 ]; then + warn "MCP sign-in was skipped because registration is incomplete." +elif [ "$CLAUDE_MCP_LOGIN" != 1 ]; then + warn "this Claude Code version authenticates MCP servers through its interactive menu." + warn "Start Claude Code, run /mcp, select '$MCP_NAME', and choose Authenticate." +elif [ -n "$TTY" ]; then if confirm "Sign in to the qBraid MCP now? (opens a browser)" y; then # `claude mcp login` needs a real terminal to take the redirect URL. Under # `curl … | bash` this script's stdin IS the pipe, so it must be handed the diff --git a/qbraid-code b/qbraid-code index 73b0268..0bdf192 100755 --- a/qbraid-code +++ b/qbraid-code @@ -25,6 +25,8 @@ MODEL="${QBRAID_CODE_MODEL:-}" PROXY_PORT="${QBRAID_CODE_PROXY_PORT:-8320}" PROXY_BIN="${QBRAID_CODE_PROXY_BIN:-}" PROXY_URL="http://127.0.0.1:$PROXY_PORT" +CLAUDE_MIN_VERSION="2.1.186" +CLAUDE_TESTED_MAX="2.1.238" # ---------------------------------------------------------------- GPT proxy # The gateway serves GPT models only on its OpenAI-compat surface, which @@ -95,6 +97,51 @@ requested_model() { printf '%s' "$MODEL" } +parse_claude_version() { # parse_claude_version + ( + set +o pipefail + printf '%s' "$1" | grep -o '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' | head -1 + ) || true +} + +compare_versions() { # compare_versions -> -1, 0, or 1 + awk -v left="$1" -v right="$2" 'BEGIN { + split(left, l, "."); split(right, r, ".") + for (i = 1; i <= 3; i++) { + if ((l[i] + 0) < (r[i] + 0)) { print -1; exit } + if ((l[i] + 0) > (r[i] + 0)) { print 1; exit } + } + print 0 + }' +} + +claude_version_status() { # claude_version_status + local version="$1" compared + [ -n "$version" ] || { printf 'unknown'; return; } + compared=$(compare_versions "$version" "$CLAUDE_MIN_VERSION") + [ "$compared" -ge 0 ] || { printf 'below-minimum'; return; } + compared=$(compare_versions "$version" "$CLAUDE_TESTED_MAX") + [ "$compared" -le 0 ] && printf 'tested' || printf 'newer-than-tested' +} + +claude_supports_mcp_command() { # claude_supports_mcp_command + local help + help=$(claude mcp --help 2>/dev/null || true) + printf '%s\n' "$help" | grep -Eq "^[[:space:]]+$1([[:space:]]|$)" +} + +claude_supports_mcp_http() { + local help + help=$(claude mcp add --help 2>/dev/null || true) + printf '%s\n' "$help" | grep -Eq -- '--transport.*http' +} + +claude_supports_mcp_user_scope() { + local help + help=$(claude mcp add --help 2>/dev/null || true) + printf '%s\n' "$help" | grep -Eq -- '--scope.*user' +} + missing_keys() { local missing="" [ -n "$BASE_URL" ] || missing="$missing QBRAID_CODE_BASE_URL" @@ -113,9 +160,29 @@ case "${1:-}" in fi exit 0 ;; --doctor) - command -v claude >/dev/null 2>&1 \ - && echo "claude: $(claude --version 2>/dev/null || echo present)" \ - || echo "claude: NOT INSTALLED" + CLAUDE_VERSION=""; CLAUDE_STATUS=missing + MCP_ADD=no; MCP_GET=no; MCP_LOGIN=no; MCP_HTTP=no; MCP_USER_SCOPE=no + if command -v claude >/dev/null 2>&1; then + CLAUDE_OUTPUT=$(claude --version 2>/dev/null || true) + CLAUDE_VERSION=$(parse_claude_version "$CLAUDE_OUTPUT") + CLAUDE_STATUS=$(claude_version_status "$CLAUDE_VERSION") + claude_supports_mcp_command add && MCP_ADD=yes + claude_supports_mcp_command get && MCP_GET=yes + claude_supports_mcp_command login && MCP_LOGIN=yes + claude_supports_mcp_http && MCP_HTTP=yes + claude_supports_mcp_user_scope && MCP_USER_SCOPE=yes + echo "claude: ${CLAUDE_OUTPUT:-present}" + else + echo "claude: NOT INSTALLED" + fi + case "$CLAUDE_STATUS" in + tested) echo "claude-min: PASS (requires $CLAUDE_MIN_VERSION+)"; echo "claude-tested: within tested range through $CLAUDE_TESTED_MAX" ;; + newer-than-tested) echo "claude-min: PASS (requires $CLAUDE_MIN_VERSION+)"; echo "claude-tested: NEWER than tested $CLAUDE_TESTED_MAX (not blocked)" ;; + below-minimum) echo "claude-min: FAIL (requires $CLAUDE_MIN_VERSION+, found $CLAUDE_VERSION)"; echo "claude-tested: unsupported" ;; + *) echo "claude-min: UNKNOWN (requires $CLAUDE_MIN_VERSION+)"; echo "claude-tested: unknown" ;; + esac + echo "claude-policy: ${QBRAID_CODE_CLAUDE_POLICY:-prompt}" + echo "capabilities: mcp-add=$MCP_ADD mcp-get=$MCP_GET mcp-login=$MCP_LOGIN mcp-http=$MCP_HTTP mcp-user-scope=$MCP_USER_SCOPE" MISSING=$(missing_keys) if [ -n "$MISSING" ]; then @@ -165,8 +232,14 @@ case "${1:-}" in echo "gateway: UNREACHABLE" fi - if claude mcp get qbraid >/dev/null 2>&1; then - echo "mcp: registered (run 'claude mcp login qbraid' if tools are missing)" + if [ "$MCP_GET" != yes ]; then + echo "mcp: UNAVAILABLE — upgrade Claude Code or configure it through /mcp" + elif claude mcp get qbraid >/dev/null 2>&1; then + if [ "$MCP_LOGIN" = yes ]; then + echo "mcp: registered (run 'claude mcp login qbraid' if tools are missing)" + else + echo "mcp: registered (run /mcp inside Claude Code to authenticate)" + fi else echo "mcp: NOT REGISTERED" fi diff --git a/tests/claude-compat.Tests.ps1 b/tests/claude-compat.Tests.ps1 new file mode 100644 index 0000000..b571845 --- /dev/null +++ b/tests/claude-compat.Tests.ps1 @@ -0,0 +1,158 @@ +$ErrorActionPreference = 'Stop' + +$source = Join-Path $PSScriptRoot '..\install.ps1' +$tokens = $null +$errors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile( + $source, [ref]$tokens, [ref]$errors) +if ($errors) { throw "could not parse install.ps1: $errors" } + +$needed = @( + 'ConvertFrom-ClaudeVersionString', + 'Compare-ClaudeVersion', + 'Get-ClaudeVersionStatus', + 'Get-ClaudePolicyAction', + 'Test-ClaudeMcpCommand', + 'Test-ClaudeMcpHttp', + 'Test-ClaudeMcpUserScope', + 'Update-ClaudeState', + 'Test-ClaudeRequiredCapabilities', + 'Confirm-ClaudeCompatibility' +) + +foreach ($name in $needed) { + $definition = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq $name + }, $true) | Select-Object -First 1 + if (-not $definition) { throw "could not extract $name from install.ps1" } + Invoke-Expression $definition.Extent.Text +} + +$script:Passed = 0 +$script:Failed = 0 +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -eq $Expected) { + $script:Passed++ + Write-Host " ok $Name" + } else { + $script:Failed++ + Write-Host " FAIL $Name`: got [$Actual] want [$Expected]" + } +} + +$script:ClaudeMinVersion = '2.1.186' +$script:ClaudeTestedMax = '2.1.238' + +Assert-Equal 'parse native version output' ` + (ConvertFrom-ClaudeVersionString '2.1.179 (Claude Code)') '2.1.179' +Assert-Equal 'parse version with surrounding text' ` + (ConvertFrom-ClaudeVersionString 'Claude Code version 2.1.238') '2.1.238' +Assert-Equal 'missing version parses empty' ` + (ConvertFrom-ClaudeVersionString 'present but unparseable') $null + +Assert-Equal 'older version compares below' (Compare-ClaudeVersion '2.1.185' '2.1.186') -1 +Assert-Equal 'equal version compares equal' (Compare-ClaudeVersion '2.1.186' '2.1.186') 0 +Assert-Equal 'newer patch compares above' (Compare-ClaudeVersion '2.1.238' '2.1.186') 1 +Assert-Equal 'newer minor compares above' (Compare-ClaudeVersion '2.2.0' '2.1.999') 1 + +Assert-Equal 'unknown version status' (Get-ClaudeVersionStatus $null) 'unknown' +Assert-Equal 'below minimum status' (Get-ClaudeVersionStatus '2.1.185') 'below-minimum' +Assert-Equal 'minimum is tested' (Get-ClaudeVersionStatus '2.1.186') 'tested' +Assert-Equal 'tested maximum is tested' (Get-ClaudeVersionStatus '2.1.238') 'tested' +Assert-Equal 'newer version is informational' (Get-ClaudeVersionStatus '2.1.239') 'newer-than-tested' + +Assert-Equal 'upgrade policy upgrades' (Get-ClaudePolicyAction 'upgrade' $false) 'upgrade' +Assert-Equal 'fail policy fails' (Get-ClaudePolicyAction 'fail' $true) 'fail' +Assert-Equal 'continue policy continues' (Get-ClaudePolicyAction 'continue' $false) 'continue' +Assert-Equal 'interactive prompt prompts' (Get-ClaudePolicyAction 'prompt' $true) 'prompt' +Assert-Equal 'noninteractive prompt fails' (Get-ClaudePolicyAction 'prompt' $false) 'fail' + +function global:claude { + $joined = $args -join ' ' + if ($joined -eq '--version') { + "$script:FakeClaudeVersion (Claude Code)" + } elseif ($joined -eq 'mcp --help') { + 'Commands:' + ' add [options]' + ' get ' + if ($script:FakeMcpLogin) { ' login ' } + } elseif ($joined -eq 'mcp add --help') { + ' --transport stdio, sse, or http' + if ($script:FakeUserScope) { ' --scope local, project, or user' } + } +} + +$script:FakeClaudeVersion = '2.1.179' +$script:FakeMcpLogin = $false +$script:FakeUserScope = $true +Assert-Equal 'detect mcp add' (Test-ClaudeMcpCommand 'add') $true +Assert-Equal 'detect mcp get' (Test-ClaudeMcpCommand 'get') $true +Assert-Equal 'detect missing mcp login' (Test-ClaudeMcpCommand 'login') $false +$script:FakeMcpLogin = $true +Assert-Equal 'detect available mcp login' (Test-ClaudeMcpCommand 'login') $true +Assert-Equal 'detect HTTP transport' (Test-ClaudeMcpHttp) $true +Assert-Equal 'detect user scope' (Test-ClaudeMcpUserScope) $true + +function Warn { param($Message) $script:Messages += "WARN $Message" } +function Ok { param($Message) $script:Messages += "OK $Message" } +function Die { param($Message) throw "DIE $Message" } +function Test-InteractiveConsole { return $script:Interactive } +function Confirm-Step { return $script:ConfirmResult } +function Install-ClaudeStable { $script:FakeClaudeVersion = '2.1.238' } + +$env:QBRAID_CODE_CLAUDE_POLICY = 'continue' +$script:Interactive = $false +$script:Messages = @() +Confirm-ClaudeCompatibility +Assert-Equal 'continue flow keeps old version' $script:FakeClaudeVersion '2.1.179' +Assert-Equal 'continue flow warns' ($script:Messages -join "`n" -match 'unsupported Claude Code') $true + +$env:QBRAID_CODE_CLAUDE_POLICY = 'fail' +$script:Messages = @() +$failedClosed = $false +try { Confirm-ClaudeCompatibility } catch { $failedClosed = $_.Exception.Message -match 'incompatible' } +Assert-Equal 'fail flow rejects old version' $failedClosed $true + +$env:QBRAID_CODE_CLAUDE_POLICY = 'upgrade' +$script:FakeClaudeVersion = '2.1.179' +$script:Messages = @() +Confirm-ClaudeCompatibility +Assert-Equal 'upgrade flow reaches stable version' $script:FakeClaudeVersion '2.1.238' + +$env:QBRAID_CODE_CLAUDE_POLICY = 'prompt' +$script:FakeClaudeVersion = '2.1.179' +$script:Interactive = $false +$failedClosed = $false +try { Confirm-ClaudeCompatibility } catch { $failedClosed = $_.Exception.Message -match 'incompatible' } +Assert-Equal 'noninteractive prompt fails closed' $failedClosed $true + +$script:Interactive = $true +$script:ConfirmResult = $false +$script:Messages = @() +Confirm-ClaudeCompatibility +Assert-Equal 'interactive prompt can decline upgrade' ` + ($script:Messages -join "`n" -match 'reduced compatibility') $true + +$env:QBRAID_CODE_CLAUDE_POLICY = 'upgrade' +$script:FakeClaudeVersion = '2.1.239' +$script:FakeUserScope = $false +$refusedDowngrade = $false +try { Confirm-ClaudeCompatibility } catch { $refusedDowngrade = $_.Exception.Message -match 'Refusing to downgrade' } +Assert-Equal 'upgrade never downgrades a newer CLI' $refusedDowngrade $true +Assert-Equal 'newer CLI version remains installed' $script:FakeClaudeVersion '2.1.239' + +$env:QBRAID_CODE_CLAUDE_POLICY = 'prompt' +$script:Interactive = $true +$script:Messages = @() +Confirm-ClaudeCompatibility +Assert-Equal 'prompt never downgrades a newer CLI' ` + ($script:Messages -join "`n" -match 'refusing to downgrade') $true + +Remove-Item Env:\QBRAID_CODE_CLAUDE_POLICY +Remove-Item Function:\claude + +Write-Host "`n$($script:Passed) passed, $($script:Failed) failed" +if ($script:Failed -ne 0) { exit 1 } diff --git a/tests/claude-compat.sh b/tests/claude-compat.sh new file mode 100755 index 0000000..791886d --- /dev/null +++ b/tests/claude-compat.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Exercise the installer compatibility policy through its real helper +# definitions. The fake claude executable keeps the tests deterministic and +# avoids changing whichever Claude Code version is installed on the runner. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 + +extract_fn() { # extract_fn + awk -v fn="$1" ' + $0 ~ "^"fn"\\(\\) \\{" { inside = 1 } + inside { print } + inside && /^\}$/ { exit } + ' install.sh +} + +FUNCTIONS="" +for fn in parse_claude_version compare_versions claude_version_status \ + claude_policy_action claude_supports_mcp_command \ + claude_supports_mcp_http claude_supports_mcp_user_scope; do + src=$(extract_fn "$fn") + if [ -z "$src" ]; then + printf ' FAIL could not extract %s from install.sh\n' "$fn" + exit 1 + fi + FUNCTIONS="$FUNCTIONS +$src" +done + +pass=0; fail=0 +check_eq() { # check_eq + local name="$1" want="$2" got + shift 2 + got=$(bash -c "set -euo pipefail; CLAUDE_MIN_VERSION=2.1.186; CLAUDE_TESTED_MAX=2.1.238; $FUNCTIONS; $*" 2>/dev/null) || got="" + if [ "$got" = "$want" ]; then + pass=$((pass + 1)); printf ' ok %s\n' "$name" + else + fail=$((fail + 1)); printf ' FAIL %s: got [%s] want [%s]\n' "$name" "$got" "$want" + fi +} + +check_eq "parse native version output" 2.1.179 \ + "parse_claude_version '2.1.179 (Claude Code)'" +check_eq "parse version with surrounding text" 2.1.238 \ + "parse_claude_version 'Claude Code version 2.1.238'" +check_eq "missing version parses empty" "" \ + "parse_claude_version 'present but unparseable'" + +check_eq "older version compares below" -1 "compare_versions 2.1.185 2.1.186" +check_eq "equal version compares equal" 0 "compare_versions 2.1.186 2.1.186" +check_eq "newer patch compares above" 1 "compare_versions 2.1.238 2.1.186" +check_eq "newer minor compares above" 1 "compare_versions 2.2.0 2.1.999" + +check_eq "unknown version status" unknown "claude_version_status ''" +check_eq "below minimum status" below-minimum "claude_version_status 2.1.185" +check_eq "minimum is tested" tested "claude_version_status 2.1.186" +check_eq "tested maximum is tested" tested "claude_version_status 2.1.238" +check_eq "newer version is informational" newer-than-tested "claude_version_status 2.1.239" + +check_eq "upgrade policy upgrades" upgrade "claude_policy_action upgrade no" +check_eq "fail policy fails" fail "claude_policy_action fail yes" +check_eq "continue policy continues" continue "claude_policy_action continue no" +check_eq "interactive prompt prompts" prompt "claude_policy_action prompt yes" +check_eq "noninteractive prompt fails" fail "claude_policy_action prompt no" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +cat > "$tmp/claude" <<'EOF' +#!/usr/bin/env bash +case "${1:-} ${2:-}" in + "mcp --help") + printf '%s\n' 'Commands:' ' add [options]' ' get ' + [ "${FAKE_MCP_LOGIN:-no}" = yes ] && printf '%s\n' ' login ' + ;; + "mcp add") + [ "${3:-}" = --help ] && printf '%s\n' \ + ' --transport stdio, sse, or http' \ + ' --scope local, project, or user' + ;; +esac +EOF +chmod +x "$tmp/claude" + +check_cap() { # check_cap + local name="$1" login="$2" want="$3" expr="$4" got + got=$(PATH="$tmp:$PATH" FAKE_MCP_LOGIN="$login" bash -c \ + "set -euo pipefail; $FUNCTIONS; if $expr; then printf yes; else printf no; fi") + if [ "$got" = "$want" ]; then + pass=$((pass + 1)); printf ' ok %s\n' "$name" + else + fail=$((fail + 1)); printf ' FAIL %s: got [%s] want [%s]\n' "$name" "$got" "$want" + fi +} + +check_cap "detect mcp add" no yes "claude_supports_mcp_command add" +check_cap "detect mcp get" no yes "claude_supports_mcp_command get" +check_cap "detect missing mcp login" no no "claude_supports_mcp_command login" +check_cap "detect available mcp login" yes yes "claude_supports_mcp_command login" +check_cap "detect HTTP transport" no yes "claude_supports_mcp_http" +check_cap "detect user scope" no yes "claude_supports_mcp_user_scope" + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] diff --git a/tests/claude-installed-compat.sh b/tests/claude-installed-compat.sh new file mode 100755 index 0000000..837d119 --- /dev/null +++ b/tests/claude-installed-compat.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Smoke-check a real Claude Code release installed by the CI version matrix. +set -euo pipefail + +version=$(claude --version) +printf 'claude: %s\n' "$version" + +help=$(claude mcp --help) +for command in add get login; do + printf '%s\n' "$help" | grep -Eq "^[[:space:]]+$command([[:space:]]|$)" || { + printf 'missing required mcp command: %s\n' "$command" >&2 + exit 1 + } +done + +add_help=$(claude mcp add --help) +printf '%s\n' "$add_help" | grep -Eq -- '--transport.*http' || { + printf 'mcp add does not advertise HTTP transport\n' >&2 + exit 1 +} +printf '%s\n' "$add_help" | grep -Eq -- '--scope.*user' || { + printf 'mcp add does not advertise user scope\n' >&2 + exit 1 +} + +printf 'Claude Code MCP compatibility checks passed\n' diff --git a/tests/claude-policy-flow.sh b/tests/claude-policy-flow.sh new file mode 100755 index 0000000..5b05d36 --- /dev/null +++ b/tests/claude-policy-flow.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Drive the real top-level compatibility decision against a fake Claude binary. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 + +extract_fn() { # extract_fn + awk -v fn="$1" ' + $0 ~ "^"fn"\\(\\) \\{" { inside = 1 } + inside { print } + inside && /^\}$/ { exit } + ' install.sh +} + +FUNCTIONS="" +for fn in parse_claude_version compare_versions claude_version_status \ + claude_policy_action claude_supports_mcp_command \ + claude_supports_mcp_http claude_supports_mcp_user_scope \ + refresh_claude_state claude_required_capabilities_present \ + ensure_claude_compatible; do + src=$(extract_fn "$fn") + [ -n "$src" ] || { printf ' FAIL could not extract %s\n' "$fn"; exit 1; } + FUNCTIONS="$FUNCTIONS +$src" +done + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +state="$tmp/version" +printf '2.1.179' > "$state" +cat > "$tmp/claude" <<'EOF' +#!/usr/bin/env bash +version=$(cat "$FAKE_CLAUDE_STATE") +case "${1:-} ${2:-}" in + "--version ") printf '%s (Claude Code)\n' "$version" ;; + "mcp --help") + printf '%s\n' 'Commands:' ' add [options]' ' get ' + [ "$version" != 2.1.179 ] && printf '%s\n' ' login ' + ;; + "mcp add") + if [ "${3:-}" = --help ]; then + printf '%s\n' ' --transport stdio, sse, or http' + [ "${FAKE_USER_SCOPE:-yes}" = yes ] \ + && printf '%s\n' ' --scope local, project, or user' + fi + ;; +esac +EOF +chmod +x "$tmp/claude" + +HARNESS=" +CLAUDE_MIN_VERSION=2.1.186 +CLAUDE_TESTED_MAX=2.1.238 +warn() { printf 'WARN %s\\n' \"\$*\"; } +ok() { printf 'OK %s\\n' \"\$*\"; } +die() { printf 'DIE %s\\n' \"\$*\"; exit 1; } +confirm() { [ \"\${CONFIRM_RESULT:-no}\" = yes ]; } +install_claude_stable() { printf '2.1.238' > \"\$FAKE_CLAUDE_STATE\"; } +$FUNCTIONS +ensure_claude_compatible +" + +pass=0; fail=0 +run_case() { # run_case [scope] [version] + local name="$1" policy="$2" tty="$3" want_rc="$4" want_text="$5" + local user_scope="${6:-yes}" initial_version="${7:-2.1.179}" output rc=0 + printf '%s' "$initial_version" > "$state" + if [ "$tty" = yes ]; then tty_value=/dev/tty; else tty_value=""; fi + output=$(PATH="$tmp:$PATH" FAKE_CLAUDE_STATE="$state" FAKE_USER_SCOPE="$user_scope" \ + QBRAID_CODE_CLAUDE_POLICY="$policy" TTY="$tty_value" \ + bash -c "set -euo pipefail; $HARNESS" 2>&1) || rc=$? + if [ "$rc" = "$want_rc" ] && printf '%s\n' "$output" | grep -Fq "$want_text"; then + pass=$((pass + 1)); printf ' ok %s\n' "$name" + else + fail=$((fail + 1)); printf ' FAIL %s: rc=%s output=[%s]\n' "$name" "$rc" "$output" + fi +} + +run_case "continue keeps old version" continue no 0 \ + "continuing with an unsupported Claude Code" +run_case "fail rejects old version" fail no 1 \ + "the installed Claude Code is incompatible" +run_case "upgrade reaches stable version" upgrade no 0 "OK Claude Code 2.1.238" +run_case "noninteractive prompt fails closed" prompt no 1 \ + "the installed Claude Code is incompatible" +run_case "continue degrades missing capability" continue no 0 \ + "required HTTP MCP commands are unavailable" no +run_case "upgrade never downgrades a newer CLI" upgrade no 1 \ + "Refusing to downgrade it" no 2.1.239 +run_case "prompt never downgrades a newer CLI" prompt yes 0 \ + "refusing to downgrade it and continuing" no 2.1.239 + +printf '2.1.179' > "$state" +rc=0 +prompt_output=$(PATH="$tmp:$PATH" FAKE_CLAUDE_STATE="$state" \ + QBRAID_CODE_CLAUDE_POLICY=prompt TTY=/dev/tty CONFIRM_RESULT=no \ + bash -c "set -euo pipefail; $HARNESS" 2>&1) || rc=$? +if [ "$rc" = 0 ] && printf '%s\n' "$prompt_output" | grep -Fq \ + "continuing with reduced compatibility at your request"; then + pass=$((pass + 1)); printf ' ok interactive prompt can decline upgrade\n' +else + fail=$((fail + 1)); printf ' FAIL interactive prompt decline: rc=%s output=[%s]\n' "$rc" "$prompt_output" +fi + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] diff --git a/tests/doctor.sh b/tests/doctor.sh new file mode 100755 index 0000000..12c7f83 --- /dev/null +++ b/tests/doctor.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# The doctor must explain unsupported versions and use the interactive MCP +# fallback without contacting qBraid or relying on the host's Claude install. +set -euo pipefail +cd "$(dirname "$0")/.." || exit 1 + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/home/.qbraid-code" "$tmp/bin" +cat > "$tmp/home/.qbraid-code/env" <<'EOF' +QBRAID_CODE_BASE_URL= +QBRAID_CODE_API_BASE= +QBRAID_CODE_TOKEN= +QBRAID_CODE_MODEL=claude-sonnet-4-6 +QBRAID_CODE_PROXY_BIN= +EOF + +cat > "$tmp/bin/claude" <<'EOF' +#!/usr/bin/env bash +case "${1:-} ${2:-}" in + "--version ") printf '%s\n' "${FAKE_CLAUDE_VERSION:-2.1.179} (Claude Code)" ;; + "mcp --help") + printf '%s\n' 'Commands:' ' add [options]' ' get ' + [ "${FAKE_MCP_LOGIN:-no}" = yes ] && printf '%s\n' ' login ' + ;; + "mcp add") + [ "${3:-}" = --help ] && printf '%s\n' \ + ' --transport stdio, sse, or http' \ + ' --scope local, project, or user' + ;; + "mcp get") [ "${3:-}" = qbraid ] ;; +esac +EOF +chmod +x "$tmp/bin/claude" + +pass=0; fail=0 +check_contains() { # check_contains + local name="$1" output="$2" text="$3" + if printf '%s\n' "$output" | grep -Fq "$text"; then + pass=$((pass + 1)); printf ' ok %s\n' "$name" + else + fail=$((fail + 1)); printf ' FAIL %s: missing [%s]\n' "$name" "$text" + fi +} + +old=$(HOME="$tmp/home" PATH="$tmp/bin:$PATH" ./qbraid-code --doctor) +check_contains "doctor rejects old version" "$old" \ + "claude-min: FAIL (requires 2.1.186+, found 2.1.179)" +check_contains "doctor reports missing login capability" "$old" "mcp-login=no" +check_contains "doctor gives interactive MCP fallback" "$old" \ + "registered (run /mcp inside Claude Code to authenticate)" + +new=$(HOME="$tmp/home" PATH="$tmp/bin:$PATH" \ + FAKE_CLAUDE_VERSION=2.1.239 FAKE_MCP_LOGIN=yes ./qbraid-code --doctor) +check_contains "doctor permits newer version" "$new" \ + "claude-min: PASS (requires 2.1.186+)" +check_contains "doctor labels newer version without blocking" "$new" \ + "claude-tested: NEWER than tested 2.1.238 (not blocked)" +check_contains "doctor reports login capability" "$new" "mcp-login=yes" + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] From d5400b501922f0cc15a4544764d5609ede8dc823 Mon Sep 17 00:00:00 2001 From: Kenny-Heitritter Date: Fri, 21 Aug 2026 10:09:10 -0500 Subject: [PATCH 2/4] Harden compatibility fallbacks --- README.md | 13 ++++++----- doctor.ps1 | 6 +++++- install.ps1 | 24 ++++++++++++++------- install.sh | 37 ++++++++++++++++++-------------- qbraid-code | 6 +++++- tests/claude-compat.Tests.ps1 | 15 +++++++++++++ tests/claude-installed-compat.sh | 8 ++++++- tests/claude-policy-flow.sh | 4 ++++ tests/doctor.sh | 7 +++++- 9 files changed, 87 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 20e1351..05d3477 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,21 @@ Re-running it is safe. ### Claude Code compatibility qbraid-code currently supports Claude Code **2.1.186 or newer** and is tested -through **2.1.238**. A newer version is never blocked or downgraded; the -installer only notes that it is newer than the latest version covered by CI. +through **2.1.238**. A newer version is never automatically downgraded. It is +allowed by default, but `fail` and `upgrade` stop rather than replacing it if +required capabilities have disappeared; `prompt` and `continue` keep it and +skip unavailable features. If an existing Claude Code is too old or is missing the HTTP MCP commands that qbraid-code needs, the installer offers to move it to Anthropic's `stable` -channel. It always asks before changing an existing installation. Set -`QBRAID_CODE_CLAUDE_POLICY` to choose the behavior explicitly: +channel. The default `prompt` policy asks before changing an existing +installation; `upgrade` explicitly opts into doing that without a prompt. Set +`QBRAID_CODE_CLAUDE_POLICY` to choose the behavior: | Value | Behavior | |---|---| | `prompt` | Ask before installing or upgrading (default with a terminal) | -| `upgrade` | Install or upgrade to the stable channel without prompting | +| `upgrade` | Install or upgrade to stable without prompting, but never downgrade a newer or unknown version | | `fail` | Stop rather than changing Claude Code (default without a terminal) | | `continue` | Keep an unsupported version and skip or degrade unavailable features | diff --git a/doctor.ps1 b/doctor.ps1 index 856db13..b39ad8c 100644 --- a/doctor.ps1 +++ b/doctor.ps1 @@ -129,7 +129,11 @@ if (-not $claudePresent -or -not $mcpGet) { Write-Host 'mcp: registered (run /mcp inside Claude Code to authenticate)' } } else { - Write-Host 'mcp: NOT REGISTERED' + if ($mcpLogin) { + Write-Host 'mcp: NOT REGISTERED - re-run the installer' + } else { + Write-Host 'mcp: NOT REGISTERED - configure and authenticate through /mcp' + } } } diff --git a/install.ps1 b/install.ps1 index 00deab7..0b3a279 100644 --- a/install.ps1 +++ b/install.ps1 @@ -208,6 +208,13 @@ function Confirm-ClaudeCompatibility { } else { Die "Claude Code $($script:ClaudeVersion) is newer than tested and lacks required capabilities. Refusing to downgrade it; set QBRAID_CODE_CLAUDE_POLICY=continue to skip unavailable features." } + } elseif ($script:ClaudeVersionStatus -eq 'unknown') { + if ($action -in @('continue', 'prompt')) { + Warn "Claude Code's version is unknown; refusing to replace it with stable because that could downgrade it, and continuing with reduced compatibility." + $action = 'handled' + } else { + Die "Claude Code's version is unknown. Refusing to replace it with stable because that could downgrade it; update Claude Code manually or set QBRAID_CODE_CLAUDE_POLICY=continue." + } } if ($action -eq 'upgrade') { Warn "the installed Claude Code is incompatible: $issue" @@ -647,20 +654,21 @@ if ($Global) { Say 'qBraid MCP' $mcpRegistered = $false -if (-not (Test-ClaudeRequiredCapabilities)) { - Warn 'this Claude Code version cannot register an HTTP MCP server from the command line.' - Warn "Start Claude Code, run /mcp, and add $McpUrl manually; or upgrade Claude Code." -} else { +if ($script:ClaudeMcpGet) { claude mcp get $McpName *> $null + if ($LASTEXITCODE -eq 0) { + $mcpRegistered = $true + Ok 'already registered' + } } -if ((Test-ClaudeRequiredCapabilities) -and $LASTEXITCODE -eq 0) { - $mcpRegistered = $true - Ok 'already registered' -} elseif (Test-ClaudeRequiredCapabilities) { +if (-not $mcpRegistered -and (Test-ClaudeRequiredCapabilities)) { claude mcp add --transport http $McpName $McpUrl --scope user *> $null if ($LASTEXITCODE -ne 0) { Die 'could not register the qBraid MCP server.' } $mcpRegistered = $true Ok "registered $McpUrl" +} elseif (-not $mcpRegistered) { + Warn 'this Claude Code version cannot register an HTTP MCP server from the command line.' + Warn "Start Claude Code, run /mcp, and add $McpUrl manually; or upgrade Claude Code." } # The MCP endpoint is JWT-only (OAuth + dynamic client registration): the API diff --git a/install.sh b/install.sh index fbbd867..0119cdd 100755 --- a/install.sh +++ b/install.sh @@ -243,17 +243,22 @@ ensure_claude_compatible() { if [ -n "$issue" ]; then action=$(claude_policy_action "$policy" "$interactive") - if [ "$CLAUDE_VERSION_STATUS" = newer-than-tested ]; then - case "$action" in - continue|prompt) - warn "Claude Code $CLAUDE_VERSION is newer than tested and lacks required capabilities; refusing to downgrade it and continuing with reduced compatibility." - ;; - *) - die "Claude Code $CLAUDE_VERSION is newer than tested and lacks required capabilities. Refusing to downgrade it; set QBRAID_CODE_CLAUDE_POLICY=continue to skip unavailable features." - ;; - esac - action=handled - fi + case "$CLAUDE_VERSION_STATUS" in + newer-than-tested) + case "$action" in + continue|prompt) warn "Claude Code $CLAUDE_VERSION is newer than tested and lacks required capabilities; refusing to downgrade it and continuing with reduced compatibility." ;; + *) die "Claude Code $CLAUDE_VERSION is newer than tested and lacks required capabilities. Refusing to downgrade it; set QBRAID_CODE_CLAUDE_POLICY=continue to skip unavailable features." ;; + esac + action=handled + ;; + unknown) + case "$action" in + continue|prompt) warn "Claude Code's version is unknown; refusing to replace it with stable because that could downgrade it, and continuing with reduced compatibility." ;; + *) die "Claude Code's version is unknown. Refusing to replace it with stable because that could downgrade it; update Claude Code manually or set QBRAID_CODE_CLAUDE_POLICY=continue." ;; + esac + action=handled + ;; + esac case "$action" in upgrade) warn "the installed Claude Code is incompatible: $issue" @@ -873,17 +878,17 @@ fi say "qBraid MCP" MCP_REGISTERED=0 -if ! claude_required_capabilities_present; then - warn "this Claude Code version cannot register an HTTP MCP server from the command line." - warn "Start Claude Code, run /mcp, and add $MCP_URL manually; or upgrade Claude Code." -elif claude mcp get "$MCP_NAME" >/dev/null 2>&1; then +if [ "$CLAUDE_MCP_GET" = 1 ] && claude mcp get "$MCP_NAME" >/dev/null 2>&1; then MCP_REGISTERED=1 ok "already registered" -else +elif claude_required_capabilities_present; then claude mcp add --transport http "$MCP_NAME" "$MCP_URL" --scope user >/dev/null \ || die "could not register the qBraid MCP server." MCP_REGISTERED=1 ok "registered $MCP_URL" +else + warn "this Claude Code version cannot register an HTTP MCP server from the command line." + warn "Start Claude Code, run /mcp, and add $MCP_URL manually; or upgrade Claude Code." fi # The MCP endpoint is JWT-only (OAuth + dynamic client registration): the API diff --git a/qbraid-code b/qbraid-code index 0bdf192..0e257c2 100755 --- a/qbraid-code +++ b/qbraid-code @@ -241,7 +241,11 @@ case "${1:-}" in echo "mcp: registered (run /mcp inside Claude Code to authenticate)" fi else - echo "mcp: NOT REGISTERED" + if [ "$MCP_LOGIN" = yes ]; then + echo "mcp: NOT REGISTERED — re-run the installer" + else + echo "mcp: NOT REGISTERED — configure and authenticate through /mcp" + fi fi echo "model: ${MODEL:-not set}" diff --git a/tests/claude-compat.Tests.ps1 b/tests/claude-compat.Tests.ps1 index b571845..4d78021 100644 --- a/tests/claude-compat.Tests.ps1 +++ b/tests/claude-compat.Tests.ps1 @@ -151,6 +151,21 @@ Confirm-ClaudeCompatibility Assert-Equal 'prompt never downgrades a newer CLI' ` ($script:Messages -join "`n" -match 'refusing to downgrade') $true +$env:QBRAID_CODE_CLAUDE_POLICY = 'upgrade' +$script:FakeClaudeVersion = 'mystery-version' +$script:FakeUserScope = $true +$unknownRefused = $false +try { Confirm-ClaudeCompatibility } catch { $unknownRefused = $_.Exception.Message -match 'could downgrade' } +Assert-Equal 'upgrade never replaces an unknown version' $unknownRefused $true +Assert-Equal 'unknown version remains installed' $script:FakeClaudeVersion 'mystery-version' + +$env:QBRAID_CODE_CLAUDE_POLICY = 'prompt' +$script:Interactive = $true +$script:Messages = @() +Confirm-ClaudeCompatibility +Assert-Equal 'prompt never replaces an unknown version' ` + ($script:Messages -join "`n" -match 'could downgrade') $true + Remove-Item Env:\QBRAID_CODE_CLAUDE_POLICY Remove-Item Function:\claude diff --git a/tests/claude-installed-compat.sh b/tests/claude-installed-compat.sh index 837d119..b49942d 100755 --- a/tests/claude-installed-compat.sh +++ b/tests/claude-installed-compat.sh @@ -6,13 +6,19 @@ version=$(claude --version) printf 'claude: %s\n' "$version" help=$(claude mcp --help) -for command in add get login; do +for command in add get; do printf '%s\n' "$help" | grep -Eq "^[[:space:]]+$command([[:space:]]|$)" || { printf 'missing required mcp command: %s\n' "$command" >&2 exit 1 } done +if printf '%s\n' "$help" | grep -Eq '^[[:space:]]+login([[:space:]]|$)'; then + printf 'mcp login: available\n' +else + printf 'mcp login: unavailable; interactive /mcp fallback required\n' +fi + add_help=$(claude mcp add --help) printf '%s\n' "$add_help" | grep -Eq -- '--transport.*http' || { printf 'mcp add does not advertise HTTP transport\n' >&2 diff --git a/tests/claude-policy-flow.sh b/tests/claude-policy-flow.sh index 5b05d36..9de486b 100755 --- a/tests/claude-policy-flow.sh +++ b/tests/claude-policy-flow.sh @@ -88,6 +88,10 @@ run_case "upgrade never downgrades a newer CLI" upgrade no 1 \ "Refusing to downgrade it" no 2.1.239 run_case "prompt never downgrades a newer CLI" prompt yes 0 \ "refusing to downgrade it and continuing" no 2.1.239 +run_case "upgrade never replaces an unknown version" upgrade no 1 \ + "could downgrade it" yes mystery-version +run_case "prompt never replaces an unknown version" prompt yes 0 \ + "could downgrade it" yes mystery-version printf '2.1.179' > "$state" rc=0 diff --git a/tests/doctor.sh b/tests/doctor.sh index 12c7f83..4ed1a1f 100755 --- a/tests/doctor.sh +++ b/tests/doctor.sh @@ -28,7 +28,7 @@ case "${1:-} ${2:-}" in ' --transport stdio, sse, or http' \ ' --scope local, project, or user' ;; - "mcp get") [ "${3:-}" = qbraid ] ;; + "mcp get") [ "${3:-}" = qbraid ] && [ "${FAKE_MCP_REGISTERED:-yes}" = yes ] ;; esac EOF chmod +x "$tmp/bin/claude" @@ -50,6 +50,11 @@ check_contains "doctor reports missing login capability" "$old" "mcp-login=no" check_contains "doctor gives interactive MCP fallback" "$old" \ "registered (run /mcp inside Claude Code to authenticate)" +unregistered=$(HOME="$tmp/home" PATH="$tmp/bin:$PATH" \ + FAKE_MCP_REGISTERED=no ./qbraid-code --doctor) +check_contains "doctor gives fallback when MCP is unregistered" "$unregistered" \ + "NOT REGISTERED — configure and authenticate through /mcp" + new=$(HOME="$tmp/home" PATH="$tmp/bin:$PATH" \ FAKE_CLAUDE_VERSION=2.1.239 FAKE_MCP_LOGIN=yes ./qbraid-code --doctor) check_contains "doctor permits newer version" "$new" \ From 1ddae413d414dee17ec1cad46f8054a0c3951b29 Mon Sep 17 00:00:00 2001 From: BeLazy167 Date: Fri, 21 Aug 2026 10:52:46 -0500 Subject: [PATCH 3/4] test: scope real Claude checks to release jobs --- tests/claude-installed-compat.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/claude-installed-compat.sh b/tests/claude-installed-compat.sh index 32b15f0..e2fa5aa 100755 --- a/tests/claude-installed-compat.sh +++ b/tests/claude-installed-compat.sh @@ -2,6 +2,12 @@ # Smoke-check a real Claude Code release installed by the CI version matrix. set -euo pipefail +if ! command -v claude >/dev/null 2>&1; then + printf 'Claude Code is not installed; real-release compatibility check skipped +' + exit 0 +fi + version=$(claude --version) printf 'claude: %s\n' "$version" From 916d8466c60af3ac457fe2ae7932fa340c42e680 Mon Sep 17 00:00:00 2001 From: BeLazy167 Date: Fri, 21 Aug 2026 10:54:31 -0500 Subject: [PATCH 4/4] test: capture Windows doctor output --- tests/windows-profiles.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/windows-profiles.ps1 b/tests/windows-profiles.ps1 index 446997e..42103b3 100644 --- a/tests/windows-profiles.ps1 +++ b/tests/windows-profiles.ps1 @@ -120,7 +120,7 @@ echo args=%* return [pscustomobject]@{} } $env:QBRAID_CODE_PROFILE_HOME = $beta - $doctor = (& (Join-Path $root 'doctor.ps1')) -join "`n" + $doctor = (& (Join-Path $root 'doctor.ps1') 6>&1) -join "`n" if ($doctor -notmatch 'claude:\s+2\.1\.238' -or $doctor -notmatch 'model:\s+claude-haiku-4-5' -or $doctor -notmatch 'proxy:\s+installed \(starts once per launch\)') {