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>
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
# 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' }
|
||||
}
|
||||
Reference in New Issue
Block a user