@inbrace-tech/tokenline 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Inbrace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # tokenline
2
+
3
+ > A cache-aware statusline for AI coding CLIs — your context, prompt-cache, and token economics at a glance.
4
+
5
+ ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
6
+ ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2-blue)
7
+ ![Shell](https://img.shields.io/badge/shell-bash%204%2B-lightgrey)
8
+
9
+ `tokenline` turns the one-line status bar of your AI coding CLI into a live cockpit:
10
+ which model you're on, how much context you've burned, how long your prompt cache
11
+ stays hot, how many tokens you're **saving** by reusing it, and how close you are to
12
+ your 5h / 7d rate limits.
13
+
14
+ It is **cross-CLI** (Claude Code, Antigravity) and **cross-provider** (Anthropic,
15
+ Gemini) — detecting the active client and model provider at runtime and adjusting
16
+ the cost equivalents accordingly.
17
+
18
+ ## Preview
19
+
20
+ ![Tokenline Preview](https://raw.githubusercontent.com/inbrace-tech/tokenline/main/assets/tokenline.png)
21
+
22
+ - **Line 1** — model · context used (tokens + %) · cache TTL with a live HOT→COLD countdown.
23
+ - **Line 2** — per-turn token economics: `read / write / new / output`, the equivalent
24
+ billed tokens (`eq`), and the **`saving %`** you get from prompt caching.
25
+ - **Line 3** — 5h and 7d rate-limit bars with reset ETA and a **pace marker**
26
+ (`!!` = you're burning the window faster than it refills).
27
+
28
+ > Lines 2 and 3 appear only when there's something to show (a turn happened, limits
29
+ > exist), so the bar stays quiet when idle.
30
+
31
+ ## Why tokenline?
32
+
33
+ Most statuslines show the model and the context bar. `tokenline` adds the two things
34
+ that actually drive cost and flow on long agent sessions:
35
+
36
+ - **Cache visibility.** Anthropic and Gemini bill cached input tokens at a fraction of
37
+ the price — but the cache expires. `tokenline` shows the TTL countdown (5m or 1h,
38
+ detected from the data) so you know whether your next turn lands warm or cold.
39
+ - **Savings, quantified.** The `saving %` makes the value of prompt caching concrete
40
+ instead of invisible.
41
+ - **Rate-limit pacing.** The `!!` marker warns you when you're on track to hit the 5h
42
+ or 7d ceiling before it resets.
43
+
44
+ ## Requirements
45
+
46
+ v1 targets **Linux / WSL2**:
47
+
48
+ - `bash` 4 or newer
49
+ - [`jq`](https://jqlang.github.io/jq/)
50
+ - GNU coreutils (`date -d`, `stat -c`)
51
+
52
+ > macOS and Windows support are on the [roadmap](#roadmap). `install.sh` checks all of
53
+ > the above and tells you exactly what's missing.
54
+
55
+ ## Install
56
+
57
+ ### With npm (recommended)
58
+
59
+ If you have **Node 18+**, one command installs the script and wires it into your
60
+ Claude Code settings:
61
+
62
+ ```bash
63
+ npx @inbrace-tech/tokenline init
64
+ ```
65
+
66
+ It copies `tokenline.sh` to `~/.claude/` and adds the `statusLine` block to
67
+ `~/.claude/settings.json`. Use `--project` to configure the current project
68
+ instead of your global settings, and `--dry-run` to preview without writing.
69
+ Then restart Claude Code.
70
+
71
+ > The statusline runs as `bash tokenline.sh` — Node is used only at install
72
+ > time, never in the per-second hot path. `jq` is still required at runtime.
73
+
74
+ ### Without Node (clone + install.sh)
75
+
76
+ No Node? Clone the repo and run the dependency checker, which prints a
77
+ ready-to-paste snippet:
78
+
79
+ ```bash
80
+ git clone https://github.com/inbrace-tech/tokenline.git
81
+ cd tokenline
82
+ ./install.sh
83
+ ```
84
+
85
+ Add the printed block to `~/.claude/settings.json` (global) or your project's
86
+ `.claude/settings.json`, inside the top-level object:
87
+
88
+ ```json
89
+ "statusLine": {
90
+ "type": "command",
91
+ "command": "bash /absolute/path/to/tokenline/tokenline.sh",
92
+ "refreshInterval": 1
93
+ }
94
+ ```
95
+
96
+ Then restart Claude Code.
97
+
98
+ ### What the installer does
99
+
100
+ `npx @inbrace-tech/tokenline init` is deliberately transparent about touching
101
+ your config:
102
+
103
+ - **Writes** `tokenline.sh` to `~/.claude/` (or `./.claude/` with `--project`).
104
+ - **Merges** only the `statusLine` key into `settings.json` — every other
105
+ setting is preserved.
106
+ - **Backs up** `settings.json` to `settings.json.bak` before writing.
107
+ - **Never clobbers** invalid JSON: if it can't parse your `settings.json`, it
108
+ stops and prints the block to paste manually.
109
+ - Is **idempotent**, and won't replace a different existing `statusLine` unless
110
+ you pass `--force`.
111
+
112
+ Other commands: `doctor` (check dependencies and config, change nothing) and
113
+ `uninstall` (remove the block; `--purge` also deletes the script).
114
+
115
+ ### Antigravity CLI
116
+
117
+ `tokenline` detects the Antigravity CLI from the transcript path and switches to its
118
+ provider equivalents automatically. Point Antigravity's statusline command at the same
119
+ `tokenline.sh` — no extra flags needed.
120
+
121
+ ## How it works
122
+
123
+ On every refresh the host CLI pipes a JSON snapshot of the session to the script over
124
+ stdin. `tokenline` parses it in a single `jq` pass (to keep the per-second refresh
125
+ cheap), reads the last turn's timestamp from the transcript to drive the cache
126
+ countdown, and renders up to three lines. Per-turn timestamps are cached in a
127
+ per-user `0700` directory under `$XDG_RUNTIME_DIR` (tmpfs, cleared on logout).
128
+
129
+ ## Troubleshooting
130
+
131
+ | Symptom | Cause / fix |
132
+ | --- | --- |
133
+ | Blank statusline, or `[tokenline] jq not found` | Install `jq` (`apt install jq` / `brew install jq`), then re-run `./install.sh`. |
134
+ | Cache shows `COLD` immediately | Normal right after a long idle gap — the cache window has elapsed. It goes `HOT` again on your next turn. |
135
+ | Colors look wrong / show escape codes | Your terminal must support 256-color ANSI. Most modern terminals do; check your `$TERM`. |
136
+ | Nothing renders on macOS | Expected on v1 — macOS uses BSD `date`/`stat`. See the [roadmap](#roadmap). |
137
+
138
+ ## Roadmap
139
+
140
+ - [ ] macOS support (BSD `date`/`stat`, bash 3.2 fallback)
141
+ - [ ] Windows support (Git Bash / PowerShell)
142
+ - [ ] Configurable colors and thresholds via `TOKENLINE_*` env vars
143
+
144
+ (Issues for these are tracked in the repo — contributions welcome.)
145
+
146
+ ## Contributing
147
+
148
+ Issues and PRs are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). The CI runs
149
+ [ShellCheck](https://www.shellcheck.net/) on every push, so please keep the script
150
+ lint-clean.
151
+
152
+ ## About Inbrace
153
+
154
+ `tokenline` is built and maintained by **Inbrace** — a software house based in
155
+ Campinas, Brazil, building software with technical and human responsibility. We work
156
+ where security is non-negotiable and architecture is treated as a strategic asset:
157
+ intentional engineering, grounded decisions, and systems built to last.
158
+
159
+ > Inbrace. The human side of software.
160
+
161
+ Learn more at [inbrace.com.br](https://inbrace.com.br) ·
162
+ [LinkedIn](https://www.linkedin.com/company/inbrace-tech/)
163
+
164
+ ## Credits
165
+
166
+ Built by [@ropdias](https://github.com/ropdias) at [Inbrace](https://inbrace.com.br).
167
+
168
+ ## License
169
+
170
+ [MIT](LICENSE) © 2026 Inbrace.
package/dist/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ "use strict";var A=require("fs"),D=require("path");var N=require("os"),l=require("path"),J=(0,l.join)(__dirname,"..","tokenline.sh"),_=e=>e.project?(0,l.resolve)(".claude"):(0,l.join)((0,N.homedir)(),".claude"),$=e=>e.dir?(0,l.resolve)(e.dir,"tokenline.sh"):(0,l.join)(_(e),"tokenline.sh"),d=e=>(0,l.join)(_(e),"settings.json"),q=e=>e.includes(" ")?`bash "${e}"`:`bash ${e}`;var p=require("fs");function f(e){if(!(0,p.existsSync)(e))return{exists:!1,data:{}};let t=(0,p.readFileSync)(e,"utf8");if(t.trim()==="")return{exists:!0,data:{},raw:t};try{return{exists:!0,data:JSON.parse(t),raw:t}}catch{return{exists:!0,data:null,raw:t}}}function x(e){let t=`${e}.bak`;return(0,p.copyFileSync)(e,t),t}var S=e=>typeof e=="string"&&/tokenline\.sh/.test(e);var O=require("child_process"),I=require("os");var E=!!process.stdout.isTTY&&!process.env.NO_COLOR,h=(e,t)=>E?`\x1B[${e}m${t}\x1B[0m`:t,s=e=>h("1",e),W=e=>h("2",e),k=e=>h("32",e),G=e=>h("31",e),K=e=>h("33",e),m=e=>console.log(`${k("\u2713")} ${e}`),i=e=>console.log(`${K("!")} ${e}`),u=e=>console.error(`${G("\u2717")} ${e}`),r=e=>console.log(`${W("\u2192")} ${e}`);function w(){let e=(0,I.platform)();return e==="linux"?(m(`platform: ${e} (supported)`),!0):(i(`platform: ${e} \u2014 v1 targets Linux/WSL2; the bash statusline likely won't render yet (see roadmap). Use --force to install anyway.`),!1)}function v(){let e=(0,O.spawnSync)("jq",["--version"],{encoding:"utf8"});return e.status===0?(m(String(e.stdout).trim()),!0):(i("jq not found \u2014 required at runtime. Install it (apt install jq / brew install jq)."),!1)}function C(){let e=(0,O.spawnSync)("bash",["--version"],{encoding:"utf8"});if(e.status===0){let t=String(e.stdout).match(/version (\d+)\.(\d+)/);return t&&Number(t[1])>=4?(m(`bash ${t[1]}.${t[2]}`),!0):(i(`bash ${t?`${t[1]}.${t[2]}`:"?"} found \u2014 the script needs bash 4+.`),!1)}return i("bash not found \u2014 the statusline runs as a bash script."),!1}function P(){console.log(s(`
3
+ tokenline \u2014 environment check
4
+ `)),w(),C(),v();let e=[{label:"global",project:!1},{label:"project",project:!0}];for(let t of e){let o=d({project:t.project,dir:null}),n=f(o);n.exists&&n.data&&S(n.data.statusLine?.command)?m(`${t.label} settings: tokenline configured (${o})`):n.exists&&n.data===null?i(`${t.label} settings: invalid JSON (${o})`):r(`${t.label} settings: not configured (${o})`)}console.log()}var a=require("fs"),j=require("path");function U(e){if(console.log(s(`
5
+ tokenline \u2014 installing the statusline
6
+ `)),!w()&&!e.force){u("Unsupported platform. Re-run with --force to install anyway."),process.exitCode=1;return}C(),v();let o=$(e),n=d(e),b={type:"command",command:q(o),refreshInterval:1},c=f(n);if(c.exists&&c.data===null){u(`Could not parse ${n} (invalid JSON). Leaving it untouched.`),console.log(`
7
+ Add this block manually, inside the top-level object:
8
+ `),console.log(JSON.stringify({statusLine:b},null,2)+`
9
+ `),process.exitCode=1;return}let y=c.data?c.data.statusLine:void 0,L=y!==void 0&&y.command===b.command,R=y!==void 0&&!L;if(R&&!e.force){u(`A different statusLine is already configured in ${n}:`),console.log(` ${JSON.stringify(y)}`),i("Re-run with --force to replace it."),process.exitCode=1;return}if(e.dryRun){console.log(s(`
10
+ [dry-run] would:`)),r(`write script \u2192 ${o}`),r(`${c.exists?"patch":"create"} settings \u2192 ${n}`),c.exists&&r(`backup \u2192 ${n}.bak`),console.log(`
11
+ statusLine block:
12
+ ${JSON.stringify(b,null,2)}
13
+ `);return}(0,a.mkdirSync)((0,j.dirname)(o),{recursive:!0}),(0,a.copyFileSync)(J,o),(0,a.chmodSync)(o,493),r(`wrote ${o}`);let T=c.data??{};c.exists?(x(n),r(`backed up ${n} \u2192 settings.json.bak`)):(0,a.mkdirSync)((0,j.dirname)(n),{recursive:!0}),T.statusLine=b,(0,a.writeFileSync)(n,JSON.stringify(T,null,2)+`
14
+ `),r(`${L?"confirmed":R?"replaced":"added"} statusLine in ${n}`),console.log(`
15
+ ${k("Done.")} Restart Claude Code to see the statusline.
16
+ `)}var g=require("fs");function B(e){console.log(s(`
17
+ tokenline \u2014 uninstall
18
+ `));let t=d(e),o=f(t);if(!o.exists||o.data===null?i(`No usable settings at ${t} \u2014 nothing to remove.`):S(o.data.statusLine?.command)?e.dryRun?r(`[dry-run] would remove statusLine from ${t}`):(x(t),delete o.data.statusLine,(0,g.writeFileSync)(t,JSON.stringify(o.data,null,2)+`
19
+ `),r(`removed statusLine from ${t} (backup: settings.json.bak)`)):r(`No tokenline statusLine in ${t} \u2014 left untouched.`),e.purge){let n=$(e);(0,g.existsSync)(n)&&(e.dryRun?r(`[dry-run] would delete ${n}`):((0,g.unlinkSync)(n),r(`deleted ${n}`)))}console.log(`
20
+ ${k("Done.")} Restart Claude Code.
21
+ `)}var Y=JSON.parse((0,A.readFileSync)((0,D.join)(__dirname,"..","package.json"),"utf8"));function z(e){let t={_:[],dir:null,project:!1,dryRun:!1,force:!1,purge:!1,help:!1,version:!1,unknown:null};for(let o=0;o<e.length;o++){let n=e[o];switch(n){case"--dir":t.dir=e[++o]??null;break;case"--project":case"--local":t.project=!0;break;case"--dry-run":t.dryRun=!0;break;case"--force":t.force=!0;break;case"--purge":t.purge=!0;break;case"-h":case"--help":t.help=!0;break;case"-v":case"--version":t.version=!0;break;default:n!==void 0&&n.startsWith("-")?t.unknown=n:n!==void 0&&t._.push(n)}}return t}function F(){console.log(`
22
+ ${s("tokenline")} \u2014 a cache-aware statusline for AI coding CLIs
23
+
24
+ ${s("Usage")}
25
+ npx @inbrace-tech/tokenline <command> [options]
26
+
27
+ ${s("Commands")}
28
+ init Install tokenline.sh and wire it into your Claude Code settings
29
+ doctor Check dependencies and current config \u2014 changes nothing
30
+ uninstall Remove the tokenline statusLine block from settings
31
+
32
+ ${s("Options")}
33
+ --project Target ./.claude (project) instead of ~/.claude (global)
34
+ --dir <path> Write tokenline.sh to a custom directory
35
+ --dry-run Show what would happen without writing anything
36
+ --force Proceed on an unsupported platform / replace an existing statusLine
37
+ --purge (uninstall) also delete the installed tokenline.sh
38
+ -h, --help Show this help
39
+ -v, --version Show version
40
+
41
+ ${s("Notes")}
42
+ The statusline runs as 'bash tokenline.sh' \u2014 no Node in the per-second hot path.
43
+ jq is required at runtime; this installer checks for it but cannot install it.
44
+ `)}function H(){let e=z(process.argv.slice(2));if(e.unknown&&i(`ignoring unknown option: ${e.unknown}`),e.version){console.log(Y.version);return}if(e.help||e._.length===0){F();return}try{switch(e._[0]){case"init":U(e);break;case"doctor":P();break;case"uninstall":case"remove":B(e);break;default:u(`Unknown command: ${e._[0]}`),F(),process.exitCode=1}}catch(t){u(t instanceof Error?t.message:String(t)),process.exitCode=1}}H();
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@inbrace-tech/tokenline",
3
+ "version": "1.0.0",
4
+ "description": "A cache-aware statusline for AI coding CLIs — installs tokenline.sh and wires it into your Claude Code settings.",
5
+ "bin": {
6
+ "tokenline": "dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "tokenline.sh"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "antigravity",
18
+ "statusline",
19
+ "tokens",
20
+ "prompt-cache",
21
+ "cli"
22
+ ],
23
+ "author": "Inbrace (https://inbrace.com.br)",
24
+ "contributors": [
25
+ "Rodrigo Pinheiro Dias (https://github.com/ropdias)"
26
+ ],
27
+ "license": "MIT",
28
+ "homepage": "https://github.com/inbrace-tech/tokenline#readme",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/inbrace-tech/tokenline.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/inbrace-tech/tokenline/issues"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "10.0.1",
41
+ "@types/node": "24.13.2",
42
+ "eslint": "10.5.0",
43
+ "eslint-config-prettier": "10.1.8",
44
+ "eslint-plugin-prettier": "5.5.6",
45
+ "eslint-plugin-simple-import-sort": "13.0.0",
46
+ "globals": "17.6.0",
47
+ "prettier": "3.8.4",
48
+ "tsup": "^8.5.1",
49
+ "typescript": "5.9.3",
50
+ "typescript-eslint": "8.61.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "typecheck": "tsc --noEmit -p tsconfig.json",
55
+ "lint": "eslint . --cache --cache-location node_modules/.cache/.eslintcache --fix",
56
+ "format": "prettier --write \"src/**/*.ts\"",
57
+ "format:check": "prettier --check \"src/**/*.ts\""
58
+ }
59
+ }
package/tokenline.sh ADDED
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # ==============================================================================
4
+ # tokenline — a cache-aware statusline for AI coding CLIs
5
+ #
6
+ # Cross-CLI (Claude Code, Antigravity) and cross-provider (Anthropic, Gemini).
7
+ # Renders: model · context · cache TTL (HOT/COLD) · per-turn token economics
8
+ # (read / write / new / output / eq / saving %) · 5h + 7d rate-limit pacing.
9
+ #
10
+ # Repo: https://github.com/inbrace-tech/tokenline
11
+ # License: MIT
12
+ # Requires: bash 4+, jq, GNU coreutils (date -d, stat -c). Linux/WSL2 for v1.
13
+ # ==============================================================================
14
+
15
+ # --- Colors & Formatting Constants ---
16
+ COLOR_GRAY=$'\033[38;5;244m'
17
+ COLOR_DARK_GRAY=$'\033[38;5;240m'
18
+ COLOR_CYAN=$'\033[38;5;51m'
19
+ COLOR_YELLOW=$'\033[38;5;226m'
20
+ COLOR_MAGENTA=$'\033[38;5;201m'
21
+ COLOR_ORANGE=$'\033[38;5;208m'
22
+ COLOR_RED=$'\033[38;5;196m'
23
+ COLOR_GREEN=$'\033[38;5;46m'
24
+ COLOR_RESET=$'\033[00m'
25
+ STYLE_BLINK=$'\033[1;5m'
26
+
27
+ # --- Dependency guard ---
28
+ # A statusline must never crash the host CLI's prompt. Without jq we cannot
29
+ # parse the input JSON, so emit a minimal, explicit hint instead of a blank
30
+ # line, and exit 0 (never signal an error code to the host).
31
+ if ! command -v jq >/dev/null 2>&1; then
32
+ printf '%s[tokenline] jq not found — install jq to enable the statusline%s\n' \
33
+ "$COLOR_GRAY" "$COLOR_RESET"
34
+ exit 0
35
+ fi
36
+
37
+ # --- Runtime state directory ---
38
+ # Per-turn timestamp / TTL are cached between the 1s refreshes. Prefer a
39
+ # per-user dir (0700) under XDG_RUNTIME_DIR: it avoids predictable, world-
40
+ # readable paths in shared /tmp (and the symlink/collision risks they carry),
41
+ # and is tmpfs cleared on logout — so no orphan-file cleanup is needed. Falls
42
+ # back to /tmp when XDG_RUNTIME_DIR is unset.
43
+ _runtime_dir="${XDG_RUNTIME_DIR:-/tmp}/tokenline-${UID:-$(id -u)}"
44
+ mkdir -p "$_runtime_dir" 2>/dev/null && chmod 700 "$_runtime_dir" 2>/dev/null
45
+ [ -d "$_runtime_dir" ] || _runtime_dir="/tmp"
46
+
47
+ # --- 1. Parse JSON Standard Input and Prepare State Variables ---
48
+ parse_and_prepare_paths() {
49
+ # Read full stdin JSON representing active session state from the CLI client
50
+ local input
51
+ input=$(cat)
52
+
53
+ # Single jq execution to parse all required fields into an array at once (reduces forks)
54
+ mapfile -t _f < <(printf '%s' "$input" | jq -r '
55
+ (.model.display_name // ""),
56
+ (.context_window.used_percentage // ""),
57
+ (.context_window.context_window_size // ""),
58
+ (.transcript_path // ""),
59
+ (.session_id // ""),
60
+ (.rate_limits.five_hour.used_percentage // ""),
61
+ (.rate_limits.five_hour.resets_at // ""),
62
+ (.rate_limits.seven_day.used_percentage // ""),
63
+ (.rate_limits.seven_day.resets_at // ""),
64
+ (.context_window.current_usage.input_tokens // 0),
65
+ (.context_window.current_usage.output_tokens // 0),
66
+ (.context_window.current_usage.cache_creation_input_tokens // 0),
67
+ (.context_window.current_usage.cache_read_input_tokens // 0)' 2>/dev/null)
68
+
69
+ # Malformed or empty stdin: jq emits nothing, so the array is empty. Degrade to
70
+ # a silent no-op render rather than leaking parse errors or rendering garbage —
71
+ # the host CLI always sends valid JSON, so this path only guards against abuse.
72
+ [ "${#_f[@]}" -eq 0 ] && exit 0
73
+
74
+ model="${_f[0]}"
75
+ used_pct="${_f[1]}"
76
+ tokens_limit="${_f[2]}"
77
+ transcript_path="${_f[3]}"
78
+ session_id="${_f[4]}"
79
+ rl_5h_pct="${_f[5]}"
80
+ rl_5h_reset="${_f[6]}"
81
+ rl_7d_pct="${_f[7]}"
82
+ rl_7d_reset="${_f[8]}"
83
+ cur_input="${_f[9]}"
84
+ cur_output="${_f[10]}"
85
+ cur_cwrite="${_f[11]}"
86
+ cur_cread="${_f[12]}"
87
+
88
+ # Computed: total input-only tokens used in the current context window
89
+ tokens_used=$((cur_input + cur_cwrite + cur_cread))
90
+
91
+ # If Claude Code sends a subagent transcript, resolve it to the parent session instead
92
+ if [[ "$transcript_path" == */subagents/* ]]; then
93
+ transcript_path="$(dirname "$(dirname "$transcript_path")").jsonl"
94
+ fi
95
+
96
+ # Detect active CLI Client
97
+ cli_client="claude-code"
98
+ if [[ "$transcript_path" == *"/antigravity"* ]] || [[ "$transcript_path" == *"/antigravity-cli"* ]]; then
99
+ cli_client="antigravity"
100
+ fi
101
+
102
+ # Dynamic Path Correction: Translate /antigravity/ to /antigravity-cli/ if client is Antigravity CLI
103
+ if [ "$cli_client" = "antigravity" ] && [[ "$transcript_path" == *"/antigravity/"* ]]; then
104
+ transcript_path="${transcript_path/\/antigravity\//\/antigravity-cli\/}"
105
+ fi
106
+
107
+ # Detect if Gemini model is active
108
+ is_gemini=false
109
+ if [[ "$model" =~ [Gg]emini ]]; then
110
+ is_gemini=true
111
+ fi
112
+
113
+ # Get the current epoch timestamp once to be reused across all calculations
114
+ now=$(date +%s)
115
+ }
116
+
117
+ # --- 2. Formatting Helpers ---
118
+ fmt_k() {
119
+ # Formats token counts nicely (e.g. 1500000 -> 1.5M, 25600 -> 25.6k).
120
+ # Value is passed via -v (defaulted to 0) so a missing or non-numeric arg
121
+ # can never break awk's program syntax.
122
+ awk -v v="${1:-0}" 'BEGIN {
123
+ if (v >= 1000000) printf "%.1fM", v/1000000
124
+ else if (v >= 1000) printf "%.1fk", v/1000
125
+ else printf "%d", v }'
126
+ }
127
+
128
+ fmt_eta() {
129
+ # Formats raw seconds remaining into a human readable string (e.g., 3600 -> 1h, 90 -> 1m30s)
130
+ local secs=$1
131
+ if [ "$secs" -le 0 ]; then
132
+ printf 'now'
133
+ elif [ "$secs" -lt 3600 ]; then
134
+ printf '%dm' $((secs / 60))
135
+ elif [ "$secs" -lt 86400 ]; then
136
+ local h=$((secs / 3600)) m=$(((secs % 3600) / 60))
137
+ if [ "$m" -gt 0 ]; then printf '%dh%dm' "$h" "$m"; else printf '%dh' "$h"; fi
138
+ else
139
+ local d=$((secs / 86400)) h=$(((secs % 86400) / 3600))
140
+ if [ "$h" -gt 0 ]; then printf '%dd%dh' "$d" "$h"; else printf '%dd' "$d"; fi
141
+ fi
142
+ }
143
+
144
+ # --- 3. Cache Timer Logic ---
145
+ compute_cache_timer() {
146
+ cache_info=""
147
+ local ts_cache_file="$_runtime_dir/lastts-${session_id:-default}"
148
+ local ttl_cache_file="$_runtime_dir/ttl-${session_id:-default}"
149
+ local tokens_cache_file="$_runtime_dir/lasttokens-${session_id:-default}"
150
+
151
+ # Update the cached turn timestamp if token usage changed (signaling a new turn)
152
+ local last_tokens
153
+ last_tokens=$(cat "$tokens_cache_file" 2>/dev/null)
154
+ if [ "$tokens_used" -ne "${last_tokens:-0}" ] 2>/dev/null; then
155
+ printf '%s\n' "$now" > "$ts_cache_file" 2>/dev/null
156
+ printf '%s\n' "$tokens_used" > "$tokens_cache_file" 2>/dev/null
157
+ fi
158
+
159
+ local last_ts=""
160
+ local e5m=0
161
+ local e1h=0
162
+
163
+ # Read the last turn's timestamp directly from the transcript file (if readable)
164
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ] && [ -r "$transcript_path" ]; then
165
+ # General Query: Matches .type=="assistant" (Claude Code) or .type=="PLANNER_RESPONSE" (Antigravity CLI)
166
+ # Extracts the dynamic timestamp using (.timestamp // .created_at) and caching flags
167
+ IFS=$'\t' read -r iso e5m e1h < <(
168
+ tail -n 200 "$transcript_path" 2>/dev/null \
169
+ | jq -r 'select(.type=="assistant" or .type=="PLANNER_RESPONSE")
170
+ | [
171
+ (.timestamp // .created_at),
172
+ (.message.usage.cache_creation.ephemeral_5m_input_tokens // 0),
173
+ (.message.usage.cache_creation.ephemeral_1h_input_tokens // 0)
174
+ ]
175
+ | @tsv' 2>/dev/null \
176
+ | tail -n 1
177
+ )
178
+ if [ -n "$iso" ]; then
179
+ last_ts=$(date -d "$iso" +%s 2>/dev/null)
180
+ fi
181
+ # Mtime fallback if parsing is unsuccessful
182
+ [ -z "$last_ts" ] && last_ts=$(stat -c %Y "$transcript_path" 2>/dev/null)
183
+ fi
184
+
185
+ # Fallback to local session caching file if transcript read is unavailable or cached timestamp is newer
186
+ local cached_ts
187
+ cached_ts=$(cat "$ts_cache_file" 2>/dev/null)
188
+ if [ -n "$cached_ts" ]; then
189
+ if [ -z "$last_ts" ] || [ "$cached_ts" -gt "$last_ts" ] 2>/dev/null; then
190
+ last_ts="$cached_ts"
191
+ fi
192
+ fi
193
+
194
+ # Ensure there is always a valid timestamp to fall back to
195
+ if [ -z "$last_ts" ]; then
196
+ last_ts="$now"
197
+ printf '%s\n' "$last_ts" > "$ts_cache_file" 2>/dev/null
198
+ fi
199
+
200
+ # Determine cache TTL window (Gemini has 5m default; Anthropic determines it via tokens fields)
201
+ local ttl
202
+ if [ "$is_gemini" = true ]; then
203
+ ttl=300
204
+ ttl_label="5m"
205
+ else
206
+ if [ "${e1h:-0}" -gt 0 ]; then
207
+ ttl=3600
208
+ ttl_label="1h"
209
+ elif [ "${e5m:-0}" -gt 0 ]; then
210
+ ttl=300
211
+ ttl_label="5m"
212
+ else
213
+ # Retrieve previously determined session TTL if latest turn did not populate these fields (e.g. hit only)
214
+ ttl=$(awk '{print $1}' "$ttl_cache_file" 2>/dev/null)
215
+ ttl_label=$(awk '{print $2}' "$ttl_cache_file" 2>/dev/null)
216
+ [ -z "$ttl" ] && { ttl=300; ttl_label="5m"; }
217
+ fi
218
+ fi
219
+ printf '%s %s\n' "$ttl" "$ttl_label" > "$ttl_cache_file" 2>/dev/null
220
+
221
+ # Calculate remaining time and format the cache information display
222
+ local elapsed
223
+ local remaining
224
+ elapsed=$((now - last_ts))
225
+ remaining=$((ttl - elapsed))
226
+ if [ "$remaining" -gt 0 ]; then
227
+ local mins=$((remaining / 60))
228
+ local secs=$((remaining % 60))
229
+ local pct10=$((remaining * 10 / ttl))
230
+ local fg=""
231
+
232
+ # Custom HSL-based gradient colors for remaining time
233
+ if [ "$pct10" -ge 8 ]; then fg="$COLOR_GREEN"
234
+ elif [ "$pct10" -ge 6 ]; then fg=$'\033[38;5;154m'
235
+ elif [ "$pct10" -ge 4 ]; then fg="$COLOR_YELLOW"
236
+ elif [ "$pct10" -ge 2 ]; then fg="$COLOR_ORANGE"
237
+ elif [ "$pct10" -ge 1 ]; then fg="$COLOR_RED"
238
+ else fg="${COLOR_RED}${STYLE_BLINK}" # Blinking red if < 10%
239
+ fi
240
+
241
+ local suffix="HOT"
242
+ [ "$pct10" -lt 1 ] && suffix="HOT !"
243
+ cache_info=$(printf '%s[%s] cache: %s%d:%02d %s%s' "$COLOR_GRAY" "$ttl_label" "$fg" "$mins" "$secs" "$suffix" "$COLOR_RESET")
244
+ else
245
+ cache_info=$(printf '%s[%s] cache: \033[1;5m%sCOLD%s' "$COLOR_GRAY" "$ttl_label" "$COLOR_RED" "$COLOR_RESET")
246
+ fi
247
+ }
248
+
249
+ # --- 4. Context Window Computation ---
250
+ compute_context_info() {
251
+ ctx_info=""
252
+ if [ -n "$used_pct" ]; then
253
+ local pct
254
+ local ctx_color
255
+ pct=$(printf '%.0f' "$used_pct")
256
+ if [ "$pct" -ge 80 ]; then ctx_color=$'\033[01;31m' # Bold Red
257
+ elif [ "$pct" -ge 50 ]; then ctx_color=$'\033[01;33m' # Bold Yellow
258
+ else ctx_color=$'\033[01;32m' # Bold Green
259
+ fi
260
+
261
+ if [ "${tokens_used:-0}" -gt 0 ] && [ "${tokens_limit:-0}" -gt 0 ]; then
262
+ ctx_info=$(printf '%sctx: %s%s%s/%s (%s%%)%s' \
263
+ "$COLOR_GRAY" "$COLOR_RESET" "$ctx_color" "$(fmt_k "$tokens_used")" "$(fmt_k "$tokens_limit")" "$pct" "$COLOR_RESET")
264
+ else
265
+ ctx_info=$(printf '%sctx: %s%s%s%%%s' "$COLOR_GRAY" "$COLOR_RESET" "$ctx_color" "$pct" "$COLOR_RESET")
266
+ fi
267
+ fi
268
+ }
269
+
270
+ # --- 5. Rate Limit Windows Heuristics and Bars ---
271
+ rl_color_for_pct() {
272
+ local pct=$1
273
+ if [ "$pct" -ge 90 ]; then echo "${COLOR_RED}${STYLE_BLINK}" # Blinking red
274
+ elif [ "$pct" -ge 75 ]; then echo "$COLOR_RED"
275
+ elif [ "$pct" -ge 50 ]; then echo "$COLOR_ORANGE"
276
+ elif [ "$pct" -ge 25 ]; then echo "$COLOR_YELLOW"
277
+ else echo "$COLOR_GREEN"
278
+ fi
279
+ }
280
+
281
+ rl_bar() {
282
+ local pct=$1
283
+ local color=$2
284
+ local width=10
285
+ local filled=$((pct * width / 100))
286
+ [ "$filled" -lt 0 ] && filled=0
287
+ [ "$filled" -gt "$width" ] && filled=$width
288
+
289
+ local empty=$((width - filled))
290
+ local bar="$color"
291
+ local i
292
+ for ((i=0; i<filled; i++)); do bar+="█"; done
293
+ bar+="$COLOR_DARK_GRAY"
294
+ for ((i=0; i<empty; i++)); do bar+="░"; done
295
+ bar+="$COLOR_RESET"
296
+ printf '%s' "$bar"
297
+ }
298
+
299
+ rl_segment() {
300
+ local label=$1
301
+ local pct=$2
302
+ local reset_at=$3
303
+ local window_secs=$4
304
+ local now_ts=$5
305
+ [ -z "$pct" ] && return
306
+ local pct_int; pct_int=$(printf '%.0f' "$pct")
307
+
308
+ local eta_secs=0
309
+ if [ -n "$reset_at" ] && [ "$reset_at" != "null" ]; then
310
+ eta_secs=$((reset_at - now_ts))
311
+ [ "$eta_secs" -lt 0 ] && eta_secs=0
312
+ fi
313
+
314
+ # Pace heuristic: check if we are burning the API limits faster than scheduling
315
+ local pace_marker=""
316
+ if [ "$pct_int" -ge 20 ] && [ "$eta_secs" -gt 0 ] && [ "$window_secs" -gt 0 ]; then
317
+ local elapsed_secs=$((window_secs - eta_secs))
318
+ [ "$elapsed_secs" -lt 0 ] && elapsed_secs=0
319
+ local min_elapsed=$((window_secs / 10))
320
+ if [ "$elapsed_secs" -ge "$min_elapsed" ]; then
321
+ local fast
322
+ fast=$(awk "BEGIN{
323
+ pace = ($pct_int * $window_secs) / ($elapsed_secs * 100)
324
+ if (pace >= 1.5) print 2
325
+ else if (pace >= 1.25) print 1
326
+ else print 0
327
+ }")
328
+ if [ "$fast" = "2" ]; then pace_marker=$(printf '%s!!%s' "${COLOR_RED}${STYLE_BLINK}" "$COLOR_RESET")
329
+ elif [ "$fast" = "1" ]; then pace_marker=$(printf '%s!%s' "$COLOR_ORANGE" "$COLOR_RESET")
330
+ fi
331
+ fi
332
+ fi
333
+
334
+ local color; color=$(rl_color_for_pct "$pct_int")
335
+ local bar; bar=$(rl_bar "$pct_int" "$color")
336
+ local reset_str=""
337
+ [ "$eta_secs" -gt 0 ] && reset_str=$(printf ' (%s to reset)' "$(fmt_eta "$eta_secs")")
338
+
339
+ printf '%s%s: %s%s %s%d%%%s%s%s' \
340
+ "$COLOR_GRAY" "$label" "$COLOR_RESET" \
341
+ "$bar" \
342
+ "$color" "$pct_int" "$COLOR_RESET" \
343
+ "$reset_str" \
344
+ "${pace_marker:+ $pace_marker}"
345
+ }
346
+
347
+ compute_rate_limits() {
348
+ rl_5h_info=""
349
+ rl_7d_info=""
350
+ # Gemini models do not have five_hour / seven_day rate limits; only compute for non-Gemini (Anthropic)
351
+ if [ "$is_gemini" = false ]; then
352
+ [ -n "$rl_5h_pct" ] && rl_5h_info=$(rl_segment "5h" "$rl_5h_pct" "$rl_5h_reset" 18000 "$now")
353
+ [ -n "$rl_7d_pct" ] && rl_7d_info=$(rl_segment "7d" "$rl_7d_pct" "$rl_7d_reset" 604800 "$now")
354
+ fi
355
+ }
356
+
357
+ # --- 6. Last-Turn Token Economics Breakdown & Equivalents ---
358
+ compute_turn_breakdown() {
359
+ last_info=""
360
+ if [ "$cur_cread" -gt 0 ] || [ "$cur_cwrite" -gt 0 ] || [ "$cur_input" -gt 0 ] || [ "$cur_output" -gt 0 ]; then
361
+ local read_mult
362
+ local write_mult
363
+ local input_mult
364
+ local output_mult
365
+
366
+ # Multipliers based on active provider (Gemini equivalents vs Anthropic Claude)
367
+ if [ "$is_gemini" = true ]; then
368
+ read_mult="0.25"
369
+ write_mult="1.0"
370
+ input_mult="1"
371
+ output_mult="4"
372
+ else
373
+ read_mult="0.1"
374
+ write_mult="1.25"
375
+ [ "${ttl_label:-5m}" = "1h" ] && write_mult="2"
376
+ input_mult="1"
377
+ output_mult="5"
378
+ fi
379
+
380
+ # Equivalent tokens formula
381
+ local eq_tokens
382
+ local uncached_eq
383
+ eq_tokens=$(awk "BEGIN { printf \"%d\", ($cur_cread * $read_mult) + ($cur_cwrite * $write_mult) + ($cur_input * $input_mult) + ($cur_output * $output_mult) }")
384
+ uncached_eq=$(awk "BEGIN { printf \"%d\", ($cur_cread + $cur_cwrite + $cur_input) * $input_mult + ($cur_output * $output_mult) }")
385
+
386
+ local saving_pct=0
387
+ [ "$uncached_eq" -gt 0 ] && saving_pct=$(awk "BEGIN { printf \"%d\", 100 * ($uncached_eq - $eq_tokens) / $uncached_eq }")
388
+
389
+ local read_lbl="${read_mult}x"
390
+ local write_lbl="${write_mult}x"
391
+ local input_lbl="${input_mult}x"
392
+ local output_lbl="${output_mult}x"
393
+
394
+ local save_color
395
+ if [ "$saving_pct" -ge 90 ]; then save_color="$COLOR_GREEN"
396
+ elif [ "$saving_pct" -ge 70 ]; then save_color="$COLOR_YELLOW"
397
+ elif [ "$saving_pct" -ge 50 ]; then save_color="$COLOR_ORANGE"
398
+ else save_color="$COLOR_RED"
399
+ fi
400
+
401
+ # Format the complete per-turn breakdown line
402
+ last_info=$(printf '%sread(%s): %s%s%s %swrite(%s): %s%s%s %snew(%s): %s%s%s %soutput(%s): %s%s%s %seq: %s%s%s %ssaving: %s%d%%%s' \
403
+ "$COLOR_GRAY" "$read_lbl" "$COLOR_CYAN" "$(fmt_k "$cur_cread")" "$COLOR_RESET" \
404
+ "$COLOR_GRAY" "$write_lbl" "$COLOR_YELLOW" "$(fmt_k "$cur_cwrite")" "$COLOR_RESET" \
405
+ "$COLOR_GRAY" "$input_lbl" "$COLOR_MAGENTA" "$(fmt_k "$cur_input")" "$COLOR_RESET" \
406
+ "$COLOR_GRAY" "$output_lbl" "$COLOR_GREEN" "$(fmt_k "$cur_output")" "$COLOR_RESET" \
407
+ "$COLOR_GRAY" "$COLOR_ORANGE" "$(fmt_k "$eq_tokens")" "$COLOR_RESET" \
408
+ "$COLOR_GRAY" "$save_color" "$saving_pct" "$COLOR_RESET")
409
+ fi
410
+ }
411
+
412
+ # --- 7. Compose and Render Output ---
413
+ render_statusline() {
414
+ # Line 1: client/model | ctx | cache TTL
415
+ local display_header=""
416
+ if [ "$cli_client" = "antigravity" ]; then
417
+ # Custom styled brand display for Antigravity CLI users
418
+ display_header="${COLOR_CYAN}🌌 Antigravity${COLOR_RESET} (${model})"
419
+ else
420
+ # Default display for Claude Code
421
+ display_header="${model}"
422
+ fi
423
+
424
+ local line1="$display_header"
425
+ [ -n "$ctx_info" ] && line1="$line1 | $ctx_info"
426
+ [ -n "$cache_info" ] && line1="$line1 | $cache_info"
427
+ printf "%s\n" "$line1"
428
+
429
+ # Line 2: breakdown of cache / raw / saving (only shown when activity happens)
430
+ [ -n "$last_info" ] && printf "%s\n" "$last_info"
431
+
432
+ # Line 3: API rate limits and progress bars (Only shown for Claude models where limits exist)
433
+ if [ "$is_gemini" = false ] && { [ -n "$rl_5h_info" ] || [ -n "$rl_7d_info" ]; }; then
434
+ local sep_line="${COLOR_DARK_GRAY}──────────────────────────────${COLOR_RESET}"
435
+ printf "%s\n" "$sep_line"
436
+ local line_rl=""
437
+ [ -n "$rl_5h_info" ] && line_rl="$rl_5h_info"
438
+ [ -n "$rl_7d_info" ] && line_rl="${line_rl:+$line_rl }$rl_7d_info"
439
+ printf "%s\n" "$line_rl"
440
+ fi
441
+ }
442
+
443
+ # --- Orchestrated Execution Flow ---
444
+ parse_and_prepare_paths
445
+ compute_cache_timer
446
+ compute_context_info
447
+ compute_rate_limits
448
+ compute_turn_breakdown
449
+ render_statusline