@jetcrab/pi-desk-usage 0.0.0-stage → 1.0.1-dev.9001
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 +201 -0
- package/README.md +4 -2
- package/dist/browser/chunks/browser-PWPSWHE7.js +12 -0
- package/dist/browser/chunks/browser-session-view-FVGFXMQB.js +4 -0
- package/dist/browser/chunks/chunk-JHHRG2FT.js +1 -0
- package/dist/browser/entry.js +1 -0
- package/dist/l4-usage-aggregate.d.ts +9 -0
- package/dist/l4-usage-aggregate.js +59 -0
- package/dist/l4-usage-calendar.d.ts +31 -0
- package/dist/l4-usage-calendar.js +186 -0
- package/dist/l4-usage-files.d.ts +5 -0
- package/dist/l4-usage-files.js +97 -0
- package/dist/l4-usage-format.d.ts +1 -0
- package/dist/l4-usage-format.js +12 -0
- package/dist/pi-desk.d.ts +2 -0
- package/dist/pi-desk.js +18 -0
- package/dist/protocol.d.ts +189 -0
- package/dist/protocol.js +1 -0
- package/dist/usage-runtime.d.ts +53 -0
- package/dist/usage-runtime.js +816 -0
- package/dist/usage-scan.d.ts +82 -0
- package/dist/usage-scan.js +467 -0
- package/dist/usage-timeline.d.ts +15 -0
- package/dist/usage-timeline.js +342 -0
- package/dist/usage-worker.d.ts +1 -0
- package/dist/usage-worker.js +13 -0
- package/package.json +56 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { UsageAnalysisCallKind, UsageStructureCounts, UsageTimelineIgnoreMode, UsageTimelineMarkerKind, UsageTotals } from './protocol.js';
|
|
2
|
+
export interface UsageFileDescriptor {
|
|
3
|
+
path: string;
|
|
4
|
+
size: number;
|
|
5
|
+
modifiedAt: number;
|
|
6
|
+
nestedSubagent: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ParsedUsageEvent {
|
|
9
|
+
identity: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
order: number;
|
|
12
|
+
kind: UsageAnalysisCallKind;
|
|
13
|
+
modelKey: string;
|
|
14
|
+
requestedModelKey: string | null;
|
|
15
|
+
provider: string | null;
|
|
16
|
+
model: string | null;
|
|
17
|
+
label: string;
|
|
18
|
+
totals: UsageTotals;
|
|
19
|
+
}
|
|
20
|
+
export interface ParsedUsageMarker {
|
|
21
|
+
identity: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
order: number;
|
|
24
|
+
kind: UsageTimelineMarkerKind;
|
|
25
|
+
mode: UsageTimelineIgnoreMode | null;
|
|
26
|
+
projectedTokens: number | null;
|
|
27
|
+
tokensBefore: number | null;
|
|
28
|
+
}
|
|
29
|
+
export interface ParsedToolActivity {
|
|
30
|
+
identity: string;
|
|
31
|
+
toolCallId: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
order: number;
|
|
34
|
+
toolName: string;
|
|
35
|
+
reasoning: string | null;
|
|
36
|
+
endAt: number | null;
|
|
37
|
+
status: 'completed' | 'failed' | 'running';
|
|
38
|
+
}
|
|
39
|
+
export interface ChildSessionReference {
|
|
40
|
+
identity: string;
|
|
41
|
+
taskId: string;
|
|
42
|
+
agentType: string;
|
|
43
|
+
title: string;
|
|
44
|
+
sessionFile: string;
|
|
45
|
+
startedAt: number;
|
|
46
|
+
parentSessionId: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ChildSessionTerminal {
|
|
49
|
+
taskId: string;
|
|
50
|
+
status: 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
51
|
+
sessionFile: string;
|
|
52
|
+
endedAt: number;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ParsedUsageFile {
|
|
56
|
+
path: string;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
cwd: string;
|
|
59
|
+
name: string | null;
|
|
60
|
+
firstMessage: string | null;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
parentSessionPath: string | null;
|
|
64
|
+
events: ParsedUsageEvent[];
|
|
65
|
+
markers: ParsedUsageMarker[];
|
|
66
|
+
tools: ParsedToolActivity[];
|
|
67
|
+
childSessions: ChildSessionReference[];
|
|
68
|
+
childTerminals: ChildSessionTerminal[];
|
|
69
|
+
counts: UsageStructureCounts;
|
|
70
|
+
parseErrors: number;
|
|
71
|
+
}
|
|
72
|
+
export type UsageFileScanResult = {
|
|
73
|
+
descriptor: UsageFileDescriptor;
|
|
74
|
+
file: ParsedUsageFile;
|
|
75
|
+
error: null;
|
|
76
|
+
} | {
|
|
77
|
+
descriptor: UsageFileDescriptor;
|
|
78
|
+
file: null;
|
|
79
|
+
error: string;
|
|
80
|
+
};
|
|
81
|
+
export declare function scanUsageFile(descriptor: UsageFileDescriptor): Promise<ParsedUsageFile>;
|
|
82
|
+
export declare function scanUsageFiles(descriptors: readonly UsageFileDescriptor[], concurrency?: number): Promise<UsageFileScanResult[]>;
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
const TOOLS_SUMMARIES_KEY = 'Tools/summaries';
|
|
6
|
+
function record(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: null;
|
|
10
|
+
}
|
|
11
|
+
function text(value) {
|
|
12
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
13
|
+
}
|
|
14
|
+
function number(value) {
|
|
15
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
16
|
+
}
|
|
17
|
+
function optionalNumber(value) {
|
|
18
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, value) : null;
|
|
19
|
+
}
|
|
20
|
+
function timestamp(value) {
|
|
21
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
22
|
+
return value;
|
|
23
|
+
if (typeof value !== 'string')
|
|
24
|
+
return 0;
|
|
25
|
+
const parsed = Date.parse(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
27
|
+
}
|
|
28
|
+
function usageTotals(value) {
|
|
29
|
+
const usage = record(value);
|
|
30
|
+
if (!usage)
|
|
31
|
+
return null;
|
|
32
|
+
const cost = record(usage.cost);
|
|
33
|
+
const totals = {
|
|
34
|
+
input: number(usage.input),
|
|
35
|
+
output: number(usage.output),
|
|
36
|
+
cacheRead: number(usage.cacheRead),
|
|
37
|
+
cacheWrite: number(usage.cacheWrite),
|
|
38
|
+
cost: number(cost?.total)
|
|
39
|
+
};
|
|
40
|
+
return totals.input > 0 ||
|
|
41
|
+
totals.output > 0 ||
|
|
42
|
+
totals.cacheRead > 0 ||
|
|
43
|
+
totals.cacheWrite > 0 ||
|
|
44
|
+
totals.cost > 0
|
|
45
|
+
? totals
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
function messageText(content) {
|
|
49
|
+
if (typeof content === 'string')
|
|
50
|
+
return text(content);
|
|
51
|
+
if (!Array.isArray(content))
|
|
52
|
+
return null;
|
|
53
|
+
const value = content
|
|
54
|
+
.flatMap((part) => {
|
|
55
|
+
const item = record(part);
|
|
56
|
+
return item?.type === 'text' && typeof item.text === 'string' ? [item.text] : [];
|
|
57
|
+
})
|
|
58
|
+
.join(' ')
|
|
59
|
+
.replace(/\s+/g, ' ')
|
|
60
|
+
.trim();
|
|
61
|
+
return value ? value.slice(0, 120) : null;
|
|
62
|
+
}
|
|
63
|
+
function eventIdentity(entry, line) {
|
|
64
|
+
const digest = createHash('sha256').update(line).digest('hex').slice(0, 24);
|
|
65
|
+
return typeof entry.id === 'string' ? `${entry.id}:${digest}` : digest;
|
|
66
|
+
}
|
|
67
|
+
function usageEvent(entry, line, order) {
|
|
68
|
+
const entryType = entry.type;
|
|
69
|
+
if (entryType === 'message') {
|
|
70
|
+
const message = record(entry.message);
|
|
71
|
+
if (!message)
|
|
72
|
+
return null;
|
|
73
|
+
if (message.role === 'assistant') {
|
|
74
|
+
const totals = usageTotals(message.usage);
|
|
75
|
+
if (!totals)
|
|
76
|
+
return null;
|
|
77
|
+
const provider = text(message.provider) ?? 'unknown';
|
|
78
|
+
const requestedModel = text(message.model) ?? 'unknown';
|
|
79
|
+
const model = text(message.responseModel) ?? requestedModel;
|
|
80
|
+
const key = `${provider}/${model}`;
|
|
81
|
+
return {
|
|
82
|
+
identity: eventIdentity(entry, line),
|
|
83
|
+
timestamp: timestamp(entry.timestamp) || timestamp(message.timestamp),
|
|
84
|
+
order,
|
|
85
|
+
kind: 'assistant',
|
|
86
|
+
modelKey: key,
|
|
87
|
+
requestedModelKey: `${provider}/${requestedModel}`,
|
|
88
|
+
provider,
|
|
89
|
+
model,
|
|
90
|
+
label: key,
|
|
91
|
+
totals
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (message.role === 'toolResult') {
|
|
95
|
+
const totals = usageTotals(message.usage);
|
|
96
|
+
if (!totals)
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
identity: eventIdentity(entry, line),
|
|
100
|
+
timestamp: timestamp(entry.timestamp) || timestamp(message.timestamp),
|
|
101
|
+
order,
|
|
102
|
+
kind: 'tool-result',
|
|
103
|
+
modelKey: TOOLS_SUMMARIES_KEY,
|
|
104
|
+
requestedModelKey: null,
|
|
105
|
+
provider: null,
|
|
106
|
+
model: null,
|
|
107
|
+
label: '工具与摘要',
|
|
108
|
+
totals
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (entryType === 'usage') {
|
|
114
|
+
const totals = usageTotals(entry.usage);
|
|
115
|
+
if (!totals)
|
|
116
|
+
return null;
|
|
117
|
+
const provider = text(entry.provider) ?? 'unknown';
|
|
118
|
+
const model = text(entry.model) ?? 'unknown';
|
|
119
|
+
const key = `${provider}/${model}`;
|
|
120
|
+
return {
|
|
121
|
+
identity: eventIdentity(entry, line),
|
|
122
|
+
timestamp: timestamp(entry.timestamp),
|
|
123
|
+
order,
|
|
124
|
+
kind: entry.kind === 'cache_warm' ? 'cache-warm' : 'usage',
|
|
125
|
+
modelKey: key,
|
|
126
|
+
requestedModelKey: null,
|
|
127
|
+
provider,
|
|
128
|
+
model,
|
|
129
|
+
label: key,
|
|
130
|
+
totals
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (entryType !== 'compaction' && entryType !== 'branch_summary')
|
|
134
|
+
return null;
|
|
135
|
+
const totals = usageTotals(entry.usage);
|
|
136
|
+
if (!totals)
|
|
137
|
+
return null;
|
|
138
|
+
return {
|
|
139
|
+
identity: eventIdentity(entry, line),
|
|
140
|
+
timestamp: timestamp(entry.timestamp),
|
|
141
|
+
order,
|
|
142
|
+
kind: entryType === 'compaction' ? 'compaction' : 'branch-summary',
|
|
143
|
+
modelKey: TOOLS_SUMMARIES_KEY,
|
|
144
|
+
requestedModelKey: null,
|
|
145
|
+
provider: null,
|
|
146
|
+
model: null,
|
|
147
|
+
label: '工具与摘要',
|
|
148
|
+
totals
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function toolActivities(entry, line, order) {
|
|
152
|
+
if (entry.type !== 'message')
|
|
153
|
+
return [];
|
|
154
|
+
const message = record(entry.message);
|
|
155
|
+
if (!message || message.role !== 'assistant' || !Array.isArray(message.content))
|
|
156
|
+
return [];
|
|
157
|
+
const timestampValue = timestamp(entry.timestamp) || timestamp(message.timestamp);
|
|
158
|
+
if (!timestampValue)
|
|
159
|
+
return [];
|
|
160
|
+
return message.content.flatMap((value, index) => {
|
|
161
|
+
const block = record(value);
|
|
162
|
+
if (!block || block.type !== 'toolCall')
|
|
163
|
+
return [];
|
|
164
|
+
const toolCallId = text(block.id);
|
|
165
|
+
const toolName = text(block.name);
|
|
166
|
+
if (!toolCallId || !toolName)
|
|
167
|
+
return [];
|
|
168
|
+
const argumentsValue = record(block.arguments);
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
identity: `${eventIdentity(entry, line)}:${index}`,
|
|
172
|
+
toolCallId,
|
|
173
|
+
timestamp: timestampValue,
|
|
174
|
+
order,
|
|
175
|
+
toolName,
|
|
176
|
+
reasoning: text(argumentsValue?.reasoning),
|
|
177
|
+
endAt: null,
|
|
178
|
+
status: 'running'
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function childReference(entry, line, parentSessionId) {
|
|
184
|
+
let details = null;
|
|
185
|
+
if (entry.type === 'message') {
|
|
186
|
+
const message = record(entry.message);
|
|
187
|
+
if (message?.role === 'toolResult' &&
|
|
188
|
+
(message.toolName === 'agent' || message.toolName === 'agent_resume')) {
|
|
189
|
+
details = record(message.details);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (entry.type === 'custom' &&
|
|
193
|
+
(entry.customType === 'pi-desk-subagent:launch:v1' ||
|
|
194
|
+
entry.customType === 'pi-super-subagent:launch:v1')) {
|
|
195
|
+
details = record(entry.data);
|
|
196
|
+
}
|
|
197
|
+
if (!details || details.version !== 1 || details.kind !== 'launch')
|
|
198
|
+
return null;
|
|
199
|
+
const taskId = text(details.taskId);
|
|
200
|
+
const agentType = text(details.agentType);
|
|
201
|
+
const title = text(details.title);
|
|
202
|
+
const sessionFile = text(details.sessionFile);
|
|
203
|
+
if (!taskId || !agentType || !title || !sessionFile)
|
|
204
|
+
return null;
|
|
205
|
+
return {
|
|
206
|
+
identity: eventIdentity(entry, line),
|
|
207
|
+
taskId,
|
|
208
|
+
agentType,
|
|
209
|
+
title,
|
|
210
|
+
sessionFile: resolve(sessionFile),
|
|
211
|
+
startedAt: timestamp(details.startedAt),
|
|
212
|
+
parentSessionId
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function childTerminal(entry) {
|
|
216
|
+
const details = entry.type === 'custom_message' &&
|
|
217
|
+
(entry.customType === 'pi-desk-subagent:completion:v1' ||
|
|
218
|
+
entry.customType === 'pi-super-subagent:completion:v1')
|
|
219
|
+
? record(entry.details)
|
|
220
|
+
: entry.type === 'custom' &&
|
|
221
|
+
(entry.customType === 'pi-desk-subagent:state:v1' ||
|
|
222
|
+
entry.customType === 'pi-super-subagent:state:v1')
|
|
223
|
+
? record(entry.data)
|
|
224
|
+
: null;
|
|
225
|
+
if (!details || details.version !== 1 || details.kind !== 'terminal')
|
|
226
|
+
return null;
|
|
227
|
+
const taskId = text(details.taskId);
|
|
228
|
+
const sessionFile = text(details.sessionFile);
|
|
229
|
+
const status = details.status;
|
|
230
|
+
if (!taskId ||
|
|
231
|
+
!sessionFile ||
|
|
232
|
+
(status !== 'completed' &&
|
|
233
|
+
status !== 'failed' &&
|
|
234
|
+
status !== 'stopped' &&
|
|
235
|
+
status !== 'interrupted')) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
taskId,
|
|
240
|
+
status,
|
|
241
|
+
sessionFile: resolve(sessionFile),
|
|
242
|
+
endedAt: timestamp(details.endedAt),
|
|
243
|
+
timestamp: timestamp(entry.timestamp)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function parseIgnoreBoundary(value) {
|
|
247
|
+
const state = record(value);
|
|
248
|
+
if (!state || typeof state.enabled !== 'boolean')
|
|
249
|
+
return null;
|
|
250
|
+
const mode = state.mode === 'internal-turns' || state.mode === 'combined' ? state.mode : 'user-turns';
|
|
251
|
+
const cutoffTimestamp = optionalNumber(state.cutoffTimestamp);
|
|
252
|
+
const internalTurnCutoffs = Array.isArray(state.internalTurnCutoffs)
|
|
253
|
+
? state.internalTurnCutoffs.flatMap((value) => {
|
|
254
|
+
const cutoff = record(value);
|
|
255
|
+
const cutoffTimestamp = optionalNumber(cutoff?.cutoffTimestamp);
|
|
256
|
+
if (cutoffTimestamp === null)
|
|
257
|
+
return [];
|
|
258
|
+
return [
|
|
259
|
+
{
|
|
260
|
+
cutoffTimestamp,
|
|
261
|
+
userTimestamp: optionalNumber(cutoff?.userTimestamp)
|
|
262
|
+
}
|
|
263
|
+
];
|
|
264
|
+
})
|
|
265
|
+
: [];
|
|
266
|
+
return {
|
|
267
|
+
fingerprint: JSON.stringify({
|
|
268
|
+
enabled: state.enabled,
|
|
269
|
+
mode,
|
|
270
|
+
cutoffTimestamp,
|
|
271
|
+
internalTurnCutoffs
|
|
272
|
+
}),
|
|
273
|
+
enabled: state.enabled,
|
|
274
|
+
mode,
|
|
275
|
+
projectedTokens: optionalNumber(state.estimatedContextTokens)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function emptyCounts() {
|
|
279
|
+
return {
|
|
280
|
+
userMessages: 0,
|
|
281
|
+
assistantMessages: 0,
|
|
282
|
+
toolCalls: 0,
|
|
283
|
+
toolResults: 0,
|
|
284
|
+
compactions: 0,
|
|
285
|
+
branchSummaries: 0
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export async function scanUsageFile(descriptor) {
|
|
289
|
+
const input = createReadStream(descriptor.path, { encoding: 'utf8' });
|
|
290
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
291
|
+
let sessionId = '';
|
|
292
|
+
let cwd = '';
|
|
293
|
+
let name = null;
|
|
294
|
+
let firstMessage = null;
|
|
295
|
+
let createdAt = 0;
|
|
296
|
+
let updatedAt = descriptor.modifiedAt;
|
|
297
|
+
let parentSessionPath = null;
|
|
298
|
+
let parseErrors = 0;
|
|
299
|
+
const events = [];
|
|
300
|
+
const markers = [];
|
|
301
|
+
const tools = [];
|
|
302
|
+
const toolsByCallId = new Map();
|
|
303
|
+
const childSessions = [];
|
|
304
|
+
const childTerminals = [];
|
|
305
|
+
const counts = emptyCounts();
|
|
306
|
+
let entryOrder = 0;
|
|
307
|
+
let previousIgnoreBoundary = null;
|
|
308
|
+
for await (const rawLine of lines) {
|
|
309
|
+
const line = rawLine.trim();
|
|
310
|
+
if (!line)
|
|
311
|
+
continue;
|
|
312
|
+
let entry;
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(line);
|
|
315
|
+
const parsedRecord = record(parsed);
|
|
316
|
+
if (!parsedRecord) {
|
|
317
|
+
parseErrors += 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
entry = parsedRecord;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
parseErrors += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
entryOrder += 1;
|
|
327
|
+
if (entry.type === 'session') {
|
|
328
|
+
sessionId = text(entry.id) ?? sessionId;
|
|
329
|
+
cwd = text(entry.cwd) ?? cwd;
|
|
330
|
+
createdAt = timestamp(entry.timestamp) || createdAt;
|
|
331
|
+
const parentSession = text(entry.parentSession);
|
|
332
|
+
parentSessionPath = parentSession ? resolve(parentSession) : null;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const entryAt = timestamp(entry.timestamp);
|
|
336
|
+
if (entryAt > updatedAt)
|
|
337
|
+
updatedAt = entryAt;
|
|
338
|
+
if (entry.type === 'session_info')
|
|
339
|
+
name = text(entry.name);
|
|
340
|
+
if (entry.type === 'message') {
|
|
341
|
+
const message = record(entry.message);
|
|
342
|
+
if (message?.role === 'user') {
|
|
343
|
+
counts.userMessages += 1;
|
|
344
|
+
if (!firstMessage)
|
|
345
|
+
firstMessage = messageText(message.content);
|
|
346
|
+
}
|
|
347
|
+
else if (message?.role === 'assistant') {
|
|
348
|
+
counts.assistantMessages += 1;
|
|
349
|
+
if (Array.isArray(message.content)) {
|
|
350
|
+
counts.toolCalls += message.content.filter((part) => record(part)?.type === 'toolCall').length;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (message?.role === 'toolResult') {
|
|
354
|
+
counts.toolResults += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (entry.type === 'compaction') {
|
|
358
|
+
counts.compactions += 1;
|
|
359
|
+
}
|
|
360
|
+
else if (entry.type === 'branch_summary') {
|
|
361
|
+
counts.branchSummaries += 1;
|
|
362
|
+
}
|
|
363
|
+
const event = usageEvent(entry, line, entryOrder);
|
|
364
|
+
if (event && event.timestamp > 0)
|
|
365
|
+
events.push(event);
|
|
366
|
+
for (const tool of toolActivities(entry, line, entryOrder)) {
|
|
367
|
+
tools.push(tool);
|
|
368
|
+
toolsByCallId.set(tool.toolCallId, tool);
|
|
369
|
+
}
|
|
370
|
+
if (entry.type === 'message') {
|
|
371
|
+
const message = record(entry.message);
|
|
372
|
+
if (message?.role === 'toolResult') {
|
|
373
|
+
const toolCallId = text(message.toolCallId);
|
|
374
|
+
const tool = toolCallId ? toolsByCallId.get(toolCallId) : undefined;
|
|
375
|
+
if (tool) {
|
|
376
|
+
tool.endAt = timestamp(entry.timestamp) || timestamp(message.timestamp) || null;
|
|
377
|
+
tool.status = message.isError === true ? 'failed' : 'completed';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (entry.type === 'compaction' && entryAt > 0) {
|
|
382
|
+
markers.push({
|
|
383
|
+
identity: eventIdentity(entry, line),
|
|
384
|
+
timestamp: entryAt,
|
|
385
|
+
order: entryOrder,
|
|
386
|
+
kind: 'compaction',
|
|
387
|
+
mode: null,
|
|
388
|
+
projectedTokens: null,
|
|
389
|
+
tokensBefore: optionalNumber(entry.tokensBefore)
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
else if (entry.type === 'custom' && entry.customType === 'context-ignore-state') {
|
|
393
|
+
const boundary = parseIgnoreBoundary(entry.data);
|
|
394
|
+
if (boundary) {
|
|
395
|
+
const changed = boundary.fingerprint !== previousIgnoreBoundary;
|
|
396
|
+
previousIgnoreBoundary = boundary.fingerprint;
|
|
397
|
+
if (boundary.enabled && changed && entryAt > 0) {
|
|
398
|
+
markers.push({
|
|
399
|
+
identity: eventIdentity(entry, line),
|
|
400
|
+
timestamp: entryAt,
|
|
401
|
+
order: entryOrder,
|
|
402
|
+
kind: 'context-ignore',
|
|
403
|
+
mode: boundary.mode,
|
|
404
|
+
projectedTokens: boundary.projectedTokens,
|
|
405
|
+
tokensBefore: null
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const child = childReference(entry, line, sessionId);
|
|
411
|
+
if (child)
|
|
412
|
+
childSessions.push(child);
|
|
413
|
+
const terminal = childTerminal(entry);
|
|
414
|
+
if (terminal)
|
|
415
|
+
childTerminals.push(terminal);
|
|
416
|
+
}
|
|
417
|
+
if (!sessionId)
|
|
418
|
+
throw new Error('Session header 缺少 id');
|
|
419
|
+
if (!cwd)
|
|
420
|
+
throw new Error('Session header 缺少 cwd');
|
|
421
|
+
if (!createdAt)
|
|
422
|
+
createdAt = events[0]?.timestamp ?? descriptor.modifiedAt;
|
|
423
|
+
return {
|
|
424
|
+
path: resolve(descriptor.path),
|
|
425
|
+
sessionId,
|
|
426
|
+
cwd,
|
|
427
|
+
name,
|
|
428
|
+
firstMessage,
|
|
429
|
+
createdAt,
|
|
430
|
+
updatedAt,
|
|
431
|
+
parentSessionPath,
|
|
432
|
+
events,
|
|
433
|
+
markers,
|
|
434
|
+
tools,
|
|
435
|
+
childSessions,
|
|
436
|
+
childTerminals,
|
|
437
|
+
counts,
|
|
438
|
+
parseErrors
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
export async function scanUsageFiles(descriptors, concurrency = 2) {
|
|
442
|
+
const results = new Array(descriptors.length);
|
|
443
|
+
let nextIndex = 0;
|
|
444
|
+
async function run() {
|
|
445
|
+
while (nextIndex < descriptors.length) {
|
|
446
|
+
const index = nextIndex;
|
|
447
|
+
nextIndex += 1;
|
|
448
|
+
const descriptor = descriptors[index];
|
|
449
|
+
try {
|
|
450
|
+
results[index] = {
|
|
451
|
+
descriptor,
|
|
452
|
+
file: await scanUsageFile(descriptor),
|
|
453
|
+
error: null
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
results[index] = {
|
|
458
|
+
descriptor,
|
|
459
|
+
file: null,
|
|
460
|
+
error: error instanceof Error ? error.message : String(error)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, concurrency), descriptors.length) }, () => run()));
|
|
466
|
+
return results;
|
|
467
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UsageActivity, UsageSessionTimelineSnapshot, UsageTimelineRun } from './protocol.js';
|
|
2
|
+
import type { ParsedToolActivity, ParsedUsageEvent, ParsedUsageMarker } from './usage-scan.js';
|
|
3
|
+
export interface UsageTimelineSourceInput {
|
|
4
|
+
key: string;
|
|
5
|
+
kind: 'main' | 'subagent';
|
|
6
|
+
title: string;
|
|
7
|
+
agentType: string | null;
|
|
8
|
+
updatedAt: number;
|
|
9
|
+
runs: UsageTimelineRun[];
|
|
10
|
+
events: ParsedUsageEvent[];
|
|
11
|
+
markers: ParsedUsageMarker[];
|
|
12
|
+
tools: ParsedToolActivity[];
|
|
13
|
+
}
|
|
14
|
+
export declare function createUsageActivities(inputs: readonly UsageTimelineSourceInput[], startAt: number, endAt: number): UsageActivity[];
|
|
15
|
+
export declare function createUsageTimeline(inputs: readonly UsageTimelineSourceInput[], requestedStartAt?: number, requestedEndAt?: number, timeZone?: string): UsageSessionTimelineSnapshot;
|