zscripts-token-savers/token-count.ps1
KellyMichels 3546a12564 Evomedia.net Token Savers - initial public release
Config-driven PowerShell scripts to run infrastructure tasks (deploy, restart, backup, diagnostics) yourself instead of having an AI agent orchestrate them, to save agent tokens. Environment specifics live in zconfig.json (gitignored).
2026-07-15 21:33:22 -05:00

173 lines
8.1 KiB
PowerShell

# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
# Created by Kelly Michels · dev@evomedia.net
# Licensed under the MIT License. See LICENSE.
# 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-Host ""