@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 +21 -0
- package/README.md +170 -0
- package/dist/cli.js +44 -0
- package/package.json +59 -0
- package/tokenline.sh +449 -0
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
|
+

|
|
6
|
+

|
|
7
|
+

|
|
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
|
+

|
|
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
|