feat(bash): add token-usage tracking to the bash port

Adds z_track_start/z_track_stop + z_record to zhelpers.sh and wires
z_track_start into every command script. Each run now captures its own output
volume (FIFO+tee, ANSI stripped), prints the '--- N lines / N chars / ~N tokens
est. (Claude Code) ---' footer, and appends one JSONL row per top-level run in
the same shape as the PowerShell tokens.jsonl. A nested-run guard keeps
zrestart from double-counting its zkill/zstart children.

Data dir precedence: $ZTOKENS_DATA, config ztokens.dataDir, sibling
../../ztokens/data, else ~/.ztokens/data. Docs + example config updated.

Verified on WSL (isolated data dir): single run records correctly; nested
zrestart produces one combined record, not three; est = round(chars/3.5).
This commit is contained in:
KellyMichels 2026-07-22 15:51:38 -05:00
parent cdeec77609
commit 09b98bc77b
18 changed files with 118 additions and 0 deletions

View File

@ -54,6 +54,30 @@ command argument:
Flags use GNU style (`--port 3000`, `--detached`) instead of PowerShell style
(`-Port`, `-Detached`). Detached dev servers log to `/tmp/zstart-<project>.log`.
## Usage tracking
Every run prints a footer with its own output volume:
```
--- 1,048 lines / 69,009 chars / ~19,717 tokens est. (Claude Code) ---
```
and (when `jq` is available) appends one JSON line per run to `tokens.jsonl`,
so you can see how much infrastructure output you keep out of an AI agent's
context over time. Fields: `ts, script, projects, lines, chars, est, model`
(`est` ≈ `chars / 3.5`). Nested runs (the `zkill`/`zstart` inside `zrestart`)
are counted once, not double.
Where it's written, first match wins:
1. `$ZTOKENS_DATA`
2. `ztokens.dataDir` in `zconfig.json`
3. the sibling `../ztokens/data` directory, if present
4. `~/.ztokens/data` (created on first run)
Set `ZTOKENS_MODEL` to tag records with a specific model; it defaults to
`est. chars/3.5`.
## Differences from the PowerShell versions
- `zsync <viteproject>` mirrors `dist/` with `rsync -a --delete` (robocopy /MIR equivalent).

View File

@ -14,6 +14,7 @@
set -uo pipefail
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$_HERE/zhelpers.sh"
z_track_start "$@"
install=0; at_time="02:00"
while [ $# -gt 0 ]; do

View File

@ -16,6 +16,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require zip

View File

@ -15,6 +15,7 @@
set -uo pipefail
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$_HERE/zhelpers.sh"
z_track_start "$@"
z_need_config
printf '\n'

View File

@ -16,6 +16,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require ssh scp

View File

@ -16,6 +16,11 @@
"oneDriveBackups": ""
},
"ztokens": {
"_comment": "Optional. Where each run's token-usage stats (tokens.jsonl) are recorded. Omit to use ~/.ztokens/data.",
"dataDir": "/home/youruser/.ztokens/data"
},
"projects": {
"_comment": "Rename these keys to your own project names — the key IS the command argument: zstart pyapp, zdeploy viteapp, zbackup nextapp. Add as many projects as you like. Keys starting with _ are ignored.",

View File

@ -17,6 +17,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require ssh scp zip curl jq

View File

@ -12,6 +12,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require curl

View File

@ -15,6 +15,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require curl ssh

View File

@ -237,3 +237,77 @@ kill_listeners() {
done
printf '%s' "$n"
}
# ---- output tracking (ztokens) ----------------------------------------------
# Mirrors the PowerShell Start-/Stop-ZTracking + tokens.jsonl feed: capture this
# script's own output volume, print the "--- N lines / N chars / ~N tokens ---"
# footer, and append one JSONL record per top-level run. Nested z-scripts (e.g.
# zkill/zstart under zrestart) inherit the capture and do NOT double-count.
# Call `z_track_start "$@"` once near the top; an EXIT trap covers every exit.
z_track_start() {
[ "${Z_TRACK_ACTIVE:-0}" = "1" ] && return # nested under another z-script
command -v mktemp >/dev/null 2>&1 || return
export Z_TRACK_ACTIVE=1
Z_TRACK_SCRIPT="$(basename "$0")"
Z_TRACK_ARGS="$*"
Z_TRACK_FILE="$(mktemp "${TMPDIR:-/tmp}/ztrack.XXXXXX")" || { Z_TRACK_FILE=""; return; }
Z_TRACK_FIFO="${Z_TRACK_FILE}.fifo"
if ! mkfifo "$Z_TRACK_FIFO" 2>/dev/null; then rm -f "$Z_TRACK_FILE"; Z_TRACK_FILE=""; return; fi
# tee copies the FIFO to the log file while still showing output on the terminal.
tee "$Z_TRACK_FILE" < "$Z_TRACK_FIFO" &
Z_TRACK_TEE=$!
exec 3>&1 4>&2 >"$Z_TRACK_FIFO" 2>&1
trap z_track_stop EXIT
}
z_track_stop() {
set +e # never let cleanup abort mid-way
[ -n "${Z_TRACK_FILE:-}" ] || return
exec 1>&3 2>&4 3>&- 4>&- # restore stdout/stderr, close the pipe
[ -n "${Z_TRACK_TEE:-}" ] && wait "$Z_TRACK_TEE" 2>/dev/null
rm -f "${Z_TRACK_FIFO:-}" 2>/dev/null
local esc plain lines chars tok
esc=$'\033'
plain="${Z_TRACK_FILE}.plain"
sed "s/${esc}\[[0-9;]*m//g" "$Z_TRACK_FILE" > "$plain" 2>/dev/null || cp "$Z_TRACK_FILE" "$plain" 2>/dev/null
lines="$(grep -cve '^[[:space:]]*$' "$plain" 2>/dev/null)"; [ -n "$lines" ] || lines=0
chars="$(wc -m < "$plain" 2>/dev/null | tr -d ' ')"; [ -n "$chars" ] || chars=0
tok="$(awk -v c="$chars" 'BEGIN{printf "%d", int(c/3.5 + 0.5)}')"
printf '\n%s--- %s lines / %s chars / ~%s tokens est. (Claude Code) ---%s\n' \
"${C_GRAY:-}" "$(_z_commafy "$lines")" "$(_z_commafy "$chars")" "$(_z_commafy "$tok")" "${C_RESET:-}"
z_record "$lines" "$chars" "$tok"
rm -f "$Z_TRACK_FILE" "$plain" 2>/dev/null
Z_TRACK_FILE=""
}
# Portable thousands separators (avoids locale-dependent printf %'d).
_z_commafy() { printf '%s' "$1" | sed -E ':a;s/([0-9])([0-9]{3})($|[^0-9])/\1,\2\3/;ta'; }
# Append a run to the ztokens JSONL store. Data dir precedence: $ZTOKENS_DATA,
# then config "ztokens.dataDir", then the sibling ../../ztokens/data if present
# (dev layout), else ~/.ztokens/data. No-op without jq. Model tag is
# $ZTOKENS_MODEL or "est. chars/3.5".
z_record() { # <lines> <chars> <est>
command -v jq >/dev/null 2>&1 || return
local lines="$1" chars="$2" est="$3" dir model projects
dir="${ZTOKENS_DATA:-}"
[ -z "$dir" ] && [ -f "$ZCONFIG" ] && dir="$(jq -r '.ztokens.dataDir // empty' "$ZCONFIG" 2>/dev/null)"
if [ -z "$dir" ]; then
if [ -d "$_ZDIR/../../ztokens/data" ]; then dir="$_ZDIR/../../ztokens/data"; else dir="$HOME/.ztokens/data"; fi
fi
mkdir -p "$dir" 2>/dev/null || return
model="${ZTOKENS_MODEL:-est. chars/3.5}"
projects=""
[ -n "${Z_TRACK_ARGS:-}" ] && projects="$(printf '%s\n' $Z_TRACK_ARGS | grep -v '^-' | paste -sd',' - 2>/dev/null)"
jq -cn \
--arg ts "$(date +%Y-%m-%dT%H:%M:%S%z)" \
--arg script "${Z_TRACK_SCRIPT%.*}" \
--arg projects "$projects" \
--argjson lines "${lines:-0}" --argjson chars "${chars:-0}" --argjson est "${est:-0}" \
--arg model "$model" \
'{ts:$ts,script:$script,projects:$projects,lines:$lines,chars:$chars,est:$est,model:$model}' \
>> "$dir/tokens.jsonl" 2>/dev/null
}

View File

@ -10,6 +10,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require lsof

View File

@ -14,6 +14,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require ssh curl

View File

@ -11,6 +11,7 @@
set -uo pipefail
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$_HERE/zhelpers.sh"
z_track_start "$@"
z_need_config
projects=(); port_override=0; kill_all=0; no_restart=0; detached=0; bind_host="127.0.0.1"

View File

@ -11,6 +11,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require ssh

View File

@ -14,6 +14,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
projects=(); port_override=0; bind_host="127.0.0.1"; detached=0

View File

@ -15,6 +15,7 @@
set -uo pipefail
_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$_HERE/zhelpers.sh"
z_track_start "$@"
z_require docker
build=0; attached=0; solo=0

View File

@ -12,6 +12,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
z_require ssh

View File

@ -15,6 +15,7 @@
set -uo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/zhelpers.sh"
z_track_start "$@"
z_need_config
projects=(); dest="${ZSYNC_DEST:-}"