@inbrace-tech/tokenline 1.2.0 → 1.2.2
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 +12 -7
- package/dist/cli.js +23 -22
- package/package.json +13 -7
- package/tokenline.sh +15 -3
package/README.md
CHANGED
|
@@ -9,17 +9,23 @@
|
|
|
9
9
|
## Quickstart
|
|
10
10
|
|
|
11
11
|
### 1. Install (via npm)
|
|
12
|
-
If you have **Node 18+**, run this inside your project repository to configure the statusline locally.
|
|
13
12
|
|
|
13
|
+
Run the installer for your AI coding CLI:
|
|
14
|
+
|
|
15
|
+
**Claude Code Per-project installation (`./.claude/settings.json`)**
|
|
14
16
|
```bash
|
|
15
17
|
npx @inbrace-tech/tokenline init
|
|
16
18
|
```
|
|
17
|
-
|
|
18
|
-
*Want it globally for all your projects? Add `--global` to the command.*
|
|
19
|
+
**Claude Code Global installation (`~/.claude/settings.json`)**
|
|
19
20
|
```bash
|
|
20
21
|
npx @inbrace-tech/tokenline init --global
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
**Antigravity CLI Global installation (`~/.gemini/antigravity-cli/settings.json`)**
|
|
25
|
+
```bash
|
|
26
|
+
npx @inbrace-tech/tokenline init --antigravity
|
|
27
|
+
```
|
|
28
|
+
|
|
23
29
|
### 2. Restart your CLI
|
|
24
30
|
|
|
25
31
|
Simply restart Claude Code or Antigravity, and your new statusline will be live.
|
|
@@ -61,7 +67,7 @@ LLMs are stateless — by default, they must reread your entire codebase context
|
|
|
61
67
|
|
|
62
68
|
**Prompt Caching** solves this by saving that processed state in the provider's memory:
|
|
63
69
|
- **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.
|
|
70
|
+
- **Cache Hit (Warm):** If your prompt prefix matches the cached state exactly, the model skips the reading phase. This is **~90% cheaper** (**97.5%** on Claude Fable 5.1 / Mythos 5.1, which bill cache hits at 0.025x base input) and starts responding almost instantly. `tokenline` picks the right multiplier from the active model.
|
|
65
71
|
- **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
72
|
|
|
67
73
|
### 💡 Pro-tip: Forcing a 5m TTL to save rate limits
|
|
@@ -117,7 +123,7 @@ Then restart Claude Code.
|
|
|
117
123
|
|
|
118
124
|
`npx @inbrace-tech/tokenline init` is deliberately transparent about touching your config:
|
|
119
125
|
|
|
120
|
-
- **Writes** `tokenline.sh` to `./.claude/` (or `~/.claude/` with `--global`).
|
|
126
|
+
- **Writes** `tokenline.sh` to `./.claude/` (or `~/.claude/` with `--global`, or `~/.gemini/antigravity-cli/` with `--antigravity`).
|
|
121
127
|
- **Merges** only the `statusLine` key into `settings.json` — every other setting is preserved.
|
|
122
128
|
- **Backs up** `settings.json` to `settings.json.bak` before writing.
|
|
123
129
|
- **Never clobbers** invalid JSON: if it can't parse your `settings.json`, it stops and prints the block to paste manually.
|
|
@@ -127,8 +133,7 @@ Other commands: `doctor` (check dependencies and config, change nothing) and `un
|
|
|
127
133
|
|
|
128
134
|
### Antigravity CLI
|
|
129
135
|
|
|
130
|
-
`tokenline` detects the Antigravity CLI from the transcript path and switches to its provider equivalents automatically.
|
|
131
|
-
`tokenline.sh` — no extra flags needed.
|
|
136
|
+
`tokenline` detects the Antigravity CLI from the transcript path and switches to its provider equivalents automatically. Use `npx @inbrace-tech/tokenline init --antigravity` to write `tokenline.sh` and patch `~/.gemini/antigravity-cli/settings.json`.
|
|
132
137
|
|
|
133
138
|
## How it works
|
|
134
139
|
|
package/dist/cli.js
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var F=require("fs"),E=require("path");var L=require("os"),l=require("path"),I=(0,l.join)(__dirname,"..","tokenline.sh"),J=t=>t.targetCli==="antigravity"?(0,l.join)((0,L.homedir)(),".gemini","antigravity-cli"):t.global?(0,l.join)((0,L.homedir)(),".claude"):(0,l.resolve)(".claude");var $=t=>t.dir?(0,l.resolve)(t.dir,"tokenline.sh"):(0,l.join)(J(t),"tokenline.sh"),g=t=>(0,l.join)(J(t),"settings.json"),_=t=>t.targetCli==="antigravity"?"antigravity (global)":t.global?"claude (global)":"claude (project)",q=t=>t.includes(" ")?`bash "${t}"`:`bash ${t}`;var p=require("fs");function f(t){if(!(0,p.existsSync)(t))return{exists:!1,data:{}};let e=(0,p.readFileSync)(t,"utf8");if(e.trim()==="")return{exists:!0,data:{},raw:e};try{return{exists:!0,data:JSON.parse(e),raw:e}}catch{return{exists:!0,data:null,raw:e}}}function x(t){let e=`${t}.bak`;return(0,p.copyFileSync)(t,e),e}var v=t=>typeof t=="string"&&/tokenline\.sh/.test(t);var O=require("child_process"),P=require("os");var G=!!process.stdout.isTTY&&!process.env.NO_COLOR,k=(t,e)=>G?`\x1B[${t}m${e}\x1B[0m`:e,i=t=>k("1",t),K=t=>k("2",t),b=t=>k("32",t),Y=t=>k("31",t),z=t=>k("33",t),m=t=>console.log(`${b("\u2713")} ${t}`),a=t=>console.log(`${z("!")} ${t}`),d=t=>console.error(`${Y("\u2717")} ${t}`),s=t=>console.log(`${K("\u2192")} ${t}`);function S(){let t=(0,P.platform)();return t==="linux"||t==="darwin"?(m(`platform: ${t} (supported)`),!0):(a(`platform: ${t} \u2014 supported on Linux/WSL2 and macOS; the bash statusline likely won't render yet (see roadmap). Use --force to install anyway.`),!1)}function w(){let t=(0,O.spawnSync)("jq",["--version"],{encoding:"utf8"});return t.status===0?(m(String(t.stdout).trim()),!0):(a("jq not found \u2014 required at runtime. Install it (apt install jq / brew install jq)."),!1)}function C(){let t=(0,O.spawnSync)("bash",["--version"],{encoding:"utf8"});if(t.status===0){let e=String(t.stdout).match(/version (\d+)\.(\d+)/);return e&&Number(e[1])>=4?(m(`bash ${e[1]}.${e[2]}`),!0):(a(`bash ${e?`${e[1]}.${e[2]}`:"?"} found \u2014 the script needs bash 4+.`),!1)}return a("bash not found \u2014 the statusline runs as a bash script."),!1}function U(){console.log(i(`
|
|
3
3
|
tokenline \u2014 environment check
|
|
4
|
-
`)),
|
|
4
|
+
`)),S(),C(),w();let t=[{global:!1,dir:null,targetCli:"claude"},{global:!0,dir:null,targetCli:"claude"},{global:!0,dir:null,targetCli:"antigravity"}];for(let e of t){let r=_(e),n=g(e),o=f(n);o.exists&&o.data&&v(o.data.statusLine?.command)?m(`${r} settings: tokenline configured (${n})`):o.exists&&o.data===null?a(`${r} settings: invalid JSON (${n})`):s(`${r} settings: not configured (${n})`)}console.log()}var c=require("fs"),T=require("path");function A(t){if(console.log(i(`
|
|
5
5
|
tokenline \u2014 installing the statusline
|
|
6
|
-
`)),!
|
|
6
|
+
`)),!S()&&!t.force){d("Unsupported platform. Re-run with --force to install anyway."),process.exitCode=1;return}C(),w();let r=$(t),n=g(t),o={type:"command",command:q(r),refreshInterval:1},u=f(n);if(u.exists&&u.data===null){d(`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:`)),s(`write script \u2192 ${
|
|
8
|
+
`),console.log(JSON.stringify({statusLine:o},null,2)+`
|
|
9
|
+
`),process.exitCode=1;return}let y=u.data?u.data.statusLine:void 0,R=y!==void 0&&y.command===o.command,N=y!==void 0&&!R;if(N&&!t.force){d(`A different statusLine is already configured in ${n}:`),console.log(` ${JSON.stringify(y)}`),a("Re-run with --force to replace it."),process.exitCode=1;return}if(t.dryRun){console.log(i(`
|
|
10
|
+
[dry-run] would:`)),s(`write script \u2192 ${r}`),s(`${u.exists?"patch":"create"} settings \u2192 ${n}`),u.exists&&s(`backup \u2192 ${n}.bak`),console.log(`
|
|
11
11
|
statusLine block:
|
|
12
|
-
${JSON.stringify(
|
|
13
|
-
`);return}(0,
|
|
14
|
-
`),s(`${R?"confirmed":
|
|
15
|
-
${b("Done.")} Restart
|
|
16
|
-
`)}var
|
|
12
|
+
${JSON.stringify(o,null,2)}
|
|
13
|
+
`);return}(0,c.mkdirSync)((0,T.dirname)(r),{recursive:!0}),(0,c.copyFileSync)(I,r),(0,c.chmodSync)(r,493),s(`wrote ${r}`);let j=u.data??{};u.exists?(x(n),s(`backed up ${n} \u2192 settings.json.bak`)):(0,c.mkdirSync)((0,T.dirname)(n),{recursive:!0}),j.statusLine=o,(0,c.writeFileSync)(n,JSON.stringify(j,null,2)+`
|
|
14
|
+
`),s(`${R?"confirmed":N?"replaced":"added"} statusLine in ${n}`);let W=t.targetCli==="antigravity"?"Antigravity CLI":"Claude Code";console.log(`
|
|
15
|
+
${b("Done.")} Restart ${W} to see the statusline.
|
|
16
|
+
`)}var h=require("fs");function B(t){console.log(i(`
|
|
17
17
|
tokenline \u2014 uninstall
|
|
18
|
-
`));let
|
|
19
|
-
`),s(`removed statusLine from ${
|
|
20
|
-
${b("Done.")} Restart
|
|
21
|
-
`)}var
|
|
22
|
-
${
|
|
18
|
+
`));let e=g(t),r=f(e);if(!r.exists||r.data===null?a(`No usable settings at ${e} \u2014 nothing to remove.`):v(r.data.statusLine?.command)?t.dryRun?s(`[dry-run] would remove statusLine from ${e}`):(x(e),delete r.data.statusLine,(0,h.writeFileSync)(e,JSON.stringify(r.data,null,2)+`
|
|
19
|
+
`),s(`removed statusLine from ${e} (backup: settings.json.bak)`)):s(`No tokenline statusLine in ${e} \u2014 left untouched.`),t.purge){let o=$(t);(0,h.existsSync)(o)&&(t.dryRun?s(`[dry-run] would delete ${o}`):((0,h.unlinkSync)(o),s(`deleted ${o}`)))}let n=t.targetCli==="antigravity"?"Antigravity CLI":"Claude Code";console.log(`
|
|
20
|
+
${b("Done.")} Restart ${n}.
|
|
21
|
+
`)}var H=JSON.parse((0,F.readFileSync)((0,E.join)(__dirname,"..","package.json"),"utf8"));function M(t){let e={_:[],targetCli:"claude",dir:null,global:!1,dryRun:!1,force:!1,purge:!1,help:!1,version:!1,unknown:[]};for(let r=0;r<t.length;r++){let n=t[r];switch(n){case"--dir":e.dir=t[++r]??null;break;case"--antigravity":e.targetCli="antigravity",e.global=!0;break;case"--global":e.global=!0;break;case"--dry-run":e.dryRun=!0;break;case"--force":e.force=!0;break;case"--purge":e.purge=!0;break;case"-h":case"--help":e.help=!0;break;case"-v":case"--version":e.version=!0;break;default:n!==void 0&&n.startsWith("-")?e.unknown.push(n):n!==void 0&&e._.push(n)}}return e}function D(){console.log(`
|
|
22
|
+
${i("tokenline")} \u2014 a cache-aware statusline for AI coding CLIs
|
|
23
23
|
|
|
24
|
-
${
|
|
24
|
+
${i("Usage")}
|
|
25
25
|
npx @inbrace-tech/tokenline <command> [options]
|
|
26
26
|
|
|
27
|
-
${
|
|
28
|
-
init Install tokenline.sh and wire it into
|
|
27
|
+
${i("Commands")}
|
|
28
|
+
init Install tokenline.sh and wire it into 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
|
-
${
|
|
32
|
+
${i("Options")}
|
|
33
|
+
--antigravity Target Antigravity CLI (~/.gemini/antigravity-cli)
|
|
33
34
|
--global Target ~/.claude (global) instead of ./.claude (project)
|
|
34
35
|
--dir <path> Write tokenline.sh to a custom directory
|
|
35
36
|
--dry-run Show what would happen without writing anything
|
|
@@ -38,7 +39,7 @@ ${r("Options")}
|
|
|
38
39
|
-h, --help Show this help
|
|
39
40
|
-v, --version Show version
|
|
40
41
|
|
|
41
|
-
${
|
|
42
|
+
${i("Notes")}
|
|
42
43
|
The statusline runs as 'bash tokenline.sh' \u2014 no Node in the per-second hot path.
|
|
43
44
|
jq is required at runtime; this installer checks for it but cannot install it.
|
|
44
|
-
`)}function
|
|
45
|
+
`)}function Q(){let t=M(process.argv.slice(2));if(t.unknown.length>0&&t.unknown.forEach(e=>a(`ignoring unknown option: ${e}`)),t.version){console.log(H.version);return}if(t.help||t._.length===0){D();return}try{switch(t._[0]){case"init":A(t);break;case"doctor":U();break;case"uninstall":case"remove":B(t);break;default:d(`Unknown command: ${t._[0]}`),D(),process.exitCode=1}}catch(e){d(e instanceof Error?e.message:String(e)),process.exitCode=1}}Q();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inbrace-tech/tokenline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "A cache-aware statusline for AI coding CLIs — installs tokenline.sh and wires it into your Claude Code settings.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tokenline": "dist/cli.js"
|
|
@@ -39,23 +39,29 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@changesets/cli": "^2.31.0",
|
|
41
41
|
"@eslint/js": "10.0.1",
|
|
42
|
-
"@types/node": "24.13.
|
|
43
|
-
"eslint": "
|
|
42
|
+
"@types/node": "24.13.3",
|
|
43
|
+
"@vitest/eslint-plugin": "1.6.23",
|
|
44
|
+
"eslint": "10.7.0",
|
|
44
45
|
"eslint-config-prettier": "10.1.8",
|
|
46
|
+
"eslint-plugin-n": "18.2.2",
|
|
45
47
|
"eslint-plugin-prettier": "5.5.6",
|
|
46
48
|
"eslint-plugin-simple-import-sort": "13.0.0",
|
|
47
|
-
"globals": "17.
|
|
48
|
-
"prettier": "3.
|
|
49
|
+
"globals": "17.7.0",
|
|
50
|
+
"prettier": "3.9.5",
|
|
49
51
|
"tsup": "^8.5.1",
|
|
50
52
|
"typescript": "5.9.3",
|
|
51
|
-
"typescript-eslint": "8.
|
|
53
|
+
"typescript-eslint": "8.64.0",
|
|
54
|
+
"vitest": "4.1.10"
|
|
52
55
|
},
|
|
53
56
|
"scripts": {
|
|
54
57
|
"build": "tsup",
|
|
55
|
-
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p scripts/tsconfig.json",
|
|
56
59
|
"lint": "eslint . --cache --cache-location node_modules/.cache/.eslintcache --fix",
|
|
57
60
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
58
61
|
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"audit:lockfile": "node --experimental-strip-types scripts/check-lockfile-release-age.mts",
|
|
64
|
+
"test:watch": "vitest",
|
|
59
65
|
"changeset": "changeset",
|
|
60
66
|
"release": "pnpm build && changeset publish"
|
|
61
67
|
}
|
package/tokenline.sh
CHANGED
|
@@ -95,7 +95,8 @@ parse_and_prepare_paths() {
|
|
|
95
95
|
(.context_window.current_usage.input_tokens // 0),
|
|
96
96
|
(.context_window.current_usage.output_tokens // 0),
|
|
97
97
|
(.context_window.current_usage.cache_creation_input_tokens // 0),
|
|
98
|
-
(.context_window.current_usage.cache_read_input_tokens // 0)
|
|
98
|
+
(.context_window.current_usage.cache_read_input_tokens // 0),
|
|
99
|
+
(.model.id // "")' 2>/dev/null)
|
|
99
100
|
|
|
100
101
|
# Malformed or empty stdin: jq emits nothing, so the array is empty. Degrade to
|
|
101
102
|
# a silent no-op render rather than leaking parse errors or rendering garbage —
|
|
@@ -108,13 +109,14 @@ parse_and_prepare_paths() {
|
|
|
108
109
|
transcript_path="${_f[3]}"
|
|
109
110
|
session_id="${_f[4]}"
|
|
110
111
|
rl_5h_pct="${_f[5]}"
|
|
111
|
-
rl_5h_reset
|
|
112
|
+
rl_5h_reset=$(epoch_from_iso "${_f[6]}")
|
|
112
113
|
rl_7d_pct="${_f[7]}"
|
|
113
|
-
rl_7d_reset
|
|
114
|
+
rl_7d_reset=$(epoch_from_iso "${_f[8]}")
|
|
114
115
|
cur_input="${_f[9]}"
|
|
115
116
|
cur_output="${_f[10]}"
|
|
116
117
|
cur_cwrite="${_f[11]}"
|
|
117
118
|
cur_cread="${_f[12]}"
|
|
119
|
+
model_id="${_f[13]}"
|
|
118
120
|
|
|
119
121
|
# Computed: total input-only tokens used in the current context window
|
|
120
122
|
tokens_used=$((cur_input + cur_cwrite + cur_cread))
|
|
@@ -141,6 +143,15 @@ parse_and_prepare_paths() {
|
|
|
141
143
|
is_gemini=true
|
|
142
144
|
fi
|
|
143
145
|
|
|
146
|
+
# Claude Fable 5.1 and Mythos 5.1 bill cache hits at 0.025x base input instead of
|
|
147
|
+
# the standard 0.1x. Match the API id (claude-fable-5-1[1m]) or the display name
|
|
148
|
+
# (Fable 5.1) so either field alone is enough.
|
|
149
|
+
discounted_cache_read=false
|
|
150
|
+
local discounted_re='(fable|mythos)[- ]5[-.]1([^0-9]|$)'
|
|
151
|
+
if [[ "${model_id,,}" =~ $discounted_re ]] || [[ "${model,,}" =~ $discounted_re ]]; then
|
|
152
|
+
discounted_cache_read=true
|
|
153
|
+
fi
|
|
154
|
+
|
|
144
155
|
# Get the current epoch timestamp once to be reused across all calculations
|
|
145
156
|
now=$(date +%s)
|
|
146
157
|
}
|
|
@@ -402,6 +413,7 @@ compute_turn_breakdown() {
|
|
|
402
413
|
output_mult="4"
|
|
403
414
|
else
|
|
404
415
|
read_mult="0.1"
|
|
416
|
+
[ "$discounted_cache_read" = true ] && read_mult="0.025"
|
|
405
417
|
write_mult="1.25"
|
|
406
418
|
[ "${ttl_label:-5m}" = "1h" ] && write_mult="2"
|
|
407
419
|
input_mult="1"
|