|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | + Wire usage-collect.ps1 as the Claude Code statusLine, so the account's plan limits get published. |
| 4 | +
|
| 5 | +.DESCRIPTION |
| 6 | + Run this ONCE, from a plain terminal. It writes `statusLine` into the USER-level |
| 7 | + ~/.claude/settings.json, so every session on this machine publishes -- and reads -- the same |
| 8 | + account-wide quota state. See usage-collect.ps1 for why the statusLine is the only source. |
| 9 | +
|
| 10 | + IT TAKES EFFECT IN NEWLY STARTED SESSIONS. Existing sessions keep the config they booted with, the |
| 11 | + same as the coordination hooks. And it only ever runs in an INTERACTIVE session: the statusLine is |
| 12 | + part of the TUI's render tree and never executes under `claude -p` or the SDK, so a headless |
| 13 | + coordinator can read what this publishes but can never publish it itself. |
| 14 | +
|
| 15 | + WHY IT POINTS AT AN ABSOLUTE PATH rather than resolving the repo per invocation: the statusLine runs |
| 16 | + on every assistant message behind a 300ms debounce, and a `git rev-parse` per fire is latency on the |
| 17 | + render path for a value that never changes. The trade is that moving or deleting the checkout breaks |
| 18 | + it -- so the wired command TESTS FOR THE SCRIPT and degrades to a quiet marker instead of erroring |
| 19 | + into the status bar on every message. |
| 20 | +
|
| 21 | + refreshInterval is set because statusLine updates are EVENT-DRIVEN -- a new assistant message, |
| 22 | + /compact, a permission-mode change -- and go silent when a session is idle. Anthropic's own docs |
| 23 | + name "a coordinator waits on background subagents" as the case where that leaves you blind, which is |
| 24 | + exactly this repo's situation. |
| 25 | +
|
| 26 | +.EXAMPLE |
| 27 | + pwsh -NoProfile -File scripts\coord\install-usage-statusline.ps1 |
| 28 | + pwsh -NoProfile -File scripts\coord\install-usage-statusline.ps1 -Status |
| 29 | + pwsh -NoProfile -File scripts\coord\install-usage-statusline.ps1 -Uninstall |
| 30 | +#> |
| 31 | +[CmdletBinding(SupportsShouldProcess)] |
| 32 | +param( |
| 33 | + [switch]$Uninstall, |
| 34 | + [switch]$Status, |
| 35 | + # Milliseconds. Minimum honoured by Claude Code is 1000. |
| 36 | + [int]$RefreshInterval = 10000, |
| 37 | + [string]$SettingsPath = (Join-Path $env:USERPROFILE ".claude\settings.json"), |
| 38 | + # Which collector to wire. Defaults to the PRIMARY checkout's copy, deliberately: a worktree is |
| 39 | + # disposable and a user-level statusLine pointing into one dies with it. Overridable so tests can |
| 40 | + # drive the real installer against a fixture instead of asserting a copy of its rules. |
| 41 | + [string]$CollectorPath |
| 42 | +) |
| 43 | + |
| 44 | +$ErrorActionPreference = "Stop" |
| 45 | +$MARKER = "mefor-usage" |
| 46 | + |
| 47 | +# The primary checkout, not this worktree: a worktree is disposable and the statusLine outlives it. |
| 48 | +$common = (& git rev-parse --path-format=absolute --git-common-dir 2>$null) |
| 49 | +if ($LASTEXITCODE -ne 0 -or -not $common) { throw "Not inside a git repository -- run this from the MessageFoundry checkout." } |
| 50 | +$primary = Split-Path ($common.Trim()) -Parent |
| 51 | +$script = if ($CollectorPath) { $CollectorPath } else { Join-Path $primary "scripts/coord/usage-collect.ps1" } |
| 52 | + |
| 53 | +function Get-Settings { |
| 54 | + if (-not (Test-Path -LiteralPath $SettingsPath)) { return [ordered]@{} } |
| 55 | + $raw = Get-Content -LiteralPath $SettingsPath -Raw |
| 56 | + if (-not $raw.Trim()) { return [ordered]@{} } |
| 57 | + return ($raw | ConvertFrom-Json -AsHashtable) |
| 58 | +} |
| 59 | + |
| 60 | +if ($Status) { |
| 61 | + $s = Get-Settings |
| 62 | + $sl = $s['statusLine'] |
| 63 | + Write-Host "" |
| 64 | + if (-not $sl) { Write-Host "statusLine: NOT CONFIGURED" -ForegroundColor Yellow } |
| 65 | + else { |
| 66 | + $isOurs = ([string]$sl['command']) -like "*$MARKER*" |
| 67 | + Write-Host ("statusLine: CONFIGURED" + $(if ($isOurs) { " (ours)" } else { " (SOMEONE ELSE'S -- install would replace it)" })) -ForegroundColor $(if ($isOurs) { "Green" } else { "Yellow" }) |
| 68 | + Write-Host " command : $($sl['command'])" |
| 69 | + Write-Host " refreshInterval: $($sl['refreshInterval'])" |
| 70 | + } |
| 71 | + Write-Host " script exists : $(Test-Path -LiteralPath $script) ($script)" |
| 72 | + $latest = Join-Path $env:USERPROFILE ".claude\mefor-usage\latest.json" |
| 73 | + # A RECEIPT, NOT A CONFIG READ. Whether the settings file names the script says nothing about |
| 74 | + # whether it has ever run -- that distinction is the one this repo keeps paying for. |
| 75 | + Write-Host " has published : $(Test-Path -LiteralPath $latest) ($latest)" -ForegroundColor $(if (Test-Path -LiteralPath $latest) { "Green" } else { "Yellow" }) |
| 76 | + Write-Host "" |
| 77 | + exit 0 |
| 78 | +} |
| 79 | + |
| 80 | +$settings = Get-Settings |
| 81 | + |
| 82 | +if ($Uninstall) { |
| 83 | + if ($settings['statusLine'] -and ([string]$settings['statusLine']['command']) -like "*$MARKER*") { |
| 84 | + $settings.Remove('statusLine') |
| 85 | + if ($PSCmdlet.ShouldProcess($SettingsPath, "remove the mefor-usage statusLine")) { |
| 86 | + Copy-Item -LiteralPath $SettingsPath -Destination "$SettingsPath.bak-usage" -Force -ErrorAction SilentlyContinue |
| 87 | + ($settings | ConvertTo-Json -Depth 20) | Set-Content -LiteralPath $SettingsPath -Encoding UTF8 |
| 88 | + Write-Host "statusLine REMOVED from $SettingsPath" -ForegroundColor Yellow |
| 89 | + } |
| 90 | + } |
| 91 | + else { Write-Host "Nothing to remove: the statusLine is absent or is not ours." -ForegroundColor Yellow } |
| 92 | + exit 0 |
| 93 | +} |
| 94 | + |
| 95 | +if (-not (Test-Path -LiteralPath $script)) { |
| 96 | + throw "Collector not found at $script. The primary checkout ($primary) does not carry it yet -- merge the branch that adds it, or advance the primary, before installing." |
| 97 | +} |
| 98 | + |
| 99 | +if ($settings['statusLine'] -and ([string]$settings['statusLine']['command']) -notlike "*$MARKER*") { |
| 100 | + Write-Host "" |
| 101 | + Write-Host "REFUSING: a statusLine is already configured and it is not ours." -ForegroundColor Red |
| 102 | + Write-Host " command: $($settings['statusLine']['command'])" |
| 103 | + Write-Host "" |
| 104 | + Write-Host "Silently replacing someone's status bar is not this script's call. Remove it yourself, or" |
| 105 | + Write-Host "merge the two commands by hand, then re-run." |
| 106 | + exit 1 |
| 107 | +} |
| 108 | + |
| 109 | +# The guard is inline so a missing script degrades to a marker rather than erroring into the status bar |
| 110 | +# on every single message -- a statusLine that shouts an exception is worse than one that says nothing. |
| 111 | +$cmd = "# $MARKER`n" + |
| 112 | +"`$s = '$($script -replace "'", "''")'; if (Test-Path -LiteralPath `$s) { & pwsh -NoProfile -File `$s } else { Write-Output '${MARKER}: collector missing' }" |
| 113 | + |
| 114 | +$settings['statusLine'] = [ordered]@{ |
| 115 | + type = "command" |
| 116 | + command = $cmd |
| 117 | + refreshInterval = $RefreshInterval |
| 118 | +} |
| 119 | + |
| 120 | +if ($PSCmdlet.ShouldProcess($SettingsPath, "install the mefor-usage statusLine")) { |
| 121 | + if (Test-Path -LiteralPath $SettingsPath) { Copy-Item -LiteralPath $SettingsPath -Destination "$SettingsPath.bak-usage" -Force } |
| 122 | + $json = $settings | ConvertTo-Json -Depth 20 |
| 123 | + # Never leave the file unparseable: a broken settings.json degrades every session on this machine. |
| 124 | + try { $null = $json | ConvertFrom-Json } catch { throw "Refusing to write: generated settings JSON is invalid. $_" } |
| 125 | + $json | Set-Content -LiteralPath $SettingsPath -Encoding UTF8 |
| 126 | + Write-Host "" |
| 127 | + Write-Host "statusLine INSTALLED (user level -- every session on this machine)" -ForegroundColor Green |
| 128 | + Write-Host " collector : $script" |
| 129 | + Write-Host " refreshInterval: $RefreshInterval ms" |
| 130 | + Write-Host " publishes to : $(Join-Path $env:USERPROFILE '.claude\mefor-usage\latest.json')" |
| 131 | + Write-Host " backup : $SettingsPath.bak-usage" |
| 132 | + Write-Host "" |
| 133 | + Write-Host " Takes effect in NEWLY STARTED sessions. Interactive only -- never under 'claude -p'." |
| 134 | + Write-Host " Then read it with: pwsh -NoProfile -File scripts\coord\usage.ps1" |
| 135 | + Write-Host "" |
| 136 | +} |
0 commit comments