@inbrace-tech/tokenline 1.1.2 → 1.2.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 +22 -14
- package/dist/cli.js +23 -22
- package/package.json +11 -6
- package/tokenline.sh +36 -5
package/README.md
CHANGED
|
@@ -3,21 +3,28 @@
|
|
|
3
3
|
> See your AI coding costs in real time. Tokenline adds context usage, prompt-cache savings, TTL countdown and rate-limit pacing to Claude Code and Gemini CLI.
|
|
4
4
|
|
|
5
5
|

|
|
6
|
-

|
|
6
|
+

|
|
7
7
|

|
|
8
8
|
|
|
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**
|
|
14
16
|
```bash
|
|
17
|
+
# Per-project installation (./.claude/settings.json)
|
|
15
18
|
npx @inbrace-tech/tokenline init
|
|
19
|
+
|
|
20
|
+
# Global installation (~/.claude/settings.json)
|
|
21
|
+
npx @inbrace-tech/tokenline init --global
|
|
16
22
|
```
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
**Antigravity CLI**
|
|
19
25
|
```bash
|
|
20
|
-
|
|
26
|
+
# Global installation (~/.gemini/antigravity-cli/settings.json)
|
|
27
|
+
npx @inbrace-tech/tokenline init --antigravity
|
|
21
28
|
```
|
|
22
29
|
|
|
23
30
|
### 2. Restart your CLI
|
|
@@ -78,14 +85,16 @@ If `tokenline` reveals you are getting 1-hour cache writes, keep in mind they bu
|
|
|
78
85
|
|
|
79
86
|
## Requirements
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
Runs on **Linux / WSL2** and **macOS**:
|
|
82
89
|
|
|
83
|
-
- `bash` 4 or newer
|
|
90
|
+
- `bash` 4 or newer — macOS ships 3.2, so `brew install bash`
|
|
84
91
|
- [`jq`](https://jqlang.github.io/jq/)
|
|
85
|
-
-
|
|
92
|
+
- `date` and `stat` — GNU (`-d` / `-c`) or BSD (`-j` / `-f`); both are handled
|
|
93
|
+
|
|
94
|
+
On macOS: `brew install bash jq`. BSD `date`/`stat` work as-is; no `coreutils` needed.
|
|
86
95
|
|
|
87
|
-
>
|
|
88
|
-
>
|
|
96
|
+
> Windows support is on the [roadmap](#roadmap). `install.sh` checks all of the
|
|
97
|
+
> above and tells you exactly what's missing.
|
|
89
98
|
|
|
90
99
|
## Advanced Installation
|
|
91
100
|
|
|
@@ -115,7 +124,7 @@ Then restart Claude Code.
|
|
|
115
124
|
|
|
116
125
|
`npx @inbrace-tech/tokenline init` is deliberately transparent about touching your config:
|
|
117
126
|
|
|
118
|
-
- **Writes** `tokenline.sh` to `./.claude/` (or `~/.claude/` with `--global`).
|
|
127
|
+
- **Writes** `tokenline.sh` to `./.claude/` (or `~/.claude/` with `--global`, or `~/.gemini/antigravity-cli/` with `--antigravity`).
|
|
119
128
|
- **Merges** only the `statusLine` key into `settings.json` — every other setting is preserved.
|
|
120
129
|
- **Backs up** `settings.json` to `settings.json.bak` before writing.
|
|
121
130
|
- **Never clobbers** invalid JSON: if it can't parse your `settings.json`, it stops and prints the block to paste manually.
|
|
@@ -125,8 +134,7 @@ Other commands: `doctor` (check dependencies and config, change nothing) and `un
|
|
|
125
134
|
|
|
126
135
|
### Antigravity CLI
|
|
127
136
|
|
|
128
|
-
`tokenline` detects the Antigravity CLI from the transcript path and switches to its provider equivalents automatically.
|
|
129
|
-
`tokenline.sh` — no extra flags needed.
|
|
137
|
+
`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`.
|
|
130
138
|
|
|
131
139
|
## How it works
|
|
132
140
|
|
|
@@ -139,11 +147,11 @@ On every refresh the host CLI pipes a JSON snapshot of the session to the script
|
|
|
139
147
|
| Blank statusline, or `[tokenline] jq not found` | Install `jq` (`apt install jq` / `brew install jq`), then re-run `./install.sh`. |
|
|
140
148
|
| Cache shows `COLD` immediately | Normal right after a long idle gap — the cache window has elapsed. It goes `HOT` again on your next turn. |
|
|
141
149
|
| Colors look wrong / show escape codes | Your terminal must support 256-color ANSI. Most modern terminals do; check your `$TERM`. |
|
|
142
|
-
| Nothing renders on macOS |
|
|
150
|
+
| Nothing renders on macOS | Install `bash` 4+ and `jq`: `brew install bash jq`. Stock bash 3.2 lacks `mapfile`. |
|
|
143
151
|
|
|
144
152
|
## Roadmap
|
|
145
153
|
|
|
146
|
-
- [
|
|
154
|
+
- [x] macOS support (BSD `date`/`stat`, Homebrew bash)
|
|
147
155
|
- [ ] Windows support (Git Bash / PowerShell)
|
|
148
156
|
- [ ] Configurable colors and thresholds via `TOKENLINE_*` env vars
|
|
149
157
|
|
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.1
|
|
3
|
+
"version": "1.2.1",
|
|
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,16 +39,19 @@
|
|
|
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",
|
|
@@ -56,6 +59,8 @@
|
|
|
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
|
+
"test:watch": "vitest",
|
|
59
64
|
"changeset": "changeset",
|
|
60
65
|
"release": "pnpm build && changeset publish"
|
|
61
66
|
}
|
package/tokenline.sh
CHANGED
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
#
|
|
10
10
|
# Repo: https://github.com/inbrace-tech/tokenline
|
|
11
11
|
# License: MIT
|
|
12
|
-
# Requires: bash 4+, jq
|
|
12
|
+
# Requires: bash 4+, jq. Linux/WSL2 or macOS (brew install bash jq).
|
|
13
13
|
# ==============================================================================
|
|
14
14
|
|
|
15
|
+
# Pin C locale: a comma-decimal locale (e.g. pt_BR) makes awk/printf emit
|
|
16
|
+
# "46,2k" and reject dotted input. LC_ALL beats LC_NUMERIC, so set LC_ALL to
|
|
17
|
+
# stay deterministic even when the user exports LC_ALL. Output is ASCII/bytes.
|
|
18
|
+
export LC_ALL=C
|
|
19
|
+
|
|
15
20
|
# --- Colors & Formatting Constants ---
|
|
16
21
|
COLOR_GRAY=$'\033[38;5;244m'
|
|
17
22
|
COLOR_DARK_GRAY=$'\033[38;5;240m'
|
|
@@ -34,6 +39,32 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
34
39
|
exit 0
|
|
35
40
|
fi
|
|
36
41
|
|
|
42
|
+
# --- GNU vs BSD coreutils (Linux vs macOS) ---
|
|
43
|
+
# Probe behavior, not `uname`, so Homebrew coreutils is picked up automatically.
|
|
44
|
+
if date -d "@0" >/dev/null 2>&1; then _date_gnu=1; else _date_gnu=0; fi
|
|
45
|
+
if stat -c %Y . >/dev/null 2>&1; then _stat_gnu=1; else _stat_gnu=0; fi
|
|
46
|
+
|
|
47
|
+
epoch_from_iso() {
|
|
48
|
+
# ISO-8601 -> epoch seconds; empty on failure (callers fall back to mtime).
|
|
49
|
+
local iso="$1"
|
|
50
|
+
if [ "$_date_gnu" -eq 1 ]; then
|
|
51
|
+
date -d "$iso" +%s 2>/dev/null
|
|
52
|
+
else
|
|
53
|
+
# BSD date needs an explicit format and rejects fractional secs / 'Z'.
|
|
54
|
+
# First 19 chars are always 'YYYY-MM-DDTHH:MM:SS'; transcripts are UTC.
|
|
55
|
+
date -u -j -f "%Y-%m-%dT%H:%M:%S" "${iso:0:19}" +%s 2>/dev/null
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
file_mtime() {
|
|
60
|
+
local f="$1"
|
|
61
|
+
if [ "$_stat_gnu" -eq 1 ]; then
|
|
62
|
+
stat -c %Y "$f" 2>/dev/null
|
|
63
|
+
else
|
|
64
|
+
stat -f %m "$f" 2>/dev/null
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
# --- Runtime state directory ---
|
|
38
69
|
# Per-turn timestamp / TTL are cached between the 1s refreshes. Prefer a
|
|
39
70
|
# per-user dir (0700) under XDG_RUNTIME_DIR: it avoids predictable, world-
|
|
@@ -77,9 +108,9 @@ parse_and_prepare_paths() {
|
|
|
77
108
|
transcript_path="${_f[3]}"
|
|
78
109
|
session_id="${_f[4]}"
|
|
79
110
|
rl_5h_pct="${_f[5]}"
|
|
80
|
-
rl_5h_reset
|
|
111
|
+
rl_5h_reset=$(epoch_from_iso "${_f[6]}")
|
|
81
112
|
rl_7d_pct="${_f[7]}"
|
|
82
|
-
rl_7d_reset
|
|
113
|
+
rl_7d_reset=$(epoch_from_iso "${_f[8]}")
|
|
83
114
|
cur_input="${_f[9]}"
|
|
84
115
|
cur_output="${_f[10]}"
|
|
85
116
|
cur_cwrite="${_f[11]}"
|
|
@@ -176,10 +207,10 @@ compute_cache_timer() {
|
|
|
176
207
|
| tail -n 1
|
|
177
208
|
)
|
|
178
209
|
if [ -n "$iso" ]; then
|
|
179
|
-
last_ts=$(
|
|
210
|
+
last_ts=$(epoch_from_iso "$iso")
|
|
180
211
|
fi
|
|
181
212
|
# Mtime fallback if parsing is unsuccessful
|
|
182
|
-
[ -z "$last_ts" ] && last_ts=$(
|
|
213
|
+
[ -z "$last_ts" ] && last_ts=$(file_mtime "$transcript_path")
|
|
183
214
|
fi
|
|
184
215
|
|
|
185
216
|
# Fallback to local session caching file if transcript read is unavailable or cached timestamp is newer
|