@z80020100/claude-code-statusline 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cliff Wu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,113 @@
1
+ # claude-code-statusline
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md)
4
+
5
+ [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 用カスタムステータスライン — モデル情報、コンテキスト使用量グラデーションバー、トークン統計、コスト、Git ステータス、レート制限を表示します。
6
+
7
+ ![Demo](assets/claude-code-statusline-demo.png)
8
+
9
+ ## 機能
10
+
11
+ - **コンテキスト使用量グラデーションバー** — 緑から赤への4段階カラースペクトラム
12
+ - **トークンとコスト追跡** — 入出力トークン数、キャッシュヒット率、セッションコスト
13
+ - **セッションタイミング** — 実時間と API 応答時間を並列表示
14
+ - **Git 連携** — ブランチ名、変更フラグ、worktree インジケーター、main との差分統計
15
+ - **レート制限モニタリング** — 現在 (5h) と週間 (7d) の使用量およびリセット時刻
16
+ - **Sandbox インジケーター** — sandbox モードのオフ、オン、自動を表示
17
+ - **パス圧縮** — 長いパスを自動短縮して80カラムに収める
18
+ - **ランタイム依存ゼロ** — Node.js 組み込みモジュールのみ使用
19
+
20
+ ## インストール
21
+
22
+ ```sh
23
+ npm install -g @z80020100/claude-code-statusline
24
+ ```
25
+
26
+ ## セットアップ
27
+
28
+ ```sh
29
+ claude-code-statusline setup
30
+ ```
31
+
32
+ `~/.claude/settings.json` に `statusLine` 設定を書き込みます:
33
+
34
+ ```json
35
+ {
36
+ "statusLine": {
37
+ "type": "command",
38
+ "command": "claude-code-statusline"
39
+ }
40
+ }
41
+ ```
42
+
43
+ 設定を削除する場合:
44
+
45
+ ```sh
46
+ claude-code-statusline setup --uninstall
47
+ ```
48
+
49
+ ## 表示レイアウト
50
+
51
+ 全フィールドを最大幅で表示した場合:
52
+
53
+ ![全フィールド](assets/claude-code-statusline-simulation.png)
54
+
55
+ ステータスラインは最大6行で表示されます — 各行は80文字以内に制限されます:
56
+
57
+ | 行 | 内容 |
58
+ | --- | ------------------------------------------------------------------------------------------------ |
59
+ | 1 | バージョン、sandbox モード、セッション名と ID |
60
+ | 2 | モデル名、effort レベル、コンテキスト使用量バーとパーセンテージ、最終更新時刻 |
61
+ | 3 | トークン数 (入力/出力)、キャッシュヒット率、コスト、セッション/API 所要時間、増減行数、200K 警告 |
62
+ | 4 | プロジェクトディレクトリ、git ブランチ、変更フラグ、worktree インジケーター、main との差分 |
63
+ | 5 | 現在の作業ディレクトリ(プロジェクトルートと異なる場合のみ表示) |
64
+ | 6 | レート制限 — 現在 (5h) と週間 (7d) の使用量およびリセット時刻 |
65
+
66
+ ### カラーゾーン
67
+
68
+ コンテキストとレート制限のバーは4段階グラデーションを使用します:
69
+
70
+ | 範囲 | 色 | 意味 |
71
+ | ------- | -------- | ------ |
72
+ | 0–49% | グリーン | 正常 |
73
+ | 50–69% | ゴールド | 中程度 |
74
+ | 70–89% | コーラル | やや高 |
75
+ | 90–100% | レッド | 危険 |
76
+
77
+ ## 動作原理
78
+
79
+ Claude Code は各レンダリングサイクルで stdin を通じて `statusLine` コマンドに JSON オブジェクトを送信します。JSON には現在のセッション状態(モデル、トークン、コスト、ワークスペース、レート制限など)が含まれます。本ツールはそれを解析し ANSI カラーテキストを stdout に出力します。
80
+
81
+ 設計上の決定事項:
82
+
83
+ - **依存関係ゼロ** — Node.js 組み込みモジュールのみ使用 (`fs`, `path`, `os`, `child_process`)
84
+ - **Git キャッシュ** — ブランチと差分統計を5秒間キャッシュしサブプロセス呼び出しを削減
85
+ - **設定キャッシュ** — effort レベルと sandbox モードは mtime ベースのキャッシュで冗長なファイル読み込みを回避
86
+ - **80カラム制限** — 自動テストで強制;長いパスは自動的に圧縮
87
+ - **256色 ANSI** — ターミナル間で一貫した描画;Claude ブランドオレンジは24ビットトゥルーカラーを使用
88
+
89
+ ## 動作要件
90
+
91
+ | 依存関係 | Tier 1(CI テスト済) | Tier 2(best-effort) |
92
+ | ----------- | ------------------------------------------- | --------------------- |
93
+ | Node.js | >= 20 | 18 |
94
+ | Claude Code | >= 2.1.80(`rate_limits` フィールドが必要) | |
95
+
96
+ ## 開発
97
+
98
+ ```sh
99
+ git clone https://github.com/z80020100/claude-code-statusline.git
100
+ cd claude-code-statusline
101
+ npm install # prepare で pre-commit hooks も自動有効化
102
+
103
+ npm run check # lint + フォーマットチェック + 幅チェック + CLI テスト
104
+ npm run fix # lint とフォーマットの問題を自動修正
105
+ npm test # 幅チェック + CLI テストのみ
106
+ npm run lint # ESLint + shellcheck + actionlint
107
+ npm run simulate # worst-case ステータスラインを描画し幅レポートを表示
108
+ npm run ci:local # act で CI ワークフローをローカル実行(Docker が必要)
109
+ ```
110
+
111
+ ## ライセンス
112
+
113
+ [MIT](LICENSE)
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # claude-code-statusline
2
+
3
+ [English](https://github.com/z80020100/claude-code-statusline/blob/main/README.md) | [繁體中文](https://github.com/z80020100/claude-code-statusline/blob/main/README.zh-TW.md) | [日本語](https://github.com/z80020100/claude-code-statusline/blob/main/README.ja.md)
4
+
5
+ Custom status line for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — model info, context usage gradient bar, token stats, cost, git status, and rate limits.
6
+
7
+ ![Demo](https://raw.githubusercontent.com/z80020100/claude-code-statusline/main/assets/claude-code-statusline-demo.png)
8
+
9
+ ## Features
10
+
11
+ - **Context usage gradient bar** — 4-zone color spectrum from green to red
12
+ - **Token and cost tracking** — input/output tokens, cache hit ratio, session cost
13
+ - **Session timing** — wall-clock and API duration side by side
14
+ - **Git integration** — branch, dirty flag, worktree indicator, diff stats vs main
15
+ - **Rate limit monitoring** — current (5h) and weekly (7d) usage with reset times
16
+ - **Sandbox indicator** — shows whether sandbox mode is off, on, or auto
17
+ - **Path compression** — long paths auto-shorten to fit within 80 columns
18
+ - **Zero runtime dependencies** — Node.js built-ins only
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ npm install -g @z80020100/claude-code-statusline
24
+ ```
25
+
26
+ ## Setup
27
+
28
+ ```sh
29
+ claude-code-statusline setup
30
+ ```
31
+
32
+ This writes the `statusLine` entry to `~/.claude/settings.json`:
33
+
34
+ ```json
35
+ {
36
+ "statusLine": {
37
+ "type": "command",
38
+ "command": "claude-code-statusline"
39
+ }
40
+ }
41
+ ```
42
+
43
+ To remove:
44
+
45
+ ```sh
46
+ claude-code-statusline setup --uninstall
47
+ ```
48
+
49
+ ## Display Layout
50
+
51
+ All fields at maximum width:
52
+
53
+ ![All fields](https://raw.githubusercontent.com/z80020100/claude-code-statusline/main/assets/claude-code-statusline-simulation.png)
54
+
55
+ The status line renders up to 6 lines — each constrained to 80 visible columns:
56
+
57
+ | Line | Content |
58
+ | ---- | ------------------------------------------------------------------------------------------------- |
59
+ | 1 | Version, sandbox mode, session name and ID |
60
+ | 2 | Model name, effort level, context usage bar with percentage, last updated time |
61
+ | 3 | Token counts (in/out), cache hit %, cost, session/API duration, lines added/removed, 200K warning |
62
+ | 4 | Project directory, git branch, dirty flag, worktree indicator, diff vs main |
63
+ | 5 | Current working directory (only when different from project root) |
64
+ | 6 | Rate limit usage — current (5h) and weekly (7d) with reset times |
65
+
66
+ ### Color Zones
67
+
68
+ Context and rate limit bars use a 4-zone gradient:
69
+
70
+ | Range | Color | Meaning |
71
+ | ------- | ----- | -------- |
72
+ | 0–49% | Green | Normal |
73
+ | 50–69% | Gold | Moderate |
74
+ | 70–89% | Coral | Elevated |
75
+ | 90–100% | Red | Critical |
76
+
77
+ ## How It Works
78
+
79
+ Claude Code pipes a JSON object to the `statusLine` command via stdin on each render cycle. The JSON contains the current session state (model, tokens, cost, workspace, rate limits, etc.). This tool parses it and returns ANSI-colored lines to stdout.
80
+
81
+ Design decisions:
82
+
83
+ - **Zero dependencies** — only Node.js built-ins (`fs`, `path`, `os`, `child_process`)
84
+ - **Git caching** — branch and diff stats are cached for 5 seconds to avoid repeated subprocess calls
85
+ - **Settings caching** — effort level and sandbox mode use mtime-based caching to skip redundant file reads
86
+ - **80-column constraint** — enforced by automated tests; long paths are compressed automatically
87
+ - **256-color ANSI** — consistent rendering across terminals; Claude brand orange uses 24-bit true color
88
+
89
+ ## Requirements
90
+
91
+ | Dependency | Tier 1 (CI-tested) | Tier 2 (best-effort) |
92
+ | ----------- | ----------------------------------- | -------------------- |
93
+ | Node.js | >= 20 | 18 |
94
+ | Claude Code | >= 2.1.80 (for `rate_limits` field) | |
95
+
96
+ ## Development
97
+
98
+ ```sh
99
+ git clone https://github.com/z80020100/claude-code-statusline.git
100
+ cd claude-code-statusline
101
+ npm install # also enables pre-commit hooks via prepare
102
+
103
+ npm run check # lint + format check + width check + CLI tests
104
+ npm run fix # auto-fix lint and format issues
105
+ npm test # width check + CLI tests only
106
+ npm run lint # ESLint + shellcheck + actionlint
107
+ npm run simulate # render worst-case status line with width report
108
+ npm run ci:local # run CI workflow locally via act (requires Docker)
109
+ ```
110
+
111
+ ## License
112
+
113
+ [MIT](LICENSE)
@@ -0,0 +1,113 @@
1
+ # claude-code-statusline
2
+
3
+ [English](README.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md)
4
+
5
+ [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 的自訂狀態列 — 顯示模型資訊、上下文使用量漸層條、Token 統計、費用、Git 狀態和速率限制。
6
+
7
+ ![Demo](assets/claude-code-statusline-demo.png)
8
+
9
+ ## 功能
10
+
11
+ - **上下文使用量漸層條** — 綠到紅的 4 段色彩頻譜
12
+ - **Token 與費用追蹤** — 輸入/輸出 Token 數、快取命中率、Session 費用
13
+ - **Session 計時** — 同時顯示實際時間與 API 回應時間
14
+ - **Git 整合** — 分支名稱、修改標記、worktree 指示器、與 main 的差異統計
15
+ - **速率限制監控** — 當前 (5h) 和每週 (7d) 使用量及重置時間
16
+ - **Sandbox 指示器** — 顯示 sandbox 模式為關閉、開啟或自動
17
+ - **路徑壓縮** — 長路徑自動縮短以符合 80 欄限制
18
+ - **零執行時期依賴** — 僅使用 Node.js 內建模組
19
+
20
+ ## 安裝
21
+
22
+ ```sh
23
+ npm install -g @z80020100/claude-code-statusline
24
+ ```
25
+
26
+ ## 設定
27
+
28
+ ```sh
29
+ claude-code-statusline setup
30
+ ```
31
+
32
+ 這會將 `statusLine` 寫入 `~/.claude/settings.json`:
33
+
34
+ ```json
35
+ {
36
+ "statusLine": {
37
+ "type": "command",
38
+ "command": "claude-code-statusline"
39
+ }
40
+ }
41
+ ```
42
+
43
+ 移除設定:
44
+
45
+ ```sh
46
+ claude-code-statusline setup --uninstall
47
+ ```
48
+
49
+ ## 顯示配置
50
+
51
+ 所有欄位最大寬度的呈現:
52
+
53
+ ![所有欄位](assets/claude-code-statusline-simulation.png)
54
+
55
+ 狀態列最多顯示 6 行 — 每行限制在 80 個可見字元內:
56
+
57
+ | 行 | 內容 |
58
+ | --- | --------------------------------------------------------------------------------- |
59
+ | 1 | 版本、sandbox 模式、session 名稱和 ID |
60
+ | 2 | 模型名稱、effort 等級、上下文使用量條與百分比、最後更新時間 |
61
+ | 3 | Token 數 (輸入/輸出)、快取命中率、費用、session/API 持續時間、增減行數、200K 警告 |
62
+ | 4 | 專案目錄、git 分支、修改標記、worktree 指示器、與 main 的差異 |
63
+ | 5 | 目前工作目錄(僅在與專案根目錄不同時顯示) |
64
+ | 6 | 速率限制 — 當前 (5h) 和每週 (7d) 使用量及重置時間 |
65
+
66
+ ### 色彩區間
67
+
68
+ 上下文和速率限制的進度條使用 4 段漸層:
69
+
70
+ | 範圍 | 顏色 | 意義 |
71
+ | ------- | ---- | ---- |
72
+ | 0–49% | 綠色 | 正常 |
73
+ | 50–69% | 金色 | 中等 |
74
+ | 70–89% | 珊瑚 | 偏高 |
75
+ | 90–100% | 紅色 | 危險 |
76
+
77
+ ## 運作原理
78
+
79
+ Claude Code 在每次渲染時透過 stdin 傳入 JSON 物件給 `statusLine` 指令。JSON 包含目前的 session 狀態(模型、Token、費用、工作區、速率限制等)。本工具解析後輸出 ANSI 彩色文字到 stdout。
80
+
81
+ 設計決策:
82
+
83
+ - **零依賴** — 僅使用 Node.js 內建模組 (`fs`, `path`, `os`, `child_process`)
84
+ - **Git 快取** — 分支和差異統計快取 5 秒以避免重複呼叫子程序
85
+ - **設定快取** — effort 等級和 sandbox 模式使用 mtime 快取以減少檔案讀取
86
+ - **80 欄限制** — 由自動化測試強制執行;長路徑自動壓縮
87
+ - **256 色 ANSI** — 跨終端一致性渲染;Claude 品牌橘色使用 24-bit true color
88
+
89
+ ## 系統需求
90
+
91
+ | 依賴 | Tier 1(CI 測試) | Tier 2(best-effort) |
92
+ | ----------- | ------------------------------------ | --------------------- |
93
+ | Node.js | >= 20 | 18 |
94
+ | Claude Code | >= 2.1.80(需要 `rate_limits` 欄位) | |
95
+
96
+ ## 開發
97
+
98
+ ```sh
99
+ git clone https://github.com/z80020100/claude-code-statusline.git
100
+ cd claude-code-statusline
101
+ npm install # 同時透過 prepare 啟用 pre-commit hooks
102
+
103
+ npm run check # lint + 格式檢查 + 寬度檢查 + CLI 測試
104
+ npm run fix # 自動修正 lint 和格式問題
105
+ npm test # 寬度檢查 + CLI 測試
106
+ npm run lint # ESLint + shellcheck + actionlint
107
+ npm run simulate # 渲染 worst-case 狀態列並顯示寬度報告
108
+ npm run ci:local # 透過 act 在本地執行 CI(需要 Docker)
109
+ ```
110
+
111
+ ## 授權
112
+
113
+ [MIT](LICENSE)
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const arg = process.argv[2];
6
+
7
+ if (arg === "setup") {
8
+ require("../lib/setup.js").run(process.argv.slice(3));
9
+ } else if (arg === "--version" || arg === "-v") {
10
+ console.log(require("../package.json").version);
11
+ } else if (arg === "--help" || arg === "-h" || process.stdin.isTTY) {
12
+ const pkg = require("../package.json");
13
+ const repo = pkg.repository.url.replace(/^git\+/, "").replace(/\.git$/, "");
14
+ console.log(`${pkg.name} v${pkg.version}
15
+ ${pkg.description}
16
+
17
+ Usage:
18
+ claude-code-statusline Read JSON from stdin and render status line
19
+ claude-code-statusline setup Configure Claude Code to use this status line
20
+ claude-code-statusline setup --uninstall Remove status line configuration
21
+ claude-code-statusline --help Show this help message
22
+ claude-code-statusline --version Show version
23
+
24
+ Author: ${pkg.author}
25
+ License: ${pkg.license}
26
+ Repository: ${repo}`);
27
+ } else {
28
+ const fs = require("fs");
29
+ const { render } = require("../lib/statusline.js");
30
+ try {
31
+ const data = JSON.parse(fs.readFileSync(0, "utf8"));
32
+ const lines = render(data);
33
+ for (const line of lines) {
34
+ console.log(line);
35
+ }
36
+ } catch {
37
+ // Silent fail — Claude Code expects no output on error
38
+ }
39
+ }
package/lib/setup.js ADDED
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const readline = require("readline");
7
+
8
+ const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
9
+ const STATUS_LINE_VALUE = {
10
+ type: "command",
11
+ command: "claude-code-statusline",
12
+ };
13
+
14
+ function readSettings() {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf8"));
17
+ } catch (err) {
18
+ if (err.code === "ENOENT") return {};
19
+ throw err;
20
+ }
21
+ }
22
+
23
+ function writeSettings(data) {
24
+ const dir = path.dirname(SETTINGS_PATH);
25
+ if (!fs.existsSync(dir)) {
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ }
28
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n");
29
+ }
30
+
31
+ function setup() {
32
+ const settings = readSettings();
33
+
34
+ // Already configured
35
+ if (
36
+ settings.statusLine &&
37
+ settings.statusLine.command === "claude-code-statusline"
38
+ ) {
39
+ console.log("Already configured. No changes needed.");
40
+ return;
41
+ }
42
+
43
+ // Existing different value — ask to overwrite
44
+ if (settings.statusLine) {
45
+ console.log("Current statusLine setting:");
46
+ console.log(" " + JSON.stringify(settings.statusLine));
47
+ console.log();
48
+
49
+ const rl = readline.createInterface({
50
+ input: process.stdin,
51
+ output: process.stdout,
52
+ });
53
+ rl.question("Overwrite? (y/N) ", (answer) => {
54
+ rl.close();
55
+ if (answer.toLowerCase() !== "y") {
56
+ console.log("Aborted.");
57
+ return;
58
+ }
59
+ applySetup(settings);
60
+ });
61
+ return;
62
+ }
63
+
64
+ applySetup(settings);
65
+ }
66
+
67
+ function applySetup(settings) {
68
+ console.log("Before:");
69
+ console.log(" " + JSON.stringify(settings.statusLine ?? null));
70
+ settings.statusLine = STATUS_LINE_VALUE;
71
+ writeSettings(settings);
72
+ console.log("After:");
73
+ console.log(" " + JSON.stringify(settings.statusLine));
74
+ console.log();
75
+ console.log("Wrote " + SETTINGS_PATH);
76
+ }
77
+
78
+ function uninstall() {
79
+ const settings = readSettings();
80
+ if (!settings.statusLine) {
81
+ console.log("No statusLine setting found. No changes needed.");
82
+ return;
83
+ }
84
+ console.log("Removing statusLine:");
85
+ console.log(" " + JSON.stringify(settings.statusLine));
86
+ delete settings.statusLine;
87
+ writeSettings(settings);
88
+ console.log();
89
+ console.log("Wrote " + SETTINGS_PATH);
90
+ }
91
+
92
+ function run(args) {
93
+ try {
94
+ if (args.includes("--uninstall")) {
95
+ uninstall();
96
+ } else {
97
+ setup();
98
+ }
99
+ } catch (err) {
100
+ console.error("Error: " + err.message);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ module.exports = { run };
@@ -0,0 +1,478 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { execSync } = require("child_process");
5
+ const path = require("path");
6
+ const os = require("os");
7
+
8
+ /**
9
+ * Render Claude Code status line.
10
+ * @param {object} data - JSON data from Claude Code stdin
11
+ * @param {object} [options] - Overrides for testability and cross-platform
12
+ * @param {object} [options.git] - { branch, dirty, isWorktree, diffAdded, diffRemoved }
13
+ * @param {string} [options.effort] - Effort level override
14
+ * @param {string} [options.sandboxMode] - Sandbox mode override ("", "auto", "on")
15
+ * @param {string} [options.home] - Home directory override
16
+ * @param {Date} [options.now] - Current time override
17
+ * @returns {string[]} Array of ANSI-colored lines
18
+ */
19
+ function render(data, options = {}) {
20
+ const model = data.model?.display_name || "?";
21
+ const usedPct = data.context_window?.used_percentage ?? 0;
22
+ const cost = data.cost?.total_cost_usd ?? 0;
23
+ const linesAdded = data.cost?.total_lines_added ?? 0;
24
+ const linesRemoved = data.cost?.total_lines_removed ?? 0;
25
+ const exceeds200k = data.exceeds_200k_tokens ?? false;
26
+ const totalInput = data.context_window?.total_input_tokens ?? 0;
27
+ const totalOutput = data.context_window?.total_output_tokens ?? 0;
28
+ const uncachedInput = data.context_window?.current_usage?.input_tokens ?? 0;
29
+ const cacheRead =
30
+ data.context_window?.current_usage?.cache_read_input_tokens ?? 0;
31
+ const cacheCreate =
32
+ data.context_window?.current_usage?.cache_creation_input_tokens ?? 0;
33
+ const ctxSize = data.context_window?.context_window_size ?? 0;
34
+ const durationMs = data.cost?.total_duration_ms ?? 0;
35
+ const apiDurationMs = data.cost?.total_api_duration_ms ?? 0;
36
+ const projectDir = data.workspace?.project_dir || "";
37
+ const cwd = data.workspace?.current_dir || "";
38
+ const version = data.version || "";
39
+ const sessionName = data.session_name || "";
40
+ const sessionId = data.session_id || "";
41
+
42
+ // ANSI styles — 256-color for cross-terminal consistency
43
+ // Palette: Developer Tool / Modern Dark (desaturated, semantic)
44
+ // Exception: claudeOrange uses 24-bit true color for brand accuracy (#da7756)
45
+ const reset = "\x1b[0m";
46
+ const bold = "\x1b[1m";
47
+ const dim = "\x1b[2m";
48
+ // Foreground hierarchy
49
+ const white = "\x1b[38;5;255m"; // headlines, key values
50
+ const softWhite = "\x1b[38;5;252m"; // secondary values
51
+ const gray = "\x1b[38;5;247m"; // muted labels, token counts
52
+ const darkGray = "\x1b[38;5;239m"; // separators, empty bar segments
53
+ // Semantic accent
54
+ const violet = "\x1b[38;5;141m"; // primary accent: effort, branch
55
+ const blue = "\x1b[38;5;75m"; // info: cwd path, agent
56
+ const dimBlue = "\x1b[38;5;68m"; // subdued: project path
57
+ const teal = "\x1b[38;5;116m"; // secondary: cost, worktree
58
+ // Status spectrum (progress bars, +/- lines)
59
+ const green = "\x1b[38;5;114m"; // good / positive
60
+ const gold = "\x1b[38;5;179m"; // moderate
61
+ const coral = "\x1b[38;5;209m"; // elevated
62
+ const red = "\x1b[38;5;167m"; // critical
63
+ // Identity
64
+ const claudeOrange = "\x1b[38;2;218;119;86m"; // #da7756, 24-bit true color
65
+
66
+ // ── Helpers ──────────────────────────────────────────
67
+
68
+ // Shared thresholds: single source of truth for color zones
69
+ const pctZones = [
70
+ { at: 90, color: red },
71
+ { at: 70, color: coral },
72
+ { at: 50, color: gold },
73
+ { at: 0, color: green },
74
+ ];
75
+
76
+ function colorForPct(pct) {
77
+ for (const z of pctZones) {
78
+ if (pct >= z.at) return z.color;
79
+ }
80
+ return green;
81
+ }
82
+
83
+ // Gradient bar — each filled segment colored by its zone position
84
+ function buildBar(pct, width) {
85
+ const clamped = Math.max(0, Math.min(100, pct));
86
+ const filled = Math.round((clamped / 100) * width);
87
+ let s = "";
88
+ for (let i = 0; i < width; i++) {
89
+ if (i < filled) {
90
+ s += colorForPct((i / width) * 100) + "\u2501";
91
+ } else {
92
+ s += darkGray + "\u2501";
93
+ }
94
+ }
95
+ return s + reset;
96
+ }
97
+
98
+ function fmt(n) {
99
+ if (n >= 999950) return (n / 1e6).toFixed(1) + "M";
100
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
101
+ return String(n);
102
+ }
103
+
104
+ function fmtDuration(ms) {
105
+ const totalSec = Math.floor(ms / 1000);
106
+ const h = Math.floor(totalSec / 3600);
107
+ const m = Math.floor((totalSec % 3600) / 60);
108
+ const s = totalSec % 60;
109
+ if (h > 0) return `${h}h${String(m).padStart(2, "0")}m`;
110
+ if (m > 0) return `${m}m${String(s).padStart(2, "0")}s`;
111
+ return `${s}s`;
112
+ }
113
+
114
+ const now = options.now ?? new Date();
115
+
116
+ function fmtResetTime(epochSec) {
117
+ if (!epochSec) return "";
118
+ const d = new Date(epochSec * 1000);
119
+ if (isNaN(d.getTime())) return "";
120
+ const hh = String(d.getHours()).padStart(2, "0");
121
+ const mm = String(d.getMinutes()).padStart(2, "0");
122
+ // Same day → time only; different day → date + time
123
+ if (
124
+ d.getFullYear() === now.getFullYear() &&
125
+ d.getMonth() === now.getMonth() &&
126
+ d.getDate() === now.getDate()
127
+ ) {
128
+ return `${hh}:${mm}`;
129
+ }
130
+ return `${d.getMonth() + 1}/${d.getDate()} ${hh}:${mm}`;
131
+ }
132
+
133
+ const ansiRe = /\x1b\[[0-9;]*m/g;
134
+ function visibleLen(s) {
135
+ return s.replace(ansiRe, "").length;
136
+ }
137
+
138
+ function compressPath(p, maxLen) {
139
+ if (p.length <= maxLen) return p;
140
+ const parts = p.split("/");
141
+ // Abbreviate from left (skip ~ at index 0 and last component)
142
+ for (let i = 1; i < parts.length - 1; i++) {
143
+ if (parts[i].length > 1) {
144
+ parts[i] = parts[i][0];
145
+ const result = parts.join("/");
146
+ if (result.length <= maxLen) return result;
147
+ }
148
+ }
149
+ return parts.join("/");
150
+ }
151
+
152
+ const maxCols = 80;
153
+ const sep = ` ${darkGray}\u2502${reset} `;
154
+ const barWidth = 10;
155
+
156
+ // ── Directory paths ──────────────────────────────────
157
+ const home = options.home ?? os.homedir();
158
+ function tildify(p) {
159
+ return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
160
+ }
161
+ const dirPath = projectDir ? tildify(projectDir) : "";
162
+ const cwdPath = cwd && cwd !== projectDir ? tildify(cwd) : "";
163
+
164
+ // ── Git branch + dirty + worktree + diff vs main (cached) ──
165
+ let branch = "";
166
+ let dirty = "";
167
+ let isWorktree = false;
168
+ let diffAdded = 0;
169
+ let diffRemoved = 0;
170
+ if (options.git) {
171
+ ({
172
+ branch = "",
173
+ dirty = "",
174
+ isWorktree = false,
175
+ diffAdded = 0,
176
+ diffRemoved = 0,
177
+ } = options.git);
178
+ } else if (cwd) {
179
+ const cacheFile = path.join(os.tmpdir(), ".claude-statusline-git.json");
180
+ const cacheMaxAge = 5000;
181
+ let useCache = false;
182
+ try {
183
+ const stat = fs.statSync(cacheFile);
184
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
185
+ if (cached.cwd === cwd && Date.now() - stat.mtimeMs < cacheMaxAge) {
186
+ branch = cached.branch;
187
+ dirty = cached.dirty;
188
+ isWorktree = cached.isWorktree ?? false;
189
+ diffAdded = cached.diffAdded ?? 0;
190
+ diffRemoved = cached.diffRemoved ?? 0;
191
+ useCache = true;
192
+ }
193
+ } catch {}
194
+ if (!useCache) {
195
+ try {
196
+ const statusOut = execSync("git status --short --branch", {
197
+ cwd,
198
+ timeout: 2000,
199
+ encoding: "utf8",
200
+ stdio: ["pipe", "pipe", "ignore"],
201
+ }).trim();
202
+ const lines = statusOut.split("\n");
203
+ // First line: "## branch...tracking" or "## HEAD (no branch)"
204
+ const branchMatch = lines[0].match(/^## (\S+?)(?:\.\.\.|$)/);
205
+ branch = branchMatch ? branchMatch[1] : "";
206
+ // Remaining lines = changed files
207
+ dirty = lines.length > 1 ? "*" : "";
208
+ // Detect git worktree: git-dir differs from git-common-dir
209
+ try {
210
+ const revOut = execSync(
211
+ "git rev-parse --path-format=absolute --git-dir --git-common-dir",
212
+ {
213
+ cwd,
214
+ timeout: 1000,
215
+ encoding: "utf8",
216
+ stdio: ["pipe", "pipe", "ignore"],
217
+ },
218
+ ).trim();
219
+ const [gitDir, commonDir] = revOut.split("\n");
220
+ isWorktree = gitDir !== commonDir;
221
+ } catch {}
222
+ // Diff stats relative to main (or master) branch
223
+ for (const base of ["main", "master"]) {
224
+ try {
225
+ const diffOut = execSync(`git diff ${base} --shortstat`, {
226
+ cwd,
227
+ timeout: 2000,
228
+ encoding: "utf8",
229
+ stdio: ["pipe", "pipe", "ignore"],
230
+ }).trim();
231
+ const insMatch = diffOut.match(/(\d+) insertion/);
232
+ const delMatch = diffOut.match(/(\d+) deletion/);
233
+ if (insMatch) diffAdded = Number(insMatch[1]);
234
+ if (delMatch) diffRemoved = Number(delMatch[1]);
235
+ break;
236
+ } catch {}
237
+ }
238
+ fs.writeFileSync(
239
+ cacheFile,
240
+ JSON.stringify({
241
+ cwd,
242
+ branch,
243
+ dirty,
244
+ isWorktree,
245
+ diffAdded,
246
+ diffRemoved,
247
+ }),
248
+ "utf8",
249
+ );
250
+ } catch {}
251
+ }
252
+ }
253
+
254
+ // ── Effort level (cached by mtime) ─────────────────
255
+ // Priority: env CLAUDE_CODE_EFFORT_LEVEL → settings.effortLevel → "default"
256
+ let effort = "default";
257
+ if (options.effort !== undefined) {
258
+ effort = options.effort;
259
+ } else {
260
+ const settingsPath = path.join(home, ".claude", "settings.json");
261
+ const settingsCacheFile = path.join(
262
+ os.tmpdir(),
263
+ ".claude-statusline-settings.json",
264
+ );
265
+ try {
266
+ const mtime = fs.statSync(settingsPath).mtimeMs;
267
+ let useSettingsCache = false;
268
+ try {
269
+ const cached = JSON.parse(fs.readFileSync(settingsCacheFile, "utf8"));
270
+ if (cached.mtime === mtime) {
271
+ effort = cached.effort;
272
+ useSettingsCache = true;
273
+ }
274
+ } catch {}
275
+ if (!useSettingsCache) {
276
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
277
+ const envEffort =
278
+ process.env.CLAUDE_CODE_EFFORT_LEVEL ||
279
+ settings.env?.CLAUDE_CODE_EFFORT_LEVEL;
280
+ effort = envEffort || settings.effortLevel || "default";
281
+ fs.writeFileSync(
282
+ settingsCacheFile,
283
+ JSON.stringify({ mtime, effort }),
284
+ "utf8",
285
+ );
286
+ }
287
+ } catch {}
288
+ }
289
+
290
+ // ── Sandbox state (cached by mtime) ─────────────────
291
+ // mtime = 0 when file absent so the cache still works (measure script seeds mtime: 0)
292
+ let sandboxMode = ""; // "", "auto", "on"
293
+ if (options.sandboxMode !== undefined) {
294
+ sandboxMode = options.sandboxMode;
295
+ } else if (projectDir) {
296
+ const localSettingsPath = path.join(
297
+ projectDir,
298
+ ".claude",
299
+ "settings.local.json",
300
+ );
301
+ const sandboxCacheFile = path.join(
302
+ os.tmpdir(),
303
+ ".claude-statusline-sandbox.json",
304
+ );
305
+ let fileMtime = 0;
306
+ try {
307
+ fileMtime = fs.statSync(localSettingsPath).mtimeMs;
308
+ } catch {}
309
+ let useSandboxCache = false;
310
+ try {
311
+ const cached = JSON.parse(fs.readFileSync(sandboxCacheFile, "utf8"));
312
+ if (cached.mtime === fileMtime && cached.projectDir === projectDir) {
313
+ sandboxMode = cached.sandboxMode;
314
+ useSandboxCache = true;
315
+ }
316
+ } catch {}
317
+ if (!useSandboxCache) {
318
+ try {
319
+ const localSettings = JSON.parse(
320
+ fs.readFileSync(localSettingsPath, "utf8"),
321
+ );
322
+ if (localSettings.sandbox?.enabled) {
323
+ sandboxMode = localSettings.sandbox.autoAllowBashIfSandboxed
324
+ ? "auto"
325
+ : "on";
326
+ }
327
+ } catch {}
328
+ try {
329
+ fs.writeFileSync(
330
+ sandboxCacheFile,
331
+ JSON.stringify({ mtime: fileMtime, projectDir, sandboxMode }),
332
+ "utf8",
333
+ );
334
+ } catch {}
335
+ }
336
+ }
337
+
338
+ const effortDisplay = {
339
+ max: `${red}\u25CF ${effort}${reset}`,
340
+ high: `${violet}\u25CF ${effort}${reset}`,
341
+ medium: `${dim}\u25D1 ${effort}${reset}`,
342
+ default: `${dim}\u25D1 ${effort}${reset}`,
343
+ low: `${dim}\u25D4 ${effort}${reset}`,
344
+ };
345
+ const effortStr = effortDisplay[effort] ?? effortDisplay.default;
346
+
347
+ // ── Rate limit usage (native field, v2.1.80+) ──────
348
+ const usageData = data.rate_limits || null;
349
+
350
+ // ── Build output ────────────────────────────────────
351
+
352
+ const ctxLabel = ctxSize > 0 ? fmt(ctxSize) : "";
353
+ const costStr = cost < 0.01 && cost > 0 ? "<0.01" : cost.toFixed(2);
354
+ const pctColor = colorForPct(usedPct);
355
+
356
+ // Sandbox indicator for version line (always shown)
357
+ const sandboxIcons = {
358
+ auto: `${green}\uf132\uf0e7${reset}`,
359
+ on: `${teal}\uf132${reset}`,
360
+ "": `${dim}\uf132${reset}`,
361
+ };
362
+ const sandboxStr = sandboxIcons[sandboxMode];
363
+
364
+ // Version banner: ✻ (U+273B) matches Claude Code startup style
365
+ const shortId = sessionId ? sessionId.slice(0, 7) : "";
366
+ const sessionSuffix = shortId
367
+ ? sessionName
368
+ ? `${sep}${softWhite}${sessionName}${reset} ${gray}(${shortId})${reset}`
369
+ : `${sep}${gray}(${shortId})${reset}`
370
+ : "";
371
+ const versionLine = version
372
+ ? `${claudeOrange}\u273B ${bold}${claudeOrange}Claude Code${reset} ${dim}v${version}${reset} ${sandboxStr}${sessionSuffix}`
373
+ : "";
374
+
375
+ // Model line: identity, effort │ bar, ctx size │ updated
376
+ // Usage line: tokens, cache │ cost, duration, diff │ warning
377
+ const identityGroup = `${bold}${white}${model}${reset} ${effortStr}`;
378
+ const currentTurnInput = uncachedInput + cacheCreate + cacheRead;
379
+ const cacheStr =
380
+ currentTurnInput > 0 && cacheRead > 0
381
+ ? ` ${teal}\uf1c0 ${softWhite}${Math.round((cacheRead / currentTurnInput) * 100)}%${reset}`
382
+ : "";
383
+ const barGroup =
384
+ `${buildBar(usedPct, barWidth)} ${pctColor}${bold}${usedPct}%${reset}` +
385
+ (ctxLabel ? ` ${darkGray}(${gray}${ctxLabel}${darkGray})${reset}` : "");
386
+ const tokenGroup =
387
+ `${gray}${fmt(totalInput)}\u2191 ${fmt(totalOutput)}\u2193${reset}` +
388
+ cacheStr;
389
+ const costParts = [`${teal}$${costStr}${reset}`];
390
+ if (durationMs > 0) {
391
+ let timeStr = `${gray}\uf253 ${fmtDuration(durationMs)}${reset}`;
392
+ if (apiDurationMs > 0) {
393
+ timeStr += ` ${gold}\uf0e7 ${gray}${fmtDuration(apiDurationMs)}${reset}`;
394
+ }
395
+ costParts.push(timeStr);
396
+ }
397
+ const hhmm = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
398
+ const updatedStr = `${gray}updated${reset} ${darkGray}\uf017${reset} ${softWhite}${hhmm}${reset}`;
399
+ const modelParts = [identityGroup, barGroup, updatedStr];
400
+ const usageParts = [tokenGroup, costParts.join(" ")];
401
+ if (linesAdded > 0 || linesRemoved > 0) {
402
+ usageParts.push(
403
+ `${green}+${linesAdded}${reset} ${red}-${linesRemoved}${reset}`,
404
+ );
405
+ }
406
+ if (exceeds200k) {
407
+ usageParts.push(`${red}${bold}\u26A0 200K+${reset}`);
408
+ }
409
+
410
+ // Workspace line: path, branch, diff vs main
411
+ const workspaceParts = [];
412
+ if (dirPath) {
413
+ workspaceParts.push(`${blue}\uf041 ${dirPath}${reset}`);
414
+ }
415
+ if (branch) {
416
+ const dirtyStr = dirty ? `${coral}${dirty}${reset}` : "";
417
+ const diffStr =
418
+ diffAdded > 0 || diffRemoved > 0
419
+ ? ` ${green}+${diffAdded}${reset} ${red}-${diffRemoved}${reset}`
420
+ : "";
421
+ const wtStr = isWorktree ? ` ${teal}\uf1bb${reset}` : "";
422
+ workspaceParts.push(
423
+ `${violet}\ue725 ${branch}${dirtyStr}${wtStr}${diffStr}${reset}`,
424
+ );
425
+ }
426
+ // Compress leading path in a parts array when line exceeds maxCols
427
+ function compressLeadingPath(parts, rawPath, prefix) {
428
+ if (!rawPath || parts.length === 0) return;
429
+ const totalWidth = visibleLen(parts.join(sep));
430
+ if (totalWidth <= maxCols) return;
431
+ const maxPathLen = Math.max(1, maxCols - (totalWidth - rawPath.length));
432
+ parts[0] = `${prefix}${compressPath(rawPath, maxPathLen)}${reset}`;
433
+ }
434
+ compressLeadingPath(workspaceParts, dirPath, `${blue}\uf041 `);
435
+
436
+ // CWD line: shown only when cwd differs from project_dir
437
+ const cwdParts = [];
438
+ if (cwdPath) {
439
+ cwdParts.push(`${dimBlue}\uf124 ${cwdPath}${reset}`);
440
+ }
441
+ compressLeadingPath(cwdParts, cwdPath, `${dimBlue}\uf124 `);
442
+
443
+ // Rate limits line: current │ weekly │ updated
444
+ function fmtRate(rateWindow, label) {
445
+ if (
446
+ !rateWindow ||
447
+ rateWindow.used_percentage === undefined ||
448
+ rateWindow.used_percentage === null
449
+ ) {
450
+ const bar = buildBar(0, barWidth);
451
+ return `${gray}${label}${reset} ${bar} ${darkGray}--% \uf017 --:--${reset}`;
452
+ }
453
+ const pct = Math.round(rateWindow.used_percentage);
454
+ const rs = fmtResetTime(rateWindow.resets_at);
455
+ const bar = `${buildBar(pct, barWidth)} ${colorForPct(pct)}${String(pct).padStart(3)}%${reset}`;
456
+ const resetStr = rs
457
+ ? ` ${darkGray}\uf017${reset} ${softWhite}${rs}${reset}`
458
+ : "";
459
+ return `${gray}${label}${reset} ${bar}${resetStr}`;
460
+ }
461
+ const rateParts = [
462
+ fmtRate(usageData?.five_hour, "current"),
463
+ fmtRate(usageData?.seven_day, "weekly"),
464
+ ];
465
+
466
+ // ── Collect output lines ────────────────────────────
467
+ const output = [];
468
+ if (versionLine) output.push(versionLine);
469
+ output.push(modelParts.join(sep));
470
+ output.push(usageParts.join(sep));
471
+ if (workspaceParts.length > 0) output.push(workspaceParts.join(sep));
472
+ if (cwdParts.length > 0) output.push(cwdParts.join(sep));
473
+ output.push(rateParts.join(sep));
474
+
475
+ return output;
476
+ }
477
+
478
+ module.exports = { render };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@z80020100/claude-code-statusline",
3
+ "version": "0.1.0",
4
+ "description": "Custom status line for Claude Code — model info, context usage gradient bar, token stats, cost, git status, and rate limits",
5
+ "main": "lib/statusline.js",
6
+ "bin": {
7
+ "claude-code-statusline": "bin/claude-code-statusline.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "check": "npm run lint && npm run format:check && npm run test",
17
+ "fix": "npm run lint:fix && npm run format:fix",
18
+ "test": "node test/measure-width.js --check && node test/cli.js",
19
+ "simulate": "node test/measure-width.js",
20
+ "ci:local": "act push --container-architecture linux/amd64",
21
+ "lint": "npm run lint:js && npm run lint:sh && npm run lint:yml",
22
+ "lint:js": "npx --yes eslint .",
23
+ "lint:sh": "shellcheck .githooks/pre-commit",
24
+ "lint:yml": "actionlint .github/workflows/*.yml",
25
+ "lint:fix": "npx --yes eslint --fix .",
26
+ "format:check": "npm run format:check:js && npm run format:check:sh",
27
+ "format:check:js": "npx --yes prettier --check .",
28
+ "format:check:sh": "shfmt -i 4 -d .githooks/pre-commit",
29
+ "format:fix": "npm run format:fix:js && npm run format:fix:sh",
30
+ "format:fix:js": "npx --yes prettier --write .",
31
+ "format:fix:sh": "shfmt -i 4 -w .githooks/pre-commit",
32
+ "prepare": "git config --local core.hooksPath .githooks",
33
+ "postinstall": "node -e \"console.log('\\n Run setup to configure Claude Code automatically:\\n claude-code-statusline setup\\n')\"",
34
+ "prepublishOnly": "npm run test"
35
+ },
36
+ "keywords": [
37
+ "claude-code",
38
+ "statusline",
39
+ "status-line",
40
+ "cli",
41
+ "terminal",
42
+ "ansi"
43
+ ],
44
+ "license": "MIT",
45
+ "author": "Cliff Wu <z800201002005@gmail.com>",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/z80020100/claude-code-statusline.git"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }