@inbrace-tech/tokenline 1.2.3 → 1.3.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 +32 -3
- package/dist/cli.js +30 -21
- package/package.json +5 -3
- package/tokenline.sh +79 -10
package/README.md
CHANGED
|
@@ -30,6 +30,24 @@ 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. About once a day, an installed copy looks up the latest published version in the background. When a newer one exists, it adds one gray line to the statusline:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
↑ tokenline 1.3.0 available · npx @inbrace-tech/tokenline@latest update --global
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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).
|
|
50
|
+
|
|
33
51
|
---
|
|
34
52
|
|
|
35
53
|
## Preview
|
|
@@ -67,7 +85,7 @@ LLMs are stateless — by default, they must reread your entire codebase context
|
|
|
67
85
|
|
|
68
86
|
**Prompt Caching** solves this by saving that processed state in the provider's memory:
|
|
69
87
|
- **Cache Write (Cold):** The model reads everything and stores the state. This costs slightly more than base tokens.
|
|
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.
|
|
88
|
+
- **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 **95%** on Claude Opus 5.5 at 0.05x) and starts responding almost instantly. `tokenline` picks the right multiplier from the active model.
|
|
71
89
|
- **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**.
|
|
72
90
|
|
|
73
91
|
### 💡 Pro-tip: Forcing a 5m TTL to save rate limits
|
|
@@ -97,9 +115,19 @@ On macOS: `brew install bash jq`. BSD `date`/`stat` work as-is; no `coreutils` n
|
|
|
97
115
|
|
|
98
116
|
## Advanced Installation
|
|
99
117
|
|
|
118
|
+
### Without Node (curl)
|
|
119
|
+
|
|
120
|
+
No Node? Download the latest release's `tokenline.sh` to `~/.claude/` in one line:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
curl -fsSL https://github.com/inbrace-tech/tokenline/releases/latest/download/install.sh | bash
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
100
128
|
### Without Node (clone + install.sh)
|
|
101
129
|
|
|
102
|
-
|
|
130
|
+
Prefer a clone? Run the dependency checker, which prints a ready-to-paste snippet:
|
|
103
131
|
|
|
104
132
|
```bash
|
|
105
133
|
git clone https://github.com/inbrace-tech/tokenline.git
|
|
@@ -128,8 +156,9 @@ Then restart Claude Code.
|
|
|
128
156
|
- **Backs up** `settings.json` to `settings.json.bak` before writing.
|
|
129
157
|
- **Never clobbers** invalid JSON: if it can't parse your `settings.json`, it stops and prints the block to paste manually.
|
|
130
158
|
- Is **idempotent**, and won't replace a different existing `statusLine` unless you pass `--force`.
|
|
159
|
+
- **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
160
|
|
|
132
|
-
Other commands: `doctor` (check dependencies and config, change nothing) and `uninstall` (remove the block; `--purge` also deletes the script).
|
|
161
|
+
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
162
|
|
|
134
163
|
### Antigravity CLI
|
|
135
164
|
|
package/dist/cli.js
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var Q=require("fs"),X=require("path");var _=require("os"),u=require("path"),D=(0,u.join)(__dirname,"..","tokenline.sh"),J=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)(J(t),"tokenline.sh"),p=t=>(0,u.join)(J(t),"settings.json"),q=t=>t.targetCli==="antigravity"?"antigravity (global)":t.global?"claude (global)":"claude (project)",A=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 j=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}`),g=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,j.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,j.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
|
-
`)),
|
|
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=q(e),o=p(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+$/,E=t=>{let e="npx @inbrace-tech/tokenline@latest update";return t.targetCli==="antigravity"?`${e} --antigravity`:t.global?`${e} --global`:e};function K(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((v,S)=>v===d?[S]:[]);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 M(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=K((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
|
-
`)),!
|
|
8
|
+
`)),!T()&&!t.force){g("Unsupported platform. Re-run with --force to install anyway."),process.exitCode=1;return}N(),O();let o=w(t),r=p(t),d={type:"command",command:A(o),refreshInterval:1},c=m(r);if(c.exists&&c.data===null){g(`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:
|
|
9
|
-
`),process.exitCode=1;return}let
|
|
10
|
-
[dry-run] would:`)),
|
|
10
|
+
`),console.log(JSON.stringify({statusLine:d},null,2)+`
|
|
11
|
+
`),process.exitCode=1;return}let f=c.data?c.data.statusLine:void 0,v=f!==void 0&&f.command===d.command,S=f!==void 0&&!v;if(S&&!t.force){g(`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(
|
|
13
|
-
`);return}(
|
|
14
|
-
`),
|
|
15
|
-
${
|
|
16
|
-
`)}var
|
|
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(`${v?"confirmed":S?"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=
|
|
19
|
-
`),
|
|
20
|
-
${
|
|
21
|
-
`)}var
|
|
22
|
-
|
|
20
|
+
`));let e=p(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=p(t),o=m(n),r=M(o.data?.statusLine?.command);if(r===null||!(0,Y.existsSync)(r)){g(`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 P=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
|
-
${
|
|
32
|
+
${a("Usage")}
|
|
25
33
|
npx @inbrace-tech/tokenline <command> [options]
|
|
26
34
|
|
|
27
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
|
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(P.version);return}if(t.help||t._.length===0){H();return}try{switch(t._[0]){case"init":W(t,P.version);break;case"update":z(t,P.version);break;case"doctor":B();break;case"uninstall":case"remove":G(t);break;default:g(`Unknown command: ${t._[0]}`),H(),process.exitCode=1}}catch(e){g(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.
|
|
3
|
+
"version": "1.3.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"
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
+
"@changesets/changelog-github": "1.0.1",
|
|
40
41
|
"@changesets/cli": "^3.0.2",
|
|
41
42
|
"@eslint/js": "10.0.1",
|
|
42
|
-
"@types/node": "24.13.
|
|
43
|
+
"@types/node": "24.13.5",
|
|
43
44
|
"@vitest/eslint-plugin": "1.6.27",
|
|
44
45
|
"eslint": "10.10.0",
|
|
45
46
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"eslint-plugin-prettier": "5.5.6",
|
|
48
49
|
"eslint-plugin-simple-import-sort": "13.0.0",
|
|
49
50
|
"globals": "17.12.0",
|
|
50
|
-
"prettier": "3.9.
|
|
51
|
+
"prettier": "3.9.8",
|
|
51
52
|
"tsup": "^8.5.1",
|
|
52
53
|
"typescript": "5.9.3",
|
|
53
54
|
"typescript-eslint": "8.70.0",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"audit:lockfile": "node --experimental-strip-types scripts/check-lockfile-release-age.mts",
|
|
64
65
|
"test:watch": "vitest",
|
|
65
66
|
"changeset": "changeset",
|
|
67
|
+
"changeset:check": "node --experimental-strip-types scripts/check-changeset-size.mts",
|
|
66
68
|
"release": "pnpm build && changeset publish"
|
|
67
69
|
}
|
|
68
70
|
}
|
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'
|
|
@@ -156,13 +163,17 @@ parse_and_prepare_paths() {
|
|
|
156
163
|
is_gemini=true
|
|
157
164
|
fi
|
|
158
165
|
|
|
159
|
-
# Claude Fable 5.1
|
|
160
|
-
#
|
|
161
|
-
# (Fable 5.1) so either field alone is enough.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
# Cache hits bill at 0.1x base input, except Claude Fable 5.1 / Mythos 5.1 (0.025x)
|
|
167
|
+
# and Claude Opus 5.5 (0.05x). Match the API id (claude-fable-5-1[1m]) or the
|
|
168
|
+
# display name (Fable 5.1) so either field alone is enough. Pin the full version:
|
|
169
|
+
# claude-opus-5 must stay 0.1x, and a later version's price is confirmed, not guessed.
|
|
170
|
+
claude_read_mult="0.1"
|
|
171
|
+
local fable_re='(fable|mythos)[- ]5[-.]1([^0-9]|$)'
|
|
172
|
+
local opus_re='opus[- ]5[-.]5([^0-9]|$)'
|
|
173
|
+
if [[ "${model_id,,}" =~ $fable_re ]] || [[ "${model,,}" =~ $fable_re ]]; then
|
|
174
|
+
claude_read_mult="0.025"
|
|
175
|
+
elif [[ "${model_id,,}" =~ $opus_re ]] || [[ "${model,,}" =~ $opus_re ]]; then
|
|
176
|
+
claude_read_mult="0.05"
|
|
166
177
|
fi
|
|
167
178
|
|
|
168
179
|
# Get the current epoch timestamp once to be reused across all calculations
|
|
@@ -425,8 +436,7 @@ compute_turn_breakdown() {
|
|
|
425
436
|
input_mult="1"
|
|
426
437
|
output_mult="4"
|
|
427
438
|
else
|
|
428
|
-
read_mult="
|
|
429
|
-
[ "$discounted_cache_read" = true ] && read_mult="0.025"
|
|
439
|
+
read_mult="$claude_read_mult"
|
|
430
440
|
write_mult="1.25"
|
|
431
441
|
[ "${ttl_label:-5m}" = "1h" ] && write_mult="2"
|
|
432
442
|
input_mult="1"
|
|
@@ -465,7 +475,60 @@ compute_turn_breakdown() {
|
|
|
465
475
|
fi
|
|
466
476
|
}
|
|
467
477
|
|
|
468
|
-
# --- 7.
|
|
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
|
+
# At most once per 24h a detached curl stores the latest published version in
|
|
496
|
+
# the private runtime dir, and the render path only reads that file. Nothing
|
|
497
|
+
# from the session is sent, and nothing downloaded is ever executed.
|
|
498
|
+
# Opt out with TOKENLINE_NO_UPDATE_CHECK=1 (DO_NOT_TRACK=1 is honored too).
|
|
499
|
+
update_info=""
|
|
500
|
+
local semver='^[0-9]+\.[0-9]+\.[0-9]+$'
|
|
501
|
+
[[ "$TOKENLINE_VERSION" =~ $semver ]] || return 0
|
|
502
|
+
[ "${TOKENLINE_NO_UPDATE_CHECK:-}" = "1" ] && return 0
|
|
503
|
+
[ "${DO_NOT_TRACK:-}" = "1" ] && return 0
|
|
504
|
+
# Only a private dir we own: never create or follow files in shared /tmp.
|
|
505
|
+
[ "$_runtime_dir" != "/tmp" ] && [ -O "$_runtime_dir" ] || return 0
|
|
506
|
+
|
|
507
|
+
local latest_file="$_runtime_dir/latest-version"
|
|
508
|
+
local checked_at
|
|
509
|
+
checked_at=$(file_mtime "$latest_file")
|
|
510
|
+
if { [ -z "$checked_at" ] || [ $((now - checked_at)) -ge 86400 ]; } \
|
|
511
|
+
&& command -v curl >/dev/null 2>&1; then
|
|
512
|
+
# Mark the check first, so the refreshes during the fetch don't start another.
|
|
513
|
+
touch "$latest_file" 2>/dev/null
|
|
514
|
+
(
|
|
515
|
+
v=$(curl -fsS --max-time 5 https://registry.npmjs.org/@inbrace-tech/tokenline/latest \
|
|
516
|
+
| jq -r '.version // empty')
|
|
517
|
+
[[ "$v" =~ $semver ]] && printf '%s\n' "$v" > "$latest_file.tmp" \
|
|
518
|
+
&& mv -f "$latest_file.tmp" "$latest_file"
|
|
519
|
+
) < /dev/null > /dev/null 2>&1 &
|
|
520
|
+
disown 2>/dev/null
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
local latest=""
|
|
524
|
+
read -r latest 2>/dev/null < "$latest_file"
|
|
525
|
+
[[ "$latest" =~ $semver ]] || return 0
|
|
526
|
+
version_gt "$latest" "$TOKENLINE_VERSION" || return 0
|
|
527
|
+
update_info=$(printf '%s↑ tokenline %s available · %s%s' \
|
|
528
|
+
"$COLOR_DARK_GRAY" "$latest" "$TOKENLINE_UPDATE_CMD" "$COLOR_RESET")
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# --- 8. Compose and Render Output ---
|
|
469
532
|
render_statusline() {
|
|
470
533
|
# Line 1: client/model | ctx | cache TTL
|
|
471
534
|
local display_header=""
|
|
@@ -494,6 +557,11 @@ render_statusline() {
|
|
|
494
557
|
[ -n "$rl_7d_info" ] && line_rl="${line_rl:+$line_rl }$rl_7d_info"
|
|
495
558
|
printf "%s\n" "$line_rl"
|
|
496
559
|
fi
|
|
560
|
+
|
|
561
|
+
# Last line: update notice (stamped installs with a newer version published)
|
|
562
|
+
if [ -n "$update_info" ]; then
|
|
563
|
+
printf "%s\n" "$update_info"
|
|
564
|
+
fi
|
|
497
565
|
}
|
|
498
566
|
|
|
499
567
|
# --- Orchestrated Execution Flow ---
|
|
@@ -502,4 +570,5 @@ compute_cache_timer
|
|
|
502
570
|
compute_context_info
|
|
503
571
|
compute_rate_limits
|
|
504
572
|
compute_turn_breakdown
|
|
573
|
+
compute_update_notice
|
|
505
574
|
render_statusline
|