zscripts-token-savers/token-count.ps1
Kelly Michels 4230566e47
feat: two blank lines after every z-script run (#57)
zdeploy ended with two blank lines and nothing else did, so its output
was the only one that did not butt up against the next prompt. Applied
everywhere.

Implemented in Stop-ZTracking rather than per script, because every
tracked script ends by calling it - including the usage and guard paths
that do `Stop-ZTracking; exit 1`. One place therefore covers every exit
of sixteen scripts.

  * Write-ZTrailer emits the two lines. Stop-ZTracking calls it on both
    paths, including the early return when tracking never started - a
    script that printed output still deserves the separation.
  * -FinalNote prints one last line AFTER the tracking footer and BEFORE
    the blanks. zdeploy's "Last deployed at ..." now goes through it, so
    it stays the last thing on screen instead of being followed by the
    token count.
  * The standalone tools (zchecksums, zversion, zrelease) get a local
    three-line copy at each exit rather than dot-sourcing ZHelpers -
    they are deliberately dependency-free so they run from an extracted
    release zip with nothing beside them.
  * Redundant trailing blanks were removed where a script already
    printed one before exiting, so the count is exactly two, not three.

Also fixes a related gap found while testing: the error exits inside
Get-ZConfig and Get-ZProject bypassed Stop-ZTracking entirely, so a run
that died on a missing zconfig.json printed no trailer AND no token
footer. Those three exits now route through Stop-ZTracking.

zkill/zrestart are one-line wrappers around scripts that already trail,
so they are untouched - adding it would double the blanks.

Verified by running each script and counting the trailing blank lines in
its captured output: 2 on every success path, every usage path, and the
config-error path. Suite 231/231. CHECKSUMS.txt refreshed.
2026-08-25 15:37:45 -05:00

174 lines
8.1 KiB
PowerShell

# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
# Created by Kelly Michels · dev@evomedia.net
# Licensed under the MIT License. See LICENSE.
# Version: v1.0.0.0.14
# token-count.ps1 — measure script output volume to estimate AI agent token costs.
#
# Runs each z-script and captures lines + characters of output. Use the results
# to validate or update the figures in TOKEN_SAVINGS.md.
#
# Usage:
# .\token-count.ps1 <project> # run all scripts against a project key
# .\token-count.ps1 <project> -Skip zbackup_ec2,zrepair # skip slow/destructive ones
# .\token-count.ps1 <project> -Only zdeploy,zec2 # run specific scripts only
#
# Output: a summary table with lines, chars, and estimated tokens per script.
# Estimated tokens = chars / 3.5 (Claude's approximate tokenization rate).
param(
[Parameter(Position = 0, Mandatory = $true)]
[string]$Project,
[string[]]$Skip = @(),
[string[]]$Only = @()
)
$ErrorActionPreference = "Stop"
. (Join-Path $PSScriptRoot "ZHelpers.ps1")
$CHARS_PER_TOKEN = 3.5
$results = [System.Collections.Generic.List[hashtable]]::new()
function Invoke-Measured {
param(
[string]$Label,
[string]$ScriptName,
[scriptblock]$Block,
[string]$Note = ""
)
if ($Only.Count -gt 0 -and $Only -notcontains $ScriptName) { return }
if ($Skip -contains $ScriptName) {
$results.Add(@{ label = $Label; skipped = $true; note = "skipped" })
return
}
Write-Host "Running $Label..." -ForegroundColor DarkGray -NoNewline
$tp = Join-Path ([System.IO.Path]::GetTempPath()) ("_zm_" + [System.Guid]::NewGuid().ToString("N") + ".txt")
$global:_ZMeasuring = $true
try {
# Use Start-Transcript so Write-Host (stream 6) is captured.
# _ZMeasuring tells Start-ZTracking in all called scripts to no-op,
# so no inner script can replace or stop our transcript.
Start-Transcript -Path $tp -NoClobber | Out-Null
try { & $Block } catch {}
try { Stop-Transcript | Out-Null } catch {}
$raw = Get-Content -LiteralPath $tp -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
if (-not $raw) { $raw = "" }
$allLines = @($raw -split "`n")
$si = 0
for ($i = 0; $i -lt $allLines.Count; $i++) {
if ($allLines[$i] -match '^Transcript started') { $si = $i + 1; break }
}
$ei = $allLines.Count
for ($i = $allLines.Count - 1; $i -ge 0; $i--) {
if ($allLines[$i] -match '^\*{4}') { $ei = $i; break }
}
$body = if ($ei -gt $si) { @($allLines[$si..($ei - 1)]) } else { @() }
$text = $body -join "`n"
$lc = ($body | Where-Object { $_.Trim() -ne "" }).Count
$cc = $text.Length
$tokens = [math]::Round($cc / $CHARS_PER_TOKEN)
$results.Add(@{
label = $Label
script = $ScriptName
lines = $lc
chars = $cc
tokens = $tokens
note = $Note
})
Write-Host " $lc lines / $cc chars / ~$tokens tokens" -ForegroundColor Green
} catch {
$results.Add(@{ label = $Label; script = $ScriptName; error = $_.Exception.Message })
Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
} finally {
$global:_ZMeasuring = $false
try { Stop-Transcript | Out-Null } catch {}
Remove-Item -LiteralPath $tp -Force -ErrorAction SilentlyContinue
}
}
Write-Host ""
Write-Host "=== Token Count - project: $Project ===" -ForegroundColor Cyan
Write-Host "Chars per token: $CHARS_PER_TOKEN (Claude approximate)" -ForegroundColor DarkGray
Write-Host ""
# ── Local dev ──────────────────────────────────────────────────────────────────
Invoke-Measured "zstart " "zstart" { & (Join-Path $PSScriptRoot "zstart.ps1") $Project -Detached } `
-Note "detached - output may be minimal"
Invoke-Measured "zkill " "zkill" { & (Join-Path $PSScriptRoot "ZKillOnly.ps1") $Project }
Invoke-Measured "zrestart " "zrestart" { & (Join-Path $PSScriptRoot "ZKiller.ps1") $Project -Detached } `
-Note "detached"
# Takes no project key; brings up zscripts\docker\docker-compose.yml. Needs Docker
# Desktop running AND that compose file present, or it only emits an error.
Invoke-Measured "zstart_docker" "zstart_docker" { & (Join-Path $PSScriptRoot "zstart_docker.ps1") } `
-Note "detached docker stack - needs Docker Desktop + docker/docker-compose.yml"
# ── Deploy ─────────────────────────────────────────────────────────────────────
Invoke-Measured "zdeploy " "zdeploy" { & (Join-Path $PSScriptRoot "zdeploy.ps1") $Project } `
-Note "includes docker build - run twice; first may be inflated by package installs"
# ── Diagnostics ────────────────────────────────────────────────────────────────
Invoke-Measured "zec2 " "zec2" { & (Join-Path $PSScriptRoot "zec2.ps1") $Project }
Invoke-Measured "zec2online" "zec2online" { & (Join-Path $PSScriptRoot "zec2online.ps1") $Project }
Invoke-Measured "zrepair " "zrepair" { & (Join-Path $PSScriptRoot "zrepair.ps1") $Project } `
-Note "may restart containers on EC2"
# ── Backups ────────────────────────────────────────────────────────────────────
Invoke-Measured "zbackup " "zbackup" { & (Join-Path $PSScriptRoot "zbackup.ps1") $Project }
Invoke-Measured "zbackup_ec2" "zbackup_ec2" { & (Join-Path $PSScriptRoot "zbackup_ec2.ps1") $Project } `
-Note "pulls from EC2 - skippable if slow"
Invoke-Measured "zsync " "zsync" { & (Join-Path $PSScriptRoot "zsync.ps1") }
# ── Summary table ──────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "=== Summary ===" -ForegroundColor Cyan
Write-Host ("{0,-14} {1,8} {2,10} {3,10} {4}" -f "Script", "Lines", "Chars", "~Tokens", "Notes") -ForegroundColor Yellow
Write-Host ("-" * 70) -ForegroundColor DarkGray
$totalTokens = 0
foreach ($r in $results) {
if ($r.skipped) {
Write-Host ("{0,-14} {1,8}" -f $r.label, "(skipped)") -ForegroundColor DarkGray
} elseif ($r.error) {
Write-Host ("{0,-14} {1}" -f $r.label, "ERROR: $($r.error)") -ForegroundColor Red
} else {
Write-Host ("{0,-14} {1,8} {2,10} {3,10} {4}" -f `
$r.label, $r.lines, $r.chars, $r.tokens, $r.note) -ForegroundColor White
$totalTokens += $r.tokens
}
}
Write-Host ("-" * 70) -ForegroundColor DarkGray
Write-Host ("{0,-14} {1,8} {2,10} {3,10}" -f "TOTAL", "", "", $totalTokens) -ForegroundColor Cyan
# ── Dollar cost at current pricing ─────────────────────────────────────────────
Write-Host ""
Write-Host "=== Estimated cost if Claude orchestrated all of the above ===" -ForegroundColor Cyan
$models = @(
@{ name = "Haiku 4.5"; blended = 2.25 },
@{ name = "Sonnet 5 "; blended = 9.00 },
@{ name = "Opus 4.8 "; blended = 15.00 },
@{ name = "Fable 5 "; blended = 30.00 }
)
foreach ($m in $models) {
$cost = [math]::Round($totalTokens / 1000000 * $m.blended, 6)
Write-Host (" {0} ~{1,8} tokens x `${2}/1M blended = `${3}" -f `
$m.name, $totalTokens, $m.blended, $cost) -ForegroundColor White
}
Write-Host ""
Write-Host "Note: zdeploy output varies significantly between runs." -ForegroundColor DarkGray
Write-Host " First run after package updates can be 2-3x larger than a cached run." -ForegroundColor DarkGray
Write-Host " Run zdeploy twice and use the second (cached) figure for TOKEN_SAVINGS.md." -ForegroundColor DarkGray
Write-ZTrailer