@inbrace-tech/tokenline 1.0.1 → 1.1.1
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/README.md +70 -69
- package/dist/cli.js +21 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,40 +6,75 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
your
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
### 1. Install (via npm)
|
|
12
|
+
If you have **Node 18+**, run this inside your project repository to configure the statusline locally.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @inbrace-tech/tokenline init
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
*Want it globally for all your projects? Add `--global` to the command.*
|
|
19
|
+
```bash
|
|
20
|
+
npx @inbrace-tech/tokenline init --global
|
|
21
|
+
```
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
### 2. Restart your CLI
|
|
24
|
+
|
|
25
|
+
Simply restart Claude Code or Antigravity, and your new statusline will be live.
|
|
26
|
+
|
|
27
|
+
---
|
|
17
28
|
|
|
18
29
|
## Preview
|
|
19
30
|
|
|
20
31
|

|
|
21
32
|
|
|
22
33
|
- **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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
- **Line 2** — per-turn token economics: `read / write / new / output`, the equivalent billed tokens (`eq`), and the **`saving %`** you get from prompt caching.
|
|
35
|
+
- **Line 3** — 5h and 7d rate-limit bars with reset ETA and a **pace marker** (`!!` = you're burning the window faster than it refills).
|
|
36
|
+
|
|
37
|
+
> Lines 2 and 3 appear only when there's something to show (a turn happened, limits exist), so the bar stays quiet when idle.
|
|
38
|
+
|
|
39
|
+
## About
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
`tokenline` turns the one-line status bar of your AI coding CLI into a live cockpit:
|
|
42
|
+
* Which model you're on.
|
|
43
|
+
* How much context you've burned.
|
|
44
|
+
* How long your prompt cache stays hot.
|
|
45
|
+
* How many tokens you're saving by reusing it.
|
|
46
|
+
* How close you are to your 5h / 7d rate limits.
|
|
47
|
+
|
|
48
|
+
It is **cross-CLI** (Claude Code, Antigravity) and **cross-provider** (Anthropic, Gemini) — detecting the active client and model provider at runtime and adjusting the cost equivalents accordingly.
|
|
30
49
|
|
|
31
50
|
## Why tokenline?
|
|
32
51
|
|
|
33
|
-
Most statuslines show the model and the context bar. `tokenline` adds the two things
|
|
34
|
-
|
|
52
|
+
Most statuslines show the model and the context bar. `tokenline` adds the two things that actually drive cost and flow on long agent sessions:
|
|
53
|
+
|
|
54
|
+
- **Cache visibility.** Anthropic and Gemini bill cached input tokens at a fraction of the price — but the cache expires. `tokenline` shows the TTL countdown (5m or 1h, detected from the data) so you know whether your next turn lands warm or cold.
|
|
55
|
+
- **Savings, quantified.** The `saving %` makes the value of prompt caching concrete instead of invisible.
|
|
56
|
+
- **Rate-limit pacing.** The `!!` marker warns you when you're on track to hit the 5h or 7d ceiling before it resets.
|
|
57
|
+
|
|
58
|
+
## A 30-second guide to Prompt Caching
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
- **
|
|
42
|
-
|
|
60
|
+
LLMs are stateless — by default, they must reread your entire codebase context (system prompt, tools, file history) on *every single turn* before generating a response. This phase (Prefill) is slow and expensive.
|
|
61
|
+
|
|
62
|
+
**Prompt Caching** solves this by saving that processed state in the provider's memory:
|
|
63
|
+
- **Cache Write (Cold):** The model reads everything and stores the state. This costs slightly more than base tokens.
|
|
64
|
+
- **Cache Hit (Warm):** If your prompt prefix matches the cached state exactly, the model skips the reading phase. This is **~90% cheaper** and starts responding almost instantly.
|
|
65
|
+
- **TTL (Time-To-Live):** The cache operates as a **sliding window**. Every cache hit resets the countdown to its full duration for free. The secret to minimizing rate-limit drain is adjusting your workflow pace to keep the cache continuously **HOT**.
|
|
66
|
+
|
|
67
|
+
### 💡 Pro-tip: Forcing a 5m TTL to save rate limits
|
|
68
|
+
|
|
69
|
+
Anthropic's rules for assigning cache TTLs are dynamic and can change without notice (e.g., historically granting a 1-hour TTL to Claude.ai Max users). This is exactly why `tokenline` is valuable: it doesn't guess your TTL based on your plan. It parses the actual CLI telemetry in real-time to detect whether your last turn was processed as a 5-minute or 1-hour write, and adapts the UI countdown and token economics accordingly.
|
|
70
|
+
|
|
71
|
+
If `tokenline` reveals you are getting 1-hour cache writes, keep in mind they burn **2x** the tokens from your 5h/7d rate limit (compared to 1.25x for a 5-minute write). If you find this is exhausting your quota too quickly—especially when running multiple subagents that cool down fast—you can force the CLI to fall back to the cheaper 5-minute writes by adding this to your `.claude/settings.json`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
"env": {
|
|
75
|
+
"FORCE_PROMPT_CACHING_5M": "1"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
43
78
|
|
|
44
79
|
## Requirements
|
|
45
80
|
|
|
@@ -52,29 +87,11 @@ v1 targets **Linux / WSL2**:
|
|
|
52
87
|
> macOS and Windows support are on the [roadmap](#roadmap). `install.sh` checks all of
|
|
53
88
|
> the above and tells you exactly what's missing.
|
|
54
89
|
|
|
55
|
-
##
|
|
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.
|
|
90
|
+
## Advanced Installation
|
|
73
91
|
|
|
74
92
|
### Without Node (clone + install.sh)
|
|
75
93
|
|
|
76
|
-
No Node? Clone the repo and run the dependency checker, which prints a
|
|
77
|
-
ready-to-paste snippet:
|
|
94
|
+
No Node? Clone the repo and run the dependency checker, which prints a ready-to-paste snippet:
|
|
78
95
|
|
|
79
96
|
```bash
|
|
80
97
|
git clone https://github.com/inbrace-tech/tokenline.git
|
|
@@ -82,8 +99,7 @@ cd tokenline
|
|
|
82
99
|
./install.sh
|
|
83
100
|
```
|
|
84
101
|
|
|
85
|
-
Add the printed block to `~/.claude/settings.json`
|
|
86
|
-
`.claude/settings.json`, inside the top-level object:
|
|
102
|
+
Add the printed block to your project's `.claude/settings.json` (or `~/.claude/settings.json` for global), inside the top-level object:
|
|
87
103
|
|
|
88
104
|
```json
|
|
89
105
|
"statusLine": {
|
|
@@ -97,34 +113,24 @@ Then restart Claude Code.
|
|
|
97
113
|
|
|
98
114
|
### What the installer does
|
|
99
115
|
|
|
100
|
-
`npx @inbrace-tech/tokenline init` is deliberately transparent about touching
|
|
101
|
-
your config:
|
|
116
|
+
`npx @inbrace-tech/tokenline init` is deliberately transparent about touching your config:
|
|
102
117
|
|
|
103
|
-
- **Writes** `tokenline.sh` to
|
|
104
|
-
- **Merges** only the `statusLine` key into `settings.json` — every other
|
|
105
|
-
setting is preserved.
|
|
118
|
+
- **Writes** `tokenline.sh` to `./.claude/` (or `~/.claude/` with `--global`).
|
|
119
|
+
- **Merges** only the `statusLine` key into `settings.json` — every other setting is preserved.
|
|
106
120
|
- **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
|
-
|
|
109
|
-
- Is **idempotent**, and won't replace a different existing `statusLine` unless
|
|
110
|
-
you pass `--force`.
|
|
121
|
+
- **Never clobbers** invalid JSON: if it can't parse your `settings.json`, it stops and prints the block to paste manually.
|
|
122
|
+
- Is **idempotent**, and won't replace a different existing `statusLine` unless you pass `--force`.
|
|
111
123
|
|
|
112
|
-
Other commands: `doctor` (check dependencies and config, change nothing) and
|
|
113
|
-
`uninstall` (remove the block; `--purge` also deletes the script).
|
|
124
|
+
Other commands: `doctor` (check dependencies and config, change nothing) and `uninstall` (remove the block; `--purge` also deletes the script).
|
|
114
125
|
|
|
115
126
|
### Antigravity CLI
|
|
116
127
|
|
|
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
|
|
128
|
+
`tokenline` detects the Antigravity CLI from the transcript path and switches to its provider equivalents automatically. Point Antigravity's statusline command at the same
|
|
119
129
|
`tokenline.sh` — no extra flags needed.
|
|
120
130
|
|
|
121
131
|
## How it works
|
|
122
132
|
|
|
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).
|
|
133
|
+
On every refresh the host CLI pipes a JSON snapshot of the session to the script over stdin. `tokenline` parses it in a single `jq` pass (to keep the per-second refresh cheap), reads the last turn's timestamp from the transcript to drive the cache countdown, and renders up to three lines. Per-turn timestamps are cached in a per-user `0700` directory under `$XDG_RUNTIME_DIR` (tmpfs, cleared on logout).
|
|
128
134
|
|
|
129
135
|
## Troubleshooting
|
|
130
136
|
|
|
@@ -145,16 +151,11 @@ per-user `0700` directory under `$XDG_RUNTIME_DIR` (tmpfs, cleared on logout).
|
|
|
145
151
|
|
|
146
152
|
## Contributing
|
|
147
153
|
|
|
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.
|
|
154
|
+
Issues and PRs are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). The CI runs [ShellCheck](https://www.shellcheck.net/) on every push, so please keep the script lint-clean.
|
|
151
155
|
|
|
152
156
|
## About Inbrace
|
|
153
157
|
|
|
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
|
+
`tokenline` is built and maintained by **Inbrace** — a software house based in Campinas, Brazil, building software with technical and human responsibility. We work where security is non-negotiable and architecture is treated as a strategic asset: intentional engineering, grounded decisions, and systems built to last.
|
|
158
159
|
|
|
159
160
|
> Inbrace. The human side of software.
|
|
160
161
|
|
package/dist/cli.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var A=require("fs"),D=require("path");var
|
|
2
|
+
"use strict";var A=require("fs"),D=require("path");var j=require("os"),c=require("path"),J=(0,c.join)(__dirname,"..","tokenline.sh"),_=e=>e.global?(0,c.join)((0,j.homedir)(),".claude"):(0,c.resolve)(".claude"),$=e=>e.dir?(0,c.resolve)(e.dir,"tokenline.sh"):(0,c.join)(_(e),"tokenline.sh"),d=e=>(0,c.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,r=e=>h("1",e),W=e=>h("2",e),b=e=>h("32",e),G=e=>h("31",e),K=e=>h("33",e),g=e=>console.log(`${b("\u2713")} ${e}`),i=e=>console.log(`${K("!")} ${e}`),u=e=>console.error(`${G("\u2717")} ${e}`),s=e=>console.log(`${W("\u2192")} ${e}`);function w(){let e=(0,I.platform)();return e==="linux"?(g(`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?(g(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?(g(`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(r(`
|
|
3
3
|
tokenline \u2014 environment check
|
|
4
|
-
`)),w(),C(),v();let e=[{label:"
|
|
4
|
+
`)),w(),C(),v();let e=[{label:"project",global:!1},{label:"global",global:!0}];for(let t of e){let o=d({global:t.global,dir:null}),n=f(o);n.exists&&n.data&&S(n.data.statusLine?.command)?g(`${t.label} settings: tokenline configured (${o})`):n.exists&&n.data===null?i(`${t.label} settings: invalid JSON (${o})`):s(`${t.label} settings: not configured (${o})`)}console.log()}var a=require("fs"),L=require("path");function U(e){if(console.log(r(`
|
|
5
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),
|
|
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),k={type:"command",command:q(o),refreshInterval:1},l=f(n);if(l.exists&&l.data===null){u(`Could not parse ${n} (invalid JSON). Leaving it untouched.`),console.log(`
|
|
7
7
|
Add this block manually, inside the top-level object:
|
|
8
|
-
`),console.log(JSON.stringify({statusLine:
|
|
9
|
-
`),process.exitCode=1;return}let y=
|
|
10
|
-
[dry-run] would:`)),
|
|
8
|
+
`),console.log(JSON.stringify({statusLine:k},null,2)+`
|
|
9
|
+
`),process.exitCode=1;return}let y=l.data?l.data.statusLine:void 0,R=y!==void 0&&y.command===k.command,T=y!==void 0&&!R;if(T&&!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(r(`
|
|
10
|
+
[dry-run] would:`)),s(`write script \u2192 ${o}`),s(`${l.exists?"patch":"create"} settings \u2192 ${n}`),l.exists&&s(`backup \u2192 ${n}.bak`),console.log(`
|
|
11
11
|
statusLine block:
|
|
12
|
-
${JSON.stringify(
|
|
13
|
-
`);return}(0,a.mkdirSync)((0,
|
|
14
|
-
`),
|
|
15
|
-
${
|
|
16
|
-
`)}var
|
|
12
|
+
${JSON.stringify(k,null,2)}
|
|
13
|
+
`);return}(0,a.mkdirSync)((0,L.dirname)(o),{recursive:!0}),(0,a.copyFileSync)(J,o),(0,a.chmodSync)(o,493),s(`wrote ${o}`);let N=l.data??{};l.exists?(x(n),s(`backed up ${n} \u2192 settings.json.bak`)):(0,a.mkdirSync)((0,L.dirname)(n),{recursive:!0}),N.statusLine=k,(0,a.writeFileSync)(n,JSON.stringify(N,null,2)+`
|
|
14
|
+
`),s(`${R?"confirmed":T?"replaced":"added"} statusLine in ${n}`),console.log(`
|
|
15
|
+
${b("Done.")} Restart Claude Code to see the statusline.
|
|
16
|
+
`)}var m=require("fs");function B(e){console.log(r(`
|
|
17
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?
|
|
19
|
-
`),
|
|
20
|
-
${
|
|
21
|
-
`)}var Y=JSON.parse((0,A.readFileSync)((0,D.join)(__dirname,"..","package.json"),"utf8"));function z(e){let t={_:[],dir:null,
|
|
22
|
-
${
|
|
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?s(`[dry-run] would remove statusLine from ${t}`):(x(t),delete o.data.statusLine,(0,m.writeFileSync)(t,JSON.stringify(o.data,null,2)+`
|
|
19
|
+
`),s(`removed statusLine from ${t} (backup: settings.json.bak)`)):s(`No tokenline statusLine in ${t} \u2014 left untouched.`),e.purge){let n=$(e);(0,m.existsSync)(n)&&(e.dryRun?s(`[dry-run] would delete ${n}`):((0,m.unlinkSync)(n),s(`deleted ${n}`)))}console.log(`
|
|
20
|
+
${b("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,global:!1,dryRun:!1,force:!1,purge:!1,help:!1,version:!1,unknown:[]};for(let o=0;o<e.length;o++){let n=e[o];switch(n){case"--dir":t.dir=e[++o]??null;break;case"--global":t.global=!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.push(n):n!==void 0&&t._.push(n)}}return t}function F(){console.log(`
|
|
22
|
+
${r("tokenline")} \u2014 a cache-aware statusline for AI coding CLIs
|
|
23
23
|
|
|
24
|
-
${
|
|
24
|
+
${r("Usage")}
|
|
25
25
|
npx @inbrace-tech/tokenline <command> [options]
|
|
26
26
|
|
|
27
|
-
${
|
|
27
|
+
${r("Commands")}
|
|
28
28
|
init Install tokenline.sh and wire it into your Claude Code settings
|
|
29
29
|
doctor Check dependencies and current config \u2014 changes nothing
|
|
30
30
|
uninstall Remove the tokenline statusLine block from settings
|
|
31
31
|
|
|
32
|
-
${
|
|
33
|
-
--
|
|
32
|
+
${r("Options")}
|
|
33
|
+
--global Target ~/.claude (global) instead of ./.claude (project)
|
|
34
34
|
--dir <path> Write tokenline.sh to a custom directory
|
|
35
35
|
--dry-run Show what would happen without writing anything
|
|
36
36
|
--force Proceed on an unsupported platform / replace an existing statusLine
|
|
@@ -38,7 +38,7 @@ ${s("Options")}
|
|
|
38
38
|
-h, --help Show this help
|
|
39
39
|
-v, --version Show version
|
|
40
40
|
|
|
41
|
-
${
|
|
41
|
+
${r("Notes")}
|
|
42
42
|
The statusline runs as 'bash tokenline.sh' \u2014 no Node in the per-second hot path.
|
|
43
43
|
jq is required at runtime; this installer checks for it but cannot install it.
|
|
44
44
|
`)}function H(){let e=z(process.argv.slice(2));if(e.unknown.length>0&&e.unknown.forEach(t=>i(`ignoring unknown option: ${t}`)),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
CHANGED