ccus-cli 0.1.5 → 0.1.8

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.
@@ -9,8 +9,11 @@ exports.buildAggregatedDailyRows = buildAggregatedDailyRows;
9
9
  exports.buildAggregatedWeeklyRows = buildAggregatedWeeklyRows;
10
10
  const promises_1 = __importDefault(require("node:fs/promises"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
+ const node_util_1 = require("node:util");
13
+ const node_zlib_1 = require("node:zlib");
12
14
  const payload_1 = require("./payload");
13
15
  const time_1 = require("./time");
16
+ const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
14
17
  function isRecord(value) {
15
18
  return typeof value === "object" && value !== null && !Array.isArray(value);
16
19
  }
@@ -59,6 +62,10 @@ function weekKey(date) {
59
62
  function toPersonKey(gitUserEmail, gitUserName) {
60
63
  return (0, time_1.extractGitEmailAccount)(gitUserEmail) ?? gitUserName ?? "unknown";
61
64
  }
65
+ /** bundle 文件可以是明文 `.json`,也可以是 `ccus export` 默认输出的 gzip 压缩 `.json.gz`。 */
66
+ function isBundleFileName(name) {
67
+ return name.endsWith(".json") || name.endsWith(".json.gz");
68
+ }
62
69
  async function collectBundleJsonFiles(directoryPath) {
63
70
  const entries = await promises_1.default.readdir(directoryPath, { withFileTypes: true });
64
71
  const nested = await Promise.all(entries.map(async (entry) => {
@@ -66,10 +73,19 @@ async function collectBundleJsonFiles(directoryPath) {
66
73
  if (entry.isDirectory()) {
67
74
  return collectBundleJsonFiles(fullPath);
68
75
  }
69
- return entry.isFile() && entry.name.endsWith(".json") ? [fullPath] : [];
76
+ return entry.isFile() && isBundleFileName(entry.name) ? [fullPath] : [];
70
77
  }));
71
78
  return nested.flat();
72
79
  }
80
+ /** 读取 bundle 文件内容:`.gz` 结尾的先 gunzip 再按 UTF-8 解码,其它按明文读取。 */
81
+ async function readBundleFileContent(filePath) {
82
+ if (filePath.endsWith(".gz")) {
83
+ const compressed = await promises_1.default.readFile(filePath);
84
+ const decompressed = await gunzipAsync(compressed);
85
+ return decompressed.toString("utf8");
86
+ }
87
+ return promises_1.default.readFile(filePath, "utf8");
88
+ }
73
89
  /** 读取目录里的 export bundle json 文件。 */
74
90
  async function loadWeeklyExportBundles(inputDir) {
75
91
  const files = await collectBundleJsonFiles(inputDir);
@@ -77,7 +93,7 @@ async function loadWeeklyExportBundles(inputDir) {
77
93
  const invalidFiles = [];
78
94
  for (const filePath of files) {
79
95
  try {
80
- const content = await promises_1.default.readFile(filePath, "utf8");
96
+ const content = await readBundleFileContent(filePath);
81
97
  const parsed = JSON.parse(content);
82
98
  if (isWeeklyExportBundle(parsed)) {
83
99
  bundles.push({ filePath, bundle: parsed });
@@ -13,9 +13,13 @@ exports.buildSummaryCsv = buildSummaryCsv;
13
13
  exports.buildAggregatedDailyCsv = buildAggregatedDailyCsv;
14
14
  exports.buildAggregatedWeeklyCsv = buildAggregatedWeeklyCsv;
15
15
  exports.writeTextFile = writeTextFile;
16
+ exports.writeGzipFile = writeGzipFile;
16
17
  const promises_1 = __importDefault(require("node:fs/promises"));
17
18
  const node_path_1 = __importDefault(require("node:path"));
19
+ const node_util_1 = require("node:util");
20
+ const node_zlib_1 = require("node:zlib");
18
21
  const time_1 = require("./time");
22
+ const gzipAsync = (0, node_util_1.promisify)(node_zlib_1.gzip);
19
23
  /** CSV 字符串字段统一做转义,避免逗号和引号破坏列结构。 */
20
24
  function quoteCsv(value) {
21
25
  return `"${value.replaceAll("\"", "\"\"")}"`;
@@ -128,9 +132,9 @@ function buildRawJsonl(events) {
128
132
  function buildWeeklySummaryJson(summary) {
129
133
  return `${JSON.stringify(summary, null, 2)}\n`;
130
134
  }
131
- /** 默认导出把原始事件与周汇总一起打包成一个 JSON 文件。 */
135
+ /** 默认导出把原始事件与周汇总一起打包成一个 JSON 文件,用紧凑序列化避免缩进空白撑大体积。 */
132
136
  function buildWeeklyExportBundleJson(bundle) {
133
- return `${JSON.stringify(bundle, null, 2)}\n`;
137
+ return `${JSON.stringify(bundle)}\n`;
134
138
  }
135
139
  /** 按天汇总 usage 数据,生成 summary 模式的中间结果。 */
136
140
  function buildSummaryRows(events) {
@@ -272,4 +276,15 @@ async function writeTextFile(outputPath, content) {
272
276
  await promises_1.default.mkdir(node_path_1.default.dirname(outputPath), { recursive: true });
273
277
  await promises_1.default.writeFile(outputPath, content, "utf8");
274
278
  }
279
+ /**
280
+ * 把内容 gzip 压缩后写文件,父目录不存在时自动创建。
281
+ *
282
+ * 仅压缩存储/传输层,写入的字节解压后与 `writeTextFile` 完全一致,
283
+ * 不改变 bundle 字段集合,也不影响 schemaVersion。
284
+ */
285
+ async function writeGzipFile(outputPath, content) {
286
+ await promises_1.default.mkdir(node_path_1.default.dirname(outputPath), { recursive: true });
287
+ const compressed = await gzipAsync(content);
288
+ await promises_1.default.writeFile(outputPath, compressed);
289
+ }
275
290
  //# sourceMappingURL=export.js.map
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.readSessionId = readSessionId;
7
7
  exports.extractWorkspaceDir = extractWorkspaceDir;
8
8
  exports.parseStatuslinePayload = parseStatuslinePayload;
9
+ exports.resolveCtxTier = resolveCtxTier;
10
+ exports.resolveCtxRedThresholds = resolveCtxRedThresholds;
9
11
  exports.formatStatusLine = formatStatusLine;
10
12
  exports.createPersistedStatuslineEvent = createPersistedStatuslineEvent;
11
13
  exports.computeStatuslineEvent = computeStatuslineEvent;
@@ -146,16 +148,103 @@ function parseStatuslinePayload(input) {
146
148
  const parsed = JSON.parse(trimmed);
147
149
  return isRecord(parsed) ? parsed : {};
148
150
  }
151
+ /** statusline 里 ctx 段标红用的 ANSI 颜色码,仅作用于展示,不进任何落盘/导出契约。 */
152
+ const ANSI_RED = "\x1b[31m";
153
+ const ANSI_RESET = "\x1b[0m";
154
+ /** contextMax 超过该值就当作 1M 长上下文窗口,否则按 200K 档。 */
155
+ const CTX_TIER_1M_MIN_MAX = 400_000;
156
+ /**
157
+ * 各档位的内置默认阈值(百分比超阈值或已用 token 超阈值,任一满足即标红)。
158
+ *
159
+ * 200K 窗口余量小、到 80%(约 160K)才提醒;1M 窗口虽大但 50%(约 500K)已用很多,提前提醒。
160
+ */
161
+ const CTX_TIER_DEFAULTS = {
162
+ "200k": { pct: 80, tokens: null },
163
+ "1m": { pct: 50, tokens: null },
164
+ };
165
+ /** 根据 contextMax 判断当前上下文窗口档位;拿不到 contextMax 时回退到 200K 档。 */
166
+ function resolveCtxTier(contextMax) {
167
+ return contextMax !== null && contextMax > CTX_TIER_1M_MIN_MAX ? "1m" : "200k";
168
+ }
169
+ /** 把 `120000` / `120k` / `0.5m` 这类写法解析成整数 token;非法或空返回 null。 */
170
+ function parseTokenThreshold(raw) {
171
+ const text = raw.trim().toLowerCase();
172
+ if (text === "") {
173
+ return null;
174
+ }
175
+ const multiplier = text.endsWith("m") ? 1_000_000 : text.endsWith("k") ? 1_000 : 1;
176
+ const numericPart = multiplier === 1 ? text : text.slice(0, -1);
177
+ const parsed = Number(numericPart) * multiplier;
178
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
179
+ }
180
+ /** 按顺序读第一个能解析成非负数的百分比环境变量;全部缺失/非法时返回 fallback。 */
181
+ function readPctEnv(env, keys, fallback) {
182
+ for (const key of keys) {
183
+ const raw = (env[key] ?? "").trim();
184
+ if (raw === "") {
185
+ continue;
186
+ }
187
+ const parsed = Number(raw);
188
+ if (Number.isFinite(parsed) && parsed >= 0) {
189
+ return parsed;
190
+ }
191
+ }
192
+ return fallback;
193
+ }
194
+ /** 按顺序读第一个非空的 token 阈值环境变量;全部缺失时返回 fallback。 */
195
+ function readTokensEnv(env, keys, fallback) {
196
+ for (const key of keys) {
197
+ const raw = env[key] ?? "";
198
+ if (raw.trim() === "") {
199
+ continue;
200
+ }
201
+ return parseTokenThreshold(raw);
202
+ }
203
+ return fallback;
204
+ }
205
+ /**
206
+ * 解析某个上下文档位的 ctx 标红阈值,两个条件取「或」:百分比超阈值,或已用 token 超阈值。
207
+ *
208
+ * 优先级:档位专属环境变量 > 通用环境变量 > 档位内置默认。
209
+ *
210
+ * - 百分比:`CCUS_CTX_RED_PCT_200K` / `CCUS_CTX_RED_PCT_1M` → `CCUS_CTX_RED_PCT` → 档位默认(200K=80,1M=50)。
211
+ * - token:`CCUS_CTX_RED_TOKENS_200K` / `CCUS_CTX_RED_TOKENS_1M` → `CCUS_CTX_RED_TOKENS` → 档位默认(默认不启用)。
212
+ * token 支持 `120000` / `120k` / `0.5m` 写法。
213
+ *
214
+ * 阈值只影响 statusline 颜色展示,不改变 stdin/stdout 文本契约,也不落盘。
215
+ */
216
+ function resolveCtxRedThresholds(contextMax, env = process.env) {
217
+ const tier = resolveCtxTier(contextMax);
218
+ const defaults = CTX_TIER_DEFAULTS[tier];
219
+ const tierSuffix = tier === "1m" ? "1M" : "200K";
220
+ const pct = readPctEnv(env, [`CCUS_CTX_RED_PCT_${tierSuffix}`, "CCUS_CTX_RED_PCT"], defaults.pct);
221
+ const tokens = readTokensEnv(env, [`CCUS_CTX_RED_TOKENS_${tierSuffix}`, "CCUS_CTX_RED_TOKENS"], defaults.tokens);
222
+ return { tier, pct, tokens };
223
+ }
224
+ /** 判断 ctx 是否达到标红条件:百分比超阈值,或已用 token 超阈值,任一满足即标红。 */
225
+ function isContextHot(contextWindowPct, contextUsed, thresholds) {
226
+ if (contextWindowPct !== null && contextWindowPct > thresholds.pct) {
227
+ return true;
228
+ }
229
+ if (thresholds.tokens !== null && contextUsed !== null && contextUsed >= thresholds.tokens) {
230
+ return true;
231
+ }
232
+ return false;
233
+ }
149
234
  /**
150
235
  * 生成真正显示在 Claude Code statusline 上的短文本。
151
236
  *
152
237
  * 这里必须保持单行、紧凑,避免污染 statusline 展示区域。
153
238
  */
154
- function formatStatusLine(event, gitBranch = null) {
239
+ function formatStatusLine(event, gitBranch = null, env = process.env) {
155
240
  const timeLabel = (0, time_1.formatClock)(new Date(event.timestamp));
156
241
  const usageLabel = event.usagePct === null ? "5h --" : `5h ${event.usagePct.toFixed(1)}%`;
157
242
  const sevenDayLabel = event.sevenDayUsagePct === null ? "7d --" : `7d ${event.sevenDayUsagePct.toFixed(1)}%`;
158
- const contextLabel = event.contextWindowPct === null ? "ctx --" : `ctx ${event.contextWindowPct.toFixed(1)}%`;
243
+ const contextText = event.contextWindowPct === null ? "ctx --" : `ctx ${event.contextWindowPct.toFixed(1)}%`;
244
+ // ctx 占用超阈值时整段标红,提醒上下文快满;阈值按窗口大小(200K / 1M)分档,由 resolveCtxRedThresholds 决定。
245
+ const contextLabel = isContextHot(event.contextWindowPct, event.contextUsed ?? null, resolveCtxRedThresholds(event.contextMax ?? null, env))
246
+ ? `${ANSI_RED}${contextText}${ANSI_RESET}`
247
+ : contextText;
159
248
  const modelLabel = event.modelName ?? "model --";
160
249
  const workspaceLabel = event.workspaceName ?? "workspace --";
161
250
  const segments = [usageLabel, sevenDayLabel, contextLabel, modelLabel, workspaceLabel];
package/package.json CHANGED
@@ -1,35 +1,39 @@
1
- {
2
- "name": "ccus-cli",
3
- "version": "0.1.5",
4
- "description": "Claude Code statusline usage logger and dashboard CLI",
5
- "type": "commonjs",
6
- "bin": {
7
- "ccus": "dist/cli.js"
8
- },
9
- "files": [
10
- "dist/cli.js",
11
- "dist/lib/**/*.js",
12
- "dist/types.js"
13
- ],
14
- "scripts": {
15
- "build": "tsc -p tsconfig.json",
16
- "prepublishOnly": "npm run build",
17
- "test": "node --test dist/test",
18
- "test:src": "node --import tsx --test src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts"
19
- },
20
- "keywords": [
21
- "claude-code",
22
- "statusline",
23
- "cli",
24
- "dashboard"
25
- ],
26
- "license": "MIT",
27
- "engines": {
28
- "node": ">=20"
29
- },
30
- "devDependencies": {
31
- "@types/node": "^24.0.0",
32
- "tsx": "^4.20.0",
33
- "typescript": "^5.8.3"
34
- }
35
- }
1
+ {
2
+ "name": "ccus-cli",
3
+ "version": "0.1.8",
4
+ "description": "Claude Code statusline usage logger and dashboard CLI",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "ccus": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/cli.js",
11
+ "dist/lib/**/*.js",
12
+ "dist/types.js"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "node --test dist/test",
18
+ "test:src": "node --import tsx --test src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts"
19
+ },
20
+ "keywords": [
21
+ "claude-code",
22
+ "statusline",
23
+ "cli",
24
+ "dashboard"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/gaoshang212/ccus-cli.git"
29
+ },
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^24.0.0",
36
+ "tsx": "^4.20.0",
37
+ "typescript": "^5.8.3"
38
+ }
39
+ }