@agent-finops/core 0.5.8 → 0.6.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/README.md +6 -0
- package/dist/agentInventory.d.ts +11 -14
- package/dist/agentInventory.js +180 -46
- package/dist/analyze.js +165 -113
- package/dist/contextHealth.d.ts +10 -1
- package/dist/contextHealth.js +216 -44
- package/dist/cutList.d.ts +9 -3
- package/dist/cutList.js +99 -41
- package/dist/deadContext.d.ts +8 -4
- package/dist/deadContext.js +83 -26
- package/dist/discovery.d.ts +6 -2
- package/dist/discovery.js +36 -14
- package/dist/glance.d.ts +9 -2
- package/dist/glance.js +107 -24
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/insights.js +53 -30
- package/dist/localAgentLogs.d.ts +80 -2
- package/dist/localAgentLogs.js +480 -88
- package/dist/modelPricing.js +0 -1
- package/dist/providerConnectors.d.ts +3 -2
- package/dist/providerConnectors.js +673 -89
- package/dist/sampleData.js +32 -4
- package/dist/schema.d.ts +105 -27
- package/dist/schema.js +110 -2
- package/dist/sourceRegistry.d.ts +30 -5
- package/dist/sourceRegistry.js +250 -21
- package/dist/sourceStatus.d.ts +65 -0
- package/dist/sourceStatus.js +147 -0
- package/dist/stateTrust.d.ts +37 -0
- package/dist/stateTrust.js +277 -0
- package/dist/toolInvocations.d.ts +47 -18
- package/dist/toolInvocations.js +200 -48
- package/package.json +1 -1
- package/samples/anthropic-usage.csv +4 -4
- package/samples/openai-usage.csv +7 -7
package/dist/localAgentLogs.js
CHANGED
|
@@ -3,17 +3,67 @@ import { basename, isAbsolute, join, resolve, sep } from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { estimateTokenCostUsd } from "./modelPricing.js";
|
|
5
5
|
import { redactSecrets } from "./discovery.js";
|
|
6
|
+
import { createCodexInvocationCollector } from "./toolInvocations.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the repository root most recently observed in transcript metadata.
|
|
9
|
+
*
|
|
10
|
+
* CLI, MCP, and Glance use this only to scope read-only project inventory when
|
|
11
|
+
* the caller did not explicitly choose a path. Absolute working directories
|
|
12
|
+
* never enter rendered output.
|
|
13
|
+
*/
|
|
14
|
+
export function latestObservedWorkingDirectory(calls) {
|
|
15
|
+
return calls
|
|
16
|
+
.filter((call) => call.workingDirectory)
|
|
17
|
+
.sort((left, right) => right.timestamp.localeCompare(left.timestamp))[0]
|
|
18
|
+
?.workingDirectory;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Codex rollout/compaction files can repeat the same session's cumulative
|
|
22
|
+
* token counter. Keep only the latest snapshot per session so financial value,
|
|
23
|
+
* Glance, and project totals never add cumulative checkpoints together.
|
|
24
|
+
* Turn-scoped Claude calls and calls without a stable session id are retained.
|
|
25
|
+
*/
|
|
26
|
+
export function dedupeCumulativeSessionCalls(calls) {
|
|
27
|
+
const retained = [];
|
|
28
|
+
const cumulative = new Map();
|
|
29
|
+
for (const call of calls) {
|
|
30
|
+
if (call.usageScope !== "session_cumulative" || !call.sessionId) {
|
|
31
|
+
retained.push(call);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const key = `${call.agent}:${call.sessionId}`;
|
|
35
|
+
const prior = cumulative.get(key);
|
|
36
|
+
if (!prior || isLaterCumulativeSnapshot(call, prior)) {
|
|
37
|
+
cumulative.set(key, call);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...retained, ...cumulative.values()];
|
|
41
|
+
}
|
|
42
|
+
function isLaterCumulativeSnapshot(candidate, prior) {
|
|
43
|
+
const timestampOrder = candidate.timestamp.localeCompare(prior.timestamp);
|
|
44
|
+
if (timestampOrder !== 0)
|
|
45
|
+
return timestampOrder > 0;
|
|
46
|
+
return totalUsageTokens(candidate.usage) > totalUsageTokens(prior.usage);
|
|
47
|
+
}
|
|
48
|
+
function totalUsageTokens(usage) {
|
|
49
|
+
return usage.inputTokens +
|
|
50
|
+
usage.outputTokens +
|
|
51
|
+
(usage.cacheReadTokens ?? 0) +
|
|
52
|
+
(usage.cacheWrite5mTokens ?? 0) +
|
|
53
|
+
(usage.cacheWrite1hTokens ?? 0);
|
|
54
|
+
}
|
|
6
55
|
/** Parse one Claude Code transcript (JSONL). Exported for tests. */
|
|
7
|
-
export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
56
|
+
export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDiagnostic) {
|
|
8
57
|
const calls = [];
|
|
9
58
|
const seen = new Set();
|
|
10
|
-
const
|
|
11
|
-
const
|
|
59
|
+
const pendingPrompts = [];
|
|
60
|
+
const activityEvidence = new Map();
|
|
12
61
|
let title;
|
|
13
62
|
let lastPrompt;
|
|
14
|
-
let
|
|
63
|
+
let latestActivityKey;
|
|
15
64
|
let isSubagent = filePath.split(sep).includes("subagents");
|
|
16
65
|
let parentSessionId;
|
|
66
|
+
let malformedLines = 0;
|
|
17
67
|
for (const line of content.split("\n")) {
|
|
18
68
|
if (!line.trim())
|
|
19
69
|
continue;
|
|
@@ -22,6 +72,7 @@ export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
|
22
72
|
entry = JSON.parse(line);
|
|
23
73
|
}
|
|
24
74
|
catch {
|
|
75
|
+
malformedLines += 1;
|
|
25
76
|
continue;
|
|
26
77
|
}
|
|
27
78
|
if (!isRecord(entry))
|
|
@@ -39,15 +90,7 @@ export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
|
39
90
|
if (entry.type === "user" && message) {
|
|
40
91
|
for (const prompt of textValues(message.content)) {
|
|
41
92
|
if (isHumanPrompt(prompt))
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (entry.type === "assistant" && message) {
|
|
46
|
-
for (const item of recordValues(message.content)) {
|
|
47
|
-
if (item.type !== "tool_use")
|
|
48
|
-
continue;
|
|
49
|
-
toolCallCount += 1;
|
|
50
|
-
collectToolFiles(item.input, fileCounts);
|
|
93
|
+
pendingPrompts.push(prompt);
|
|
51
94
|
}
|
|
52
95
|
}
|
|
53
96
|
if (entry.type !== "assistant")
|
|
@@ -63,57 +106,107 @@ export function parseClaudeCodeTranscript(content, filePath = "") {
|
|
|
63
106
|
if (dedupeKey !== ":" && seen.has(dedupeKey))
|
|
64
107
|
continue;
|
|
65
108
|
seen.add(dedupeKey);
|
|
109
|
+
const timestamp = toIso(stringOf(entry.timestamp)) ?? new Date(0).toISOString();
|
|
110
|
+
if (typeof sinceMs === "number" && Date.parse(timestamp) < sinceMs) {
|
|
111
|
+
// These prompts led to a call outside the selected evidence window. Do
|
|
112
|
+
// not let them become the focus of a later in-window project/call.
|
|
113
|
+
pendingPrompts.length = 0;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
66
116
|
const cacheCreation = isRecord(usage.cache_creation) ? usage.cache_creation : undefined;
|
|
67
117
|
const write5m = numberOf(cacheCreation?.ephemeral_5m_input_tokens);
|
|
68
118
|
const write1h = numberOf(cacheCreation?.ephemeral_1h_input_tokens);
|
|
69
119
|
const writeTotal = numberOf(usage.cache_creation_input_tokens) ?? 0;
|
|
70
|
-
|
|
120
|
+
const parsedUsage = {
|
|
121
|
+
inputTokens: numberOf(usage.input_tokens) ?? 0,
|
|
122
|
+
outputTokens: numberOf(usage.output_tokens) ?? 0,
|
|
123
|
+
cacheReadTokens: numberOf(usage.cache_read_input_tokens) ?? 0,
|
|
124
|
+
// Prefer the 5m/1h breakdown; fall back to the total as 5m (cheaper bound).
|
|
125
|
+
cacheWrite5mTokens: write5m ?? writeTotal,
|
|
126
|
+
cacheWrite1hTokens: write1h ?? 0
|
|
127
|
+
};
|
|
128
|
+
const workingDirectory = absoluteWorkingDirectory(stringOf(entry.cwd));
|
|
129
|
+
const project = projectFromCwd(workingDirectory) ?? projectFromTranscriptPath(filePath);
|
|
130
|
+
const sessionId = stringOf(entry.sessionId);
|
|
131
|
+
const call = {
|
|
71
132
|
agent: "claude-code",
|
|
72
133
|
model: stringOf(message.model) ?? "claude-code",
|
|
73
|
-
timestamp
|
|
74
|
-
project
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
timestamp,
|
|
135
|
+
project,
|
|
136
|
+
workingDirectory,
|
|
137
|
+
sessionId,
|
|
138
|
+
latestTurnUsage: toTurnUsage(parsedUsage, "assistant_message_usage"),
|
|
139
|
+
usageScope: "turn",
|
|
140
|
+
usage: parsedUsage
|
|
141
|
+
};
|
|
142
|
+
calls.push(call);
|
|
143
|
+
const activityKey = localActivityScopeKey(sessionId, workingDirectory, project);
|
|
144
|
+
const evidence = activityEvidence.get(activityKey) ?? {
|
|
145
|
+
prompts: [],
|
|
146
|
+
files: new Map(),
|
|
147
|
+
toolCallCount: 0
|
|
148
|
+
};
|
|
149
|
+
evidence.prompts.push(...pendingPrompts);
|
|
150
|
+
pendingPrompts.length = 0;
|
|
151
|
+
for (const item of recordValues(message.content)) {
|
|
152
|
+
if (item.type !== "tool_use")
|
|
153
|
+
continue;
|
|
154
|
+
evidence.toolCallCount += 1;
|
|
155
|
+
collectToolFiles(item.input, evidence.files);
|
|
156
|
+
}
|
|
157
|
+
activityEvidence.set(activityKey, evidence);
|
|
158
|
+
latestActivityKey = activityKey;
|
|
85
159
|
}
|
|
86
160
|
const fallbackPrompt = lastPrompt && isHumanPrompt(lastPrompt) ? lastPrompt : undefined;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
161
|
+
const activities = new Map();
|
|
162
|
+
for (const [activityKey, evidence] of activityEvidence) {
|
|
163
|
+
const matchingCall = calls.find((call) => (localActivityScopeKey(call.sessionId, call.workingDirectory, call.project) === activityKey));
|
|
164
|
+
const isLatest = activityKey === latestActivityKey;
|
|
165
|
+
const activity = buildLocalAgentActivity({
|
|
166
|
+
title: isLatest ? title : undefined,
|
|
167
|
+
prompts: evidence.prompts.length > 0
|
|
168
|
+
? evidence.prompts
|
|
169
|
+
: isLatest && fallbackPrompt
|
|
170
|
+
? [fallbackPrompt]
|
|
171
|
+
: [],
|
|
172
|
+
files: evidence.files,
|
|
173
|
+
toolCallCount: evidence.toolCallCount,
|
|
174
|
+
project: matchingCall?.project ?? projectFromTranscriptPath(filePath),
|
|
175
|
+
isSubagent,
|
|
176
|
+
parentSessionId
|
|
177
|
+
});
|
|
178
|
+
if (activity)
|
|
179
|
+
activities.set(activityKey, activity);
|
|
180
|
+
}
|
|
181
|
+
for (const call of calls) {
|
|
182
|
+
call.activity = activities.get(localActivityScopeKey(call.sessionId, call.workingDirectory, call.project));
|
|
183
|
+
}
|
|
184
|
+
if (malformedLines > 0) {
|
|
185
|
+
onDiagnostic?.({ code: "malformed_jsonl", count: malformedLines });
|
|
99
186
|
}
|
|
100
187
|
return calls;
|
|
101
188
|
}
|
|
102
189
|
/** Parse one Codex rollout file (JSONL event stream). Exported for tests. */
|
|
103
|
-
export function parseCodexRollout(content) {
|
|
190
|
+
export function parseCodexRollout(content, onEntry, onDiagnostic) {
|
|
104
191
|
let model;
|
|
105
|
-
let
|
|
192
|
+
let rootCwd;
|
|
106
193
|
const toolWorkdirs = new Map();
|
|
107
194
|
let sessionId;
|
|
195
|
+
let rootSessionMetaSeen = false;
|
|
108
196
|
let startedAt;
|
|
197
|
+
let rootStartedAtMs;
|
|
198
|
+
let rootTaskStarted = false;
|
|
199
|
+
let inheritedUsageBaseline;
|
|
109
200
|
let lastActivityAt;
|
|
110
201
|
let lastTotal;
|
|
202
|
+
let lastTurn;
|
|
111
203
|
let lastRateLimits;
|
|
112
204
|
const prompts = [];
|
|
113
205
|
const fileCounts = new Map();
|
|
114
206
|
let toolCallCount = 0;
|
|
115
207
|
let isSubagent = false;
|
|
116
208
|
let parentSessionId;
|
|
209
|
+
let malformedLines = 0;
|
|
117
210
|
for (const line of content.split("\n")) {
|
|
118
211
|
if (!line.trim())
|
|
119
212
|
continue;
|
|
@@ -122,21 +215,50 @@ export function parseCodexRollout(content) {
|
|
|
122
215
|
entry = JSON.parse(line);
|
|
123
216
|
}
|
|
124
217
|
catch {
|
|
218
|
+
malformedLines += 1;
|
|
125
219
|
continue;
|
|
126
220
|
}
|
|
127
221
|
if (!isRecord(entry))
|
|
128
222
|
continue;
|
|
223
|
+
onEntry?.(entry);
|
|
129
224
|
const payload = isRecord(entry.payload) ? entry.payload : undefined;
|
|
130
|
-
if (entry.type === "session_meta" && payload) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
225
|
+
if (entry.type === "session_meta" && payload && !rootSessionMetaSeen) {
|
|
226
|
+
// A forked/subagent rollout can embed the parent transcript, including
|
|
227
|
+
// many later session_meta records. The first metadata record belongs to
|
|
228
|
+
// this rollout file; later records are nested/history evidence and must
|
|
229
|
+
// never replace the financial session identity or root cwd.
|
|
230
|
+
rootSessionMetaSeen = true;
|
|
231
|
+
sessionId = stringOf(payload.id);
|
|
232
|
+
rootCwd = stringOf(payload.cwd);
|
|
233
|
+
startedAt = toIso(stringOf(payload.timestamp) ?? stringOf(entry.timestamp));
|
|
234
|
+
rootStartedAtMs = timestampMilliseconds(payload.timestamp ?? entry.timestamp);
|
|
134
235
|
isSubagent = stringOf(payload.thread_source) === "subagent" || isRecord(payload.source) && "subagent" in payload.source;
|
|
135
|
-
parentSessionId = stringOf(payload.parent_thread_id)
|
|
236
|
+
parentSessionId = stringOf(payload.parent_thread_id);
|
|
136
237
|
}
|
|
137
238
|
if (entry.type === "turn_context" && payload) {
|
|
138
239
|
model = stringOf(payload.model) ?? model;
|
|
139
|
-
|
|
240
|
+
rootCwd ??= stringOf(payload.cwd);
|
|
241
|
+
}
|
|
242
|
+
if (isSubagent &&
|
|
243
|
+
!rootTaskStarted &&
|
|
244
|
+
payload?.type === "task_started" &&
|
|
245
|
+
isRootSpecificTaskStart(payload.started_at, rootStartedAtMs)) {
|
|
246
|
+
// Forked Codex rollouts copy the parent's complete event history after
|
|
247
|
+
// the child's root session_meta. The cumulative counter immediately
|
|
248
|
+
// before the child's first task is the inherited baseline, not child
|
|
249
|
+
// usage. Reset qualitative evidence at the same boundary so parent
|
|
250
|
+
// prompts/files cannot become the child's focus.
|
|
251
|
+
inheritedUsageBaseline = lastTotal;
|
|
252
|
+
lastTotal = undefined;
|
|
253
|
+
rootTaskStarted = true;
|
|
254
|
+
prompts.length = 0;
|
|
255
|
+
fileCounts.clear();
|
|
256
|
+
toolWorkdirs.clear();
|
|
257
|
+
toolCallCount = 0;
|
|
258
|
+
model = undefined;
|
|
259
|
+
lastTurn = undefined;
|
|
260
|
+
lastRateLimits = undefined;
|
|
261
|
+
lastActivityAt = toIso(stringOf(entry.timestamp)) ?? startedAt;
|
|
140
262
|
}
|
|
141
263
|
if (payload?.type === "function_call" || payload?.type === "custom_tool_call") {
|
|
142
264
|
toolCallCount += 1;
|
|
@@ -165,21 +287,58 @@ export function parseCodexRollout(content) {
|
|
|
165
287
|
const eventTimestamp = toIso(stringOf(entry.timestamp)) ?? lastActivityAt ?? startedAt;
|
|
166
288
|
const info = isRecord(payload.info) ? payload.info : undefined;
|
|
167
289
|
const total = info && isRecord(info.total_token_usage) ? info.total_token_usage : undefined;
|
|
290
|
+
const turn = info && isRecord(info.last_token_usage) ? info.last_token_usage : undefined;
|
|
168
291
|
if (total) {
|
|
169
292
|
lastTotal = total;
|
|
170
293
|
lastActivityAt = eventTimestamp;
|
|
171
294
|
}
|
|
295
|
+
if (turn) {
|
|
296
|
+
lastTurn = turn;
|
|
297
|
+
lastActivityAt = eventTimestamp;
|
|
298
|
+
}
|
|
172
299
|
const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
|
|
173
300
|
if (rateLimits) {
|
|
174
301
|
lastRateLimits = rateLimits;
|
|
175
302
|
}
|
|
176
303
|
}
|
|
177
304
|
}
|
|
178
|
-
|
|
305
|
+
// A fork without a recognized root-task boundary is ambiguous: older Codex
|
|
306
|
+
// formats may contain only inherited parent history. Omitting that child is
|
|
307
|
+
// safer than charging the parent cumulative counter again. Likewise, a
|
|
308
|
+
// recognized boundary with no later total_token_usage is not a financial
|
|
309
|
+
// call yet.
|
|
310
|
+
if (malformedLines > 0) {
|
|
311
|
+
onDiagnostic?.({ code: "malformed_jsonl", count: malformedLines });
|
|
312
|
+
}
|
|
313
|
+
if (!lastTotal || isSubagent && !rootTaskStarted)
|
|
179
314
|
return [];
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
315
|
+
const rawInput = numberOf(lastTotal.input_tokens);
|
|
316
|
+
const rawOutput = numberOf(lastTotal.output_tokens);
|
|
317
|
+
const rawCached = numberOf(lastTotal.cached_input_tokens);
|
|
318
|
+
const rawReportedTotal = numberOf(lastTotal.total_tokens);
|
|
319
|
+
const baselineInput = numberOf(inheritedUsageBaseline?.input_tokens);
|
|
320
|
+
const baselineOutput = numberOf(inheritedUsageBaseline?.output_tokens);
|
|
321
|
+
const baselineReportedTotal = numberOf(inheritedUsageBaseline?.total_tokens);
|
|
322
|
+
const currentComponentsComplete = rawInput !== undefined && rawOutput !== undefined && !((rawReportedTotal ?? 0) > 0 && rawInput === 0 && rawOutput === 0);
|
|
323
|
+
const baselineComponentsComplete = !inheritedUsageBaseline || (baselineInput !== undefined && baselineOutput !== undefined && !((baselineReportedTotal ?? 0) > 0 && baselineInput === 0 && baselineOutput === 0));
|
|
324
|
+
const usageSupport = currentComponentsComplete && baselineComponentsComplete
|
|
325
|
+
? "complete"
|
|
326
|
+
: "unsupported_token_shape";
|
|
327
|
+
if (usageSupport === "unsupported_token_shape") {
|
|
328
|
+
onDiagnostic?.({ code: "unsupported_token_shape", count: 1 });
|
|
329
|
+
}
|
|
330
|
+
const input = Math.max(0, (rawInput ?? 0) - (baselineInput ?? 0));
|
|
331
|
+
const cached = Math.max(0, (rawCached ?? 0) -
|
|
332
|
+
(numberOf(inheritedUsageBaseline?.cached_input_tokens) ?? 0));
|
|
333
|
+
const output = Math.max(0, (rawOutput ?? 0) - (baselineOutput ?? 0));
|
|
334
|
+
const reportedTotalTokens = rawReportedTotal === undefined
|
|
335
|
+
? undefined
|
|
336
|
+
: Math.max(0, rawReportedTotal - (baselineReportedTotal ?? 0));
|
|
337
|
+
const latestTurnUsage = lastTurn
|
|
338
|
+
? codexTurnUsage(lastTurn)
|
|
339
|
+
: undefined;
|
|
340
|
+
const workingDirectory = absoluteWorkingDirectory(dominantCodexCwd(rootCwd, toolWorkdirs));
|
|
341
|
+
const project = projectFromCwd(workingDirectory);
|
|
183
342
|
const activity = buildLocalAgentActivity({
|
|
184
343
|
prompts,
|
|
185
344
|
files: fileCounts,
|
|
@@ -194,13 +353,18 @@ export function parseCodexRollout(content) {
|
|
|
194
353
|
timestamp: lastActivityAt ?? startedAt ?? new Date(0).toISOString(),
|
|
195
354
|
startedAt,
|
|
196
355
|
project,
|
|
356
|
+
workingDirectory,
|
|
197
357
|
sessionId,
|
|
198
358
|
rateLimits: lastRateLimits,
|
|
199
359
|
activity,
|
|
360
|
+
latestTurnUsage,
|
|
361
|
+
usageScope: "session_cumulative",
|
|
362
|
+
usageSupport,
|
|
363
|
+
...(reportedTotalTokens !== undefined ? { reportedTotalTokens } : {}),
|
|
200
364
|
usage: {
|
|
201
365
|
// Codex input_tokens INCLUDES cached tokens; split them out.
|
|
202
366
|
inputTokens: Math.max(0, input - cached),
|
|
203
|
-
outputTokens:
|
|
367
|
+
outputTokens: output,
|
|
204
368
|
cacheReadTokens: cached
|
|
205
369
|
}
|
|
206
370
|
}];
|
|
@@ -275,38 +439,78 @@ export async function loadLocalAgentUsage(options = {}) {
|
|
|
275
439
|
const claudeDir = options.claudeProjectsDir ?? join(home, ".claude", "projects");
|
|
276
440
|
const codexDir = options.codexSessionsDir ?? join(home, ".codex", "sessions");
|
|
277
441
|
const calls = [];
|
|
442
|
+
const codexInvocationFiles = options.collectCodexInvocationEvidence
|
|
443
|
+
? []
|
|
444
|
+
: undefined;
|
|
278
445
|
let filesParsed = 0;
|
|
279
|
-
|
|
280
|
-
|
|
446
|
+
const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
|
|
447
|
+
const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
|
|
448
|
+
const diagnostics = [];
|
|
449
|
+
const sourceScans = [
|
|
450
|
+
emptySourceScan("claude-code"),
|
|
451
|
+
emptySourceScan("codex")
|
|
452
|
+
];
|
|
453
|
+
const claudeScan = sourceScans[0];
|
|
454
|
+
const codexScan = sourceScans[1];
|
|
455
|
+
for (const file of await listJsonlFiles(claudeDir, claudeScan, diagnostics)) {
|
|
456
|
+
let content;
|
|
457
|
+
try {
|
|
458
|
+
content = await readFile(file, "utf8");
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
recordUnreadableFile("claude-code", claudeScan, diagnostics, error);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
281
464
|
if (!content)
|
|
282
465
|
continue;
|
|
283
466
|
filesParsed += 1;
|
|
284
|
-
|
|
467
|
+
claudeScan.filesParsed += 1;
|
|
468
|
+
calls.push(...parseClaudeCodeTranscript(content, file, sinceMs, (diagnostic) => {
|
|
469
|
+
recordParseDiagnostic("claude-code", claudeScan, diagnostics, diagnostic);
|
|
470
|
+
}));
|
|
285
471
|
}
|
|
286
|
-
for (const file of await listJsonlFiles(codexDir)) {
|
|
472
|
+
for (const file of await listJsonlFiles(codexDir, codexScan, diagnostics)) {
|
|
287
473
|
if (!basename(file).startsWith("rollout-"))
|
|
288
474
|
continue;
|
|
289
|
-
|
|
475
|
+
let content;
|
|
476
|
+
try {
|
|
477
|
+
content = await readFile(file, "utf8");
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
recordUnreadableFile("codex", codexScan, diagnostics, error);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
290
483
|
if (!content)
|
|
291
484
|
continue;
|
|
292
485
|
filesParsed += 1;
|
|
293
|
-
|
|
486
|
+
codexScan.filesParsed += 1;
|
|
487
|
+
const collector = codexInvocationFiles
|
|
488
|
+
? createCodexInvocationCollector(sinceMs)
|
|
489
|
+
: undefined;
|
|
490
|
+
calls.push(...parseCodexRollout(content, collector?.consume, (diagnostic) => {
|
|
491
|
+
recordParseDiagnostic("codex", codexScan, diagnostics, diagnostic);
|
|
492
|
+
}));
|
|
493
|
+
if (collector)
|
|
494
|
+
codexInvocationFiles.push(collector.finish());
|
|
294
495
|
}
|
|
295
|
-
const
|
|
296
|
-
const filtered = typeof
|
|
297
|
-
?
|
|
298
|
-
:
|
|
496
|
+
const normalizedCalls = dedupeCumulativeSessionCalls(calls);
|
|
497
|
+
const filtered = typeof sinceMs === "number"
|
|
498
|
+
? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
|
|
499
|
+
: normalizedCalls;
|
|
299
500
|
return {
|
|
300
501
|
records: aggregateCalls(filtered),
|
|
301
502
|
calls: filtered,
|
|
302
503
|
filesParsed,
|
|
303
|
-
agentsDetected: [...new Set(filtered.map((call) => call.agent))]
|
|
504
|
+
agentsDetected: [...new Set(filtered.map((call) => call.agent))],
|
|
505
|
+
sourceScans,
|
|
506
|
+
diagnostics,
|
|
507
|
+
...(codexInvocationFiles ? { codexInvocationFiles } : {})
|
|
304
508
|
};
|
|
305
509
|
}
|
|
306
510
|
/** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
|
|
307
511
|
export function aggregateCalls(calls) {
|
|
308
512
|
const groups = new Map();
|
|
309
|
-
for (const call of calls) {
|
|
513
|
+
for (const call of dedupeCumulativeSessionCalls(calls)) {
|
|
310
514
|
const day = call.timestamp.slice(0, 10);
|
|
311
515
|
const key = [day, call.agent, call.model, call.project ?? "unattributed"].join("|");
|
|
312
516
|
groups.set(key, [...(groups.get(key) ?? []), call]);
|
|
@@ -321,8 +525,9 @@ export function aggregateCalls(calls) {
|
|
|
321
525
|
cacheWrite5mTokens: sum(groupCalls, (c) => c.usage.cacheWrite5mTokens ?? 0),
|
|
322
526
|
cacheWrite1hTokens: sum(groupCalls, (c) => c.usage.cacheWrite1hTokens ?? 0)
|
|
323
527
|
};
|
|
324
|
-
const
|
|
325
|
-
const
|
|
528
|
+
const usageSupported = groupCalls.every((call) => call.usageSupport !== "unsupported_token_shape");
|
|
529
|
+
const amountUsd = usageSupported ? estimateTokenCostUsd(model, usage) : undefined;
|
|
530
|
+
const priced = usageSupported && typeof amountUsd === "number";
|
|
326
531
|
records.push({
|
|
327
532
|
id: slug(["local", agent, day, model, project].join("-")),
|
|
328
533
|
timestamp: new Date(`${day}T00:00:00Z`).toISOString(),
|
|
@@ -344,31 +549,138 @@ export function aggregateCalls(calls) {
|
|
|
344
549
|
projectId: project === "unattributed" || project === "(home)" ? undefined : project,
|
|
345
550
|
agentId: agent,
|
|
346
551
|
providerCostType: "local_agent_logs",
|
|
552
|
+
usageGranularity: "daily_aggregate",
|
|
347
553
|
quantity: groupCalls.length,
|
|
348
554
|
operation: `${agent} sessions`
|
|
349
555
|
});
|
|
350
556
|
}
|
|
351
557
|
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
352
558
|
}
|
|
353
|
-
async function listJsonlFiles(root) {
|
|
354
|
-
|
|
355
|
-
|
|
559
|
+
async function listJsonlFiles(root, scan, diagnostics) {
|
|
560
|
+
let rootStat;
|
|
561
|
+
try {
|
|
562
|
+
rootStat = await stat(root);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (isNodeError(error, "ENOENT")) {
|
|
566
|
+
scan.directoryStatus = "missing";
|
|
567
|
+
diagnostics.push({
|
|
568
|
+
agent: scan.agent,
|
|
569
|
+
code: "directory_missing",
|
|
570
|
+
severity: "info",
|
|
571
|
+
message: `${agentLabel(scan.agent)} transcript directory was not found.`,
|
|
572
|
+
count: 1
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
scan.directoryStatus = "unreadable";
|
|
577
|
+
diagnostics.push({
|
|
578
|
+
agent: scan.agent,
|
|
579
|
+
code: "directory_unreadable",
|
|
580
|
+
severity: "error",
|
|
581
|
+
message: `${agentLabel(scan.agent)} transcript directory could not be read${errorCodeSuffix(error)}.`,
|
|
582
|
+
count: 1
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
if (!rootStat.isDirectory()) {
|
|
588
|
+
scan.directoryStatus = "unreadable";
|
|
589
|
+
diagnostics.push({
|
|
590
|
+
agent: scan.agent,
|
|
591
|
+
code: "directory_unreadable",
|
|
592
|
+
severity: "error",
|
|
593
|
+
message: `${agentLabel(scan.agent)} transcript path is not a readable directory.`,
|
|
594
|
+
count: 1
|
|
595
|
+
});
|
|
356
596
|
return [];
|
|
597
|
+
}
|
|
598
|
+
scan.directoryStatus = "readable";
|
|
357
599
|
const out = [];
|
|
358
600
|
const queue = [root];
|
|
359
601
|
while (queue.length > 0) {
|
|
360
602
|
const dir = queue.pop();
|
|
361
|
-
|
|
603
|
+
let entries;
|
|
604
|
+
try {
|
|
605
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
scan.directoryStatus = "unreadable";
|
|
609
|
+
diagnostics.push({
|
|
610
|
+
agent: scan.agent,
|
|
611
|
+
code: "directory_unreadable",
|
|
612
|
+
severity: "error",
|
|
613
|
+
message: `${agentLabel(scan.agent)} transcript directory could not be read${errorCodeSuffix(error)}.`,
|
|
614
|
+
count: 1
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
362
618
|
for (const entry of entries) {
|
|
363
619
|
const path = join(dir, entry.name);
|
|
364
620
|
if (entry.isDirectory())
|
|
365
621
|
queue.push(path);
|
|
366
|
-
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
622
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
367
623
|
out.push(path);
|
|
624
|
+
scan.filesDiscovered += 1;
|
|
625
|
+
}
|
|
368
626
|
}
|
|
369
627
|
}
|
|
370
628
|
return out;
|
|
371
629
|
}
|
|
630
|
+
function emptySourceScan(agent) {
|
|
631
|
+
return {
|
|
632
|
+
agent,
|
|
633
|
+
directoryStatus: "readable",
|
|
634
|
+
filesDiscovered: 0,
|
|
635
|
+
filesParsed: 0,
|
|
636
|
+
malformedLines: 0,
|
|
637
|
+
unreadableFiles: 0,
|
|
638
|
+
unsupportedUsageSnapshots: 0
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function recordUnreadableFile(agent, scan, diagnostics, error) {
|
|
642
|
+
scan.unreadableFiles += 1;
|
|
643
|
+
diagnostics.push({
|
|
644
|
+
agent,
|
|
645
|
+
code: "file_unreadable",
|
|
646
|
+
severity: "error",
|
|
647
|
+
message: `${agentLabel(agent)} transcript file could not be read${errorCodeSuffix(error)}.`,
|
|
648
|
+
count: 1
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function recordParseDiagnostic(agent, scan, diagnostics, diagnostic) {
|
|
652
|
+
if (diagnostic.code === "malformed_jsonl") {
|
|
653
|
+
scan.malformedLines += diagnostic.count;
|
|
654
|
+
diagnostics.push({
|
|
655
|
+
agent,
|
|
656
|
+
code: diagnostic.code,
|
|
657
|
+
severity: "warning",
|
|
658
|
+
message: `${diagnostic.count} malformed JSONL line(s) were skipped in ${agentLabel(agent)} transcripts.`,
|
|
659
|
+
count: diagnostic.count
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
scan.unsupportedUsageSnapshots += diagnostic.count;
|
|
664
|
+
diagnostics.push({
|
|
665
|
+
agent,
|
|
666
|
+
code: diagnostic.code,
|
|
667
|
+
severity: "warning",
|
|
668
|
+
message: `${diagnostic.count} ${agentLabel(agent)} token snapshot(s) lacked the input/output components required for pricing.`,
|
|
669
|
+
count: diagnostic.count
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function agentLabel(agent) {
|
|
673
|
+
return agent === "claude-code" ? "Claude Code" : "Codex";
|
|
674
|
+
}
|
|
675
|
+
function errorCodeSuffix(error) {
|
|
676
|
+
const code = error instanceof Error
|
|
677
|
+
? error.code
|
|
678
|
+
: undefined;
|
|
679
|
+
return code && /^[A-Z0-9_]+$/.test(code) ? ` (${code})` : "";
|
|
680
|
+
}
|
|
681
|
+
function isNodeError(error, code) {
|
|
682
|
+
return error instanceof Error && error.code === code;
|
|
683
|
+
}
|
|
372
684
|
function projectFromCwd(cwd) {
|
|
373
685
|
if (!cwd)
|
|
374
686
|
return undefined;
|
|
@@ -379,6 +691,9 @@ function projectFromCwd(cwd) {
|
|
|
379
691
|
const name = basename(cwd);
|
|
380
692
|
return name.length > 0 ? name : undefined;
|
|
381
693
|
}
|
|
694
|
+
function absoluteWorkingDirectory(cwd) {
|
|
695
|
+
return cwd && isAbsolute(cwd) ? resolve(cwd) : undefined;
|
|
696
|
+
}
|
|
382
697
|
/**
|
|
383
698
|
* Codex can be launched from HOME and do nearly all of its work through tools
|
|
384
699
|
* that declare a more specific working directory. Prefer that observed
|
|
@@ -412,12 +727,18 @@ function projectFromTranscriptPath(filePath) {
|
|
|
412
727
|
const tail = parent.split("-").filter(Boolean).pop();
|
|
413
728
|
return tail && tail.length > 0 ? tail : undefined;
|
|
414
729
|
}
|
|
730
|
+
function localActivityScopeKey(sessionId, workingDirectory, project) {
|
|
731
|
+
// The absolute cwd stays inside this ephemeral parser key and is never
|
|
732
|
+
// persisted or rendered. It prevents one transcript that changes projects
|
|
733
|
+
// from attaching Project A's prompt/file evidence to Project B's calls.
|
|
734
|
+
return [sessionId ?? "unknown-session", workingDirectory ?? project ?? "unattributed"].join("\u0000");
|
|
735
|
+
}
|
|
415
736
|
const FOCUS_STOP_WORDS = new Set([
|
|
416
|
-
"about", "after", "again", "also", "and", "are", "at", "been", "being", "but",
|
|
737
|
+
"about", "above", "after", "again", "also", "and", "are", "at", "been", "being", "but",
|
|
417
738
|
"can", "check", "could", "did", "does", "doing", "dont", "every", "from",
|
|
418
739
|
"for", "have", "here", "how", "in", "into", "its", "just", "like", "make", "more",
|
|
419
740
|
"need", "not", "now", "on", "only", "other", "our", "please", "really", "should",
|
|
420
|
-
"something", "sure", "than", "that", "the", "their", "them", "then", "there",
|
|
741
|
+
"earlier", "is", "mentioned", "one", "something", "sure", "than", "that", "the", "their", "them", "then", "there",
|
|
421
742
|
"these", "they", "thing", "think", "this", "through", "to", "too", "use", "user",
|
|
422
743
|
"users", "want", "was", "way", "we", "what", "when", "where", "which", "while",
|
|
423
744
|
"who", "why", "will", "with", "work", "working", "would", "you", "your",
|
|
@@ -493,12 +814,49 @@ function focusTopic(prompts) {
|
|
|
493
814
|
if (prompts.length === 0)
|
|
494
815
|
return undefined;
|
|
495
816
|
const recent = prompts.slice(-12);
|
|
496
|
-
const
|
|
817
|
+
const promptTokenSets = recent.map((prompt) => (new Set(topicTokens(prompt).filter((token) => !FOCUS_STOP_WORDS.has(token)))));
|
|
818
|
+
const observedTopicTokens = new Set(promptTokenSets.flatMap((tokens) => [...tokens]));
|
|
819
|
+
// Recognized product/work concepts are meaningful even in one prompt. Generic
|
|
820
|
+
// tokens must repeat across distinct prompts below; that keeps attachment
|
|
821
|
+
// prose and one-off screenshot filenames from becoming a confident topic.
|
|
822
|
+
if (observedTopicTokens.has("aibill") && observedTopicTokens.has("prompt")) {
|
|
823
|
+
return "aibill prompt";
|
|
824
|
+
}
|
|
825
|
+
if (observedTopicTokens.has("glance") && observedTopicTokens.has("hover")) {
|
|
826
|
+
return observedTopicTokens.has("ui") ? "Glance hover UI" : "Glance hover";
|
|
827
|
+
}
|
|
828
|
+
if (observedTopicTokens.has("glance") &&
|
|
829
|
+
["action", "agent", "handoff", "prompt"].some((token) => observedTopicTokens.has(token))) {
|
|
830
|
+
return "Glance agent handoff";
|
|
831
|
+
}
|
|
832
|
+
if (observedTopicTokens.has("landing") && observedTopicTokens.has("page"))
|
|
833
|
+
return "landing page";
|
|
834
|
+
if (observedTopicTokens.has("hover")) {
|
|
835
|
+
return observedTopicTokens.has("ui") ? "hover UI" : "hover interaction";
|
|
836
|
+
}
|
|
837
|
+
if (observedTopicTokens.has("mcp")) {
|
|
838
|
+
return observedTopicTokens.has("feature") ? "MCP feature" : "MCP";
|
|
839
|
+
}
|
|
840
|
+
if (observedTopicTokens.has("seo")) {
|
|
841
|
+
return observedTopicTokens.has("strategy") ? "SEO strategy" : "SEO";
|
|
842
|
+
}
|
|
843
|
+
const promptOccurrences = new Map();
|
|
844
|
+
for (const tokens of promptTokenSets) {
|
|
845
|
+
for (const token of tokens) {
|
|
846
|
+
promptOccurrences.set(token, (promptOccurrences.get(token) ?? 0) + 1);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const repeatedTokens = new Set([...promptOccurrences.entries()]
|
|
850
|
+
.filter(([, count]) => count >= 2)
|
|
851
|
+
.map(([token]) => token));
|
|
852
|
+
if (repeatedTokens.size === 0)
|
|
853
|
+
return undefined;
|
|
497
854
|
const tokenScores = new Map();
|
|
498
855
|
const pairScores = new Map();
|
|
499
856
|
recent.forEach((prompt, index) => {
|
|
500
857
|
const weight = 1 + index / Math.max(1, recent.length - 1);
|
|
501
|
-
const tokens = topicTokens(prompt)
|
|
858
|
+
const tokens = topicTokens(prompt)
|
|
859
|
+
.filter((token) => !FOCUS_STOP_WORDS.has(token) && repeatedTokens.has(token));
|
|
502
860
|
const unique = [...new Set(tokens)];
|
|
503
861
|
for (const token of unique) {
|
|
504
862
|
tokenScores.set(token, (tokenScores.get(token) ?? 0) + weight);
|
|
@@ -526,22 +884,9 @@ function focusTopic(prompts) {
|
|
|
526
884
|
if (candidateTokens.size === 0)
|
|
527
885
|
return undefined;
|
|
528
886
|
const tokens = [...candidateTokens].slice(0, 3);
|
|
529
|
-
if (tokens.includes("glance") && tokens.includes("hover")) {
|
|
530
|
-
return tokens.includes("ui") ? "Glance hover UI" : "Glance hover";
|
|
531
|
-
}
|
|
532
|
-
if (observedTopicTokens.has("glance") &&
|
|
533
|
-
["action", "agent", "handoff", "prompt"].some((token) => observedTopicTokens.has(token))) {
|
|
534
|
-
return "Glance agent handoff";
|
|
535
|
-
}
|
|
536
887
|
if (tokens.includes("hover")) {
|
|
537
888
|
return tokens.includes("ui") ? "hover UI" : "hover interaction";
|
|
538
889
|
}
|
|
539
|
-
if (tokens.includes("landing") && tokens.includes("page"))
|
|
540
|
-
return "landing page";
|
|
541
|
-
if (tokens.includes("mcp"))
|
|
542
|
-
return tokens.includes("feature") ? "MCP feature" : "MCP";
|
|
543
|
-
if (tokens.includes("seo"))
|
|
544
|
-
return tokens.includes("strategy") ? "SEO strategy" : "SEO";
|
|
545
890
|
return tokens.map(displayToken).join(" ");
|
|
546
891
|
}
|
|
547
892
|
function inferAction(prompts, title) {
|
|
@@ -619,9 +964,36 @@ function topicTokens(value) {
|
|
|
619
964
|
// Absolute paths often appear in attached-image metadata and tool-oriented
|
|
620
965
|
// prompts. They are machine context, not the user's work topic, and can
|
|
621
966
|
// otherwise outrank meaningful words when only one recent prompt exists.
|
|
622
|
-
const withoutAbsolutePaths = value.replace(/(^|[\s("'=:])(?:file:\/\/)?\/[^\s)"']+/g, "$1")
|
|
967
|
+
const withoutAbsolutePaths = value.replace(/(^|[\s("'=:])(?:file:\/\/)?\/[^\s)"']+/g, "$1")
|
|
968
|
+
.replace(/\b[^\s/\\]+\.(?:png|jpe?g|gif|webp|heic|svg|pdf|mov|mp4)\b/gi, " ")
|
|
969
|
+
.replace(/\b(?:attached|attachment|clipboard|image|images|photo|picture|screenshot|screenshots)\b/gi, " ");
|
|
623
970
|
return promptTokens(sanitizeLocalActivityText(withoutAbsolutePaths));
|
|
624
971
|
}
|
|
972
|
+
function codexTurnUsage(value) {
|
|
973
|
+
const rawInput = numberOf(value.input_tokens) ?? 0;
|
|
974
|
+
const cached = Math.min(rawInput, numberOf(value.cached_input_tokens) ?? 0);
|
|
975
|
+
const output = numberOf(value.output_tokens) ?? 0;
|
|
976
|
+
return {
|
|
977
|
+
inputTokens: Math.max(0, rawInput - cached),
|
|
978
|
+
outputTokens: output,
|
|
979
|
+
cacheReadTokens: cached,
|
|
980
|
+
contextTokens: rawInput,
|
|
981
|
+
totalTokens: numberOf(value.total_tokens) ?? rawInput + output,
|
|
982
|
+
source: "transcript_last_token_usage"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function toTurnUsage(usage, source) {
|
|
986
|
+
const contextTokens = usage.inputTokens +
|
|
987
|
+
(usage.cacheReadTokens ?? 0) +
|
|
988
|
+
(usage.cacheWrite5mTokens ?? 0) +
|
|
989
|
+
(usage.cacheWrite1hTokens ?? 0);
|
|
990
|
+
return {
|
|
991
|
+
...usage,
|
|
992
|
+
contextTokens,
|
|
993
|
+
totalTokens: contextTokens + usage.outputTokens,
|
|
994
|
+
source
|
|
995
|
+
};
|
|
996
|
+
}
|
|
625
997
|
/**
|
|
626
998
|
* Remove known and assignment-shaped credentials from metadata before it can
|
|
627
999
|
* become a topic, title, Glance field, MCP result, or copy-ready handoff.
|
|
@@ -698,6 +1070,26 @@ function toIso(value) {
|
|
|
698
1070
|
const parsed = Date.parse(value);
|
|
699
1071
|
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : undefined;
|
|
700
1072
|
}
|
|
1073
|
+
const ROOT_TASK_CLOCK_TOLERANCE_MS = 5_000;
|
|
1074
|
+
function isRootSpecificTaskStart(value, rootStartedAtMs) {
|
|
1075
|
+
const taskStartedAtMs = timestampMilliseconds(value);
|
|
1076
|
+
return typeof rootStartedAtMs === "number" &&
|
|
1077
|
+
typeof taskStartedAtMs === "number" &&
|
|
1078
|
+
taskStartedAtMs >= rootStartedAtMs - ROOT_TASK_CLOCK_TOLERANCE_MS;
|
|
1079
|
+
}
|
|
1080
|
+
function timestampMilliseconds(value) {
|
|
1081
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1082
|
+
return value < 1_000_000_000_000 ? value * 1_000 : value;
|
|
1083
|
+
}
|
|
1084
|
+
if (typeof value !== "string" || value.length === 0)
|
|
1085
|
+
return undefined;
|
|
1086
|
+
const numeric = Number(value);
|
|
1087
|
+
if (Number.isFinite(numeric)) {
|
|
1088
|
+
return numeric < 1_000_000_000_000 ? numeric * 1_000 : numeric;
|
|
1089
|
+
}
|
|
1090
|
+
const parsed = Date.parse(value);
|
|
1091
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1092
|
+
}
|
|
701
1093
|
function sum(calls, pick) {
|
|
702
1094
|
return calls.reduce((total, call) => total + pick(call), 0);
|
|
703
1095
|
}
|