From d945430807a9b975dc1377f886ac94dc9f8c2afa Mon Sep 17 00:00:00 2001 From: kellymichels Date: Wed, 22 Jul 2026 16:34:50 -0500 Subject: [PATCH] feat(bash): add token-usage tracking to the bash port (#6) 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). --- bash/README.md | 24 +++++++++++++ bash/setup_backup_schedule | 1 + bash/zbackup | 1 + bash/zbackup_and_sync | 1 + bash/zbackup_ec2 | 1 + bash/zconfig.example.json | 5 +++ bash/zdeploy | 1 + bash/zec2 | 1 + bash/zec2online | 1 + bash/zhelpers.sh | 74 ++++++++++++++++++++++++++++++++++++++ bash/zkill | 1 + bash/zrepair | 1 + bash/zrestart | 1 + bash/zsetup_mail | 1 + bash/zstart | 1 + bash/zstart_docker | 1 + bash/zstop | 1 + bash/zsync | 1 + 18 files changed, 118 insertions(+) diff --git a/bash/README.md b/bash/README.md index cd04255..5ba9e09 100644 --- a/bash/README.md +++ b/bash/README.md @@ -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-.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 ` mirrors `dist/` with `rsync -a --delete` (robocopy /MIR equivalent). diff --git a/bash/setup_backup_schedule b/bash/setup_backup_schedule index d2e5b60..e15f0d7 100644 --- a/bash/setup_backup_schedule +++ b/bash/setup_backup_schedule @@ -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 diff --git a/bash/zbackup b/bash/zbackup index 407ce67..70f2088 100644 --- a/bash/zbackup +++ b/bash/zbackup @@ -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 diff --git a/bash/zbackup_and_sync b/bash/zbackup_and_sync index 557e739..497c37b 100644 --- a/bash/zbackup_and_sync +++ b/bash/zbackup_and_sync @@ -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' diff --git a/bash/zbackup_ec2 b/bash/zbackup_ec2 index e28f1d7..828c047 100644 --- a/bash/zbackup_ec2 +++ b/bash/zbackup_ec2 @@ -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 diff --git a/bash/zconfig.example.json b/bash/zconfig.example.json index f445fc8..d0f018b 100644 --- a/bash/zconfig.example.json +++ b/bash/zconfig.example.json @@ -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.", diff --git a/bash/zdeploy b/bash/zdeploy index 20b2ce8..3e09192 100644 --- a/bash/zdeploy +++ b/bash/zdeploy @@ -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 diff --git a/bash/zec2 b/bash/zec2 index cd91fbb..37513ec 100644 --- a/bash/zec2 +++ b/bash/zec2 @@ -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 diff --git a/bash/zec2online b/bash/zec2online index 557dc63..e4ed6d1 100644 --- a/bash/zec2online +++ b/bash/zec2online @@ -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 diff --git a/bash/zhelpers.sh b/bash/zhelpers.sh index 96a4512..b4e2ad8 100644 --- a/bash/zhelpers.sh +++ b/bash/zhelpers.sh @@ -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() { # + 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 +} diff --git a/bash/zkill b/bash/zkill index e3f69ee..1a29ad2 100644 --- a/bash/zkill +++ b/bash/zkill @@ -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 diff --git a/bash/zrepair b/bash/zrepair index 5e69df4..58a0181 100644 --- a/bash/zrepair +++ b/bash/zrepair @@ -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 diff --git a/bash/zrestart b/bash/zrestart index 59b1d0f..f6e758b 100644 --- a/bash/zrestart +++ b/bash/zrestart @@ -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" diff --git a/bash/zsetup_mail b/bash/zsetup_mail index 112cbd1..f27ffb3 100644 --- a/bash/zsetup_mail +++ b/bash/zsetup_mail @@ -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 diff --git a/bash/zstart b/bash/zstart index 7f893c8..cd78def 100644 --- a/bash/zstart +++ b/bash/zstart @@ -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 diff --git a/bash/zstart_docker b/bash/zstart_docker index 6cfe2ce..86f2903 100644 --- a/bash/zstart_docker +++ b/bash/zstart_docker @@ -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 diff --git a/bash/zstop b/bash/zstop index c05614d..ccb5bd8 100644 --- a/bash/zstop +++ b/bash/zstop @@ -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 diff --git a/bash/zsync b/bash/zsync index 1caa378..4e365bf 100644 --- a/bash/zsync +++ b/bash/zsync @@ -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:-}"