codebyplan 1.10.3 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.10.3",
3
+ "version": "1.11.0",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,41 +6,33 @@ Hook registration lives in [`hooks/hooks.json`](./hooks.json) — PreToolUse, Po
6
6
 
7
7
  **`cbp-statusline.sh` is auto-wired via `settings.project.base.json`.** The `statusLine` block is shipped inside `templates/settings.project.base.json` and merged into the consumer's `.claude/settings.json` automatically by `codebyplan claude install` (and on every `codebyplan claude update`). No manual copy-paste is required.
8
8
 
9
+ **Multi-runtime renderers.** Both `cbp-statusline.sh` and `cbp-subagent-statusline.sh` are *dispatchers*: each reads a per-device renderer choice and execs a `bash`, `node` (`.mjs`), or `python` (`.py`) variant that produces **byte-identical** output. The selected `.sh` always remains the wired entry point, so the bash renderer is a universal, never-error fallback when the chosen runtime is unavailable. Selection + display options come from two `.codebyplan/` files (see [Statusline configuration](#statusline-configuration) below).
10
+
9
11
  ---
10
12
 
11
13
  ## Hooks included
12
14
 
13
15
  ### `cbp-statusline.sh` — auto-registered via `settings.project.base.json`
14
16
 
15
- Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON from stdin (Claude Code's `statusLine` stream) and emits ANSI-coloured output. All fields from the Claude Code `statusLine` schema are parsed in a single `jq` invocation.
17
+ Renders up to 6 structured lines below Claude Code's prompt area. Reads JSON from stdin (Claude Code's `statusLine` stream) and emits ANSI-coloured output. The file is a *dispatcher + bash renderer*: it selects the configured runtime (bash / node / python), execs the matching renderer when available, and otherwise renders inline in bash. The `node` (`cbp-statusline.mjs`) and `python` (`cbp-statusline.py`) variants produce **byte-identical** output.
16
18
 
17
19
  **Render lines:**
18
20
 
19
- - **Line 1 — Identity**: single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model display name and id; `effort:LEVEL` (when set); `thinking:on` (only when thinking is enabled); `style:NAME` (when output style is not "default"); `v:VERSION`; `[VIM_MODE]`
21
+ - **Line 1 — Identity**: **folder name** (basename of `cwd`) + **git branch** (always shown — taken from `worktree.branch`, else derived via `git -C <cwd> rev-parse --abbrev-ref HEAD`, so it appears even without a registered worktree); single prefix (`wt:NAME` / `session:NAME` / `agent:NAME`, priority order); model **display name only**; `effort:LEVEL` (when set); `thinking:on` (only when thinking is enabled); `style:NAME` (when output style is not "default"); `[VIM_MODE]`
20
22
  - **Line 2 — Context**: 20-character progress bar (green / yellow / red at 50% / 75%); `used% / ctx_size`; per-call token breakdown — `in:N out:N cache_cr:N cache_rd:N` (from `context_window.current_usage`); `⚠ 200k+` banner when `exceeds_200k_tokens` is true
21
23
  - **Line 3 — Cost**: session cost in USD; total wall duration (`dur:`) and API-only duration (`api:`) via human-readable format; lines added/removed
22
24
  - **Line 4 — Rate limits**: `5h: PCT% (resets in Xh)` and `7d: PCT% (resets in Xd)` with relative-time helper; colour-coded green / yellow / red at 60% / 80%; emitted only when at least one window has a non-zero `resets_at` epoch
23
25
  - **Line 5 — Repo / PR**: `host/owner/name` (from `workspace.repo`); `PR #N <review_state> <url>` (when `pr.number` is present); emitted when at least one segment is present
24
26
  - **Line 6 — Worktree**: `name @ branch (was: original_branch)` and truncated path; emitted only when `worktree.name` is present
25
27
 
26
- **Env-var toggles** set any to `1` to suppress that section:
27
-
28
- | Variable | Suppresses |
29
- |---|---|
30
- | `CBP_STATUSLINE_HIDE_IDENTITY` | Line 1 — model / session / agent identity |
31
- | `CBP_STATUSLINE_HIDE_CONTEXT` | Line 2 — context bar, token breakdown |
32
- | `CBP_STATUSLINE_HIDE_COST` | Line 3 — cost, duration, lines changed |
33
- | `CBP_STATUSLINE_HIDE_RATE_LIMITS` | Line 4 — 5h / 7d rate-limit windows |
34
- | `CBP_STATUSLINE_HIDE_REPO_PR` | Line 5 — repo identity and PR |
35
- | `CBP_STATUSLINE_HIDE_WORKTREE` | Line 6 — worktree name / branch / path |
36
- | `CBP_STATUSLINE_NO_COLOR` | Strip all ANSI colour codes (also honoured by `$NO_COLOR`) |
28
+ > **Removed in the multi-runtime rewrite:** the `v:VERSION` field, the `sid:…` session-id field, the `log:…` transcript field, and the redundant model-id-in-parens beside the display name.
37
29
 
38
30
  **Sample invocation** (pipe mock JSON, run from the hooks directory; `NO_COLOR=1` shown so the comment-output below matches verbatim without ANSI codes):
39
31
 
40
32
  ```bash
41
- echo '{"model":{"display_name":"Opus","id":"claude-opus-4"},"context_window":{"used_percentage":25,"context_window_size":200000,"current_usage":{"input_tokens":50000,"output_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' \
33
+ echo '{"model":{"display_name":"Opus 4.8","id":"claude-opus-4-8[1m]"},"cwd":"/work/myrepo","worktree":{"branch":"feat/x"},"context_window":{"used_percentage":25,"context_window_size":200000,"current_usage":{"input_tokens":50000,"output_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}' \
42
34
  | NO_COLOR=1 bash cbp-statusline.sh
43
- # Opus (claude-opus-4)
35
+ # myrepo ⎇feat/x Opus 4.8
44
36
  # ▓▓▓▓▓░░░░░░░░░░░░░░░ 25%/200.0K in:50.0K out:1.0K cache_cr:0 cache_rd:0
45
37
  # $0.0000 dur:0s api:0s +0 -0 lines
46
38
  ```
@@ -49,7 +41,57 @@ Pure renderer — no project state read, no commands run. Works on any host.
49
41
 
50
42
  **Blocks vs warns**: neither — it only renders. Cannot fail your workflow.
51
43
 
52
- **Opt out**: set `CBP_STATUSLINE_HIDE_*` env vars to suppress individual sections, or remove the `statusLine` block from your `.claude/settings.json`.
44
+ **Opt out**: hide individual lines via config or `CBP_STATUSLINE_HIDE_*` env vars (see below), or remove the `statusLine` block from your `.claude/settings.json`.
45
+
46
+ ---
47
+
48
+ ### `cbp-subagent-statusline.sh` — auto-registered via `settings.project.base.json`
49
+
50
+ Renders one row per active subagent task below the main statusline. Reads JSON from stdin (Claude Code's `subagentStatusLine` stream, with a `tasks[]` array) and emits the upstream **JSON-per-line** protocol — one `{"id":…,"content":…}` object per task. Like the main statusline it is a dispatcher with byte-identical `bash` / `node` (`cbp-subagent-statusline.mjs`) / `python` (`cbp-subagent-statusline.py`) renderers. Each row carries a status icon + colour (running `●` / pending `○` / completed `✓` / failed `✗`), name, type, description, `tokens:N`, and `age:X`, ANSI-aware-truncated to the terminal's `columns`.
51
+
52
+ ```bash
53
+ echo '{"columns":0,"tasks":[{"id":"a1","name":"Explorer","type":"general","status":"running","description":"scanning files","startTime":0,"tokenCount":12000}]}' \
54
+ | NO_COLOR=1 bash cbp-subagent-statusline.sh
55
+ # {"id":"a1","content":"● Explorer (general) — scanning files · tokens:12.0K"}
56
+ ```
57
+
58
+ ---
59
+
60
+ ### Statusline configuration
61
+
62
+ Both statuslines are driven by two `.codebyplan/` files plus environment-variable overrides. Precedence is **env > config > default**.
63
+
64
+ | File | Scope | Shape | Purpose |
65
+ |---|---|---|---|
66
+ | `.codebyplan/statusline.json` | **committed** (team-shared) | `{ "lines": { "identity", "context", "cost", "rate_limits", "repo_pr", "worktree": bool }, "no_color": bool }` | Which of the 6 main lines render, and whether colour is stripped. All lines default `true`; `no_color` defaults `false`. |
67
+ | `.codebyplan/statusline.local.json` | **gitignored** (per-device) | `{ "renderer": "bash" \| "node" \| "python" }` | Which runtime renders. Per-device because runtime availability is machine-specific. Default `bash`. |
68
+
69
+ Set or inspect the renderer with the CLI (writes the gitignored local file):
70
+
71
+ ```bash
72
+ codebyplan statusline # print the current renderer + which runtimes are available
73
+ codebyplan statusline node # switch this device to the node renderer (or: bash | python)
74
+ codebyplan statusline --python # flag form is equivalent
75
+ ```
76
+
77
+ The same `--bash` / `--node` / `--python` flags are accepted on `codebyplan claude install` and `codebyplan claude update`, and `codebyplan setup` asks once when the renderer is unset. When the chosen runtime is missing at render time, the statusline silently falls back to bash (it never errors).
78
+
79
+ **Env-var overrides** — set to `1`; these win over the committed config:
80
+
81
+ | Variable | Effect |
82
+ |---|---|
83
+ | `CBP_STATUSLINE_HIDE_IDENTITY` | Hide line 1 — folder / branch / model / identity |
84
+ | `CBP_STATUSLINE_HIDE_CONTEXT` | Hide line 2 — context bar, token breakdown |
85
+ | `CBP_STATUSLINE_HIDE_COST` | Hide line 3 — cost, duration, lines changed |
86
+ | `CBP_STATUSLINE_HIDE_RATE_LIMITS` | Hide line 4 — 5h / 7d rate-limit windows |
87
+ | `CBP_STATUSLINE_HIDE_REPO_PR` | Hide line 5 — repo identity and PR |
88
+ | `CBP_STATUSLINE_HIDE_WORKTREE` | Hide line 6 — worktree name / branch / path |
89
+ | `CBP_STATUSLINE_NO_COLOR` | Strip all ANSI colour codes (also honoured by `$NO_COLOR`) |
90
+ | `CBP_SUBAGENT_STATUSLINE_HIDE_DESCRIPTION` | Subagent rows: omit the description segment |
91
+ | `CBP_SUBAGENT_STATUSLINE_HIDE_TOKENS` | Subagent rows: omit the `tokens:N` segment |
92
+ | `CBP_SUBAGENT_STATUSLINE_NO_COLOR` | Subagent rows: strip ANSI colour codes |
93
+
94
+ The committed `statusline.json` line toggles apply only to the six main-statusline lines; the subagent rows honour only the colour / description / tokens controls above.
53
95
 
54
96
  ---
55
97
 
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ // @hook: NOT-A-HOOK (statusLine renderer, invoked via the cbp-statusline.sh dispatcher)
3
+ // Claude Code Status Line — node renderer (ESM; .mjs forces ESM regardless of the
4
+ // host repo's package.json "type", so the script is portable into any consumer).
5
+ // Byte-identical output to the bash renderer in cbp-statusline.sh and the python
6
+ // renderer in cbp-statusline.py. See that .sh file for the full option/env/seam
7
+ // contract. Selected via .codebyplan/statusline.local.json -> {"renderer":"node"}.
8
+ //
9
+ // Reads stdin JSON; resolves .codebyplan/ root from CBP_STATUSLINE_ROOT (set by the
10
+ // dispatcher) or argv[2], else the script's ../.. directory. Never throws to stdout.
11
+
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { execFileSync } from "node:child_process";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function main() {
20
+ const input = fs.readFileSync(0, "utf8");
21
+ let data;
22
+ try {
23
+ data = JSON.parse(input);
24
+ } catch {
25
+ data = {};
26
+ }
27
+
28
+ // ---- Root resolution (matches the dispatcher contract) --------------------
29
+ let root = process.env.CBP_STATUSLINE_ROOT || process.argv[2] || "";
30
+ if (!root) {
31
+ root = path.resolve(__dirname, "..", "..");
32
+ }
33
+
34
+ // ---- jq `//` semantics: default on null/undefined (and false → false) -----
35
+ const get = (obj, keys, dflt) => {
36
+ let cur = obj;
37
+ for (const k of keys) {
38
+ if (cur == null || typeof cur !== "object") return dflt;
39
+ cur = cur[k];
40
+ }
41
+ return cur == null ? dflt : cur;
42
+ };
43
+
44
+ const MODEL_ID = get(data, ["model", "id"], "");
45
+ const MODEL_NAME = get(data, ["model", "display_name"], "");
46
+ const CWD = get(data, ["cwd"], "");
47
+ const WS_CURRENT_DIR = get(data, ["workspace", "current_dir"], "");
48
+ const WS_REPO_HOST = get(data, ["workspace", "repo", "host"], "");
49
+ const WS_REPO_OWNER = get(data, ["workspace", "repo", "owner"], "");
50
+ const WS_REPO_NAME = get(data, ["workspace", "repo", "name"], "");
51
+ const COST = get(data, ["cost", "total_cost_usd"], 0);
52
+ const DURATION = get(data, ["cost", "total_duration_ms"], 0);
53
+ const API_DURATION = get(data, ["cost", "total_api_duration_ms"], 0);
54
+ const LINES_ADD = get(data, ["cost", "total_lines_added"], 0);
55
+ const LINES_DEL = get(data, ["cost", "total_lines_removed"], 0);
56
+ const CTX_SIZE = get(data, ["context_window", "context_window_size"], 200000);
57
+ const CTX_PCT = get(data, ["context_window", "used_percentage"], 0);
58
+ const CUR_IN = get(
59
+ data,
60
+ ["context_window", "current_usage", "input_tokens"],
61
+ 0
62
+ );
63
+ const CUR_OUT = get(
64
+ data,
65
+ ["context_window", "current_usage", "output_tokens"],
66
+ 0
67
+ );
68
+ const CACHE_CREATE = get(
69
+ data,
70
+ ["context_window", "current_usage", "cache_creation_input_tokens"],
71
+ 0
72
+ );
73
+ const CACHE_READ = get(
74
+ data,
75
+ ["context_window", "current_usage", "cache_read_input_tokens"],
76
+ 0
77
+ );
78
+ const EXCEEDS_200K = get(data, ["exceeds_200k_tokens"], false);
79
+ const EFFORT = get(data, ["effort", "level"], "");
80
+ const THINKING = get(data, ["thinking", "enabled"], false);
81
+ const RATE_5H_PCT = get(
82
+ data,
83
+ ["rate_limits", "five_hour", "used_percentage"],
84
+ ""
85
+ );
86
+ const RATE_5H_RESETS = get(
87
+ data,
88
+ ["rate_limits", "five_hour", "resets_at"],
89
+ 0
90
+ );
91
+ const RATE_7D_PCT = get(
92
+ data,
93
+ ["rate_limits", "seven_day", "used_percentage"],
94
+ ""
95
+ );
96
+ const RATE_7D_RESETS = get(
97
+ data,
98
+ ["rate_limits", "seven_day", "resets_at"],
99
+ 0
100
+ );
101
+ const SESSION_NAME = get(data, ["session_name"], "");
102
+ const OUTPUT_STYLE = get(data, ["output_style", "name"], "");
103
+ const VIM_MODE = get(data, ["vim", "mode"], "");
104
+ const AGENT_NAME = get(data, ["agent", "name"], "");
105
+ const PR_NUMBER = get(data, ["pr", "number"], "");
106
+ const PR_URL = get(data, ["pr", "url"], "");
107
+ const PR_REVIEW_STATE = get(data, ["pr", "review_state"], "");
108
+ const WT_NAME = get(data, ["worktree", "name"], "");
109
+ const WT_PATH = get(data, ["worktree", "path"], "");
110
+ const WT_BRANCH = get(data, ["worktree", "branch"], "");
111
+ const WT_ORIG_BRANCH = get(data, ["worktree", "original_branch"], "");
112
+
113
+ // ---- Config: line toggles + no_color --------------------------------------
114
+ const cfg = {
115
+ identity: true,
116
+ context: true,
117
+ cost: true,
118
+ rate_limits: true,
119
+ repo_pr: true,
120
+ worktree: true,
121
+ no_color: false,
122
+ };
123
+ try {
124
+ const raw = fs.readFileSync(
125
+ path.join(root, ".codebyplan", "statusline.json"),
126
+ "utf8"
127
+ );
128
+ const parsed = JSON.parse(raw);
129
+ if (parsed && typeof parsed === "object") {
130
+ if (typeof parsed.no_color === "boolean") cfg.no_color = parsed.no_color;
131
+ if (parsed.lines && typeof parsed.lines === "object") {
132
+ for (const k of [
133
+ "identity",
134
+ "context",
135
+ "cost",
136
+ "rate_limits",
137
+ "repo_pr",
138
+ "worktree",
139
+ ]) {
140
+ if (typeof parsed.lines[k] === "boolean") cfg[k] = parsed.lines[k];
141
+ }
142
+ }
143
+ }
144
+ } catch {
145
+ // absent / invalid → keep defaults
146
+ }
147
+
148
+ // env HIDE=1 > config false > default show
149
+ const shouldShow = (envSuffix, cfgValue) => {
150
+ if (process.env[`CBP_STATUSLINE_HIDE_${envSuffix}`] === "1") return false;
151
+ if (cfgValue === false) return false;
152
+ return true;
153
+ };
154
+
155
+ // ---- Colour setup (env > config) ------------------------------------------
156
+ const noColor =
157
+ (process.env.NO_COLOR != null && process.env.NO_COLOR !== "") ||
158
+ process.env.CBP_STATUSLINE_NO_COLOR === "1" ||
159
+ cfg.no_color === true;
160
+ const C = noColor
161
+ ? {
162
+ RST: "",
163
+ DIM: "",
164
+ BOLD: "",
165
+ GREEN: "",
166
+ YELLOW: "",
167
+ RED: "",
168
+ CYAN: "",
169
+ MAGENTA: "",
170
+ BLUE: "",
171
+ }
172
+ : {
173
+ RST: "\x1b[0m",
174
+ DIM: "\x1b[2m",
175
+ BOLD: "\x1b[1m",
176
+ GREEN: "\x1b[32m",
177
+ YELLOW: "\x1b[33m",
178
+ RED: "\x1b[31m",
179
+ CYAN: "\x1b[36m",
180
+ MAGENTA: "\x1b[35m",
181
+ BLUE: "\x1b[34m",
182
+ };
183
+
184
+ // ---- Numeric helpers (integer round-half-up; cross-runtime identical) ------
185
+ const numStr = (n) => {
186
+ const x = Number(n);
187
+ if (Number.isFinite(x) && Number.isInteger(x)) return String(x);
188
+ return String(n);
189
+ };
190
+
191
+ const fmtK = (val) => {
192
+ const v = Number(val);
193
+ if (v >= 1000000) {
194
+ const t = Math.floor((v + 50000) / 100000);
195
+ return `${Math.floor(t / 10)}.${t % 10}M`;
196
+ } else if (v >= 1000) {
197
+ const t = Math.floor((v + 50) / 100);
198
+ return `${Math.floor(t / 10)}.${t % 10}K`;
199
+ }
200
+ return String(Math.trunc(v));
201
+ };
202
+
203
+ const fmtCost = (c) => {
204
+ const n = Math.floor(Number(c) * 10000 + 0.5);
205
+ const frac = String(n % 10000).padStart(4, "0");
206
+ return `$${Math.floor(n / 10000)}.${frac}`;
207
+ };
208
+
209
+ const fmtDur = (ms) => {
210
+ const secs = Math.trunc(Number(ms) / 1000);
211
+ if (secs >= 3600)
212
+ return `${Math.floor(secs / 3600)}h${Math.floor((secs % 3600) / 60)}m`;
213
+ if (secs >= 60) return `${Math.floor(secs / 60)}m${secs % 60}s`;
214
+ return `${secs}s`;
215
+ };
216
+
217
+ const cbpNow = () => {
218
+ if (
219
+ process.env.CBP_STATUSLINE_NOW != null &&
220
+ process.env.CBP_STATUSLINE_NOW !== ""
221
+ ) {
222
+ return Math.trunc(Number(process.env.CBP_STATUSLINE_NOW));
223
+ }
224
+ return Math.floor(Date.now() / 1000);
225
+ };
226
+
227
+ const fmtRelTime = (epoch) => {
228
+ const delta = Math.trunc(Number(epoch)) - cbpNow();
229
+ if (delta <= 0) return "now";
230
+ if (delta >= 86400) return `${Math.floor(delta / 86400)}d`;
231
+ if (delta >= 3600) return `${Math.floor(delta / 3600)}h`;
232
+ return `${Math.floor(delta / 60)}m`;
233
+ };
234
+
235
+ const gte = (v, t) => Number(v) >= t;
236
+
237
+ // ---- Folder + branch ------------------------------------------------------
238
+ let FOLDER = "";
239
+ if (CWD) FOLDER = path.basename(CWD);
240
+ else if (WS_CURRENT_DIR) FOLDER = path.basename(WS_CURRENT_DIR);
241
+ let BRANCH = WT_BRANCH;
242
+ if (!BRANCH && CWD) {
243
+ try {
244
+ BRANCH = execFileSync(
245
+ "git",
246
+ ["-C", CWD, "rev-parse", "--abbrev-ref", "HEAD"],
247
+ {
248
+ encoding: "utf8",
249
+ stdio: ["ignore", "pipe", "ignore"],
250
+ }
251
+ ).replace(/\s+$/, "");
252
+ } catch {
253
+ BRANCH = "";
254
+ }
255
+ }
256
+
257
+ const out = [];
258
+
259
+ // ============================================================
260
+ // LINE 1 — Identity
261
+ // ============================================================
262
+ if (shouldShow("IDENTITY", cfg.identity)) {
263
+ let L1 = "";
264
+ if (FOLDER) {
265
+ L1 = `${C.BOLD}${C.BLUE}${FOLDER}${C.RST}`;
266
+ if (BRANCH) L1 += ` ${C.DIM}⎇${C.RST}${C.CYAN}${BRANCH}${C.RST}`;
267
+ L1 += " ";
268
+ }
269
+ if (WT_NAME) {
270
+ L1 += `${C.DIM}wt:${C.RST}${C.MAGENTA}${WT_NAME}${C.RST} `;
271
+ } else if (SESSION_NAME) {
272
+ L1 += `${C.DIM}session:${C.RST}${C.MAGENTA}${SESSION_NAME}${C.RST} `;
273
+ } else if (AGENT_NAME) {
274
+ L1 += `${C.DIM}agent:${C.RST}${C.MAGENTA}${AGENT_NAME}${C.RST} `;
275
+ }
276
+ if (MODEL_NAME) {
277
+ L1 += `${C.BOLD}${C.CYAN}${MODEL_NAME}${C.RST}`;
278
+ } else if (MODEL_ID) {
279
+ L1 += `${C.BOLD}${C.CYAN}${MODEL_ID}${C.RST}`;
280
+ }
281
+ if (EFFORT) L1 += ` ${C.DIM}effort:${C.RST}${EFFORT}`;
282
+ if (THINKING === true) L1 += ` ${C.YELLOW}thinking:on${C.RST}`;
283
+ if (OUTPUT_STYLE && OUTPUT_STYLE !== "default")
284
+ L1 += ` ${C.DIM}style:${C.RST}${OUTPUT_STYLE}`;
285
+ if (VIM_MODE) L1 += ` ${C.DIM}[${VIM_MODE}]${C.RST}`;
286
+ if (L1) out.push(L1);
287
+ }
288
+
289
+ // ============================================================
290
+ // LINE 2 — Context window
291
+ // ============================================================
292
+ if (shouldShow("CONTEXT", cfg.context)) {
293
+ let barColor;
294
+ if (gte(CTX_PCT, 75)) barColor = C.RED;
295
+ else if (gte(CTX_PCT, 50)) barColor = C.YELLOW;
296
+ else barColor = C.GREEN;
297
+
298
+ let filled = Math.floor((Math.trunc(Number(CTX_PCT)) + 4) / 5);
299
+ if (filled > 20) filled = 20;
300
+ const empty = 20 - filled;
301
+ const bar = "▓".repeat(filled) + "░".repeat(empty);
302
+
303
+ let L2 = `${barColor}${bar}${C.RST} ${barColor}${numStr(CTX_PCT)}%${C.RST}${C.DIM}/${fmtK(CTX_SIZE)}${C.RST}`;
304
+ L2 += ` ${C.DIM}in:${C.RST}${C.BLUE}${fmtK(CUR_IN)}${C.RST} ${C.DIM}out:${C.RST}${C.MAGENTA}${fmtK(CUR_OUT)}${C.RST} ${C.DIM}cache_cr:${C.RST}${fmtK(CACHE_CREATE)} ${C.DIM}cache_rd:${C.RST}${fmtK(CACHE_READ)}`;
305
+ if (EXCEEDS_200K === true) L2 += ` ${C.YELLOW}⚠ 200k+${C.RST}`;
306
+ out.push(L2);
307
+ }
308
+
309
+ // ============================================================
310
+ // LINE 3 — Cost
311
+ // ============================================================
312
+ if (shouldShow("COST", cfg.cost)) {
313
+ const L3 = `${C.GREEN}${fmtCost(COST)}${C.RST} ${C.DIM}dur:${C.RST}${fmtDur(DURATION)} ${C.DIM}api:${C.RST}${fmtDur(API_DURATION)} ${C.GREEN}+${numStr(LINES_ADD)}${C.RST} ${C.RED}-${numStr(LINES_DEL)}${C.RST} ${C.DIM}lines${C.RST}`;
314
+ out.push(L3);
315
+ }
316
+
317
+ // ============================================================
318
+ // LINE 4 — Rate limits
319
+ // ============================================================
320
+ if (shouldShow("RATE_LIMITS", cfg.rate_limits)) {
321
+ const has5h = RATE_5H_PCT !== "" && String(RATE_5H_RESETS) !== "0";
322
+ const has7d = RATE_7D_PCT !== "" && String(RATE_7D_RESETS) !== "0";
323
+ if (has5h || has7d) {
324
+ let L4 = "";
325
+ if (has5h) {
326
+ let c5;
327
+ if (gte(RATE_5H_PCT, 80)) c5 = C.RED;
328
+ else if (gte(RATE_5H_PCT, 60)) c5 = C.YELLOW;
329
+ else c5 = C.GREEN;
330
+ L4 = `${C.DIM}5h:${C.RST}${c5}${numStr(RATE_5H_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_5H_RESETS)})${C.RST}`;
331
+ }
332
+ if (has7d) {
333
+ let c7;
334
+ if (gte(RATE_7D_PCT, 80)) c7 = C.RED;
335
+ else if (gte(RATE_7D_PCT, 60)) c7 = C.YELLOW;
336
+ else c7 = C.GREEN;
337
+ const seg7 = `${C.DIM}7d:${C.RST}${c7}${numStr(RATE_7D_PCT)}%${C.RST} ${C.DIM}(resets in ${fmtRelTime(RATE_7D_RESETS)})${C.RST}`;
338
+ L4 = L4 ? `${L4} ${C.DIM}|${C.RST} ${seg7}` : seg7;
339
+ }
340
+ out.push(L4);
341
+ }
342
+ }
343
+
344
+ // ============================================================
345
+ // LINE 5 — Repo / PR
346
+ // ============================================================
347
+ if (shouldShow("REPO_PR", cfg.repo_pr)) {
348
+ let L5 = "";
349
+ if (WS_REPO_HOST) {
350
+ L5 = `${C.DIM}${WS_REPO_HOST}/${WS_REPO_OWNER}/${WS_REPO_NAME}${C.RST}`;
351
+ }
352
+ if (PR_NUMBER !== "") {
353
+ let prSeg = `${C.DIM}PR${C.RST} ${C.CYAN}#${numStr(PR_NUMBER)}${C.RST}`;
354
+ if (PR_REVIEW_STATE) prSeg += ` ${C.DIM}${PR_REVIEW_STATE}${C.RST}`;
355
+ if (PR_URL) prSeg += ` ${C.BLUE}${PR_URL}${C.RST}`;
356
+ L5 = L5 ? `${L5} ${C.DIM}|${C.RST} ${prSeg}` : prSeg;
357
+ }
358
+ if (L5) out.push(L5);
359
+ }
360
+
361
+ // ============================================================
362
+ // LINE 6 — Worktree
363
+ // ============================================================
364
+ if (shouldShow("WORKTREE", cfg.worktree)) {
365
+ if (WT_NAME) {
366
+ let L6 = `${C.MAGENTA}${WT_NAME}${C.RST} ${C.DIM}@${C.RST} ${C.CYAN}${WT_BRANCH}${C.RST}`;
367
+ if (WT_ORIG_BRANCH && WT_ORIG_BRANCH !== WT_BRANCH) {
368
+ L6 += ` ${C.DIM}(was: ${WT_ORIG_BRANCH})${C.RST}`;
369
+ }
370
+ let wtPathDisp = WT_PATH;
371
+ if (WT_PATH.length > 60) wtPathDisp = "..." + WT_PATH.slice(-57);
372
+ L6 += ` ${C.DIM}${wtPathDisp}${C.RST}`;
373
+ out.push(L6);
374
+ }
375
+ }
376
+
377
+ process.stdout.write(out.length ? out.join("\n") + "\n" : "");
378
+ }
379
+
380
+ try {
381
+ main();
382
+ } catch {
383
+ // Statusline must never error to stdout.
384
+ process.exit(0);
385
+ }