Files
smoidoandClaude Opus 5 4091746d4e Split the Windows script into modules; drop the Arkylx Index pieces
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>
2026-09-15 02:06:45 +03:00

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' }
}