Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/macos-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@inbrace-tech/tokenline": minor
---

Add macOS support. `tokenline.sh` now abstracts `date`/`stat` over GNU vs BSD by
probing behavior once (`epoch_from_iso`, `file_mtime`), pins `LC_ALL=C` so a
comma-decimal locale renders identically, and the installer accepts macOS
(`brew install bash jq`). Closes #2.
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ Strip ANSI to check structure: pipe the output through `sed 's/\x1b\[[0-9;]*m//g

## Platform

v1 targets Linux / WSL2. macOS and Windows are tracked as roadmap issues — the
GNU-specific calls (`mapfile`, `date -d`, `stat -c`) are the things to abstract.
Runs on Linux / WSL2 and macOS. `date`/`stat` are abstracted over GNU vs BSD by
probing behavior once (`epoch_from_iso`, `file_mtime` in `tokenline.sh`); `mapfile`
still needs bash 4+, so macOS users `brew install bash`. Windows is a roadmap issue.

## The npm installer

Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> 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.

![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2-blue)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20WSL2%20%7C%20macOS-blue)
![Shell](https://img.shields.io/badge/shell-bash%204%2B-lightgrey)

## Quickstart
Expand Down Expand Up @@ -78,14 +78,16 @@ If `tokenline` reveals you are getting 1-hour cache writes, keep in mind they bu

## Requirements

v1 targets **Linux / WSL2**:
Runs on **Linux / WSL2** and **macOS**:

- `bash` 4 or newer
- `bash` 4 or newer — macOS ships 3.2, so `brew install bash`
- [`jq`](https://jqlang.github.io/jq/)
- GNU coreutils (`date -d`, `stat -c`)
- `date` and `stat` — GNU (`-d` / `-c`) or BSD (`-j` / `-f`); both are handled

> macOS and Windows support are on the [roadmap](#roadmap). `install.sh` checks all of
> the above and tells you exactly what's missing.
On macOS: `brew install bash jq`. BSD `date`/`stat` work as-is; no `coreutils` needed.

> Windows support is on the [roadmap](#roadmap). `install.sh` checks all of the
> above and tells you exactly what's missing.

## Advanced Installation

Expand Down Expand Up @@ -139,11 +141,11 @@ On every refresh the host CLI pipes a JSON snapshot of the session to the script
| Blank statusline, or `[tokenline] jq not found` | Install `jq` (`apt install jq` / `brew install jq`), then re-run `./install.sh`. |
| Cache shows `COLD` immediately | Normal right after a long idle gap — the cache window has elapsed. It goes `HOT` again on your next turn. |
| Colors look wrong / show escape codes | Your terminal must support 256-color ANSI. Most modern terminals do; check your `$TERM`. |
| Nothing renders on macOS | Expected on v1 — macOS uses BSD `date`/`stat`. See the [roadmap](#roadmap). |
| Nothing renders on macOS | Install `bash` 4+ and `jq`: `brew install bash jq`. Stock bash 3.2 lacks `mapfile`. |

## Roadmap

- [ ] macOS support (BSD `date`/`stat`, bash 3.2 fallback)
- [x] macOS support (BSD `date`/`stat`, Homebrew bash)
- [ ] Windows support (Git Bash / PowerShell)
- [ ] Configurable colors and thresholds via `TOKENLINE_*` env vars

Expand Down
24 changes: 14 additions & 10 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ printf '%s\n' "--------------------------------"

missing=0

# bash 4+ (mapfile/associative features)
# bash 4+ (mapfile). macOS ships 3.2 — `brew install bash` provides 5.x.
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
ok "bash ${BASH_VERSION%%(*}"
else
err "bash 4+ required (found ${BASH_VERSION%%(*}). macOS ships 3.2 — see README roadmap."
err "bash 4+ required (found ${BASH_VERSION%%(*}). On macOS: brew install bash"
missing=1
fi

Expand All @@ -40,19 +40,23 @@ else
missing=1
fi

# GNU date (-d): used to parse ISO timestamps from the transcript
# date: GNU (-d) or BSD (-j) — tokenline handles both.
if date -d "@0" >/dev/null 2>&1; then
ok "GNU date (-d)"
ok "date (GNU -d)"
elif date -j -f "%s" 0 >/dev/null 2>&1; then
ok "date (BSD -j)"
else
err "GNU date (-d) missing — BSD/macOS date differs (see README roadmap)"
err "no usable date (need GNU -d or BSD -j)"
missing=1
fi

# GNU stat (-c): mtime fallback for the cache timer
# stat: GNU (-c) or BSD (-f) — tokenline handles both.
if stat -c %Y . >/dev/null 2>&1; then
ok "GNU stat (-c)"
ok "stat (GNU -c)"
elif stat -f %m . >/dev/null 2>&1; then
ok "stat (BSD -f)"
else
err "GNU stat (-c) missing — BSD/macOS stat differs (see README roadmap)"
err "no usable stat (need GNU -c or BSD -f)"
missing=1
fi

Expand All @@ -67,8 +71,8 @@ fi

printf '\n'
if [ "$missing" -ne 0 ]; then
warn "Missing dependencies above. tokenline targets Linux/WSL2 for v1;"
warn "macOS/Windows support is on the roadmap (see README)."
warn "Missing dependencies above. On macOS: brew install bash jq."
warn "Windows support is on the roadmap (see README)."
printf '\n'
fi

Expand Down
6 changes: 3 additions & 3 deletions src/infra/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { ok, warn } from '../shared/logger'

export function checkPlatform(): boolean {
const p = platform()
if (p === 'linux') {
if (p === 'linux' || p === 'darwin') {
ok(`platform: ${p} (supported)`)
return true
}
warn(
`platform: ${p} — v1 targets Linux/WSL2; the bash statusline likely won't ` +
`render yet (see roadmap). Use --force to install anyway.`,
`platform: ${p} — supported on Linux/WSL2 and macOS; the bash statusline ` +
`likely won't render yet (see roadmap). Use --force to install anyway.`,
)
return false
}
Expand Down
37 changes: 34 additions & 3 deletions tokenline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@
#
# Repo: https://github.com/inbrace-tech/tokenline
# License: MIT
# Requires: bash 4+, jq, GNU coreutils (date -d, stat -c). Linux/WSL2 for v1.
# Requires: bash 4+, jq. Linux/WSL2 or macOS (brew install bash jq).
# ==============================================================================

# Pin C locale: a comma-decimal locale (e.g. pt_BR) makes awk/printf emit
# "46,2k" and reject dotted input. LC_ALL beats LC_NUMERIC, so set LC_ALL to
# stay deterministic even when the user exports LC_ALL. Output is ASCII/bytes.
export LC_ALL=C

# --- Colors & Formatting Constants ---
COLOR_GRAY=$'\033[38;5;244m'
COLOR_DARK_GRAY=$'\033[38;5;240m'
Expand All @@ -34,6 +39,32 @@ if ! command -v jq >/dev/null 2>&1; then
exit 0
fi

# --- GNU vs BSD coreutils (Linux vs macOS) ---
# Probe behavior, not `uname`, so Homebrew coreutils is picked up automatically.
if date -d "@0" >/dev/null 2>&1; then _date_gnu=1; else _date_gnu=0; fi
if stat -c %Y . >/dev/null 2>&1; then _stat_gnu=1; else _stat_gnu=0; fi

epoch_from_iso() {
# ISO-8601 -> epoch seconds; empty on failure (callers fall back to mtime).
local iso="$1"
if [ "$_date_gnu" -eq 1 ]; then
date -d "$iso" +%s 2>/dev/null
else
# BSD date needs an explicit format and rejects fractional secs / 'Z'.
# First 19 chars are always 'YYYY-MM-DDTHH:MM:SS'; transcripts are UTC.
date -u -j -f "%Y-%m-%dT%H:%M:%S" "${iso:0:19}" +%s 2>/dev/null
fi
}

file_mtime() {
local f="$1"
if [ "$_stat_gnu" -eq 1 ]; then
stat -c %Y "$f" 2>/dev/null
else
stat -f %m "$f" 2>/dev/null
fi
}

# --- Runtime state directory ---
# Per-turn timestamp / TTL are cached between the 1s refreshes. Prefer a
# per-user dir (0700) under XDG_RUNTIME_DIR: it avoids predictable, world-
Expand Down Expand Up @@ -176,10 +207,10 @@ compute_cache_timer() {
| tail -n 1
)
if [ -n "$iso" ]; then
last_ts=$(date -d "$iso" +%s 2>/dev/null)
last_ts=$(epoch_from_iso "$iso")
fi
# Mtime fallback if parsing is unsuccessful
[ -z "$last_ts" ] && last_ts=$(stat -c %Y "$transcript_path" 2>/dev/null)
[ -z "$last_ts" ] && last_ts=$(file_mtime "$transcript_path")
fi

# Fallback to local session caching file if transcript read is unavailable or cached timestamp is newer
Expand Down