ccus-cli 0.2.17 → 0.2.18
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 +16 -0
- package/dist/cli.js +47 -4
- package/dist/lib/claude.js +1 -0
- package/dist/lib/session-mtime.js +98 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -209,6 +209,22 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
209
209
|
|
|
210
210
|
## 定时同步
|
|
211
211
|
|
|
212
|
+
session 内容已更新但文件修改时间仍旧,导致统计遗漏时,可手动修复:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
ccus sessions repair --dry-run # 预览全部待修复文件
|
|
216
|
+
ccus sessions repair # 修复全部类型
|
|
217
|
+
ccus sessions repair codex # 仅修复 Codex(含 Orca)
|
|
218
|
+
ccus sessions repair claude # 仅修复 Claude
|
|
219
|
+
ccus sessions repair codex --dry-run # 仅预览 Codex
|
|
220
|
+
ccus sessions repair codex --min-gap 3h # 至少落后 3 小时才修复
|
|
221
|
+
ccus sync # 重新统计并同步
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
预览和执行均按实际文件标识去重:Codex 与 Orca 的硬链接只显示、计数一次,修复一个路径会同时更新所有硬链接路径。普通独立副本仍分别处理。
|
|
225
|
+
|
|
226
|
+
默认扫描 Claude projects、Codex 与 Orca sessions 的 JSONL;指定类型时只扫描对应目录。从尾部分块读取最后一条非空记录。默认仅当文件修改时间落后于记录的 `timestamp`,且两者按本机时区跨自然日时修复(不是必须相差 24 小时)。`--min-gap` 改为按实际时差判断,支持非负整数加 `m`(分钟)、`h`(小时)、`d`(24 小时);达到阈值即修复,`0m` 表示任何正差值。保留更晚的修改时间,不改写会话内容。末条 JSON 不完整、时间戳缺失或无效时跳过并列出原因。此命令不限制本周,处理所选类型的所有历史 session;日常统计的修改时间预筛保持不变。
|
|
227
|
+
|
|
212
228
|
把每个人的 `ccus export` 攒到一个共享目录(团队网盘 / 共享盘)通常是手动的,容易漏。`ccus sync` 用来自动化这一步:周期到了就 `export` 一次,并把导出文件**复制**到目标目录下**按周命名的子目录**里。
|
|
213
229
|
|
|
214
230
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ const api_equivalent_cost_1 = require("./lib/api-equivalent-cost");
|
|
|
70
70
|
const version_1 = require("./lib/version");
|
|
71
71
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
72
72
|
function printHelp() {
|
|
73
|
-
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\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 与 Codex 的活跃 session 打包成 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 api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api 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`);
|
|
73
|
+
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\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 与 Codex 的活跃 session 打包成 zip,默认本周)\n ccus sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\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 api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api 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`);
|
|
74
74
|
}
|
|
75
75
|
/** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
|
|
76
76
|
function parseOptions(args) {
|
|
@@ -743,9 +743,48 @@ async function handleSessions(options) {
|
|
|
743
743
|
await fsNode.writeFile(outputPath, zipBuffer);
|
|
744
744
|
process.stdout.write(`${outputPath}\n`);
|
|
745
745
|
}
|
|
746
|
-
/**
|
|
747
|
-
|
|
748
|
-
|
|
746
|
+
/** 手动修复 session 修改时间,并报告逐文件结果。 */
|
|
747
|
+
async function handleRepairSessionMtimes(args) {
|
|
748
|
+
const startedAt = performance.now();
|
|
749
|
+
let source;
|
|
750
|
+
let dryRun = false;
|
|
751
|
+
let minGapMs;
|
|
752
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
753
|
+
const arg = args[index];
|
|
754
|
+
if (arg === "--dry-run")
|
|
755
|
+
dryRun = true;
|
|
756
|
+
else if (arg === "--min-gap" && minGapMs === undefined) {
|
|
757
|
+
const match = /^(\d+)(m|h|d)$/.exec(args[++index] ?? "");
|
|
758
|
+
if (!match)
|
|
759
|
+
throw new Error("--min-gap 需要非负整数和单位 m/h/d,例如 30m、3h、1d;0m 表示任何正差值。");
|
|
760
|
+
minGapMs = Number(match[1]) * ({ m: 60_000, h: 3_600_000, d: 86_400_000 }[match[2]]);
|
|
761
|
+
if (!Number.isSafeInteger(minGapMs))
|
|
762
|
+
throw new Error("--min-gap 数值过大。");
|
|
763
|
+
}
|
|
764
|
+
else if (["--verbose", "--debug", "-v"].includes(arg))
|
|
765
|
+
continue;
|
|
766
|
+
else if ((arg === "codex" || arg === "claude") && source === undefined)
|
|
767
|
+
source = arg;
|
|
768
|
+
else
|
|
769
|
+
throw new Error(`不支持的修复参数:${arg}。用法:ccus sessions repair [codex|claude] [--min-gap 3h] [--dry-run]`);
|
|
770
|
+
}
|
|
771
|
+
const { repairSessionMtimes } = await Promise.resolve().then(() => __importStar(require("./lib/session-mtime")));
|
|
772
|
+
const results = await repairSessionMtimes(dryRun, source, minGapMs);
|
|
773
|
+
for (const result of results) {
|
|
774
|
+
if (result.status === "repaired") {
|
|
775
|
+
process.stdout.write(`${dryRun ? "待修复" : "已修复"} ${result.filePath}: ${result.before.toISOString()} → ${result.after.toISOString()}\n`);
|
|
776
|
+
}
|
|
777
|
+
else if (result.status === "skipped" || result.status === "failed") {
|
|
778
|
+
process.stderr.write(`${result.status === "failed" ? "失败" : "跳过"} ${result.filePath}: ${result.reason}\n`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const count = (status) => results.filter((result) => result.status === status).length;
|
|
782
|
+
process.stdout.write(`扫描 ${results.length},${dryRun ? "待修复" : "已修复"} ${count("repaired")},无需修复 ${count("unchanged")},跳过 ${count("skipped")},失败 ${count("failed")}\n`);
|
|
783
|
+
process.stdout.write(`耗时:${(0, sync_1.formatSyncElapsed)(performance.now() - startedAt)}\n`);
|
|
784
|
+
if (count("failed") > 0)
|
|
785
|
+
process.exitCode = 1;
|
|
786
|
+
}
|
|
787
|
+
/** 解析 sessions 命令的参数,支持 RANGE 作为位置参数简写。 */
|
|
749
788
|
function resolveSessionsOptions(action, args, rest) {
|
|
750
789
|
if (!action || action.startsWith("--")) {
|
|
751
790
|
return parseOptions(args.slice(1));
|
|
@@ -1423,6 +1462,10 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1423
1462
|
await handleExport(exportOptions);
|
|
1424
1463
|
return;
|
|
1425
1464
|
}
|
|
1465
|
+
if (group === "sessions" && action === "repair") {
|
|
1466
|
+
await handleRepairSessionMtimes(rest);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1426
1469
|
if (group === "sessions") {
|
|
1427
1470
|
const sessionsOptions = resolveSessionsOptions(action, args, rest);
|
|
1428
1471
|
await handleSessions(sessionsOptions);
|
package/dist/lib/claude.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.collectProjectJsonlFiles = collectProjectJsonlFiles;
|
|
6
7
|
exports.summarizeClaudeProjectUsage = summarizeClaudeProjectUsage;
|
|
7
8
|
exports.summarizeClaudeProjectUsageCombined = summarizeClaudeProjectUsageCombined;
|
|
8
9
|
exports.findActiveSessionFiles = findActiveSessionFiles;
|
|
@@ -0,0 +1,98 @@
|
|
|
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.repairSessionMtimes = repairSessionMtimes;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const claude_1 = require("./claude");
|
|
10
|
+
const paths_1 = require("./paths");
|
|
11
|
+
const time_1 = require("./time");
|
|
12
|
+
/** 从尾部按块取最后一条非空 JSON;异常末行不猜测时间。 */
|
|
13
|
+
async function readLastTimestamp(file, size) {
|
|
14
|
+
let position = size;
|
|
15
|
+
const chunks = [];
|
|
16
|
+
while (position > 0) {
|
|
17
|
+
const length = Math.min(position, 16 * 1024);
|
|
18
|
+
position -= length;
|
|
19
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
20
|
+
const { bytesRead } = await file.read(buffer, 0, length, position);
|
|
21
|
+
if (bytesRead !== length)
|
|
22
|
+
return null;
|
|
23
|
+
let end = length;
|
|
24
|
+
if (chunks.length === 0) {
|
|
25
|
+
while (end > 0 && [9, 10, 13, 32].includes(buffer[end - 1]))
|
|
26
|
+
end -= 1;
|
|
27
|
+
if (end === 0)
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const newline = buffer.lastIndexOf(10, end - 1);
|
|
31
|
+
chunks.push(buffer.subarray(newline + 1, end));
|
|
32
|
+
if (newline >= 0 || position === 0) {
|
|
33
|
+
try {
|
|
34
|
+
const record = JSON.parse(Buffer.concat(chunks.reverse()).toString("utf8"));
|
|
35
|
+
if (typeof record !== "object" || record === null || !("timestamp" in record)
|
|
36
|
+
|| typeof record.timestamp !== "string")
|
|
37
|
+
return null;
|
|
38
|
+
const timestamp = new Date(record.timestamp).getTime();
|
|
39
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
async function repairSessionMtimes(dryRun, source, minGapMs) {
|
|
49
|
+
const directories = [
|
|
50
|
+
...(source !== "codex" ? [node_path_1.default.join((0, paths_1.getClaudeDataDir)(), "projects")] : []),
|
|
51
|
+
...(source !== "claude" ? (0, paths_1.getCodexSessionHomes)().map((home) => node_path_1.default.join(home, "sessions")) : []),
|
|
52
|
+
];
|
|
53
|
+
const files = [...new Set((await Promise.all(directories.map(claude_1.collectProjectJsonlFiles))).flat())];
|
|
54
|
+
const results = [];
|
|
55
|
+
const seenFiles = new Set();
|
|
56
|
+
for (const filePath of files) {
|
|
57
|
+
try {
|
|
58
|
+
const file = await promises_1.default.open(filePath, dryRun ? "r" : "r+");
|
|
59
|
+
try {
|
|
60
|
+
const before = await file.stat({ bigint: true });
|
|
61
|
+
const identity = before.ino === 0n ? `path:${filePath}` : `${before.dev}:${before.ino}`;
|
|
62
|
+
if (seenFiles.has(identity))
|
|
63
|
+
continue;
|
|
64
|
+
seenFiles.add(identity);
|
|
65
|
+
const timestamp = await readLastTimestamp(file, Number(before.size));
|
|
66
|
+
if (timestamp === null) {
|
|
67
|
+
results.push({ filePath, status: "skipped", reason: "末条记录无有效时间戳" });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const current = await file.stat({ bigint: true });
|
|
71
|
+
if (current.size !== before.size || current.mtimeMs !== before.mtimeMs) {
|
|
72
|
+
results.push({ filePath, status: "skipped", reason: "读取期间文件发生变化,请稍后重试" });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const after = new Date(timestamp);
|
|
76
|
+
const gap = timestamp - Number(current.mtimeMs);
|
|
77
|
+
const meetsThreshold = minGapMs === undefined
|
|
78
|
+
? (0, time_1.localDateKey)(current.mtime) !== (0, time_1.localDateKey)(after)
|
|
79
|
+
: gap >= minGapMs;
|
|
80
|
+
if (gap <= 0 || !meetsThreshold) {
|
|
81
|
+
results.push({ filePath, status: "unchanged" });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!dryRun)
|
|
85
|
+
await file.utimes(before.atime, after);
|
|
86
|
+
results.push({ filePath, status: "repaired", before: current.mtime, after });
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await file.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
results.push({ filePath, status: "failed", reason: error.message });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=session-mtime.js.map
|