claude-mode.ps1 was 2,407 lines. It is now 153: the help block, parameters, paths, the managed-key list, a loader, and the dispatch. The rest moved, verbatim, into eleven files under lib/ - providers, output, files, core, vault, switch, guards, health, catalogue, commands, menu - dot-sourced into the script's scope in their original order, with the same check as the bash split that every original line landed in exactly one file. Inside a module $PSScriptRoot is lib\, so the one path beside the main script (providers.json) now goes through $script:Here. install.ps1 ships lib\, clearing old modules first. The Windows suite parses every module and install.ps1 (36 checks, all green on Windows PowerShell 5.1); install.ps1 is parsed but never run, since it edits the real profile and User PATH. linux/bootstrap.sh and scripts/build-package.ps1 existed only to build and serve packages for the Arkylx Index. Both installers fetch the repository's own archive, so a push to master is the release; the two scripts, the dist/ ignore and their mentions in the docs are gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
6.5 KiB
PowerShell
149 lines
6.5 KiB
PowerShell
# lib/health.ps1 - health.json, and persistent environment variables that would override a switch.
|
|
#
|
|
# Part of claude-mode.ps1, which dot-sources it into its own script scope after
|
|
# the settings at its top. Not meant to run on its own. ASCII only: Windows
|
|
# PowerShell 5.1 reads a .ps1 without a BOM as ANSI. $PSScriptRoot here would be
|
|
# lib\, so paths beside the main script go through $script:Here.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stray environment variable detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# health.json - the machine-readable state a fleet reader (or anything else)
|
|
# reads. Written on every switch and on every `doctor`.
|
|
#
|
|
# Contract, deliberately narrow:
|
|
# * NO key material, ever. `keysConfigured` is names only; `keyBackend` says
|
|
# how they are stored, never what they are.
|
|
# * Model-id lists are [{id, anthropic}] rather than bare strings, so a reader
|
|
# never has to re-derive the Anthropic matcher. A tagged *Anthropic* id is
|
|
# normal (that is how the 1M variant is selected) and must not render as a
|
|
# fault; a tagged gateway id is the breakage.
|
|
# * `guardrailStatus` is tri-state (active|not_set|unknown) or null when not
|
|
# probed. Never collapse to a boolean - "unknown" must not read as safe.
|
|
# ---------------------------------------------------------------------------
|
|
$script:HealthPath = Join-Path $script:Root 'health.json'
|
|
$script:HealthGuardrail = $null # set by Show-GuardrailStatus when it probes
|
|
|
|
# Must serialise as a JSON array for 0, 1 and many entries. PS 5.1 unrolls a
|
|
# single-element array on return (rendering it as a bare object) and renders a
|
|
# comma-wrapped empty array as [[]]. A generic List survives both: `,` stops the
|
|
# unroll, and ConvertTo-Json always emits a List as an array.
|
|
function ConvertTo-ModelEntry {
|
|
param([string[]] $Ids)
|
|
$out = New-Object 'System.Collections.Generic.List[object]'
|
|
foreach ($i in @($Ids)) {
|
|
$out.Add([ordered]@{ id = $i; anthropic = [bool](Test-AnthropicModelId $i) })
|
|
}
|
|
return , $out
|
|
}
|
|
|
|
function Write-HealthFile {
|
|
param([string] $Mode, [string] $PresetName)
|
|
|
|
$h = [ordered]@{
|
|
schema = 1
|
|
tool = 'claude-mode'
|
|
version = $script:Version
|
|
updatedAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
|
os = 'windows'
|
|
mode = $Mode
|
|
preset = ''
|
|
}
|
|
|
|
$ids = Get-CachedModelIds
|
|
$stale = @($ids | Where-Object { Test-AnthropicModelId $_ })
|
|
$tagged = @($ids | Where-Object { Test-TaggedModelId $_ })
|
|
|
|
if ($Mode -eq 'anthropic') {
|
|
# Nothing gateway-shaped is meaningful here, and a cached Anthropic id is
|
|
# simply the model in use - not a finding.
|
|
$h['staleModelIds'] = (ConvertTo-ModelEntry @())
|
|
$h['taggedModelIds'] = ConvertTo-ModelEntry $tagged
|
|
} else {
|
|
$h['preset'] = $PresetName
|
|
try {
|
|
$p = Get-Preset $PresetName
|
|
$h['provider'] = [string]$p['provider']
|
|
$h['baseUrl'] = [string]$p['baseUrl']
|
|
$models = [ordered]@{}
|
|
foreach ($t in $script:Tiers) { if ($p['models'].Contains($t)) { $models[$t] = [string]$p['models'][$t] } }
|
|
$h['models'] = $models
|
|
$h['subagentModel'] = [string]$p['subagentModel']
|
|
$h['contextTokens'] = if ($p['contextTokens']) { [int]$p['contextTokens'] } else { $null }
|
|
$h['gatewayDiscovery'] = [bool]$p['gatewayModelDiscovery']
|
|
|
|
$auth = Get-PresetAuth $p
|
|
if ([string]$auth['mode'] -eq 'vault') {
|
|
$ref = [string]$auth['keyRef']
|
|
$h['keyBackend'] = 'dpapi'
|
|
$kc = New-Object 'System.Collections.Generic.List[object]'
|
|
if (Get-VaultKey $ref) { $kc.Add($ref) }
|
|
$h['keysConfigured'] = $kc
|
|
} else {
|
|
$h['keyBackend'] = 'inline'
|
|
$h['keysConfigured'] = (New-Object 'System.Collections.Generic.List[object]')
|
|
}
|
|
# It passed the guard, or Set-ClaudeMode would have thrown.
|
|
$h['costGuardPassed'] = $true
|
|
} catch {
|
|
$h['costGuardPassed'] = $null
|
|
}
|
|
$h['guardrailStatus'] = $script:HealthGuardrail
|
|
$h['staleModelIds'] = ConvertTo-ModelEntry $stale
|
|
$h['taggedModelIds'] = ConvertTo-ModelEntry $tagged
|
|
}
|
|
|
|
try { Write-JsonFile $script:HealthPath $h } catch { }
|
|
}
|
|
|
|
function Get-PersistentEnv {
|
|
param([string] $Name)
|
|
return [pscustomobject]@{
|
|
Name = $Name
|
|
User = [Environment]::GetEnvironmentVariable($Name, 'User')
|
|
Machine = [Environment]::GetEnvironmentVariable($Name, 'Machine')
|
|
}
|
|
}
|
|
|
|
function Test-StrayEnvVars {
|
|
param([string] $Mode)
|
|
|
|
$problems = 0
|
|
|
|
$ak = Get-PersistentEnv 'ANTHROPIC_API_KEY'
|
|
if ($Mode -ne 'anthropic') {
|
|
if ($ak.User) { Write-Err2 'ANTHROPIC_API_KEY is set at User scope - it will bypass the gateway. Fix: claude-mode doctor'; $problems++ }
|
|
if ($ak.Machine) { Write-Err2 'ANTHROPIC_API_KEY is set at Machine scope - it will bypass the gateway. Remove it (admin required).'; $problems++ }
|
|
if ($env:ANTHROPIC_API_KEY -and -not $ak.User -and -not $ak.Machine) {
|
|
Write-Warn2 'ANTHROPIC_API_KEY is set in THIS shell only. The `claude` wrapper strips it; other shells are unaffected.'
|
|
}
|
|
}
|
|
|
|
foreach ($name in $script:BaseManagedEnvKeys) {
|
|
if ($name -eq 'ANTHROPIC_API_KEY') { continue }
|
|
$v = Get-PersistentEnv $name
|
|
if ($v.User) { Write-Err2 "$name is set at User scope - it overrides claude-mode. Fix: claude-mode doctor"; $problems++ }
|
|
if ($v.Machine) { Write-Err2 "$name is set at Machine scope - remove it (admin required)."; $problems++ }
|
|
}
|
|
|
|
return $problems
|
|
}
|
|
|
|
function Repair-StrayEnvVars {
|
|
$fixed = 0
|
|
foreach ($name in $script:BaseManagedEnvKeys) {
|
|
$v = [Environment]::GetEnvironmentVariable($name, 'User')
|
|
if ($v) {
|
|
$bak = Join-Path $script:BackupDir ("userenv-$name-" + (Get-Date).ToString('yyyyMMdd-HHmmss') + '.txt')
|
|
Set-Content -LiteralPath $bak -Value $v -Encoding UTF8
|
|
Protect-FileAcl $bak
|
|
[Environment]::SetEnvironmentVariable($name, $null, 'User')
|
|
Write-Ok "removed User-scope $name (old value saved to $bak)"
|
|
$fixed++
|
|
}
|
|
}
|
|
if ($fixed -eq 0) { Write-Ok 'no User-scope overrides to remove' }
|
|
}
|