@inbrace-tech/tokenline 1.2.4 → 1.4.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
@@ -30,6 +30,26 @@ npx @inbrace-tech/tokenline init --antigravity
30
30
 
31
31
  Simply restart Claude Code or Antigravity, and your new statusline will be live.
32
32
 
33
+ ### Updating
34
+
35
+ The installed `tokenline.sh` is a copy, so it stays on the version you installed until you update it. Use the same scope flag you installed with:
36
+
37
+ ```bash
38
+ npx @inbrace-tech/tokenline@latest update --global # or: update (project), update --antigravity
39
+ ```
40
+
41
+ `update` finds the script your `statusLine` points at and replaces it. It never edits `settings.json`, and there's nothing to restart.
42
+
43
+ You don't have to remember to check. An installed copy looks up the latest published version in the background: when you open a new session (if the last check is over an hour old), and at least every 6 hours in a long one. When a newer version exists, it adds one gray line to the statusline:
44
+
45
+ ```
46
+ ↑ tokenline 1.4.0 available · update: ! npx -y @inbrace-tech/tokenline@latest update --global
47
+ ```
48
+
49
+ In Claude Code, type that line (with the leading `!`) at the prompt to run it without leaving the session. The next refresh shows the new version.
50
+
51
+ The check is a single `GET` to `registry.npmjs.org` for the version number, made only if `curl` is installed. It runs detached, so rendering never waits on the network. Nothing about your session is sent, and nothing downloaded is executed: only a plain `x.y.z` version is accepted and shown. To turn it off, set `TOKENLINE_NO_UPDATE_CHECK=1` (`DO_NOT_TRACK=1` works too).
52
+
33
53
  ---
34
54
 
35
55
  ## Preview
@@ -97,9 +117,19 @@ On macOS: `brew install bash jq`. BSD `date`/`stat` work as-is; no `coreutils` n
97
117
 
98
118
  ## Advanced Installation
99
119
 
120
+ ### Without Node (curl)
121
+
122
+ No Node? Download the latest release's `tokenline.sh` to `~/.claude/` in one line:
123
+
124
+ ```bash
125
+ curl -fsSL https://github.com/inbrace-tech/tokenline/releases/latest/download/install.sh | bash
126
+ ```
127
+
128
+ It checks the dependencies and prints the `statusLine` block to paste into `settings.json` (see below). Run the same command again to update; once the block is in place, there's nothing else to do. Set `TOKENLINE_DIR` to install somewhere else, e.g. `TOKENLINE_DIR=~/.gemini/antigravity-cli` for the Antigravity CLI.
129
+
100
130
  ### Without Node (clone + install.sh)
101
131
 
102
- No Node? Clone the repo and run the dependency checker, which prints a ready-to-paste snippet:
132
+ Prefer a clone? Run the dependency checker, which prints a ready-to-paste snippet:
103
133
 
104
134
  ```bash
105
135
  git clone https://github.com/inbrace-tech/tokenline.git
@@ -128,8 +158,9 @@ Then restart Claude Code.
128
158
  - **Backs up** `settings.json` to `settings.json.bak` before writing.
129
159
  - **Never clobbers** invalid JSON: if it can't parse your `settings.json`, it stops and prints the block to paste manually.
130
160
  - Is **idempotent**, and won't replace a different existing `statusLine` unless you pass `--force`.
161
+ - **Stamps** the copy with the installed version and the command that updates it (two lines near the top of the script). The repo's own `tokenline.sh` stays unstamped.
131
162
 
132
- Other commands: `doctor` (check dependencies and config, change nothing) and `uninstall` (remove the block; `--purge` also deletes the script).
163
+ Other commands: `update` (replace the installed script, see [Updating](#updating)), `doctor` (check dependencies and config, change nothing) and `uninstall` (remove the block; `--purge` also deletes the script).
133
164
 
134
165
  ### Antigravity CLI
135
166
 
package/dist/cli.js CHANGED
@@ -1,35 +1,44 @@
1
1
  #!/usr/bin/env node
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(`
2
+ "use strict";var Q=require("fs"),X=require("path");var _=require("os"),u=require("path"),D=(0,u.join)(__dirname,"..","tokenline.sh"),A=t=>t.targetCli==="antigravity"?(0,u.join)((0,_.homedir)(),".gemini","antigravity-cli"):t.global?(0,u.join)((0,_.homedir)(),".claude"):(0,u.resolve)(".claude");var w=t=>t.dir?(0,u.resolve)(t.dir,"tokenline.sh"):(0,u.join)(A(t),"tokenline.sh"),g=t=>(0,u.join)(A(t),"settings.json"),J=t=>t.targetCli==="antigravity"?"antigravity (global)":t.global?"claude (global)":"claude (project)",q=t=>t.includes(" ")?`bash "${t}"`:`bash ${t}`;var b=require("fs");function m(t){if(!(0,b.existsSync)(t))return{exists:!1,data:{}};let e=(0,b.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 C(t){let e=`${t}.bak`;return(0,b.copyFileSync)(t,e),e}var L=t=>typeof t=="string"&&/tokenline\.sh/.test(t);var P=require("child_process"),F=require("os");var tt=!!process.stdout.isTTY&&!process.env.NO_COLOR,x=(t,e)=>tt?`\x1B[${t}m${e}\x1B[0m`:e,a=t=>x("1",t),et=t=>x("2",t),h=t=>x("32",t),nt=t=>x("31",t),rt=t=>x("33",t),k=t=>console.log(`${h("\u2713")} ${t}`),l=t=>console.log(`${rt("!")} ${t}`),p=t=>console.error(`${nt("\u2717")} ${t}`),i=t=>console.log(`${et("\u2192")} ${t}`);function T(){let t=(0,F.platform)();return t==="linux"||t==="darwin"?(k(`platform: ${t} (supported)`),!0):(l(`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 O(){let t=(0,P.spawnSync)("jq",["--version"],{encoding:"utf8"});return t.status===0?(k(String(t.stdout).trim()),!0):(l("jq not found \u2014 required at runtime. Install it (apt install jq / brew install jq)."),!1)}function N(){let t=(0,P.spawnSync)("bash",["--version"],{encoding:"utf8"});if(t.status===0){let e=String(t.stdout).match(/version (\d+)\.(\d+)/);return e&&Number(e[1])>=4?(k(`bash ${e[1]}.${e[2]}`),!0):(l(`bash ${e?`${e[1]}.${e[2]}`:"?"} found \u2014 the script needs bash 4+.`),!1)}return l("bash not found \u2014 the statusline runs as a bash script."),!1}function B(){console.log(a(`
3
3
  tokenline \u2014 environment check
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(`
4
+ `)),T(),N(),O();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 n=J(e),o=g(e),r=m(o);r.exists&&r.data&&L(r.data.statusLine?.command)?k(`${n} settings: tokenline configured (${o})`):r.exists&&r.data===null?l(`${n} settings: invalid JSON (${o})`):i(`${n} settings: not configured (${o})`)}console.log()}var I=require("fs"),V=require("path");var ot='TOKENLINE_VERSION=""',it='TOKENLINE_UPDATE_CMD=""',st=/^\d+\.\d+\.\d+$/;var E=t=>{let e="npx -y @inbrace-tech/tokenline@latest update";return t.targetCli==="antigravity"?`${e} --antigravity`:t.global?`${e} --global`:e};function M(t,e,n){if(!st.test(e))throw new Error(`Invalid version: ${e}`);if(n.includes("'"))throw new Error("Update command must not contain a single quote");let o=t.split(`
5
+ `),r=(d,c)=>{let f=o.flatMap((S,v)=>S===d?[v]:[]);if(f.length!==1)throw new Error(`Expected exactly one "${d}" line in tokenline.sh`);o[f[0]]=c};return r(ot,`TOKENLINE_VERSION="${e}"`),r(it,`TOKENLINE_UPDATE_CMD='${n}'`),o.join(`
6
+ `)}function K(t){if(typeof t!="string")return null;let e=/^bash (?:"([^"$`\\]+)"|([^\s"'$`\\]+))$/.exec(t),n=e?e[1]??e[2]:void 0;return n!==void 0&&n.endsWith("/tokenline.sh")?n:null}var s=require("fs"),y=require("path");function R(t,e,n){let o=M((0,s.readFileSync)(D,"utf8"),e,n);at(t,o)}function at(t,e){let n=(0,s.existsSync)(t)?(0,s.realpathSync)(t):t;(0,s.mkdirSync)((0,y.dirname)(n),{recursive:!0});let o=(0,y.join)((0,y.dirname)(n),`.${(0,y.basename)(n)}.${process.pid}.tmp`);(0,s.writeFileSync)(o,e,{flag:"wx",mode:493});try{(0,s.chmodSync)(o,493),(0,s.renameSync)(o,n)}catch(r){throw(0,s.rmSync)(o,{force:!0}),r}}function W(t,e){if(console.log(a(`
5
7
  tokenline \u2014 installing the statusline
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(`
8
+ `)),!T()&&!t.force){p("Unsupported platform. Re-run with --force to install anyway."),process.exitCode=1;return}N(),O();let o=w(t),r=g(t),d={type:"command",command:q(o),refreshInterval:1},c=m(r);if(c.exists&&c.data===null){p(`Could not parse ${r} (invalid JSON). Leaving it untouched.`),console.log(`
7
9
  Add this block manually, inside the top-level object:
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(`
10
+ `),console.log(JSON.stringify({statusLine:d},null,2)+`
11
+ `),process.exitCode=1;return}let f=c.data?c.data.statusLine:void 0,S=f!==void 0&&f.command===d.command,v=f!==void 0&&!S;if(v&&!t.force){p(`A different statusLine is already configured in ${r}:`),console.log(` ${JSON.stringify(f)}`),l("Re-run with --force to replace it."),process.exitCode=1;return}if(t.dryRun){console.log(a(`
12
+ [dry-run] would:`)),i(`write script \u2192 ${o}`),i(`${c.exists?"patch":"create"} settings \u2192 ${r}`),c.exists&&i(`backup \u2192 ${r}.bak`),console.log(`
11
13
  statusLine block:
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(`
14
+ ${JSON.stringify(d,null,2)}
15
+ `);return}R(o,e,E(t)),i(`wrote ${o} (v${e})`);let U=c.data??{};c.exists?(C(r),i(`backed up ${r} \u2192 settings.json.bak`)):(0,I.mkdirSync)((0,V.dirname)(r),{recursive:!0}),U.statusLine=d,(0,I.writeFileSync)(r,JSON.stringify(U,null,2)+`
16
+ `),i(`${S?"confirmed":v?"replaced":"added"} statusLine in ${r}`);let Z=t.targetCli==="antigravity"?"Antigravity CLI":"Claude Code";console.log(`
17
+ ${h("Done.")} Restart ${Z} to see the statusline.
18
+ `)}var $=require("fs");function G(t){console.log(a(`
17
19
  tokenline \u2014 uninstall
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
20
+ `));let e=g(t),n=m(e);if(!n.exists||n.data===null?l(`No usable settings at ${e} \u2014 nothing to remove.`):L(n.data.statusLine?.command)?t.dryRun?i(`[dry-run] would remove statusLine from ${e}`):(C(e),delete n.data.statusLine,(0,$.writeFileSync)(e,JSON.stringify(n.data,null,2)+`
21
+ `),i(`removed statusLine from ${e} (backup: settings.json.bak)`)):i(`No tokenline statusLine in ${e} \u2014 left untouched.`),t.purge){let r=w(t);(0,$.existsSync)(r)&&(t.dryRun?i(`[dry-run] would delete ${r}`):((0,$.unlinkSync)(r),i(`deleted ${r}`)))}let o=t.targetCli==="antigravity"?"Antigravity CLI":"Claude Code";console.log(`
22
+ ${h("Done.")} Restart ${o}.
23
+ `)}var Y=require("fs");function z(t,e){console.log(a(`
24
+ tokenline \u2014 updating the statusline
25
+ `));let n=g(t),o=m(n),r=K(o.data?.statusLine?.command);if(r===null||!(0,Y.existsSync)(r)){p(`No installed tokenline.sh found through ${n}.`);let d=t.targetCli==="antigravity"||t.global?"":" (or --global)";console.log(`
26
+ Install it first: npx @inbrace-tech/tokenline init${d}
27
+ `),process.exitCode=1;return}if(t.dryRun){i(`[dry-run] would overwrite ${r} with v${e}`);return}R(r,e,E(t)),i(`updated ${r} \u2192 v${e}`),console.log(`
28
+ ${h("Done.")} The statusline picks up the new version on its next refresh.
29
+ `)}var j=JSON.parse((0,Q.readFileSync)((0,X.join)(__dirname,"..","package.json"),"utf8"));function lt(t){let e={_:[],targetCli:"claude",dir:null,global:!1,dryRun:!1,force:!1,purge:!1,help:!1,version:!1,unknown:[]};for(let n=0;n<t.length;n++){let o=t[n];switch(o){case"--dir":e.dir=t[++n]??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:o!==void 0&&o.startsWith("-")?e.unknown.push(o):o!==void 0&&e._.push(o)}}return e}function H(){console.log(`
30
+ ${a("tokenline")} \u2014 a cache-aware statusline for AI coding CLIs
23
31
 
24
- ${i("Usage")}
32
+ ${a("Usage")}
25
33
  npx @inbrace-tech/tokenline <command> [options]
26
34
 
27
- ${i("Commands")}
35
+ ${a("Commands")}
28
36
  init Install tokenline.sh and wire it into settings
37
+ update Replace the installed tokenline.sh with this version (run it as @latest)
29
38
  doctor Check dependencies and current config \u2014 changes nothing
30
39
  uninstall Remove the tokenline statusLine block from settings
31
40
 
32
- ${i("Options")}
41
+ ${a("Options")}
33
42
  --antigravity Target Antigravity CLI (~/.gemini/antigravity-cli)
34
43
  --global Target ~/.claude (global) instead of ./.claude (project)
35
44
  --dir <path> Write tokenline.sh to a custom directory
@@ -39,7 +48,7 @@ ${i("Options")}
39
48
  -h, --help Show this help
40
49
  -v, --version Show version
41
50
 
42
- ${i("Notes")}
51
+ ${a("Notes")}
43
52
  The statusline runs as 'bash tokenline.sh' \u2014 no Node in the per-second hot path.
44
53
  jq is required at runtime; this installer checks for it but cannot install it.
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();
54
+ `)}function ct(){let t=lt(process.argv.slice(2));if(t.unknown.length>0&&t.unknown.forEach(e=>l(`ignoring unknown option: ${e}`)),t.version){console.log(j.version);return}if(t.help||t._.length===0){H();return}try{switch(t._[0]){case"init":W(t,j.version);break;case"update":z(t,j.version);break;case"doctor":B();break;case"uninstall":case"remove":G(t);break;default:p(`Unknown command: ${t._[0]}`),H(),process.exitCode=1}}catch(e){p(e instanceof Error?e.message:String(e)),process.exitCode=1}}ct();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inbrace-tech/tokenline",
3
- "version": "1.2.4",
3
+ "version": "1.4.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"
@@ -40,7 +40,7 @@
40
40
  "@changesets/changelog-github": "1.0.1",
41
41
  "@changesets/cli": "^3.0.2",
42
42
  "@eslint/js": "10.0.1",
43
- "@types/node": "24.13.4",
43
+ "@types/node": "24.13.5",
44
44
  "@vitest/eslint-plugin": "1.6.27",
45
45
  "eslint": "10.10.0",
46
46
  "eslint-config-prettier": "10.1.8",
@@ -48,7 +48,7 @@
48
48
  "eslint-plugin-prettier": "5.5.6",
49
49
  "eslint-plugin-simple-import-sort": "13.0.0",
50
50
  "globals": "17.12.0",
51
- "prettier": "3.9.6",
51
+ "prettier": "3.9.8",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "5.9.3",
54
54
  "typescript-eslint": "8.70.0",
package/tokenline.sh CHANGED
@@ -17,6 +17,13 @@
17
17
  # stay deterministic even when the user exports LC_ALL. Output is ASCII/bytes.
18
18
  export LC_ALL=C
19
19
 
20
+ # --- Install stamp ---
21
+ # The installer (npm `init`/`update`, or the release asset) rewrites these two
22
+ # lines in the copy it installs: the version it shipped and the exact command
23
+ # that updates this copy. Left empty here, so the repo copy never checks.
24
+ TOKENLINE_VERSION=""
25
+ TOKENLINE_UPDATE_CMD=""
26
+
20
27
  # --- Colors & Formatting Constants ---
21
28
  COLOR_GRAY=$'\033[38;5;244m'
22
29
  COLOR_DARK_GRAY=$'\033[38;5;240m'
@@ -468,7 +475,79 @@ compute_turn_breakdown() {
468
475
  fi
469
476
  }
470
477
 
471
- # --- 7. Compose and Render Output ---
478
+ # --- 7. Update Notice ---
479
+ version_gt() {
480
+ # 0 when x.y.z $1 is newer than x.y.z $2 (both pre-validated). 10# keeps a
481
+ # leading zero from being read as octal.
482
+ local -a a b
483
+ IFS=. read -ra a <<< "$1"
484
+ IFS=. read -ra b <<< "$2"
485
+ local i
486
+ for i in 0 1 2; do
487
+ (( 10#${a[i]} > 10#${b[i]} )) && return 0
488
+ (( 10#${a[i]} < 10#${b[i]} )) && return 1
489
+ done
490
+ return 1
491
+ }
492
+
493
+ compute_update_notice() {
494
+ # Installed (stamped) copies only; the repo copy has no version and never checks.
495
+ # A detached curl stores the latest published version in the private runtime
496
+ # dir, and the render path only reads that file. It runs when a session that
497
+ # hasn't checked yet finds the last check 1h+ old (so a new release shows up
498
+ # when you open a new session), and at least every 6h in a long session.
499
+ # Nothing from the session is sent, and nothing downloaded is ever executed.
500
+ # Opt out with TOKENLINE_NO_UPDATE_CHECK=1 (DO_NOT_TRACK=1 is honored too).
501
+ update_info=""
502
+ local semver='^[0-9]+\.[0-9]+\.[0-9]+$'
503
+ [[ "$TOKENLINE_VERSION" =~ $semver ]] || return 0
504
+ [ "${TOKENLINE_NO_UPDATE_CHECK:-}" = "1" ] && return 0
505
+ [ "${DO_NOT_TRACK:-}" = "1" ] && return 0
506
+ # Only a private dir we own: never create or follow files in shared /tmp.
507
+ [ "$_runtime_dir" != "/tmp" ] && [ -O "$_runtime_dir" ] || return 0
508
+
509
+ local latest_file="$_runtime_dir/latest-version"
510
+ local session_file="$_runtime_dir/latest-version.session"
511
+ local checked_at checked_by="" due=false age
512
+ checked_at=$(file_mtime "$latest_file")
513
+ read -r checked_by 2>/dev/null < "$session_file"
514
+ if [ -z "$checked_at" ]; then
515
+ due=true
516
+ else
517
+ age=$((now - checked_at))
518
+ if [ "$age" -ge 21600 ]; then
519
+ due=true
520
+ elif [ "$age" -ge 3600 ] && [ -n "$session_id" ] && [ "$session_id" != "$checked_by" ]; then
521
+ due=true
522
+ fi
523
+ fi
524
+ if [ "$due" = true ] && command -v curl >/dev/null 2>&1; then
525
+ # Mark the check first (time and session), so the refreshes during the fetch
526
+ # don't start another one.
527
+ touch "$latest_file" 2>/dev/null
528
+ printf '%s\n' "$session_id" 2>/dev/null > "$session_file"
529
+ (
530
+ v=$(curl -fsS --max-time 5 https://registry.npmjs.org/@inbrace-tech/tokenline/latest \
531
+ | jq -r '.version // empty')
532
+ [[ "$v" =~ $semver ]] && printf '%s\n' "$v" > "$latest_file.tmp" \
533
+ && mv -f "$latest_file.tmp" "$latest_file"
534
+ ) < /dev/null > /dev/null 2>&1 &
535
+ disown 2>/dev/null
536
+ fi
537
+
538
+ local latest=""
539
+ read -r latest 2>/dev/null < "$latest_file"
540
+ [[ "$latest" =~ $semver ]] || return 0
541
+ version_gt "$latest" "$TOKENLINE_VERSION" || return 0
542
+ # In Claude Code, a leading "!" runs the command from the prompt, so the
543
+ # update never needs another terminal.
544
+ local run_hint=""
545
+ [ "$cli_client" = "claude-code" ] && run_hint="! "
546
+ update_info=$(printf '%s↑ tokenline %s available · update: %s%s%s' \
547
+ "$COLOR_DARK_GRAY" "$latest" "$run_hint" "$TOKENLINE_UPDATE_CMD" "$COLOR_RESET")
548
+ }
549
+
550
+ # --- 8. Compose and Render Output ---
472
551
  render_statusline() {
473
552
  # Line 1: client/model | ctx | cache TTL
474
553
  local display_header=""
@@ -497,6 +576,11 @@ render_statusline() {
497
576
  [ -n "$rl_7d_info" ] && line_rl="${line_rl:+$line_rl }$rl_7d_info"
498
577
  printf "%s\n" "$line_rl"
499
578
  fi
579
+
580
+ # Last line: update notice (stamped installs with a newer version published)
581
+ if [ -n "$update_info" ]; then
582
+ printf "%s\n" "$update_info"
583
+ fi
500
584
  }
501
585
 
502
586
  # --- Orchestrated Execution Flow ---
@@ -505,4 +589,5 @@ compute_cache_timer
505
589
  compute_context_info
506
590
  compute_rate_limits
507
591
  compute_turn_breakdown
592
+ compute_update_notice
508
593
  render_statusline