minimax-status 1.0.21 → 1.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/cli/api.js +128 -0
- package/cli/index.js +30 -2
- package/cli/status.js +50 -1
- package/cli/transcript-parser.js +207 -207
- package/package.json +1 -1
package/cli/api.js
CHANGED
|
@@ -142,6 +142,134 @@ class MinimaxAPI {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Get billing records from the account/amount API
|
|
147
|
+
* @param {number} page - Page number (1-based)
|
|
148
|
+
* @param {number} limit - Number of records per page (max 100)
|
|
149
|
+
* @returns {Promise<Object>} Billing records response
|
|
150
|
+
*/
|
|
151
|
+
async getBillingRecords(page = 1, limit = 100) {
|
|
152
|
+
try {
|
|
153
|
+
const response = await axios.get(
|
|
154
|
+
`https://www.minimaxi.com/account/amount`,
|
|
155
|
+
{
|
|
156
|
+
params: {
|
|
157
|
+
page: page,
|
|
158
|
+
limit: limit,
|
|
159
|
+
aggregate: false,
|
|
160
|
+
GroupId: this.groupId,
|
|
161
|
+
},
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: `Bearer ${this.token}`,
|
|
164
|
+
Accept: "application/json",
|
|
165
|
+
},
|
|
166
|
+
timeout: 10000,
|
|
167
|
+
httpsAgent,
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return response.data;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(`账单 API 请求失败: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fetch all billing records with pagination
|
|
179
|
+
* @param {number} maxPages - Maximum number of pages to fetch
|
|
180
|
+
* @returns {Promise<Array>} All billing records
|
|
181
|
+
*/
|
|
182
|
+
async getAllBillingRecords(maxPages = 10) {
|
|
183
|
+
const allRecords = [];
|
|
184
|
+
|
|
185
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
186
|
+
try {
|
|
187
|
+
const response = await this.getBillingRecords(page, 100);
|
|
188
|
+
const records = response.charge_records || [];
|
|
189
|
+
|
|
190
|
+
if (records.length === 0) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
allRecords.push(...records);
|
|
195
|
+
|
|
196
|
+
if (records.length < 100) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(`Failed to fetch billing records page ${page}:`, error.message);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return allRecords;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Calculate usage statistics from billing records
|
|
210
|
+
* @param {Array} records - Billing records from account/amount API
|
|
211
|
+
* @param {number} planStartTime - Plan start time in milliseconds
|
|
212
|
+
* @param {number} planEndTime - Plan end time in milliseconds
|
|
213
|
+
* @returns {Object} Usage statistics
|
|
214
|
+
*/
|
|
215
|
+
calculateUsageStats(records, planStartTime, planEndTime) {
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
|
|
218
|
+
// 账单记录是秒级时间戳,需要统一转换为毫秒
|
|
219
|
+
const planStartMs = planStartTime;
|
|
220
|
+
const planEndMs = planEndTime;
|
|
221
|
+
|
|
222
|
+
// 昨日(0点到现在)或者取最近一次账单的日期
|
|
223
|
+
// 账单记录不是实时的,当日消耗要明天才显示,所以显示"昨日"
|
|
224
|
+
const todayStart = new Date().setHours(0, 0, 0, 0);
|
|
225
|
+
const yesterdayStart = todayStart - 24 * 60 * 60 * 1000;
|
|
226
|
+
const weekAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
227
|
+
|
|
228
|
+
const stats = {
|
|
229
|
+
lastDayUsage: 0,
|
|
230
|
+
weeklyUsage: 0,
|
|
231
|
+
planTotalUsage: 0,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
for (const record of records) {
|
|
235
|
+
const tokens = parseInt(record.consume_token, 10) || 0;
|
|
236
|
+
// 账单记录的 created_at 是秒级时间戳,转换为毫秒
|
|
237
|
+
const createdAt = (record.created_at || 0) * 1000;
|
|
238
|
+
|
|
239
|
+
// 昨日消耗(从昨日0点到现在)
|
|
240
|
+
if (createdAt >= yesterdayStart && createdAt < todayStart) {
|
|
241
|
+
stats.lastDayUsage += tokens;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 近7天消耗
|
|
245
|
+
if (createdAt >= weekAgo) {
|
|
246
|
+
stats.weeklyUsage += tokens;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 套餐期内总消耗
|
|
250
|
+
if (createdAt >= planStartMs && createdAt <= planEndMs) {
|
|
251
|
+
stats.planTotalUsage += tokens;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return stats;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Format number to human readable format (万, 亿)
|
|
260
|
+
* @param {number} num - Number to format
|
|
261
|
+
* @returns {string} Formatted string
|
|
262
|
+
*/
|
|
263
|
+
formatNumber(num) {
|
|
264
|
+
if (num >= 100000000) {
|
|
265
|
+
return (num / 100000000).toFixed(1).replace(/\.0$/, "") + "亿";
|
|
266
|
+
}
|
|
267
|
+
if (num >= 10000) {
|
|
268
|
+
return (num / 10000).toFixed(1).replace(/\.0$/, "") + "万";
|
|
269
|
+
}
|
|
270
|
+
return num.toLocaleString("zh-CN");
|
|
271
|
+
}
|
|
272
|
+
|
|
145
273
|
// 清除缓存
|
|
146
274
|
clearCache() {
|
|
147
275
|
this.cache = {
|
package/cli/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// Force color output even in non-TTY environments (e.g., Claude Code statusline)
|
|
4
4
|
process.env.FORCE_COLOR = "1";
|
|
@@ -114,7 +114,35 @@ program
|
|
|
114
114
|
api.getSubscriptionDetails(),
|
|
115
115
|
]);
|
|
116
116
|
const usageData = api.parseUsageData(apiData, subscriptionData);
|
|
117
|
-
|
|
117
|
+
|
|
118
|
+
// 获取账单数据用于消耗统计
|
|
119
|
+
let usageStats = null;
|
|
120
|
+
try {
|
|
121
|
+
const billingRecords = await api.getAllBillingRecords(10);
|
|
122
|
+
if (billingRecords.length > 0) {
|
|
123
|
+
// 计算套餐开始时间:到期时间往前推1个月
|
|
124
|
+
let planStartTime = 0;
|
|
125
|
+
if (subscriptionData &&
|
|
126
|
+
subscriptionData.current_subscribe &&
|
|
127
|
+
subscriptionData.current_subscribe.current_subscribe_end_time) {
|
|
128
|
+
const expiryDateStr = subscriptionData.current_subscribe.current_subscribe_end_time;
|
|
129
|
+
const [month, day, year] = expiryDateStr.split('/').map(Number);
|
|
130
|
+
planStartTime = new Date(year, month - 2, day).getTime();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
usageStats = api.calculateUsageStats(
|
|
135
|
+
billingRecords,
|
|
136
|
+
planStartTime > 0 ? planStartTime : 0,
|
|
137
|
+
now
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} catch (billingError) {
|
|
141
|
+
// 账单数据获取失败不影响主要功能
|
|
142
|
+
console.error(chalk.gray(`消耗统计获取失败: ${billingError.message}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const statusBar = new StatusBar(usageData, usageStats, api);
|
|
118
146
|
|
|
119
147
|
spinner.succeed("状态获取成功");
|
|
120
148
|
|
package/cli/status.js
CHANGED
|
@@ -4,12 +4,56 @@ const { default: boxen } = require('boxen');
|
|
|
4
4
|
const { default: stringWidth } = require('string-width');
|
|
5
5
|
|
|
6
6
|
class StatusBar {
|
|
7
|
-
constructor(data) {
|
|
7
|
+
constructor(data, usageStats = null, api = null) {
|
|
8
8
|
this.data = data;
|
|
9
|
+
this.usageStats = usageStats;
|
|
10
|
+
this.api = api;
|
|
9
11
|
this.totalWidth = 63; // 总宽度包括边框
|
|
10
12
|
this.borderWidth = 4; // '│ ' (2) + ' │' (2) = 4
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
// 格式化数字
|
|
16
|
+
formatNumber(num) {
|
|
17
|
+
if (this.api) {
|
|
18
|
+
return this.api.formatNumber(num);
|
|
19
|
+
}
|
|
20
|
+
if (num >= 100000000) {
|
|
21
|
+
return (num / 100000000).toFixed(1).replace(/\.0$/, "") + "亿";
|
|
22
|
+
}
|
|
23
|
+
if (num >= 10000) {
|
|
24
|
+
return (num / 10000).toFixed(1).replace(/\.0$/, "") + "万";
|
|
25
|
+
}
|
|
26
|
+
return num.toLocaleString("zh-CN");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 渲染消耗统计表格
|
|
30
|
+
renderConsumptionStats() {
|
|
31
|
+
if (!this.usageStats) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push(chalk.bold('📊 Token 消耗统计'));
|
|
38
|
+
|
|
39
|
+
// 计算表格宽度
|
|
40
|
+
const leftWidth = 12; // "昨日消耗: "
|
|
41
|
+
const rightWidth = 15; // "1732.1万 "
|
|
42
|
+
const padding = this.totalWidth - this.borderWidth - leftWidth - rightWidth;
|
|
43
|
+
|
|
44
|
+
const pad = ' '.repeat(Math.max(0, padding));
|
|
45
|
+
|
|
46
|
+
const formatLine = (label, value) => {
|
|
47
|
+
return `│ ${chalk.cyan(label)}${pad}${this.formatNumber(value)}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
lines.push(formatLine('昨日消耗: ', this.usageStats.lastDayUsage));
|
|
51
|
+
lines.push(formatLine('近7天消耗: ', this.usageStats.weeklyUsage));
|
|
52
|
+
lines.push(formatLine('套餐总消耗: ', this.usageStats.planTotalUsage));
|
|
53
|
+
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
// 辅助函数:填充内容到正确长度,处理 chalk 代码和中文字符
|
|
14
58
|
padLine(leftContent, rightContent) {
|
|
15
59
|
// 移除 chalk 代码以便计算
|
|
@@ -72,6 +116,11 @@ class StatusBar {
|
|
|
72
116
|
contentLines.push(`${chalk.cyan('套餐到期:')} ${expiryText}`);
|
|
73
117
|
}
|
|
74
118
|
|
|
119
|
+
// 添加消耗统计(如果有数据)
|
|
120
|
+
if (this.usageStats) {
|
|
121
|
+
contentLines.push(this.renderConsumptionStats());
|
|
122
|
+
}
|
|
123
|
+
|
|
75
124
|
contentLines.push('');
|
|
76
125
|
|
|
77
126
|
// 状态行
|
package/cli/transcript-parser.js
CHANGED
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
|
|
7
|
-
class TranscriptParser {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.toolMap = new Map();
|
|
10
|
-
this.agentMap = new Map();
|
|
11
|
-
this.latestTodos = [];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async parse(transcriptPath) {
|
|
15
|
-
const result = {
|
|
16
|
-
tools: [],
|
|
17
|
-
agents: [],
|
|
18
|
-
todos: [],
|
|
19
|
-
sessionStart: null,
|
|
20
|
-
contextTokens: 0,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
if (typeof transcriptPath !== 'string' || !fs.existsSync(transcriptPath)) {
|
|
24
|
-
return result;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const fileStream = fs.createReadStream(transcriptPath);
|
|
29
|
-
const rl = readline.createInterface({
|
|
30
|
-
input: fileStream,
|
|
31
|
-
crlfDelay: Infinity,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
for await (const line of rl) {
|
|
35
|
-
if (!line.trim()) continue;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const entry = JSON.parse(line);
|
|
39
|
-
this.processEntry(entry, result);
|
|
40
|
-
} catch {
|
|
41
|
-
// Skip malformed lines
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// Return partial results on error
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
result.tools = Array.from(this.toolMap.values()).slice(-20);
|
|
49
|
-
result.agents = Array.from(this.agentMap.values()).slice(-10);
|
|
50
|
-
result.todos = this.latestTodos;
|
|
51
|
-
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
processEntry(entry, result) {
|
|
56
|
-
const timestamp = entry.timestamp ? new Date(entry.timestamp) : new Date();
|
|
57
|
-
|
|
58
|
-
if (!result.sessionStart && entry.timestamp) {
|
|
59
|
-
result.sessionStart = timestamp;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const content = entry.message?.content;
|
|
63
|
-
if (!content || !Array.isArray(content)) return;
|
|
64
|
-
|
|
65
|
-
for (const block of content) {
|
|
66
|
-
if (block.type === 'tool_use' && block.id && block.name) {
|
|
67
|
-
const toolEntry = {
|
|
68
|
-
id: block.id,
|
|
69
|
-
name: block.name,
|
|
70
|
-
target: this.extractTarget(block.name, block.input),
|
|
71
|
-
status: 'running',
|
|
72
|
-
startTime: timestamp,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
if (block.name === 'Task') {
|
|
76
|
-
const input = block.input || {};
|
|
77
|
-
const agentEntry = {
|
|
78
|
-
id: block.id,
|
|
79
|
-
type: input.subagent_type || 'unknown',
|
|
80
|
-
model: input.model,
|
|
81
|
-
description: input.description,
|
|
82
|
-
status: 'running',
|
|
83
|
-
startTime: timestamp,
|
|
84
|
-
};
|
|
85
|
-
this.agentMap.set(block.id, agentEntry);
|
|
86
|
-
} else if (block.name === 'TodoWrite') {
|
|
87
|
-
const input = block.input || {};
|
|
88
|
-
if (input.todos && Array.isArray(input.todos)) {
|
|
89
|
-
this.latestTodos.length = 0;
|
|
90
|
-
this.latestTodos.push(...input.todos);
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
this.toolMap.set(block.id, toolEntry);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
98
|
-
const tool = this.toolMap.get(block.tool_use_id);
|
|
99
|
-
if (tool) {
|
|
100
|
-
tool.status = block.is_error ? 'error' : 'completed';
|
|
101
|
-
tool.endTime = timestamp;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const agent = this.agentMap.get(block.tool_use_id);
|
|
105
|
-
if (agent) {
|
|
106
|
-
agent.status = 'completed';
|
|
107
|
-
agent.endTime = timestamp;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
extractTarget(toolName, input) {
|
|
114
|
-
if (!input) return undefined;
|
|
115
|
-
|
|
116
|
-
switch (toolName) {
|
|
117
|
-
case 'Read':
|
|
118
|
-
case 'Write':
|
|
119
|
-
case 'Edit':
|
|
120
|
-
return input.file_path || input.path;
|
|
121
|
-
case 'Glob':
|
|
122
|
-
return input.pattern;
|
|
123
|
-
case 'Grep':
|
|
124
|
-
return input.pattern;
|
|
125
|
-
case 'Bash':
|
|
126
|
-
const cmd = input.command;
|
|
127
|
-
if (typeof cmd === 'string') {
|
|
128
|
-
return cmd.slice(0, 30) + (cmd.length > 30 ? '...' : '');
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
calculateContextTokens(usage) {
|
|
136
|
-
const inputTokens = usage?.input_tokens || usage?.prompt_tokens || 0;
|
|
137
|
-
const outputTokens = usage?.output_tokens || usage?.completion_tokens || 0;
|
|
138
|
-
const cacheCreation = usage?.cache_creation_input_tokens || usage?.cache_creation_prompt_tokens || 0;
|
|
139
|
-
const cacheRead = usage?.cache_read_input_tokens || usage?.cache_read_prompt_tokens || usage?.cached_tokens || 0;
|
|
140
|
-
|
|
141
|
-
return inputTokens + outputTokens + cacheCreation + cacheRead;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async findLatestUsage(transcriptPath) {
|
|
145
|
-
if (typeof transcriptPath !== 'string' || !fs.existsSync(transcriptPath)) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const fileContent = fs.readFileSync(transcriptPath, 'utf8');
|
|
151
|
-
const lines = fileContent.trim().split('\n').filter(line => line.trim());
|
|
152
|
-
|
|
153
|
-
if (lines.length === 0) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const lastLine = lines[lines.length - 1].trim();
|
|
158
|
-
const lastEntry = JSON.parse(lastLine);
|
|
159
|
-
|
|
160
|
-
if (lastEntry.type === 'summary' && lastEntry.leafUuid) {
|
|
161
|
-
return this.findUsageByUuid(transcriptPath, lastEntry.leafUuid);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
165
|
-
const line = lines[i].trim();
|
|
166
|
-
if (!line) continue;
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const entry = JSON.parse(line);
|
|
170
|
-
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
171
|
-
return this.calculateContextTokens(entry.message.usage);
|
|
172
|
-
}
|
|
173
|
-
} catch {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return null;
|
|
179
|
-
} catch {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async findUsageByUuid(filePath, targetUuid) {
|
|
185
|
-
try {
|
|
186
|
-
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
187
|
-
const lines = fileContent.trim().split('\n').filter(line => line.trim());
|
|
188
|
-
|
|
189
|
-
for (const line of lines) {
|
|
190
|
-
try {
|
|
191
|
-
const entry = JSON.parse(line.trim());
|
|
192
|
-
if (entry.uuid === targetUuid && entry.message?.usage) {
|
|
193
|
-
return this.calculateContextTokens(entry.message.usage);
|
|
194
|
-
}
|
|
195
|
-
} catch {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return null;
|
|
201
|
-
} catch {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
module.exports = TranscriptParser;
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
class TranscriptParser {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.toolMap = new Map();
|
|
10
|
+
this.agentMap = new Map();
|
|
11
|
+
this.latestTodos = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async parse(transcriptPath) {
|
|
15
|
+
const result = {
|
|
16
|
+
tools: [],
|
|
17
|
+
agents: [],
|
|
18
|
+
todos: [],
|
|
19
|
+
sessionStart: null,
|
|
20
|
+
contextTokens: 0,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (typeof transcriptPath !== 'string' || !fs.existsSync(transcriptPath)) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const fileStream = fs.createReadStream(transcriptPath);
|
|
29
|
+
const rl = readline.createInterface({
|
|
30
|
+
input: fileStream,
|
|
31
|
+
crlfDelay: Infinity,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for await (const line of rl) {
|
|
35
|
+
if (!line.trim()) continue;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const entry = JSON.parse(line);
|
|
39
|
+
this.processEntry(entry, result);
|
|
40
|
+
} catch {
|
|
41
|
+
// Skip malformed lines
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Return partial results on error
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
result.tools = Array.from(this.toolMap.values()).slice(-20);
|
|
49
|
+
result.agents = Array.from(this.agentMap.values()).slice(-10);
|
|
50
|
+
result.todos = this.latestTodos;
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
processEntry(entry, result) {
|
|
56
|
+
const timestamp = entry.timestamp ? new Date(entry.timestamp) : new Date();
|
|
57
|
+
|
|
58
|
+
if (!result.sessionStart && entry.timestamp) {
|
|
59
|
+
result.sessionStart = timestamp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const content = entry.message?.content;
|
|
63
|
+
if (!content || !Array.isArray(content)) return;
|
|
64
|
+
|
|
65
|
+
for (const block of content) {
|
|
66
|
+
if (block.type === 'tool_use' && block.id && block.name) {
|
|
67
|
+
const toolEntry = {
|
|
68
|
+
id: block.id,
|
|
69
|
+
name: block.name,
|
|
70
|
+
target: this.extractTarget(block.name, block.input),
|
|
71
|
+
status: 'running',
|
|
72
|
+
startTime: timestamp,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (block.name === 'Task') {
|
|
76
|
+
const input = block.input || {};
|
|
77
|
+
const agentEntry = {
|
|
78
|
+
id: block.id,
|
|
79
|
+
type: input.subagent_type || 'unknown',
|
|
80
|
+
model: input.model,
|
|
81
|
+
description: input.description,
|
|
82
|
+
status: 'running',
|
|
83
|
+
startTime: timestamp,
|
|
84
|
+
};
|
|
85
|
+
this.agentMap.set(block.id, agentEntry);
|
|
86
|
+
} else if (block.name === 'TodoWrite') {
|
|
87
|
+
const input = block.input || {};
|
|
88
|
+
if (input.todos && Array.isArray(input.todos)) {
|
|
89
|
+
this.latestTodos.length = 0;
|
|
90
|
+
this.latestTodos.push(...input.todos);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
this.toolMap.set(block.id, toolEntry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
98
|
+
const tool = this.toolMap.get(block.tool_use_id);
|
|
99
|
+
if (tool) {
|
|
100
|
+
tool.status = block.is_error ? 'error' : 'completed';
|
|
101
|
+
tool.endTime = timestamp;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const agent = this.agentMap.get(block.tool_use_id);
|
|
105
|
+
if (agent) {
|
|
106
|
+
agent.status = 'completed';
|
|
107
|
+
agent.endTime = timestamp;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
extractTarget(toolName, input) {
|
|
114
|
+
if (!input) return undefined;
|
|
115
|
+
|
|
116
|
+
switch (toolName) {
|
|
117
|
+
case 'Read':
|
|
118
|
+
case 'Write':
|
|
119
|
+
case 'Edit':
|
|
120
|
+
return input.file_path || input.path;
|
|
121
|
+
case 'Glob':
|
|
122
|
+
return input.pattern;
|
|
123
|
+
case 'Grep':
|
|
124
|
+
return input.pattern;
|
|
125
|
+
case 'Bash':
|
|
126
|
+
const cmd = input.command;
|
|
127
|
+
if (typeof cmd === 'string') {
|
|
128
|
+
return cmd.slice(0, 30) + (cmd.length > 30 ? '...' : '');
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
calculateContextTokens(usage) {
|
|
136
|
+
const inputTokens = usage?.input_tokens || usage?.prompt_tokens || 0;
|
|
137
|
+
const outputTokens = usage?.output_tokens || usage?.completion_tokens || 0;
|
|
138
|
+
const cacheCreation = usage?.cache_creation_input_tokens || usage?.cache_creation_prompt_tokens || 0;
|
|
139
|
+
const cacheRead = usage?.cache_read_input_tokens || usage?.cache_read_prompt_tokens || usage?.cached_tokens || 0;
|
|
140
|
+
|
|
141
|
+
return inputTokens + outputTokens + cacheCreation + cacheRead;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async findLatestUsage(transcriptPath) {
|
|
145
|
+
if (typeof transcriptPath !== 'string' || !fs.existsSync(transcriptPath)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const fileContent = fs.readFileSync(transcriptPath, 'utf8');
|
|
151
|
+
const lines = fileContent.trim().split('\n').filter(line => line.trim());
|
|
152
|
+
|
|
153
|
+
if (lines.length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const lastLine = lines[lines.length - 1].trim();
|
|
158
|
+
const lastEntry = JSON.parse(lastLine);
|
|
159
|
+
|
|
160
|
+
if (lastEntry.type === 'summary' && lastEntry.leafUuid) {
|
|
161
|
+
return this.findUsageByUuid(transcriptPath, lastEntry.leafUuid);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
165
|
+
const line = lines[i].trim();
|
|
166
|
+
if (!line) continue;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const entry = JSON.parse(line);
|
|
170
|
+
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
171
|
+
return this.calculateContextTokens(entry.message.usage);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async findUsageByUuid(filePath, targetUuid) {
|
|
185
|
+
try {
|
|
186
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
187
|
+
const lines = fileContent.trim().split('\n').filter(line => line.trim());
|
|
188
|
+
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
try {
|
|
191
|
+
const entry = JSON.parse(line.trim());
|
|
192
|
+
if (entry.uuid === targetUuid && entry.message?.usage) {
|
|
193
|
+
return this.calculateContextTokens(entry.message.usage);
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = TranscriptParser;
|