ccus-cli 0.1.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/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/cli.js +555 -0
- package/dist/lib/aggregate-dashboard.js +749 -0
- package/dist/lib/aggregate.js +168 -0
- package/dist/lib/claude.js +199 -0
- package/dist/lib/dashboard.js +394 -0
- package/dist/lib/debug.js +61 -0
- package/dist/lib/export.js +275 -0
- package/dist/lib/git.js +39 -0
- package/dist/lib/install.js +73 -0
- package/dist/lib/io.js +16 -0
- package/dist/lib/open.js +26 -0
- package/dist/lib/paths.js +56 -0
- package/dist/lib/payload.js +219 -0
- package/dist/lib/storage.js +217 -0
- package/dist/lib/time.js +154 -0
- package/dist/types.js +3 -0
- package/package.json +35 -0
|
@@ -0,0 +1,168 @@
|
|
|
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.loadWeeklyExportBundles = loadWeeklyExportBundles;
|
|
7
|
+
exports.buildAggregatedDetailRows = buildAggregatedDetailRows;
|
|
8
|
+
exports.buildAggregatedDailyRows = buildAggregatedDailyRows;
|
|
9
|
+
exports.buildAggregatedWeeklyRows = buildAggregatedWeeklyRows;
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const payload_1 = require("./payload");
|
|
13
|
+
const time_1 = require("./time");
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function isPersistedStatuslineEvent(value) {
|
|
18
|
+
return isRecord(value) && typeof value.timestamp === "string" && isRecord(value.rawPayload);
|
|
19
|
+
}
|
|
20
|
+
function hasWeeklyStatuslineShape(value) {
|
|
21
|
+
return (isRecord(value) &&
|
|
22
|
+
typeof value.sampleCount === "number" &&
|
|
23
|
+
typeof value.uniqueSessions === "number" &&
|
|
24
|
+
typeof value.uniqueWorkspaces === "number" &&
|
|
25
|
+
(typeof value.fiveHourLatestUsagePct === "number" || value.fiveHourLatestUsagePct === null) &&
|
|
26
|
+
(typeof value.fiveHourPeakUsagePct === "number" || value.fiveHourPeakUsagePct === null) &&
|
|
27
|
+
(typeof value.sevenDayLatestUsagePct === "number" || value.sevenDayLatestUsagePct === null) &&
|
|
28
|
+
(typeof value.sevenDayPeakUsagePct === "number" || value.sevenDayPeakUsagePct === null));
|
|
29
|
+
}
|
|
30
|
+
function isWeeklyExportBundle(value) {
|
|
31
|
+
if (!isRecord(value)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (value.schemaVersion !== 6) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(value.rawEvents) || !Array.isArray(value.dailySummaries) || !isRecord(value.weeklySummary) || !isRecord(value.identity) || !isRecord(value.range)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return hasWeeklyStatuslineShape(value.weeklySummary.statusline);
|
|
41
|
+
}
|
|
42
|
+
function localDateKey(date) {
|
|
43
|
+
const year = date.getFullYear();
|
|
44
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
45
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
46
|
+
return `${year}-${month}-${day}`;
|
|
47
|
+
}
|
|
48
|
+
function startOfLocalWeek(date) {
|
|
49
|
+
const day = date.getDay();
|
|
50
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
51
|
+
const start = new Date(date);
|
|
52
|
+
start.setDate(date.getDate() + diff);
|
|
53
|
+
start.setHours(0, 0, 0, 0);
|
|
54
|
+
return start;
|
|
55
|
+
}
|
|
56
|
+
function weekKey(date) {
|
|
57
|
+
return localDateKey(startOfLocalWeek(date));
|
|
58
|
+
}
|
|
59
|
+
function toPersonKey(gitUserEmail, gitUserName) {
|
|
60
|
+
return (0, time_1.extractGitEmailAccount)(gitUserEmail) ?? gitUserName ?? "unknown";
|
|
61
|
+
}
|
|
62
|
+
async function collectBundleJsonFiles(directoryPath) {
|
|
63
|
+
const entries = await promises_1.default.readdir(directoryPath, { withFileTypes: true });
|
|
64
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
65
|
+
const fullPath = node_path_1.default.join(directoryPath, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
return collectBundleJsonFiles(fullPath);
|
|
68
|
+
}
|
|
69
|
+
return entry.isFile() && entry.name.endsWith(".json") ? [fullPath] : [];
|
|
70
|
+
}));
|
|
71
|
+
return nested.flat();
|
|
72
|
+
}
|
|
73
|
+
/** 读取目录里的 export bundle json 文件。 */
|
|
74
|
+
async function loadWeeklyExportBundles(inputDir) {
|
|
75
|
+
const files = await collectBundleJsonFiles(inputDir);
|
|
76
|
+
const bundles = [];
|
|
77
|
+
const invalidFiles = [];
|
|
78
|
+
for (const filePath of files) {
|
|
79
|
+
try {
|
|
80
|
+
const content = await promises_1.default.readFile(filePath, "utf8");
|
|
81
|
+
const parsed = JSON.parse(content);
|
|
82
|
+
if (isWeeklyExportBundle(parsed)) {
|
|
83
|
+
bundles.push({ filePath, bundle: parsed });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
invalidFiles.push(filePath);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (invalidFiles.length > 0) {
|
|
93
|
+
throw new Error(`Unsupported export bundle schema in files: ${invalidFiles.join(", ")}. Re-export with current ccus so aggregate receives schemaVersion 6 bundles.`);
|
|
94
|
+
}
|
|
95
|
+
return bundles;
|
|
96
|
+
}
|
|
97
|
+
/** 从 bundle.rawEvents 展开 detail.csv 明细。 */
|
|
98
|
+
function buildAggregatedDetailRows(bundles) {
|
|
99
|
+
const rows = [];
|
|
100
|
+
for (const { bundle } of bundles) {
|
|
101
|
+
const personKey = toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName);
|
|
102
|
+
const tokensByDate = new Map(bundle.dailySummaries.map((day) => [day.date, day]));
|
|
103
|
+
for (const record of bundle.rawEvents.filter(isPersistedStatuslineEvent)) {
|
|
104
|
+
const event = (0, payload_1.computeStatuslineEvent)(record);
|
|
105
|
+
const ts = new Date(event.timestamp);
|
|
106
|
+
const dateKey = localDateKey(ts);
|
|
107
|
+
const dayTokens = tokensByDate.get(dateKey);
|
|
108
|
+
rows.push({
|
|
109
|
+
...event,
|
|
110
|
+
personKey,
|
|
111
|
+
weekKey: weekKey(ts),
|
|
112
|
+
dateKey,
|
|
113
|
+
inputTokens: dayTokens?.inputTokens ?? 0,
|
|
114
|
+
outputTokens: dayTokens?.outputTokens ?? 0,
|
|
115
|
+
cacheReadInputTokens: dayTokens?.cacheReadInputTokens ?? 0,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return rows.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
|
|
120
|
+
}
|
|
121
|
+
/** 直接从 bundle.dailySummaries 展开 daily.csv。 */
|
|
122
|
+
function buildAggregatedDailyRows(bundles) {
|
|
123
|
+
const rows = [];
|
|
124
|
+
for (const { bundle } of bundles) {
|
|
125
|
+
const personKey = toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName);
|
|
126
|
+
for (const item of bundle.dailySummaries) {
|
|
127
|
+
rows.push({
|
|
128
|
+
personKey,
|
|
129
|
+
date: item.date,
|
|
130
|
+
userMessageCount: item.userMessageCount,
|
|
131
|
+
apiRequestCount: item.apiRequestCount,
|
|
132
|
+
inputTokens: item.inputTokens,
|
|
133
|
+
outputTokens: item.outputTokens,
|
|
134
|
+
cacheReadInputTokens: item.cacheReadInputTokens,
|
|
135
|
+
sampleCount: item.sampleCount,
|
|
136
|
+
fiveHourPeakUsagePct: item.fiveHourPeakUsagePct,
|
|
137
|
+
fiveHourLatestUsagePct: item.fiveHourLatestUsagePct,
|
|
138
|
+
sevenDayPeakUsagePct: item.sevenDayPeakUsagePct,
|
|
139
|
+
sevenDayLatestUsagePct: item.sevenDayLatestUsagePct,
|
|
140
|
+
uniqueSessions: item.uniqueSessions,
|
|
141
|
+
uniqueWorkspaces: item.uniqueWorkspaces,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
|
|
146
|
+
}
|
|
147
|
+
/** 直接从 bundle.weeklySummary 展开 weekly.csv。 */
|
|
148
|
+
function buildAggregatedWeeklyRows(bundles) {
|
|
149
|
+
return bundles
|
|
150
|
+
.map(({ bundle }) => ({
|
|
151
|
+
personKey: toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName),
|
|
152
|
+
week: weekKey(new Date(bundle.range.start)),
|
|
153
|
+
userMessageCount: bundle.weeklySummary.counts.userMessageCount,
|
|
154
|
+
apiRequestCount: bundle.weeklySummary.counts.apiRequestCount,
|
|
155
|
+
inputTokens: bundle.weeklySummary.tokens.inputTokens,
|
|
156
|
+
outputTokens: bundle.weeklySummary.tokens.outputTokens,
|
|
157
|
+
cacheReadInputTokens: bundle.weeklySummary.tokens.cacheReadInputTokens,
|
|
158
|
+
sampleCount: bundle.weeklySummary.statusline.sampleCount,
|
|
159
|
+
fiveHourPeakUsagePct: bundle.weeklySummary.statusline.fiveHourPeakUsagePct,
|
|
160
|
+
fiveHourLatestUsagePct: bundle.weeklySummary.statusline.fiveHourLatestUsagePct,
|
|
161
|
+
sevenDayPeakUsagePct: bundle.weeklySummary.statusline.sevenDayPeakUsagePct,
|
|
162
|
+
sevenDayLatestUsagePct: bundle.weeklySummary.statusline.sevenDayLatestUsagePct,
|
|
163
|
+
uniqueSessions: bundle.weeklySummary.statusline.uniqueSessions,
|
|
164
|
+
uniqueWorkspaces: bundle.weeklySummary.statusline.uniqueWorkspaces,
|
|
165
|
+
}))
|
|
166
|
+
.sort((left, right) => `${left.personKey}|${left.week}`.localeCompare(`${right.personKey}|${right.week}`));
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=aggregate.js.map
|
|
@@ -0,0 +1,199 @@
|
|
|
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.summarizeClaudeProjectUsage = summarizeClaudeProjectUsage;
|
|
7
|
+
exports.summarizeClaudeProjectUsageByDay = summarizeClaudeProjectUsageByDay;
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const paths_1 = require("./paths");
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function getNumber(value) {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
16
|
+
}
|
|
17
|
+
function getString(value) {
|
|
18
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
19
|
+
}
|
|
20
|
+
async function collectProjectJsonlFiles(directoryPath) {
|
|
21
|
+
try {
|
|
22
|
+
const entries = await promises_1.default.readdir(directoryPath, { withFileTypes: true });
|
|
23
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
24
|
+
const fullPath = node_path_1.default.join(directoryPath, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
return collectProjectJsonlFiles(fullPath);
|
|
27
|
+
}
|
|
28
|
+
return entry.isFile() && entry.name.endsWith(".jsonl") ? [fullPath] : [];
|
|
29
|
+
}));
|
|
30
|
+
return nested.flat();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === "ENOENT") {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function timestampInRange(timestamp, start, end) {
|
|
40
|
+
if (!timestamp) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const value = new Date(timestamp).getTime();
|
|
44
|
+
return Number.isFinite(value) && value >= start.getTime() && value <= end.getTime();
|
|
45
|
+
}
|
|
46
|
+
function localDateKey(date) {
|
|
47
|
+
const year = date.getFullYear();
|
|
48
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
49
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
50
|
+
return `${year}-${month}-${day}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 判断一条 `type:"user"` 事件是否算作一次用户请求。
|
|
54
|
+
*
|
|
55
|
+
* Claude transcript 里 `type:"user"` 还会被用作 tool_result 回填——这种伪 user 事件必须排除,
|
|
56
|
+
* 否则 userMessageCount 会被高估近 10×。sidechain(子 agent)会话里的用户提示保留计入,
|
|
57
|
+
* 因为它们仍然代表团队让 Claude 做的事,不算工具机械回填。
|
|
58
|
+
*/
|
|
59
|
+
function isHumanUserMessage(record) {
|
|
60
|
+
if (record.type !== "user") {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (record.isMeta === true) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (record.toolUseResult !== undefined && record.toolUseResult !== null) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (!isRecord(record.message)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const content = record.message.content;
|
|
73
|
+
if (Array.isArray(content)) {
|
|
74
|
+
const hasNonToolResult = content.some((item) => isRecord(item) && item.type !== "tool_result");
|
|
75
|
+
if (!hasNonToolResult) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function summarizeProjectTranscript(content, start, end) {
|
|
82
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
83
|
+
const summary = {
|
|
84
|
+
userMessageCount: 0,
|
|
85
|
+
apiRequestCount: 0,
|
|
86
|
+
inputTokens: 0,
|
|
87
|
+
outputTokens: 0,
|
|
88
|
+
cacheReadInputTokens: 0,
|
|
89
|
+
};
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
try {
|
|
92
|
+
const record = JSON.parse(line);
|
|
93
|
+
if (!isRecord(record) || !timestampInRange(getString(record.timestamp), start, end)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (isHumanUserMessage(record)) {
|
|
97
|
+
summary.userMessageCount += 1;
|
|
98
|
+
}
|
|
99
|
+
if (record.type !== "assistant" || !isRecord(record.message) || !isRecord(record.message.usage)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
summary.apiRequestCount += 1;
|
|
103
|
+
summary.inputTokens += getNumber(record.message.usage.input_tokens);
|
|
104
|
+
summary.outputTokens += getNumber(record.message.usage.output_tokens);
|
|
105
|
+
summary.cacheReadInputTokens += getNumber(record.message.usage.cache_read_input_tokens);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return summary;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 从 Claude 本地 project transcript 中统计本周消息数、请求数和 token 用量。
|
|
115
|
+
*/
|
|
116
|
+
async function summarizeClaudeProjectUsage(start, end) {
|
|
117
|
+
const claudeDataDir = (0, paths_1.getClaudeDataDir)();
|
|
118
|
+
const projectDir = node_path_1.default.join(claudeDataDir, "projects");
|
|
119
|
+
const files = await collectProjectJsonlFiles(projectDir);
|
|
120
|
+
const totals = {
|
|
121
|
+
userMessageCount: 0,
|
|
122
|
+
apiRequestCount: 0,
|
|
123
|
+
inputTokens: 0,
|
|
124
|
+
outputTokens: 0,
|
|
125
|
+
cacheReadInputTokens: 0,
|
|
126
|
+
matchedFileCount: files.length,
|
|
127
|
+
claudeDataDir,
|
|
128
|
+
};
|
|
129
|
+
for (const filePath of files) {
|
|
130
|
+
try {
|
|
131
|
+
const content = await promises_1.default.readFile(filePath, "utf8");
|
|
132
|
+
const summary = summarizeProjectTranscript(content, start, end);
|
|
133
|
+
totals.userMessageCount += summary.userMessageCount;
|
|
134
|
+
totals.apiRequestCount += summary.apiRequestCount;
|
|
135
|
+
totals.inputTokens += summary.inputTokens;
|
|
136
|
+
totals.outputTokens += summary.outputTokens;
|
|
137
|
+
totals.cacheReadInputTokens += summary.cacheReadInputTokens;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return totals;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 按天汇总 Claude project transcript 中的消息数、请求数和 token 用量。
|
|
147
|
+
*/
|
|
148
|
+
async function summarizeClaudeProjectUsageByDay(start, end) {
|
|
149
|
+
const claudeDataDir = (0, paths_1.getClaudeDataDir)();
|
|
150
|
+
const projectDir = node_path_1.default.join(claudeDataDir, "projects");
|
|
151
|
+
const files = await collectProjectJsonlFiles(projectDir);
|
|
152
|
+
const daily = new Map();
|
|
153
|
+
for (const filePath of files) {
|
|
154
|
+
let content = "";
|
|
155
|
+
try {
|
|
156
|
+
content = await promises_1.default.readFile(filePath, "utf8");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
try {
|
|
164
|
+
const record = JSON.parse(line);
|
|
165
|
+
if (!isRecord(record)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const timestamp = getString(record.timestamp);
|
|
169
|
+
if (!timestampInRange(timestamp, start, end)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const date = localDateKey(new Date(timestamp));
|
|
173
|
+
const current = daily.get(date) ?? {
|
|
174
|
+
date,
|
|
175
|
+
userMessageCount: 0,
|
|
176
|
+
apiRequestCount: 0,
|
|
177
|
+
inputTokens: 0,
|
|
178
|
+
outputTokens: 0,
|
|
179
|
+
cacheReadInputTokens: 0,
|
|
180
|
+
};
|
|
181
|
+
if (isHumanUserMessage(record)) {
|
|
182
|
+
current.userMessageCount += 1;
|
|
183
|
+
}
|
|
184
|
+
if (record.type === "assistant" && isRecord(record.message) && isRecord(record.message.usage)) {
|
|
185
|
+
current.apiRequestCount += 1;
|
|
186
|
+
current.inputTokens += getNumber(record.message.usage.input_tokens);
|
|
187
|
+
current.outputTokens += getNumber(record.message.usage.output_tokens);
|
|
188
|
+
current.cacheReadInputTokens += getNumber(record.message.usage.cache_read_input_tokens);
|
|
189
|
+
}
|
|
190
|
+
daily.set(date, current);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return daily;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=claude.js.map
|