@cxmyu/ccglance 1.2.2 → 1.4.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/README.md +30 -10
- package/README.zh-CN.md +26 -9
- package/dist/cli.js +19 -4
- package/dist/defaults/index.js +2 -2
- package/dist/readers/transcript.js +10 -4
- package/dist/segments/index.js +65 -14
- package/dist/utils/format.js +15 -7
- package/docs/assets/preview-macos.png +0 -0
- package/docs/assets/preview-windows.png +0 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
**A fast, zero-dependency, multi-line status line for [Claude Code](https://docs.anthropic.com/en/docs/claude-code)**
|
|
6
6
|
|
|
7
|
-
model · effort · context · cache · git · session — all at a glance
|
|
7
|
+
model · effort · context · prompt cache · limits · git · session — all at a glance
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
12
|
[English](./README.md) · [简体中文](./README.zh-CN.md) · [GitHub](https://github.com/CxMYu/CcGlanceLine)
|
|
13
13
|
|
|
14
|
-
**Windows 11 · Segoe UI Emoji**
|
|
14
|
+
**Windows 11 · Segoe UI Emoji · Claude Code 2.1.258 sample**
|
|
15
15
|
|
|
16
16
|

|
|
17
17
|
|
|
18
|
-
**macOS · Apple Color Emoji**
|
|
18
|
+
**macOS · Apple Color Emoji · Claude Code 2.1.258 sample**
|
|
19
19
|
|
|
20
20
|

|
|
21
21
|
|
|
@@ -23,6 +23,23 @@ model · effort · context · cache · git · session — all at a glance
|
|
|
23
23
|
|
|
24
24
|
> **stdin-first · zero runtime dependencies · fixed emoji style** — renders the JSON Claude Code pipes to its status-line command; only the `status` segment reads a bounded transcript tail. Written in TypeScript, shipped as compiled JavaScript; runs on Node.js ≥ 22 or Bun.
|
|
25
25
|
|
|
26
|
+
Current Claude Code cache and usage-limit fields are covered, including the
|
|
27
|
+
latest request's prompt-cache hit ratio and token traffic, warm/cold state, and
|
|
28
|
+
the Hour, Week, and account Limit usage windows.
|
|
29
|
+
Older Claude Code versions keep working through optional fields and the legacy
|
|
30
|
+
cache calculation.
|
|
31
|
+
|
|
32
|
+
Updated for Claude Code 2.1.309: model window badges use the actual context
|
|
33
|
+
capacity even without an `[1m]` suffix, thinking-only transcript entries stay
|
|
34
|
+
in the thinking state, and gateway limits optionally include dollar amounts:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
🤖 Opus 5.5 1M | 🧠 high | 💭
|
|
38
|
+
📊 Limit 🌔 63% $314.00/$500.00 monthly
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The platform images above remain the Claude Code 2.1.258 emoji reference samples.
|
|
42
|
+
|
|
26
43
|
**Docs:** [Overview](./docs/overview.md) · [Segments & icons](./docs/segments.md) · [Development](./docs/development.md)
|
|
27
44
|
|
|
28
45
|
## Terminal Font
|
|
@@ -97,15 +114,17 @@ Add a `statusLine` block to `~/.claude/settings.json`:
|
|
|
97
114
|
"statusLine": {
|
|
98
115
|
"type": "command",
|
|
99
116
|
"command": "ccglance",
|
|
100
|
-
"padding": 0
|
|
117
|
+
"padding": 0,
|
|
118
|
+
"refreshInterval": 5
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
```
|
|
104
122
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
`refreshInterval` is in seconds and requires Claude Code >= 2.1.97. This optional
|
|
124
|
+
5-second refresh keeps transcript-derived status, elapsed time and countdowns
|
|
125
|
+
moving while the main session is idle. Remove it for event-only refreshes or
|
|
126
|
+
older Claude Code versions. It does not make API usage update before the next
|
|
127
|
+
response, and submitting a prompt does not guarantee an immediate redraw.
|
|
109
128
|
|
|
110
129
|
If you didn't install globally, point Node at the compiled file directly:
|
|
111
130
|
|
|
@@ -118,7 +137,8 @@ If you didn't install globally, point Node at the compiled file directly:
|
|
|
118
137
|
}
|
|
119
138
|
```
|
|
120
139
|
|
|
121
|
-
On Windows use
|
|
140
|
+
On Windows use forward slashes and quote paths with spaces, e.g.
|
|
141
|
+
`"command": "node \"D:/path with spaces/ccglance/dist/cli.js\""`.
|
|
122
142
|
|
|
123
143
|
> **Cross-platform paths:** Claude Code v2.1.47+ expands a leading `~` in the
|
|
124
144
|
> command on every platform, so `node ~/path/to/dist/cli.js` works too. Prefer
|
|
@@ -127,7 +147,7 @@ On Windows use a full path, e.g. `node D:\\path\\to\\ccglance\\dist\\cli.js`.
|
|
|
127
147
|
> indent so the row starts at the left edge.
|
|
128
148
|
|
|
129
149
|
> **Windows tip:** if status-line startup latency matters, point `command`
|
|
130
|
-
> directly at the compiled file (`node D
|
|
150
|
+
> directly at the compiled file (`node D:/absolute/path/to/ccglance/dist/cli.js`)
|
|
131
151
|
> instead of the global `ccglance` command — it usually starts a bit faster.
|
|
132
152
|
|
|
133
153
|
## Contributing
|
package/README.zh-CN.md
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
**面向 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 的快速、零依赖多行状态栏**
|
|
6
6
|
|
|
7
|
-
模型 · effort · 上下文 ·
|
|
7
|
+
模型 · effort · 上下文 · prompt cache · 限额 · git · 会话 —— 一眼尽览
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
12
|
[English](./README.md) · [简体中文](./README.zh-CN.md) · [GitHub](https://github.com/CxMYu/CcGlanceLine)
|
|
13
13
|
|
|
14
|
-
**Windows 11 · Segoe UI Emoji
|
|
14
|
+
**Windows 11 · Segoe UI Emoji · Claude Code 2.1.258 示例**
|
|
15
15
|
|
|
16
16
|

|
|
17
17
|
|
|
18
|
-
**macOS · Apple Color Emoji
|
|
18
|
+
**macOS · Apple Color Emoji · Claude Code 2.1.258 示例**
|
|
19
19
|
|
|
20
20
|

|
|
21
21
|
|
|
@@ -23,6 +23,20 @@
|
|
|
23
23
|
|
|
24
24
|
> **stdin 优先 · 运行时零依赖 · 固定 emoji 风格** —— 渲染 Claude Code 经 stdin 传入的 JSON;仅 `status` 段读取 transcript 尾部固定字节。TypeScript 编写,以编译后的 JavaScript 发布,可在 Node.js ≥ 22 或 Bun 上运行。
|
|
25
25
|
|
|
26
|
+
已覆盖新版 Claude Code 的缓存和限额字段,包括最近一次请求的 prompt cache 命中率与
|
|
27
|
+
token 流量、缓存冷热状态,以及 Hour、Week 和账户 Limit 三类用量窗口。旧版 Claude Code 缺少这些可选字段时,
|
|
28
|
+
仍会自动沿用原有缓存计算逻辑。
|
|
29
|
+
|
|
30
|
+
本次按 Claude Code 2.1.309 更新:模型窗口标记优先读取实际容量,不再依赖 `[1m]` 后缀;
|
|
31
|
+
纯 thinking 记录保持思考状态;企业网关 Limit 可选追加金额与周期:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
🤖 Opus 5.5 1M | 🧠 high | 💭
|
|
35
|
+
📊 Limit 🌔 63% $314.00/$500.00 monthly
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
上方平台图片保留为 Claude Code 2.1.258 的 emoji 差异参考示例。
|
|
39
|
+
|
|
26
40
|
**文档:** [概览](./docs/overview.zh-CN.md) · [字段与图标](./docs/segments.zh-CN.md) · [开发](./docs/development.zh-CN.md)
|
|
27
41
|
|
|
28
42
|
## 终端字体
|
|
@@ -91,14 +105,16 @@ node dist/cli.js preview
|
|
|
91
105
|
"statusLine": {
|
|
92
106
|
"type": "command",
|
|
93
107
|
"command": "ccglance",
|
|
94
|
-
"padding": 0
|
|
108
|
+
"padding": 0,
|
|
109
|
+
"refreshInterval": 5
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
```
|
|
98
113
|
|
|
99
|
-
Claude Code >= 2.1.97
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
`refreshInterval` 的单位为秒,要求 Claude Code >= 2.1.97。示例每 5 秒额外刷新一次,
|
|
115
|
+
让 transcript 推断状态、会话时长和倒计时在主会话空闲时也能更新。删除该字段即可恢复
|
|
116
|
+
仅按事件刷新,或兼容更旧的 Claude Code。它不会让 API 用量提前更新;发送用户消息也
|
|
117
|
+
不保证立即触发重绘。
|
|
102
118
|
|
|
103
119
|
若未全局安装,直接用 Node 指向编译后的文件:
|
|
104
120
|
|
|
@@ -111,7 +127,8 @@ Claude Code >= 2.1.97 可在 `statusLine` 内可选增加 `"refreshInterval": 30
|
|
|
111
127
|
}
|
|
112
128
|
```
|
|
113
129
|
|
|
114
|
-
Windows
|
|
130
|
+
Windows 建议用正斜杠;路径含空格时加引号,例如
|
|
131
|
+
`"command": "node \"D:/path with spaces/ccglance/dist/cli.js\""`。
|
|
115
132
|
|
|
116
133
|
> **跨平台路径**:Claude Code v2.1.47+ 会在所有平台展开命令开头的 `~`,所以
|
|
117
134
|
> `node ~/path/to/dist/cli.js` 也能用。优先用 `~` 而非 `%USERPROFILE%`(近版本不可靠);
|
|
@@ -119,7 +136,7 @@ Windows 用完整路径,例如 `node D:\\path\\to\\ccglance\\dist\\cli.js`。
|
|
|
119
136
|
> 让状态栏从最左开始。
|
|
120
137
|
|
|
121
138
|
> **Windows 提速小贴士**:若在意状态栏启动延时,可把 `command` 直接指向编译产物
|
|
122
|
-
> `node D
|
|
139
|
+
> `node D:/absolute/path/to/ccglance/dist/cli.js`,通常比走全局 `ccglance` 命令更快一些。
|
|
123
140
|
|
|
124
141
|
## 贡献
|
|
125
142
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const readers_1 = require("./readers");
|
|
5
5
|
// preview 子命令用的内置样例(覆盖默认状态栏会展示的常规字段)
|
|
6
6
|
const PREVIEW_SAMPLE = {
|
|
7
|
-
model: { display_name: 'Opus 5', id: 'claude-opus-5
|
|
7
|
+
model: { display_name: 'Opus 5.5', id: 'claude-opus-5-5' },
|
|
8
8
|
effort: { level: 'high' },
|
|
9
9
|
thinking: { enabled: true },
|
|
10
10
|
fast_mode: true,
|
|
@@ -13,16 +13,27 @@ const PREVIEW_SAMPLE = {
|
|
|
13
13
|
used_percentage: 23.5, remaining_percentage: 76.5,
|
|
14
14
|
current_usage: { output_tokens: 1200, cache_read_input_tokens: 172000, cache_creation_input_tokens: 15000 },
|
|
15
15
|
},
|
|
16
|
+
prompt_cache: {
|
|
17
|
+
warm: true, caching_observed: true, ttl: '1h',
|
|
18
|
+
expires_at: Math.floor(Date.now() / 1000) + 45 * 60,
|
|
19
|
+
requests: 14, misses: 2, expected_rebuilds: 1, hit_ratio: 0.91,
|
|
20
|
+
cache_write_tokens: 352000, miss_recache_tokens: 310200,
|
|
21
|
+
recache_tokens_if_cold: 45000,
|
|
22
|
+
},
|
|
16
23
|
output_style: { name: 'default' },
|
|
17
24
|
rate_limits: {
|
|
18
25
|
five_hour: { used_percentage: 23.5, resets_at: Math.floor(Date.now() / 1000) + 2 * 3600 },
|
|
19
26
|
seven_day: { used_percentage: 41.2, resets_at: Math.floor(Date.now() / 1000) + 3 * 86400 },
|
|
27
|
+
spend_limit: {
|
|
28
|
+
used_percentage: 62.8, resets_at: Math.floor(Date.now() / 1000) + 30 * 86400,
|
|
29
|
+
used_usd: 314, limit_usd: 500, period: 'monthly',
|
|
30
|
+
},
|
|
20
31
|
},
|
|
21
32
|
workspace: { current_dir: process.cwd() },
|
|
22
33
|
worktree: { name: 'main', branch: 'main' },
|
|
23
34
|
session_name: 'ccglance',
|
|
24
35
|
cost: { total_cost_usd: 0.1234, total_duration_ms: 192000, total_lines_added: 156, total_lines_removed: 23 },
|
|
25
|
-
version: '2.1.
|
|
36
|
+
version: '2.1.309',
|
|
26
37
|
};
|
|
27
38
|
const argv = process.argv.slice(2);
|
|
28
39
|
// 用法 + 「安装后要做什么」指引。终端直接运行或 --help 时打印;此路径绝不读 stdin。
|
|
@@ -37,17 +48,21 @@ function printHelp() {
|
|
|
37
48
|
ccglance --help, -h 显示本帮助 (show this help)
|
|
38
49
|
ccglance --version, -v 显示版本号 (print version)
|
|
39
50
|
|
|
40
|
-
安装后配置 Setup — 装完必须做这一步 (required after \`npm install -g ccglance\`):
|
|
51
|
+
安装后配置 Setup — 装完必须做这一步 (required after \`npm install -g @cxmyu/ccglance\`):
|
|
41
52
|
把下面这段加进 ~/.claude/settings.json,然后重启 Claude Code。
|
|
42
53
|
Add this to ~/.claude/settings.json, then restart Claude Code:
|
|
43
54
|
|
|
44
55
|
{
|
|
45
56
|
"statusLine": {
|
|
46
57
|
"type": "command",
|
|
47
|
-
"command": "ccglance"
|
|
58
|
+
"command": "ccglance",
|
|
59
|
+
"refreshInterval": 5
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
Claude Code >= 2.1.97: refreshInterval 的单位为秒;删除此字段则仅按事件刷新。
|
|
64
|
+
Refresh runs locally; API usage still updates only when Claude Code receives it.
|
|
65
|
+
|
|
51
66
|
未全局安装?让 Node 直接指向编译产物 (not installed globally? point Node at the built file):
|
|
52
67
|
"command": "node /absolute/path/to/dist/cli.js"
|
|
53
68
|
|
package/dist/defaults/index.js
CHANGED
|
@@ -26,11 +26,11 @@ exports.DEFAULT_SPEC = {
|
|
|
26
26
|
cache: { enabled: true },
|
|
27
27
|
rate_limits: {
|
|
28
28
|
enabled: true,
|
|
29
|
-
show: '
|
|
29
|
+
show: 'all',
|
|
30
30
|
warnPct: 70,
|
|
31
31
|
dangerPct: 90,
|
|
32
32
|
showReset: true,
|
|
33
|
-
labels: { five_hour: 'Hour', seven_day: 'Week' },
|
|
33
|
+
labels: { five_hour: 'Hour', seven_day: 'Week', spend_limit: 'Limit' },
|
|
34
34
|
},
|
|
35
35
|
style: { enabled: true },
|
|
36
36
|
dir: { enabled: true, fullPath: false },
|
|
@@ -13,6 +13,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const paths_1 = require("../utils/paths");
|
|
15
15
|
const TAIL_BYTES = 64 * 1024; // 尾部读取上限:足够覆盖最近若干条消息
|
|
16
|
+
const STATUS_CACHE_VERSION = 2;
|
|
16
17
|
function cacheFileFor(p) {
|
|
17
18
|
const h = hashString(path_1.default.resolve(p));
|
|
18
19
|
return path_1.default.join((0, paths_1.cacheDir)('transcript'), `${h}.usage.json`);
|
|
@@ -154,15 +155,20 @@ function extractRecentStatus(lines) {
|
|
|
154
155
|
const msg = e.message;
|
|
155
156
|
if (isInterruptedEntry(e))
|
|
156
157
|
return { state: 'paused' };
|
|
158
|
+
if (e.isMeta || !msg)
|
|
159
|
+
continue;
|
|
157
160
|
if (e.type === 'assistant') {
|
|
161
|
+
if (msg.stop_reason === 'end_turn' || msg.stop_reason === 'stop_sequence')
|
|
162
|
+
return { state: 'idle' };
|
|
158
163
|
const content = blocks(msg && msg.content);
|
|
159
164
|
const toolUse = content.find((b) => b && b.type === 'tool_use');
|
|
160
165
|
if (toolUse)
|
|
161
166
|
return { state: 'tool', tool: toolUse.name };
|
|
162
|
-
//
|
|
167
|
+
// 旧版完整文本回复可能没有 stop_reason;保留文本 → idle 的近似回退。
|
|
163
168
|
// 注意:Claude Code transcript 常把完成的文本回复记为 stop_reason:null(而非 end_turn),
|
|
164
169
|
// 所以判 idle 看「有内容」而非 stop_reason,否则正常回复会被误判成 thinking。
|
|
165
|
-
|
|
170
|
+
// 只有 thinking / redacted_thinking 时仍是思考中,不能据此判定完成。
|
|
171
|
+
const hasText = content.some((b) => b && b.type === 'text' && Boolean(b.text))
|
|
166
172
|
|| (typeof (msg && msg.content) === 'string' && String(msg && msg.content).length > 0);
|
|
167
173
|
return { state: hasText ? 'idle' : 'thinking' };
|
|
168
174
|
}
|
|
@@ -187,7 +193,7 @@ function getRecentStatus(transcriptPath) {
|
|
|
187
193
|
const cf = statusCacheFileFor(transcriptPath);
|
|
188
194
|
try {
|
|
189
195
|
const c = JSON.parse(fs_1.default.readFileSync(cf, 'utf8'));
|
|
190
|
-
if (c.mtimeMs === st.mtimeMs && c.size === st.size)
|
|
196
|
+
if (c.version === STATUS_CACHE_VERSION && c.mtimeMs === st.mtimeMs && c.size === st.size)
|
|
191
197
|
return c.status;
|
|
192
198
|
}
|
|
193
199
|
catch {
|
|
@@ -196,7 +202,7 @@ function getRecentStatus(transcriptPath) {
|
|
|
196
202
|
const status = extractRecentStatus(readTailLines(transcriptPath));
|
|
197
203
|
try {
|
|
198
204
|
fs_1.default.mkdirSync(path_1.default.dirname(cf), { recursive: true });
|
|
199
|
-
const payload = { mtimeMs: st.mtimeMs, size: st.size, status };
|
|
205
|
+
const payload = { version: STATUS_CACHE_VERSION, mtimeMs: st.mtimeMs, size: st.size, status };
|
|
200
206
|
fs_1.default.writeFileSync(cf, JSON.stringify(payload));
|
|
201
207
|
}
|
|
202
208
|
catch {
|
package/dist/segments/index.js
CHANGED
|
@@ -31,8 +31,22 @@ function positive(v) {
|
|
|
31
31
|
function statusIcon(ctx, key, fallback) {
|
|
32
32
|
return opt(ctx, 'status').statusIcons?.[key] || fallback;
|
|
33
33
|
}
|
|
34
|
-
function fmtRatePct(v) {
|
|
35
|
-
|
|
34
|
+
function fmtRatePct(v, allowOver100 = false) {
|
|
35
|
+
const rounded = Math.max(0, Math.round(v));
|
|
36
|
+
return (allowOver100 ? rounded : Math.min(100, rounded)) + '%';
|
|
37
|
+
}
|
|
38
|
+
// Prompt-cache TTLs are short; minute precision stays compact and avoids a noisy seconds countdown.
|
|
39
|
+
function fmtCacheRemaining(epochSec, nowMs) {
|
|
40
|
+
const expiry = num(epochSec);
|
|
41
|
+
if (expiry == null)
|
|
42
|
+
return null;
|
|
43
|
+
const ms = expiry * 1000 - nowMs;
|
|
44
|
+
if (ms <= 0)
|
|
45
|
+
return null;
|
|
46
|
+
const minutes = Math.max(1, Math.ceil(ms / 60000));
|
|
47
|
+
const hours = Math.floor(minutes / 60);
|
|
48
|
+
const rest = minutes % 60;
|
|
49
|
+
return hours ? `${hours}h${rest ? `${rest}m` : ''}` : `${minutes}m`;
|
|
36
50
|
}
|
|
37
51
|
// 会话状态:读 transcript 尾部推断;默认仅显示图标,工具态附带工具名。
|
|
38
52
|
const status = (ctx) => {
|
|
@@ -51,7 +65,7 @@ const status = (ctx) => {
|
|
|
51
65
|
return null;
|
|
52
66
|
};
|
|
53
67
|
const model = (ctx) => ctx.data.model && (ctx.data.model.display_name || ctx.data.model.id)
|
|
54
|
-
? iconSeg(ctx, 'model', colors_1.C.model, 'model', (0, format_1.modelName)(ctx.data.model)) : null;
|
|
68
|
+
? iconSeg(ctx, 'model', colors_1.C.model, 'model', (0, format_1.modelName)(ctx.data.model, ctx.data.context_window?.context_window_size)) : null;
|
|
55
69
|
const agent = (ctx) => ctx.data.agent?.name ? iconSeg(ctx, 'agent', colors_1.C.agent, 'agent', ctx.data.agent.name) : null;
|
|
56
70
|
// 旧版只有 effort.level;新版额外提供 thinking.enabled。字段缺失时保持旧输出。
|
|
57
71
|
const effort = ({ data }) => {
|
|
@@ -93,12 +107,34 @@ const cache = (ctx) => {
|
|
|
93
107
|
const cw = ctx.data.context_window;
|
|
94
108
|
if (!cw)
|
|
95
109
|
return null;
|
|
96
|
-
const cu = cw.current_usage
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
110
|
+
const cu = cw.current_usage;
|
|
111
|
+
const read = nonNegative(cu?.cache_read_input_tokens) ?? 0;
|
|
112
|
+
const write = nonNegative(cu?.cache_creation_input_tokens) ?? 0;
|
|
113
|
+
const fresh = nonNegative(cu?.input_tokens);
|
|
114
|
+
const latestTotal = fresh != null
|
|
115
|
+
? fresh + read + write
|
|
116
|
+
: nonNegative(cw.total_input_tokens);
|
|
117
|
+
const latestHitPct = cu && latestTotal != null
|
|
118
|
+
? latestTotal > 0 ? Math.max(0, Math.min(100, read / latestTotal * 100)) : 0
|
|
119
|
+
: null;
|
|
120
|
+
const pc = ctx.data.prompt_cache;
|
|
121
|
+
const ratio = pc?.caching_observed === true ? num(pc.hit_ratio) : null;
|
|
122
|
+
const sessionHitPct = ratio != null ? Math.max(0, Math.min(1, ratio)) * 100 : null;
|
|
123
|
+
const parts = latestHitPct != null
|
|
124
|
+
? [(0, format_1.fmtPct)(latestHitPct), `↓${(0, format_1.fmtTokens)(read)}`, `↑${(0, format_1.fmtTokens)(write)}`]
|
|
125
|
+
: sessionHitPct != null
|
|
126
|
+
? [`Avg ${(0, format_1.fmtPct)(sessionHitPct)}`]
|
|
127
|
+
: ['0%', '↓0', '↑0'];
|
|
128
|
+
if (pc?.caching_observed === true && pc.warm === true) {
|
|
129
|
+
const remaining = fmtCacheRemaining(pc.expires_at, ctx.nowMs);
|
|
130
|
+
const ttl = typeof pc.ttl === 'string' ? pc.ttl.trim() : '';
|
|
131
|
+
parts.push(`🔥${remaining || ttl}`);
|
|
132
|
+
}
|
|
133
|
+
else if (pc?.caching_observed === true && pc.warm === false) {
|
|
134
|
+
const recache = positive(pc.recache_tokens_if_cold);
|
|
135
|
+
parts.push(`🧊${recache != null ? `↻${(0, format_1.fmtTokens)(recache)}` : ''}`);
|
|
136
|
+
}
|
|
137
|
+
return iconSeg(ctx, 'cache', colors_1.C.cache, 'cache', parts.join(colors_1.DOT));
|
|
102
138
|
};
|
|
103
139
|
const rateLimits = (ctx) => {
|
|
104
140
|
const rl = ctx.data.rate_limits;
|
|
@@ -110,6 +146,7 @@ const rateLimits = (ctx) => {
|
|
|
110
146
|
const labels = {
|
|
111
147
|
five_hour: o.labels?.five_hour || 'Hour',
|
|
112
148
|
seven_day: o.labels?.seven_day || 'Week',
|
|
149
|
+
spend_limit: o.labels?.spend_limit || 'Limit',
|
|
113
150
|
};
|
|
114
151
|
const normal = optTextColor(ctx, 'rate_limits', colors_1.C.rate);
|
|
115
152
|
const bold = optBold(ctx, 'rate_limits');
|
|
@@ -123,19 +160,21 @@ const rateLimits = (ctx) => {
|
|
|
123
160
|
: pct >= 10 ? '🌒' : '🌑';
|
|
124
161
|
return (0, colors_1.paint)(color, glyph, bold);
|
|
125
162
|
};
|
|
126
|
-
const pushWindow = (label, win) => {
|
|
127
|
-
|
|
163
|
+
const pushWindow = (label, win, allowOver100 = false, amounts) => {
|
|
164
|
+
const pct = nonNegative(win?.used_percentage);
|
|
165
|
+
if (pct == null)
|
|
128
166
|
return;
|
|
129
|
-
const pct = win.used_percentage;
|
|
130
167
|
const warn = o.warnPct != null ? o.warnPct : 70;
|
|
131
168
|
const danger = o.dangerPct != null ? o.dangerPct : 90;
|
|
132
169
|
const winColor = pctColor(pct, warn, danger, normal);
|
|
133
|
-
const reset = showReset ? (0, format_1.fmtResetIn)(win
|
|
170
|
+
const reset = showReset ? (0, format_1.fmtResetIn)(win?.resets_at, ctx.nowMs) : null;
|
|
134
171
|
const detail = [
|
|
135
172
|
(0, colors_1.paint)(normal, label, bold),
|
|
136
173
|
meter(pct, winColor),
|
|
137
|
-
(0, colors_1.paint)(winColor, fmtRatePct(pct), bold),
|
|
174
|
+
(0, colors_1.paint)(winColor, fmtRatePct(pct, allowOver100), bold),
|
|
138
175
|
];
|
|
176
|
+
if (amounts)
|
|
177
|
+
detail.push((0, colors_1.paint)(normal, amounts, bold));
|
|
139
178
|
if (reset)
|
|
140
179
|
detail.push((0, colors_1.paint)(colors_1.C.rateReset, '⏳', bold), (0, colors_1.paint)(colors_1.C.rateReset, reset, bold));
|
|
141
180
|
parts.push(detail.join(' '));
|
|
@@ -147,6 +186,18 @@ const rateLimits = (ctx) => {
|
|
|
147
186
|
if (wants(['all', 'both', 'seven_day'])) {
|
|
148
187
|
pushWindow(labels.seven_day, rl.seven_day);
|
|
149
188
|
}
|
|
189
|
+
if (wants(['all', 'spend_limit'])) {
|
|
190
|
+
const spend = rl.spend_limit;
|
|
191
|
+
const usedUsd = nonNegative(spend?.used_usd);
|
|
192
|
+
const limitUsd = positive(spend?.limit_usd);
|
|
193
|
+
const period = typeof spend?.period === 'string' ? spend.period.trim() : '';
|
|
194
|
+
// Dollar amounts are fetched separately and may lag the percentage. Never
|
|
195
|
+
// derive a replacement percentage from them or fabricate a missing amount.
|
|
196
|
+
const amounts = usedUsd != null && limitUsd != null
|
|
197
|
+
? `${(0, format_1.fmtCost)(usedUsd)}/${(0, format_1.fmtCost)(limitUsd)}${period ? ` ${period}` : ''}`
|
|
198
|
+
: undefined;
|
|
199
|
+
pushWindow(labels.spend_limit, spend, true, amounts);
|
|
200
|
+
}
|
|
150
201
|
if (!parts.length)
|
|
151
202
|
return null;
|
|
152
203
|
const col = pctColor(maxUsed, o.warnPct != null ? o.warnPct : 70, o.dangerPct != null ? o.dangerPct : 90, normal);
|
package/dist/utils/format.js
CHANGED
|
@@ -21,12 +21,20 @@ const TOKEN_CASE = {
|
|
|
21
21
|
llm: 'LLM',
|
|
22
22
|
vl: 'VL',
|
|
23
23
|
};
|
|
24
|
-
function contextSuffix(id) {
|
|
24
|
+
function contextSuffix(id, size) {
|
|
25
|
+
// New default-1M models may no longer carry [1m]. A valid live size also
|
|
26
|
+
// overrides a stale display-name/id hint when the session uses a smaller window.
|
|
27
|
+
if (typeof size === 'number' && Number.isFinite(size) && size > 0) {
|
|
28
|
+
return size >= 1000000 ? `${Number((size / 1000000).toFixed(2))}M` : '';
|
|
29
|
+
}
|
|
25
30
|
return /\[1m\]/i.test(id) ? '1M' : '';
|
|
26
31
|
}
|
|
27
|
-
function appendContextSuffix(name, id) {
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
function appendContextSuffix(name, id, size) {
|
|
33
|
+
if (typeof size === 'number' && Number.isFinite(size) && size > 0) {
|
|
34
|
+
name = name.replace(/\s+\d+(?:\.\d+)?\s*[km]$/i, '').trim();
|
|
35
|
+
}
|
|
36
|
+
const suffix = contextSuffix(id, size);
|
|
37
|
+
return suffix && !new RegExp(`\\b${suffix.replace(/\./g, '\\.')}\\b`, 'i').test(name) ? `${name} ${suffix}` : name;
|
|
30
38
|
}
|
|
31
39
|
function matchClaudeFamily(raw) {
|
|
32
40
|
const source = raw.toLowerCase();
|
|
@@ -45,7 +53,7 @@ function matchClaudeFamily(raw) {
|
|
|
45
53
|
function normalizeDisplayName(displayName) {
|
|
46
54
|
return displayName
|
|
47
55
|
.replace(/^Claude\s+/i, '')
|
|
48
|
-
.replace(/\s*\((\d+)\s*([km])(?:\s+context)?\)/i, ' $1$2')
|
|
56
|
+
.replace(/\s*\((\d+(?:\.\d+)?)\s*([km])(?:\s+context)?\)/i, ' $1$2')
|
|
49
57
|
.replace(/\s+/g, ' ')
|
|
50
58
|
.trim();
|
|
51
59
|
}
|
|
@@ -85,12 +93,12 @@ function readableModelId(id) {
|
|
|
85
93
|
return tokens.map(titleToken).join(' ').trim();
|
|
86
94
|
}
|
|
87
95
|
// 模型名以 Claude Code stdin 的 display_name 为准;id 只在 display_name 缺失时兜底。
|
|
88
|
-
function modelName(model) {
|
|
96
|
+
function modelName(model, contextWindowSize) {
|
|
89
97
|
const id = (model.id || '').trim();
|
|
90
98
|
const displayName = (model.display_name || '').trim();
|
|
91
99
|
const normalizedDisplay = normalizeDisplayName(displayName);
|
|
92
100
|
const baseName = normalizedDisplay || matchClaudeFamily(id) || readableModelId(id);
|
|
93
|
-
return appendContextSuffix(baseName, id);
|
|
101
|
+
return appendContextSuffix(baseName, id, contextWindowSize);
|
|
94
102
|
}
|
|
95
103
|
// 12345 → 12.3k
|
|
96
104
|
const fmtTokens = (n) => n == null ? '0' : n < 1000 ? String(n)
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cxmyu/ccglance",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A fast, zero-dependency
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "A fast, zero-dependency Claude Code status line for model, effort, context, prompt cache, usage limits, git and session.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ccglance": "dist/cli.js"
|
|
7
7
|
},
|