Every gateway provider is now an entry in providers.json - endpoint and auth template, how its model list is read, how it is probed before a switch, what setup asks, which doctor checks apply, and its title, colour and logo - installed next to the presets and read by all three consumers: the bash CLI (through cm-json.py), the Windows script, and the bar widget (through health.json). anthropic stays built in; it is the native login, not a gateway. Behaviour that differs in kind stays in code, chosen by name from the entry: catalogue parsers (openrouter, lmstudio, ollama, openai, static), probe rules (always, lenient, local), and named doctor checks. A provider that reuses them is an entry and a default preset, with no code. The widget draws providers from health.json, so a new one needs no QML change and no shell restart. The existing three are unchanged in behaviour: their blank presets come out byte-identical from the file, and setup, doctor, models and the picker run the same checks through the generic paths. Ollama: local server on :11434, placeholder token, one model for every tier, models from /api/tags. doctor reads the context each loaded model actually runs with (/api/ps) and its maximum (/api/show), because Ollama defaults to 4096 tokens unless OLLAMA_CONTEXT_LENGTH is set and silently truncates past it. A bare model name matches its :latest tag. Custom: any Anthropic-compatible endpoint. Ships with no address and is refused until it has one; key optional; models from /v1/models when the endpoint has a list, and a lenient probe so a proxy without one is not blocked. Also: preflight (and Set-ClaudeMode on Windows) refuses a preset with no server address; the server form, setup and set-auth use each provider's own default URL, key name and placeholder token instead of LM Studio's; the Windows build gains the no-models and no-address guards it never had. Tested on Linux against fake Ollama/Custom servers, and on Windows 5.1 in a USERPROFILE sandbox on winbox. 1.12.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
6.3 KiB
PowerShell
130 lines
6.3 KiB
PowerShell
<#
|
|
Build the distributable claude-mode package.
|
|
|
|
Produces, under dist/<version>/ :
|
|
claude-mode-<version>.zip the payload (script, key helper, presets, installer)
|
|
manifest.json version + package name + SHA-256
|
|
|
|
The Arkylx Index serves that directory, plus dist/bootstrap.ps1 as
|
|
/tools/claude-mode/install.ps1. See ARKYLX_INDEX_INTEGRATION.md.
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param([string] $OutRoot)
|
|
|
|
Set-StrictMode -Version 1.0
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
if (-not $OutRoot) { $OutRoot = Join-Path $root 'dist' }
|
|
|
|
$version = (Get-Content (Join-Path $root 'VERSION') -Raw).Trim()
|
|
if (-not $version) { throw 'VERSION file is empty' }
|
|
|
|
Write-Host "building claude-mode $version" -ForegroundColor Cyan
|
|
|
|
$stage = Join-Path ([System.IO.Path]::GetTempPath()) ("cm-stage-" + [Guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
New-Item -ItemType Directory -Path (Join-Path $stage 'bin') -Force | Out-Null
|
|
New-Item -ItemType Directory -Path (Join-Path $stage 'presets') -Force | Out-Null
|
|
|
|
# Everything the installer needs, and nothing else - no README, no docs, no
|
|
# .git. The payload is what lands on a user's machine.
|
|
Copy-Item (Join-Path $root 'claude-mode.ps1') $stage -Force
|
|
Copy-Item (Join-Path $root 'install.ps1') $stage -Force
|
|
Copy-Item (Join-Path $root 'profile-snippet.ps1') $stage -Force
|
|
Copy-Item (Join-Path $root 'VERSION') $stage -Force
|
|
Copy-Item (Join-Path $root 'providers.json') $stage -Force
|
|
Get-ChildItem (Join-Path $root 'bin') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $stage 'bin') -Force }
|
|
Get-ChildItem (Join-Path $root 'presets') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $stage 'presets') -Force }
|
|
|
|
# Refuse to ship a package whose main script does not parse - a broken payload
|
|
# would be installed by every user before anyone noticed.
|
|
$errs = $null
|
|
[void][System.Management.Automation.Language.Parser]::ParseFile((Join-Path $stage 'claude-mode.ps1'), [ref]$null, [ref]$errs)
|
|
if ($errs.Count -gt 0) {
|
|
$errs | ForEach-Object { Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)" -ForegroundColor Red }
|
|
throw 'claude-mode.ps1 does not parse - refusing to package'
|
|
}
|
|
foreach ($p in (Get-ChildItem (Join-Path $stage 'presets') -File)) {
|
|
try { [void](Get-Content $p.FullName -Raw | ConvertFrom-Json) }
|
|
catch { throw "preset $($p.Name) is not valid JSON - refusing to package" }
|
|
}
|
|
Write-Host ' ok payload validated' -ForegroundColor Green
|
|
|
|
$outDir = Join-Path $OutRoot $version
|
|
if (-not (Test-Path -LiteralPath $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
|
|
|
|
$zipName = "claude-mode-$version.zip"
|
|
$zipPath = Join-Path $outDir $zipName
|
|
if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
|
|
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zipPath -Force
|
|
|
|
$sha = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLower()
|
|
|
|
$manifest = [ordered]@{
|
|
tool = 'claude-mode'
|
|
version = $version
|
|
package = $zipName
|
|
sha256 = $sha
|
|
size = (Get-Item $zipPath).Length
|
|
requires = [ordered]@{ powershell = '5.1'; os = 'windows' }
|
|
entry = 'install.ps1'
|
|
}
|
|
$manifest | ConvertTo-Json -Depth 5 |
|
|
Set-Content -LiteralPath (Join-Path $outDir 'manifest.json') -Encoding UTF8
|
|
|
|
# --- POSIX payload (Linux + macOS) -----------------------------------------
|
|
# Shipped as a tar.gz because zip does not preserve the executable bit, and the
|
|
# installer must be runnable straight out of the archive.
|
|
$posixStage = Join-Path ([System.IO.Path]::GetTempPath()) ("cm-posix-" + [Guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
New-Item -ItemType Directory -Path (Join-Path $posixStage 'linux') -Force | Out-Null
|
|
New-Item -ItemType Directory -Path (Join-Path $posixStage 'presets') -Force | Out-Null
|
|
Get-ChildItem (Join-Path $root 'linux') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $posixStage 'linux') -Force }
|
|
Get-ChildItem (Join-Path $root 'presets') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $posixStage 'presets') -Force }
|
|
Copy-Item (Join-Path $root 'VERSION') $posixStage -Force
|
|
Copy-Item (Join-Path $root 'providers.json') $posixStage -Force
|
|
|
|
# Shell scripts authored on Windows carry CRLF, which makes the kernel reject
|
|
# the "#!/usr/bin/env bash" line. Normalise before packaging.
|
|
Get-ChildItem (Join-Path $posixStage 'linux') -File | ForEach-Object {
|
|
$text = [System.IO.File]::ReadAllText($_.FullName) -replace "`r`n", "`n"
|
|
[System.IO.File]::WriteAllText($_.FullName, $text, (New-Object System.Text.UTF8Encoding($false)))
|
|
}
|
|
|
|
$tarName = "claude-mode-$version-posix.tar.gz"
|
|
$tarPath = Join-Path $outDir $tarName
|
|
if (Test-Path -LiteralPath $tarPath) { Remove-Item -LiteralPath $tarPath -Force }
|
|
& tar.exe -czf $tarPath -C $posixStage 'linux' 'presets' 'VERSION' 'providers.json'
|
|
if ($LASTEXITCODE -ne 0) { throw 'tar failed - cannot build the POSIX package' }
|
|
|
|
$shaPosix = (Get-FileHash -LiteralPath $tarPath -Algorithm SHA256).Hash.ToLower()
|
|
[ordered]@{
|
|
tool = 'claude-mode'
|
|
version = $version
|
|
package = $tarName
|
|
sha256 = $shaPosix
|
|
size = (Get-Item $tarPath).Length
|
|
requires = [ordered]@{ bash = '4.0'; python3 = '3.6'; os = 'linux,darwin' }
|
|
entry = 'linux/install.sh'
|
|
} | ConvertTo-Json -Depth 5 |
|
|
Set-Content -LiteralPath (Join-Path $outDir 'manifest.posix.json') -Encoding UTF8
|
|
|
|
Remove-Item -LiteralPath $posixStage -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Host " ok $tarPath" -ForegroundColor Green
|
|
Write-Host " ok sha256 $shaPosix" -ForegroundColor Green
|
|
|
|
# "latest" is a copy rather than a symlink so a plain static file server can
|
|
# serve it with no extra configuration.
|
|
$latest = Join-Path $OutRoot 'latest'
|
|
if (-not (Test-Path -LiteralPath $latest)) { New-Item -ItemType Directory -Path $latest -Force | Out-Null }
|
|
Copy-Item $zipPath (Join-Path $latest $zipName) -Force
|
|
Copy-Item (Join-Path $outDir 'manifest.json') (Join-Path $latest 'manifest.json') -Force
|
|
Copy-Item $tarPath (Join-Path $latest $tarName) -Force
|
|
Copy-Item (Join-Path $outDir 'manifest.posix.json') (Join-Path $latest 'manifest.posix.json') -Force
|
|
|
|
Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
Write-Host " ok $zipPath" -ForegroundColor Green
|
|
Write-Host " ok sha256 $sha" -ForegroundColor Green
|
|
Write-Host " ok also copied to dist/latest/" -ForegroundColor Green
|