@inbrace-tech/tokenline 1.1.2 → 1.2.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/README.md CHANGED
@@ -3,7 +3,7 @@
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
  ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
6
- ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2-blue)
6
+ ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2%20%7C%20macOS-blue)
7
7
  ![Shell](https://img.shields.io/badge/shell-bash%204%2B-lightgrey)
8
8
 
9
9
  ## Quickstart
@@ -78,14 +78,16 @@ If `tokenline` reveals you are getting 1-hour cache writes, keep in mind they bu
78
78
 
79
79
  ## Requirements
80
80
 
81
- v1 targets **Linux / WSL2**:
81
+ Runs on **Linux / WSL2** and **macOS**:
82
82
 
83
- - `bash` 4 or newer
83
+ - `bash` 4 or newer — macOS ships 3.2, so `brew install bash`
84
84
  - [`jq`](https://jqlang.github.io/jq/)
85
- - GNU coreutils (`date -d`, `stat -c`)
85
+ - `date` and `stat` — GNU (`-d` / `-c`) or BSD (`-j` / `-f`); both are handled
86
86
 
87
- > macOS and Windows support are on the [roadmap](#roadmap). `install.sh` checks all of
88
- > the above and tells you exactly what's missing.
87
+ On macOS: `brew install bash jq`. BSD `date`/`stat` work as-is; no `coreutils` needed.
88
+
89
+ > Windows support is on the [roadmap](#roadmap). `install.sh` checks all of the
90
+ > above and tells you exactly what's missing.
89
91
 
90
92
  ## Advanced Installation
91
93
 
@@ -139,11 +141,11 @@ On every refresh the host CLI pipes a JSON snapshot of the session to the script
139
141
  | Blank statusline, or `[tokenline] jq not found` | Install `jq` (`apt install jq` / `brew install jq`), then re-run `./install.sh`. |
140
142
  | 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
143
  | 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 | Expected on v1 — macOS uses BSD `date`/`stat`. See the [roadmap](#roadmap). |
144
+ | Nothing renders on macOS | Install `bash` 4+ and `jq`: `brew install bash jq`. Stock bash 3.2 lacks `mapfile`. |
143
145
 
144
146
  ## Roadmap
145
147
 
146
- - [ ] macOS support (BSD `date`/`stat`, bash 3.2 fallback)
148
+ - [x] macOS support (BSD `date`/`stat`, Homebrew bash)
147
149
  - [ ] Windows support (Git Bash / PowerShell)
148
150
  - [ ] Configurable colors and thresholds via `TOKENLINE_*` env vars
149
151
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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(`
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),m=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"||e==="darwin"?(m(`platform: ${e} (supported)`),!0):(i(`platform: ${e} \u2014 supported on Linux/WSL2 and macOS; 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(r(`
3
3
  tokenline \u2014 environment check
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(`
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)?m(`${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
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:
@@ -13,10 +13,10 @@ ${JSON.stringify(k,null,2)}
13
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
14
  `),s(`${R?"confirmed":T?"replaced":"added"} statusLine in ${n}`),console.log(`
15
15
  ${b("Done.")} Restart Claude Code to see the statusline.
16
- `)}var m=require("fs");function B(e){console.log(r(`
16
+ `)}var g=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?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(`
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,g.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,g.existsSync)(n)&&(e.dryRun?s(`[dry-run] would delete ${n}`):((0,g.unlinkSync)(n),s(`deleted ${n}`)))}console.log(`
20
20
  ${b("Done.")} Restart Claude Code.
21
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
22
  ${r("tokenline")} \u2014 a cache-aware statusline for AI coding CLIs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inbrace-tech/tokenline",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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"
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, GNU coreutils (date -d, stat -c). Linux/WSL2 for v1.
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-
@@ -176,10 +207,10 @@ compute_cache_timer() {
176
207
  | tail -n 1
177
208
  )
178
209
  if [ -n "$iso" ]; then
179
- last_ts=$(date -d "$iso" +%s 2>/dev/null)
210
+ last_ts=$(epoch_from_iso "$iso")
180
211
  fi
181
212
  # Mtime fallback if parsing is unsuccessful
182
- [ -z "$last_ts" ] && last_ts=$(stat -c %Y "$transcript_path" 2>/dev/null)
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