ctxline-claude 0.0.7 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/package.json +2 -2
  3. package/statusline.js +49 -8
package/README.md CHANGED
@@ -103,11 +103,15 @@ Remove-Item "$env:USERPROFILE\.claude\cache\usage-cache.json" -ErrorAction Silen
103
103
  | **Context** | Visual bar of context-window usage |
104
104
  | **Current** | Live 5-hour session limit + reset countdown (subscription users) |
105
105
  | **Weekly** | Weekly usage allowance + time until the weekly reset (subscription users) |
106
+ | **Cost** | Running session cost in USD (e.g. `$0.42`) |
106
107
  | **Task** | The in-progress todo, when there is one |
107
108
 
108
109
  > [!NOTE]
109
110
  > Usage bars change color automatically as you approach your limits.
110
111
 
112
+ > [!NOTE]
113
+ > **Responsive.** On a narrow terminal the line wraps to two — directory, model, and context on the first line; usage, cost, and task on the second. Wide terminals stay on a single line. (Auto-sizing needs Claude Code v2.1.153+.)
114
+
111
115
  ## How it works
112
116
 
113
117
  - **Source** — context comes from Claude Code's session data. Usage bars are read straight from the `rate_limits` field Claude Code pipes in (no network), falling back to `https://api.anthropic.com/api/oauth/usage` (the same `/usage` data — 5-hour and weekly limits) when that field isn't present yet. API-key users skip usage entirely.
@@ -125,6 +129,13 @@ Yes — the same 5-hour and weekly limits. It reads them from the session data C
125
129
 
126
130
  </details>
127
131
 
132
+ <details>
133
+ <summary>Is the session cost my actual bill?</summary>
134
+
135
+ It's the cost Claude Code computes for the session (tokens × per-model API pricing), read straight from the session data. For subscription (Pro/Max) users it's the *equivalent* pay-as-you-go API cost — useful as a gauge of session weight, but not what you're billed (you pay the flat subscription). It's an estimate, accurate to the extent your Claude Code pricing tables are current.
136
+
137
+ </details>
138
+
128
139
  <details>
129
140
  <summary>Does it work with API keys?</summary>
130
141
 
@@ -135,7 +146,7 @@ Yes. The statusline automatically detects subscription vs API-key usage.
135
146
  <details>
136
147
  <summary>Can it break Claude Code?</summary>
137
148
 
138
- No. All failures are handled silently and the statusline always renders.
149
+ No. [Statuslines are a built-in Claude Code feature](https://code.claude.com/docs/en/statusline) — this provides the command Claude Code runs. All failures are handled silently and the statusline always renders.
139
150
 
140
151
  </details>
141
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxline-claude",
3
- "version": "0.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "A customizable statusline for Claude Code that tracks context usage and session limits",
5
5
  "bin": {
6
6
  "ctxline-claude": "bin/install.js"
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "author": "Mithun Wijayasiri",
30
30
  "license": "MIT",
31
- "homepage": "https://github.com/MithunWijayasiri/ctxline-claude#readme",
31
+ "homepage": "https://ctxline.js.org",
32
32
  "bugs": {
33
33
  "url": "https://github.com/MithunWijayasiri/ctxline-claude/issues"
34
34
  },
package/statusline.js CHANGED
@@ -19,6 +19,14 @@ const BAR_WIDTH = 6;
19
19
  // Tail-truncation keeps the start (ticket IDs like "TAMA5-32796" live there) visible.
20
20
  const MAX_BRANCH_LEN = 24;
21
21
 
22
+ // Separator between segments on a rendered line.
23
+ const SEGMENT_SEP = ' │ ';
24
+
25
+ // Cells reserved at the terminal edge when deciding to wrap to a second line.
26
+ // 0 = use the full COLUMNS; bump it if Claude Code reserves columns and the line
27
+ // truncates a char or two before wrapping.
28
+ const WIDTH_MARGIN = 0;
29
+
22
30
  // Cache configuration
23
31
  const CACHE_DIR = path.join(os.homedir(), '.claude', 'cache');
24
32
  const USAGE_CACHE_FILE = path.join(CACHE_DIR, 'usage-cache.json');
@@ -359,6 +367,15 @@ function getUsageWithCache(callback) {
359
367
  });
360
368
  }
361
369
 
370
+ // Session cost from stdin `cost.total_cost_usd` (USD float, computed client-side by
371
+ // Claude Code as tokens × per-model API pricing). Pure stdin — no network/cache.
372
+ // Returns "$0.00" rendered dim, or '' when absent/non-finite so the segment is omitted.
373
+ function getCostSegment(data) {
374
+ const usd = data?.cost?.total_cost_usd;
375
+ if (!Number.isFinite(usd)) return '';
376
+ return `${colors.dim}$${usd.toFixed(2)}${colors.reset}`;
377
+ }
378
+
362
379
  function getCurrentTask(sessionId) {
363
380
  if (!sessionId) return '';
364
381
 
@@ -383,6 +400,25 @@ function getCurrentTask(sessionId) {
383
400
  return '';
384
401
  }
385
402
 
403
+ // Visible (printable) width of a segment string: strip ANSI color codes, count code points.
404
+ function visibleWidth(str) {
405
+ return [...str.replace(/\x1b\[[0-9;]*m/g, '')].length;
406
+ }
407
+
408
+ // Responsive layout: one line when it fits the terminal, else line1 (identity + context)
409
+ // on top and line2 (usage/cost/task) below. Splits only when COLUMNS is known (Claude Code
410
+ // v2.1.153+) and the single line overflows — unknown width or an empty line2 stays single,
411
+ // so there is no regression on older clients or wide terminals.
412
+ function layout(line1Parts, line2Parts) {
413
+ const single = [...line1Parts, ...line2Parts].join(SEGMENT_SEP);
414
+ if (line2Parts.length === 0) return single;
415
+ const cols = parseInt(process.env.COLUMNS, 10);
416
+ if (Number.isFinite(cols) && cols > 0 && visibleWidth(single) > cols - WIDTH_MARGIN) {
417
+ return line1Parts.join(SEGMENT_SEP) + '\n' + line2Parts.join(SEGMENT_SEP);
418
+ }
419
+ return single;
420
+ }
421
+
386
422
  // Main
387
423
  function outputStatus(data, usage) {
388
424
  try {
@@ -395,17 +431,22 @@ function outputStatus(data, usage) {
395
431
  const remaining = data?.context_window?.remaining_percentage;
396
432
 
397
433
  const contextBar = getContextBar(remaining);
434
+ const cost = getCostSegment(data);
398
435
  const task = getCurrentTask(sessionId);
399
- const parts = [];
400
- parts.push(branch ? `${dirname} ${colors.dim}⎇ ${branch}${colors.reset}` : dirname);
401
- parts.push(effort ? `${model}${getEffortColor(effort)} · ${effort}${colors.reset}` : model);
402
- parts.push(contextBar);
403
436
 
404
- if (usage?.current) parts.push(usage.current);
405
- if (usage?.weekly) parts.push(usage.weekly);
437
+ // line1 = identity + context (always); line2 = usage/cost/task (wrap target).
438
+ const line1 = [];
439
+ line1.push(branch ? `${dirname} ${colors.dim}⎇ ${branch}${colors.reset}` : dirname);
440
+ line1.push(effort ? `${model}${getEffortColor(effort)} · ${effort}${colors.reset}` : model);
441
+ line1.push(contextBar);
442
+
443
+ const line2 = [];
444
+ if (usage?.current) line2.push(usage.current);
445
+ if (usage?.weekly) line2.push(usage.weekly);
446
+ if (cost) line2.push(cost);
447
+ if (task) line2.push(`${colors.dim}${task}${colors.reset}`);
406
448
 
407
- if (task) parts.push(`${colors.dim}${task}${colors.reset}`);
408
- process.stdout.write(parts.join(' \u2502 '));
449
+ process.stdout.write(layout(line1, line2));
409
450
  } catch (e) {
410
451
  process.stdout.write('Status unavailable');
411
452
  }