@cxmyu/ccglance 1.2.1 → 1.3.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 CHANGED
@@ -4,19 +4,31 @@
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
  [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-3c873a)](https://nodejs.org/)
10
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
11
11
 
12
12
  [English](./README.md) · [简体中文](./README.zh-CN.md) · [GitHub](https://github.com/CxMYu/CcGlanceLine)
13
13
 
14
- ![ccglance preview](./docs/assets/preview.png)
14
+ **Windows 11 · Segoe UI Emoji · Claude Code 2.1.258 sample**
15
+
16
+ ![ccglance on Windows 11](./docs/assets/preview-windows.png)
17
+
18
+ **macOS · Apple Color Emoji · Claude Code 2.1.258 sample**
19
+
20
+ ![ccglance on macOS](./docs/assets/preview-macos.png)
15
21
 
16
22
  </div>
17
23
 
18
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.
19
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
+
20
32
  **Docs:** [Overview](./docs/overview.md) · [Segments & icons](./docs/segments.md) · [Development](./docs/development.md)
21
33
 
22
34
  ## Terminal Font
@@ -29,7 +41,8 @@ ccglance intentionally outputs standard Unicode emoji. A Nerd Font improves text
29
41
  metrics and symbol coverage, but color emoji are still rendered by the operating
30
42
  system's emoji font (for example Segoe UI Emoji on Windows, Apple Color Emoji on
31
43
  macOS, and Noto Color Emoji on many Linux desktops), so their artwork can differ
32
- across platforms.
44
+ across platforms. The two previews above use the same ccglance text, colors, and
45
+ spacing; only the platform emoji font changes.
33
46
 
34
47
  ## Install
35
48
 
package/README.zh-CN.md CHANGED
@@ -4,19 +4,29 @@
4
4
 
5
5
  **面向 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 的快速、零依赖多行状态栏**
6
6
 
7
- 模型 · effort · 上下文 · 缓存 · git · 会话 —— 一眼尽览
7
+ 模型 · effort · 上下文 · prompt cache · 限额 · git · 会话 —— 一眼尽览
8
8
 
9
9
  [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-3c873a)](https://nodejs.org/)
10
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
11
11
 
12
12
  [English](./README.md) · [简体中文](./README.zh-CN.md) · [GitHub](https://github.com/CxMYu/CcGlanceLine)
13
13
 
14
- ![ccglance 预览](./docs/assets/preview.png)
14
+ **Windows 11 · Segoe UI Emoji · Claude Code 2.1.258 示例**
15
+
16
+ ![ccglance 在 Windows 11 上的效果](./docs/assets/preview-windows.png)
17
+
18
+ **macOS · Apple Color Emoji · Claude Code 2.1.258 示例**
19
+
20
+ ![ccglance 在 macOS 上的效果](./docs/assets/preview-macos.png)
15
21
 
16
22
  </div>
17
23
 
18
24
  > **stdin 优先 · 运行时零依赖 · 固定 emoji 风格** —— 渲染 Claude Code 经 stdin 传入的 JSON;仅 `status` 段读取 transcript 尾部固定字节。TypeScript 编写,以编译后的 JavaScript 发布,可在 Node.js ≥ 22 或 Bun 上运行。
19
25
 
26
+ 已覆盖新版 Claude Code 的缓存和限额字段,包括最近一次请求的 prompt cache 命中率与
27
+ token 流量、缓存冷热状态,以及 Hour、Week 和账户 Limit 三类用量窗口。旧版 Claude Code 缺少这些可选字段时,
28
+ 仍会自动沿用原有缓存计算逻辑。
29
+
20
30
  **文档:** [概览](./docs/overview.zh-CN.md) · [字段与图标](./docs/segments.zh-CN.md) · [开发](./docs/development.zh-CN.md)
21
31
 
22
32
  ## 终端字体
@@ -28,6 +38,7 @@
28
38
  ccglance 会固定输出标准 Unicode emoji。Nerd Font 可以改善文本宽度和符号覆盖,
29
39
  但彩色 emoji 仍由系统 emoji 字体渲染(例如 Windows 的 Segoe UI Emoji、
30
40
  macOS 的 Apple Color Emoji、很多 Linux 桌面的 Noto Color Emoji),所以不同平台的图案可能不同。
41
+ 上面两张效果图使用完全相同的 ccglance 文本、颜色与间距,只有平台 emoji 字体不同。
31
42
 
32
43
  ## 安装
33
44
 
package/dist/cli.js CHANGED
@@ -13,16 +13,24 @@ 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: { used_percentage: 62.8, resets_at: Math.floor(Date.now() / 1000) + 30 * 86400 },
20
28
  },
21
29
  workspace: { current_dir: process.cwd() },
22
30
  worktree: { name: 'main', branch: 'main' },
23
31
  session_name: 'ccglance',
24
32
  cost: { total_cost_usd: 0.1234, total_duration_ms: 192000, total_lines_added: 156, total_lines_removed: 23 },
25
- version: '2.1.221',
33
+ version: '2.1.258',
26
34
  };
27
35
  const argv = process.argv.slice(2);
28
36
  // 用法 + 「安装后要做什么」指引。终端直接运行或 --help 时打印;此路径绝不读 stdin。
@@ -26,11 +26,11 @@ exports.DEFAULT_SPEC = {
26
26
  cache: { enabled: true },
27
27
  rate_limits: {
28
28
  enabled: true,
29
- show: 'both',
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 },
@@ -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
- return Math.max(0, Math.min(100, Math.round(v))) + '%';
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) => {
@@ -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 used = nonNegative(cw.total_input_tokens) ?? nonNegative(cu.input_tokens) ?? 0;
98
- const read = nonNegative(cu.cache_read_input_tokens) ?? 0;
99
- const write = nonNegative(cu.cache_creation_input_tokens) ?? 0;
100
- const hitPct = used > 0 && read > 0 ? read / used * 100 : 0;
101
- return iconSeg(ctx, 'cache', colors_1.C.cache, 'cache', `${(0, format_1.fmtPct)(hitPct)}${colors_1.DOT}↓${(0, format_1.fmtTokens)(read)}${colors_1.DOT}↑${(0, format_1.fmtTokens)(write)}`);
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,18 +160,18 @@ 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
- if (!win || win.used_percentage == null)
163
+ const pushWindow = (label, win, allowOver100 = false) => {
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.resets_at, ctx.nowMs) : null;
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
  ];
139
176
  if (reset)
140
177
  detail.push((0, colors_1.paint)(colors_1.C.rateReset, '⏳', bold), (0, colors_1.paint)(colors_1.C.rateReset, reset, bold));
@@ -147,6 +184,9 @@ const rateLimits = (ctx) => {
147
184
  if (wants(['all', 'both', 'seven_day'])) {
148
185
  pushWindow(labels.seven_day, rl.seven_day);
149
186
  }
187
+ if (wants(['all', 'spend_limit'])) {
188
+ pushWindow(labels.spend_limit, rl.spend_limit, true);
189
+ }
150
190
  if (!parts.length)
151
191
  return null;
152
192
  const col = pctColor(maxUsed, o.warnPct != null ? o.warnPct : 70, o.dangerPct != null ? o.dangerPct : 90, normal);
Binary file
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cxmyu/ccglance",
3
- "version": "1.2.1",
4
- "description": "A fast, zero-dependency, multi-line status line for Claude Code — model, effort, context, cache, git and session, all at a glance.",
3
+ "version": "1.3.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
  },
@@ -13,7 +13,8 @@
13
13
  "type": "commonjs",
14
14
  "files": [
15
15
  "dist",
16
- "docs/assets/preview.png"
16
+ "docs/assets/preview-windows.png",
17
+ "docs/assets/preview-macos.png"
17
18
  ],
18
19
  "engines": {
19
20
  "node": ">=22"
Binary file