@inbrace-tech/tokenline 1.3.0 → 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
@@ -40,12 +40,14 @@ npx @inbrace-tech/tokenline@latest update --global # or: update (project), upd
40
40
 
41
41
  `update` finds the script your `statusLine` points at and replaces it. It never edits `settings.json`, and there's nothing to restart.
42
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:
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
44
 
45
45
  ```
46
- ↑ tokenline 1.3.0 available · npx @inbrace-tech/tokenline@latest update --global
46
+ ↑ tokenline 1.4.0 available · update: ! npx -y @inbrace-tech/tokenline@latest update --global
47
47
  ```
48
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
+
49
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).
50
52
 
51
53
  ---
package/dist/cli.js CHANGED
@@ -1,32 +1,32 @@
1
1
  #!/usr/bin/env node
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(`
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
- `)),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(`
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(`
7
7
  tokenline \u2014 installing the statusline
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(`
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(`
9
9
  Add this block manually, inside the top-level object:
10
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(`
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
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(`
13
13
  statusLine block:
14
14
  ${JSON.stringify(d,null,2)}
15
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(`
16
+ `),i(`${S?"confirmed":v?"replaced":"added"} statusLine in ${r}`);let Z=t.targetCli==="antigravity"?"Antigravity CLI":"Claude Code";console.log(`
17
17
  ${h("Done.")} Restart ${Z} to see the statusline.
18
18
  `)}var $=require("fs");function G(t){console.log(a(`
19
19
  tokenline \u2014 uninstall
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)+`
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
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
22
  ${h("Done.")} Restart ${o}.
23
23
  `)}var Y=require("fs");function z(t,e){console.log(a(`
24
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(`
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
26
  Install it first: npx @inbrace-tech/tokenline init${d}
27
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
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(`
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
30
  ${a("tokenline")} \u2014 a cache-aware statusline for AI coding CLIs
31
31
 
32
32
  ${a("Usage")}
@@ -51,4 +51,4 @@ ${a("Options")}
51
51
  ${a("Notes")}
52
52
  The statusline runs as 'bash tokenline.sh' \u2014 no Node in the per-second hot path.
53
53
  jq is required at runtime; this installer checks for it but cannot install it.
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();
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.3.0",
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"
package/tokenline.sh CHANGED
@@ -492,9 +492,11 @@ version_gt() {
492
492
 
493
493
  compute_update_notice() {
494
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.
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.
498
500
  # Opt out with TOKENLINE_NO_UPDATE_CHECK=1 (DO_NOT_TRACK=1 is honored too).
499
501
  update_info=""
500
502
  local semver='^[0-9]+\.[0-9]+\.[0-9]+$'
@@ -505,12 +507,25 @@ compute_update_notice() {
505
507
  [ "$_runtime_dir" != "/tmp" ] && [ -O "$_runtime_dir" ] || return 0
506
508
 
507
509
  local latest_file="$_runtime_dir/latest-version"
508
- local checked_at
510
+ local session_file="$_runtime_dir/latest-version.session"
511
+ local checked_at checked_by="" due=false age
509
512
  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
+ 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.
513
527
  touch "$latest_file" 2>/dev/null
528
+ printf '%s\n' "$session_id" 2>/dev/null > "$session_file"
514
529
  (
515
530
  v=$(curl -fsS --max-time 5 https://registry.npmjs.org/@inbrace-tech/tokenline/latest \
516
531
  | jq -r '.version // empty')
@@ -524,8 +539,12 @@ compute_update_notice() {
524
539
  read -r latest 2>/dev/null < "$latest_file"
525
540
  [[ "$latest" =~ $semver ]] || return 0
526
541
  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")
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")
529
548
  }
530
549
 
531
550
  # --- 8. Compose and Render Output ---