Package not found. Please check the package name and try again.
@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 +21 -0
- package/README.ja.md +113 -0
- package/README.md +113 -0
- package/README.zh-TW.md +113 -0
- package/bin/claude-code-statusline.js +39 -0
- package/lib/setup.js +105 -0
- package/lib/statusline.js +478 -0
- package/package.json +56 -0
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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)
|
package/README.zh-TW.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) 的自訂狀態列 — 顯示模型資訊、上下文使用量漸層條、Token 統計、費用、Git 狀態和速率限制。
|
|
6
|
+
|
|
7
|
+

|
|
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
|
+

|
|
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
|
+
}
|