ccus-cli 0.1.14 → 0.1.16

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/dist/cli.js CHANGED
@@ -61,7 +61,7 @@ const update_check_1 = require("./lib/update-check");
61
61
  const version_1 = require("./lib/version");
62
62
  /** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
63
63
  function printHelp() {
64
- process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
64
+ process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 ~/.claude/projects 本周活跃 session 打包成 zip,名如 projects_<dates>_<user>.zip)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
65
65
  }
66
66
  /** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
67
67
  function parseOptions(args) {
@@ -563,6 +563,53 @@ function resolveExportOptions(action, args, rest) {
563
563
  }
564
564
  return options;
565
565
  }
566
+ /**
567
+ * `ccus sessions`:把 ~/.claude/projects 中在指定时间范围内有活动的 session 文件打包成 zip。
568
+ *
569
+ * zip 内部结构保持 <projectDir>/<sessionId>.jsonl 层级(路径分隔符统一用 /)。
570
+ * 文件名格式:projects_<start>_<end>_<gitUserName>.zip。
571
+ * 默认输出到 <data-dir>/sessions/,加 --out 可指定完整路径。
572
+ * 位置参数作为 range 简写,例如 `ccus sessions lw` 等价于 `--range last-week`。
573
+ */
574
+ async function handleSessions(options) {
575
+ const dataDir = getDataDir(options);
576
+ const range = getStringOption(options, "range") ?? "this-week";
577
+ const out = getStringOption(options, "out");
578
+ const now = new Date();
579
+ const window = (0, time_1.expandToFullWeekWindow)((0, time_1.resolveRange)(range, now));
580
+ (0, debug_1.debugLog)("sessions", "range resolved", { range, label: window.label, start: window.start.toISOString(), end: window.end.toISOString() });
581
+ const sessions = await (0, claude_1.findActiveSessionFiles)(window.start, window.end);
582
+ (0, debug_1.debugLog)("sessions", "active sessions found", { count: sessions.length });
583
+ const fsRead = (await Promise.resolve().then(() => __importStar(require("node:fs/promises")))).readFile;
584
+ const { buildZipBuffer } = await Promise.resolve().then(() => __importStar(require("./lib/zip")));
585
+ const entries = await Promise.all(sessions.map(async (session) => ({
586
+ name: `${session.projectDir.replaceAll("\\", "/")}/${session.sessionId}.jsonl`,
587
+ data: await fsRead(session.filePath),
588
+ })));
589
+ const zipBuffer = await buildZipBuffer(entries);
590
+ const gitIdentity = await (0, git_1.readGitIdentity)();
591
+ const userName = (0, time_1.formatGitEmailFilePrefix)(gitIdentity.userEmail) ?? "unknown";
592
+ const fileLabel = (0, time_1.formatRangeFileLabel)(window.start, window.end);
593
+ const defaultFileName = `projects_${fileLabel}_${userName}.zip`;
594
+ const outputPath = node_path_1.default.resolve(out ?? node_path_1.default.join(dataDir, "sessions", defaultFileName));
595
+ const fsNode = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
596
+ await fsNode.mkdir(node_path_1.default.dirname(outputPath), { recursive: true });
597
+ await fsNode.writeFile(outputPath, zipBuffer);
598
+ process.stdout.write(`${outputPath}\n`);
599
+ }
600
+ /**
601
+ * 解析 sessions 命令的参数,支持 RANGE 作为位置参数简写。
602
+ */
603
+ function resolveSessionsOptions(action, args, rest) {
604
+ if (!action || action.startsWith("--")) {
605
+ return parseOptions(args.slice(1));
606
+ }
607
+ const options = parseOptions(rest);
608
+ if (typeof options.range !== "string") {
609
+ options.range = action;
610
+ }
611
+ return options;
612
+ }
566
613
  /** 用 readline 向用户提问,返回用户输入的一行文本。 */
567
614
  async function prompt(question) {
568
615
  const rl = (await Promise.resolve().then(() => __importStar(require("node:readline")))).createInterface({
@@ -851,6 +898,11 @@ async function main(args = process.argv.slice(2)) {
851
898
  await handleExport(exportOptions);
852
899
  return;
853
900
  }
901
+ if (group === "sessions") {
902
+ const sessionsOptions = resolveSessionsOptions(action, args, rest);
903
+ await handleSessions(sessionsOptions);
904
+ return;
905
+ }
854
906
  if (group === "aggregate") {
855
907
  if (action === "serve") {
856
908
  await handleAggregateServe(parseOptions(rest));
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.summarizeClaudeProjectUsage = summarizeClaudeProjectUsage;
7
+ exports.findActiveSessionFiles = findActiveSessionFiles;
7
8
  exports.summarizeClaudeProjectUsageByDay = summarizeClaudeProjectUsageByDay;
8
9
  const promises_1 = __importDefault(require("node:fs/promises"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
@@ -142,6 +143,49 @@ async function summarizeClaudeProjectUsage(start, end) {
142
143
  }
143
144
  return totals;
144
145
  }
146
+ /**
147
+ * 找出 ~/.claude/projects 中在指定时间范围内有活动的 session 文件,返回文件路径及结构信息。
148
+ *
149
+ * 只判断文件里是否存在范围内的记录,不过滤内容,导出时完整复制原始文件。
150
+ */
151
+ async function findActiveSessionFiles(start, end) {
152
+ const claudeDataDir = (0, paths_1.getClaudeDataDir)();
153
+ const projectsDir = node_path_1.default.join(claudeDataDir, "projects");
154
+ const files = await collectProjectJsonlFiles(projectsDir);
155
+ const result = [];
156
+ for (const filePath of files) {
157
+ try {
158
+ const content = await promises_1.default.readFile(filePath, "utf8");
159
+ const lines = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
160
+ let hasInRange = false;
161
+ for (const line of lines) {
162
+ try {
163
+ const record = JSON.parse(line);
164
+ if (!isRecord(record))
165
+ continue;
166
+ if (timestampInRange(getString(record.timestamp), start, end)) {
167
+ hasInRange = true;
168
+ break;
169
+ }
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ }
175
+ if (hasInRange) {
176
+ result.push({
177
+ filePath,
178
+ projectDir: node_path_1.default.relative(projectsDir, node_path_1.default.dirname(filePath)),
179
+ sessionId: node_path_1.default.basename(filePath, ".jsonl"),
180
+ });
181
+ }
182
+ }
183
+ catch {
184
+ continue;
185
+ }
186
+ }
187
+ return result;
188
+ }
145
189
  /**
146
190
  * 按天汇总 Claude project transcript 中的消息数、请求数和 token 用量。
147
191
  */
@@ -100,7 +100,7 @@ function renderChart(buckets) {
100
100
  usage: entry.value,
101
101
  label: (0, time_1.formatLocalTimestamp)(new Date(entry.bucket.bucketStart)),
102
102
  }));
103
- const fiveHourPoints = collectPoints((bucket) => bucket.avgUsagePct);
103
+ const fiveHourPoints = collectPoints((bucket) => bucket.maxUsagePct);
104
104
  const sevenDayPoints = collectPoints((bucket) => bucket.avgSevenDayUsagePct);
105
105
  const linePathOf = (points) => points.map((point, index) => `${index === 0 ? "M" : "L"}${point.x.toFixed(2)} ${point.y.toFixed(2)}`).join(" ");
106
106
  const fiveHourLine = linePathOf(fiveHourPoints);
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildZipBuffer = buildZipBuffer;
7
+ const node_zlib_1 = __importDefault(require("node:zlib"));
8
+ const node_util_1 = require("node:util");
9
+ const deflateRawAsync = (0, node_util_1.promisify)(node_zlib_1.default.deflateRaw);
10
+ function crc32(buf) {
11
+ const table = new Uint32Array(256);
12
+ for (let i = 0; i < 256; i++) {
13
+ let c = i;
14
+ for (let j = 0; j < 8; j++)
15
+ c = c & 1 ? (0xedb88320 ^ (c >>> 1)) : c >>> 1;
16
+ table[i] = c;
17
+ }
18
+ let crc = 0xffffffff;
19
+ for (let i = 0; i < buf.length; i++)
20
+ crc = (crc >>> 8) ^ table[(crc ^ buf[i]) & 0xff];
21
+ return (crc ^ 0xffffffff) >>> 0;
22
+ }
23
+ /**
24
+ * 把一组内存文件打包成合法的 ZIP 格式 Buffer。
25
+ *
26
+ * 使用 deflate 压缩,文件名按 UTF-8 编码(flags bit 11 置位),不写时间戳。
27
+ * 只依赖 Node.js 内置的 zlib,无外部依赖。
28
+ */
29
+ async function buildZipBuffer(entries) {
30
+ const locals = [];
31
+ const centrals = [];
32
+ let offset = 0;
33
+ for (const entry of entries) {
34
+ const name = Buffer.from(entry.name, "utf8");
35
+ const crc = crc32(entry.data);
36
+ const uncompressedSize = entry.data.length;
37
+ const compressed = await deflateRawAsync(entry.data);
38
+ const compressedSize = compressed.length;
39
+ const local = Buffer.alloc(30 + name.length);
40
+ local.writeUInt32LE(0x04034b50, 0); // local file header signature
41
+ local.writeUInt16LE(20, 4); // version needed
42
+ local.writeUInt16LE(0x0800, 6); // general purpose bit flag: UTF-8
43
+ local.writeUInt16LE(8, 8); // compression method: deflate
44
+ local.writeUInt16LE(0, 10); // last mod time
45
+ local.writeUInt16LE(0, 12); // last mod date
46
+ local.writeUInt32LE(crc, 14);
47
+ local.writeUInt32LE(compressedSize, 18);
48
+ local.writeUInt32LE(uncompressedSize, 22);
49
+ local.writeUInt16LE(name.length, 26);
50
+ local.writeUInt16LE(0, 28); // extra field length
51
+ name.copy(local, 30);
52
+ const central = Buffer.alloc(46 + name.length);
53
+ central.writeUInt32LE(0x02014b50, 0); // central directory signature
54
+ central.writeUInt16LE(20, 4); // version made by
55
+ central.writeUInt16LE(20, 6); // version needed
56
+ central.writeUInt16LE(0x0800, 8); // general purpose bit flag: UTF-8
57
+ central.writeUInt16LE(8, 10); // compression method: deflate
58
+ central.writeUInt16LE(0, 12); // last mod time
59
+ central.writeUInt16LE(0, 14); // last mod date
60
+ central.writeUInt32LE(crc, 16);
61
+ central.writeUInt32LE(compressedSize, 20);
62
+ central.writeUInt32LE(uncompressedSize, 24);
63
+ central.writeUInt16LE(name.length, 28);
64
+ central.writeUInt16LE(0, 30); // extra field length
65
+ central.writeUInt16LE(0, 32); // file comment length
66
+ central.writeUInt16LE(0, 34); // disk number start
67
+ central.writeUInt16LE(0, 36); // internal file attributes
68
+ central.writeUInt32LE(0, 38); // external file attributes
69
+ central.writeUInt32LE(offset, 42); // relative offset of local header
70
+ name.copy(central, 46);
71
+ locals.push(local, compressed);
72
+ centrals.push(central);
73
+ offset += local.length + compressedSize;
74
+ }
75
+ const centralSize = centrals.reduce((sum, c) => sum + c.length, 0);
76
+ const eocd = Buffer.alloc(22);
77
+ eocd.writeUInt32LE(0x06054b50, 0); // end of central dir signature
78
+ eocd.writeUInt16LE(0, 4); // disk number
79
+ eocd.writeUInt16LE(0, 6); // disk with central dir
80
+ eocd.writeUInt16LE(entries.length, 8);
81
+ eocd.writeUInt16LE(entries.length, 10);
82
+ eocd.writeUInt32LE(centralSize, 12);
83
+ eocd.writeUInt32LE(offset, 16);
84
+ eocd.writeUInt16LE(0, 20); // zip file comment length
85
+ return Buffer.concat([...locals, ...centrals, eocd]);
86
+ }
87
+ //# sourceMappingURL=zip.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {