aicp-tracker 1.2.7 → 1.2.9
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/package.json +1 -1
- package/src/log-parser.js +119 -110
package/package.json
CHANGED
package/src/log-parser.js
CHANGED
|
@@ -11,7 +11,6 @@ function extractTasksFromBranch(branch) {
|
|
|
11
11
|
return [...new Set(matches.map(m => m.toUpperCase()))];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// Sum usage fields from an array of assistant entries
|
|
15
14
|
function sumUsage(entries) {
|
|
16
15
|
let input = 0, cc = 0, cr = 0, out = 0;
|
|
17
16
|
let ephUser = 0, ephAsst = 0, ephTool = 0;
|
|
@@ -26,9 +25,13 @@ function sumUsage(entries) {
|
|
|
26
25
|
cc += u.cache_creation_input_tokens || 0;
|
|
27
26
|
cr += u.cache_read_input_tokens || 0;
|
|
28
27
|
out += u.output_tokens || 0;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// Ephemeral tokens may be at top level (older format) or in cache_creation object (newer format)
|
|
29
|
+
ephUser += u.ephemeral_user_input_tokens || u.cache_creation?.ephemeral_user_input_tokens || 0;
|
|
30
|
+
ephAsst += u.ephemeral_assistant_input_tokens || u.cache_creation?.ephemeral_assistant_input_tokens || 0;
|
|
31
|
+
ephTool += u.ephemeral_tool_input_tokens || u.cache_creation?.ephemeral_tool_input_tokens || 0;
|
|
32
|
+
// Also sum ephemeral cache creation tokens (5m and 1h variants)
|
|
33
|
+
ephUser += u.cache_creation?.ephemeral_5m_input_tokens || 0;
|
|
34
|
+
ephUser += u.cache_creation?.ephemeral_1h_input_tokens || 0;
|
|
32
35
|
if (!serviceTier && u.service_tier) serviceTier = u.service_tier;
|
|
33
36
|
if (!speed && u.speed) speed = u.speed;
|
|
34
37
|
|
|
@@ -40,21 +43,27 @@ function sumUsage(entries) {
|
|
|
40
43
|
if (block.name === 'web_fetch' || block.name === 'WebFetch') webFetch++;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
// Also check server_tool_use object for web requests
|
|
47
|
+
const srvTool = u.server_tool_use;
|
|
48
|
+
if (srvTool) {
|
|
49
|
+
webSearch += srvTool.web_search_requests || 0;
|
|
50
|
+
webFetch += srvTool.web_fetch_requests || 0;
|
|
51
|
+
}
|
|
43
52
|
if (e.message?.model) models.add(e.message.model);
|
|
44
53
|
if (e.uuid) uuids.push(e.uuid);
|
|
45
54
|
}
|
|
46
55
|
return { input, cc, cr, out, ephUser, ephAsst, ephTool, webSearch, webFetch, serviceTier, speed, models, uuids };
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
function makeRecord(
|
|
58
|
+
function makeRecord(ref, rootUuid, usage, filePaths) {
|
|
50
59
|
const { input, cc, cr, out, ephUser, ephAsst, ephTool, webSearch, webFetch, serviceTier, speed, models, uuids } = usage;
|
|
51
60
|
return {
|
|
52
|
-
sessionId:
|
|
61
|
+
sessionId: ref.sessionId || null,
|
|
53
62
|
uuid: rootUuid,
|
|
54
|
-
parentUuid:
|
|
55
|
-
timestamp:
|
|
56
|
-
gitBranch:
|
|
57
|
-
task_from_git_branch: extractTasksFromBranch(
|
|
63
|
+
parentUuid: ref.parentUuid || null,
|
|
64
|
+
timestamp: ref.timestamp || new Date().toISOString(),
|
|
65
|
+
gitBranch: ref.gitBranch || null,
|
|
66
|
+
task_from_git_branch: extractTasksFromBranch(ref.gitBranch),
|
|
58
67
|
model: [...models].join(',') || null,
|
|
59
68
|
input_tokens: input,
|
|
60
69
|
cache_creation_input_tokens: cc,
|
|
@@ -73,6 +82,31 @@ function makeRecord(first, rootUuid, usage, filePaths) {
|
|
|
73
82
|
};
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
function extractFilePath(entry) {
|
|
86
|
+
const content = entry.message?.content;
|
|
87
|
+
if (!Array.isArray(content)) return null;
|
|
88
|
+
for (const block of content) {
|
|
89
|
+
if (block.type === 'tool_use' && WRITE_TOOLS.has(block.name)) {
|
|
90
|
+
const fp = block.input?.file_path || block.input?.path;
|
|
91
|
+
if (fp && typeof fp === 'string') return fp;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractAllFilePaths(entry) {
|
|
98
|
+
const content = entry.message?.content;
|
|
99
|
+
if (!Array.isArray(content)) return [];
|
|
100
|
+
const paths = [];
|
|
101
|
+
for (const block of content) {
|
|
102
|
+
if (block.type === 'tool_use' && WRITE_TOOLS.has(block.name)) {
|
|
103
|
+
const fp = block.input?.file_path || block.input?.path;
|
|
104
|
+
if (fp && typeof fp === 'string') paths.push(fp);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return paths;
|
|
108
|
+
}
|
|
109
|
+
|
|
76
110
|
function parseNewLines(filePath) {
|
|
77
111
|
const carryOverPromptId = state.getLastPromptId(filePath);
|
|
78
112
|
|
|
@@ -87,11 +121,10 @@ function parseNewLines(filePath) {
|
|
|
87
121
|
fs.closeSync(fd);
|
|
88
122
|
state.setOffset(filePath, offset + read);
|
|
89
123
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const byUuid = new Map();
|
|
124
|
+
// Parse entries in file order.
|
|
125
|
+
// Deduplicate multi-block API responses sharing the same message.id — only the
|
|
126
|
+
// first occurrence carries usage fields; subsequent lines are structurally identical.
|
|
127
|
+
const entries = [];
|
|
95
128
|
const seenMsgIds = new Set();
|
|
96
129
|
|
|
97
130
|
for (const raw of buf.slice(0, read).toString('utf8').split('\n')) {
|
|
@@ -106,135 +139,111 @@ function parseNewLines(filePath) {
|
|
|
106
139
|
if (seenMsgIds.has(msgId)) continue;
|
|
107
140
|
seenMsgIds.add(msgId);
|
|
108
141
|
}
|
|
109
|
-
|
|
142
|
+
entries.push(entry);
|
|
110
143
|
}
|
|
111
144
|
|
|
112
|
-
// ──
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
145
|
+
// ── Sequential grouping ───────────────────────────────────────────────────
|
|
146
|
+
// A user entry with a promptId starts (or continues) a conversation turn.
|
|
147
|
+
// Every assistant entry with usage that follows — even entries without a
|
|
148
|
+
// promptId of their own — belongs to the current turn. This captures all
|
|
149
|
+
// tool-use round-trips that Claude Code doesn't tag with a fresh promptId.
|
|
117
150
|
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
151
|
+
// carryOverPromptId: the last promptId seen in a previous batch. Assistant
|
|
152
|
+
// entries at the start of this batch that precede any new user promptId are
|
|
153
|
+
// continuation turns of that prompt and get collected under it.
|
|
154
|
+
|
|
155
|
+
let currentPromptId = carryOverPromptId;
|
|
156
|
+
let latestPromptId = carryOverPromptId;
|
|
157
|
+
let carryOverSeenInBatch = false;
|
|
158
|
+
|
|
159
|
+
// groupOrder preserves insertion order so records are emitted chronologically.
|
|
160
|
+
const groupOrder = [];
|
|
161
|
+
const groupTurns = new Map(); // promptId → assistant entries with usage[]
|
|
162
|
+
const groupAllEntries = new Map(); // promptId → ALL entries in this turn (for file extraction)
|
|
163
|
+
const groupRef = new Map(); // promptId → reference entry (user msg or first asst turn)
|
|
164
|
+
|
|
165
|
+
function ensureGroup(pid, refEntry) {
|
|
166
|
+
if (!groupTurns.has(pid)) {
|
|
167
|
+
groupTurns.set(pid, []);
|
|
168
|
+
groupAllEntries.set(pid, []);
|
|
169
|
+
groupRef.set(pid, refEntry);
|
|
170
|
+
groupOrder.push(pid);
|
|
139
171
|
}
|
|
140
172
|
}
|
|
141
173
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// determines which turn this assistant entry belongs to.
|
|
145
|
-
// If the chain exits the batch (parent not in byUuid), fall back to the
|
|
146
|
-
// carry-over from the previous batch.
|
|
147
|
-
function resolvePromptId(entry) {
|
|
148
|
-
const visited = new Set();
|
|
149
|
-
let cur = entry;
|
|
150
|
-
while (cur && !visited.has(cur.uuid)) {
|
|
151
|
-
visited.add(cur.uuid);
|
|
152
|
-
const parentId = cur.parentUuid;
|
|
153
|
-
if (!parentId) break;
|
|
154
|
-
if (promptIdByUuid.has(parentId)) return promptIdByUuid.get(parentId);
|
|
155
|
-
const parent = byUuid.get(parentId);
|
|
156
|
-
if (!parent) return carryOverPromptId || entry.uuid; // batch boundary
|
|
157
|
-
cur = parent;
|
|
158
|
-
}
|
|
159
|
-
return carryOverPromptId || entry.uuid;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// If carryOverPromptId was re-introduced by a user entry in this batch (e.g. the
|
|
163
|
-
// real user message itself landed here), treat it as a normal in-batch group so
|
|
164
|
-
// the uuid stays as-is. Only suffix when the carry-over was the ONLY way this
|
|
165
|
-
// promptId was seen (i.e. no user entry in this batch declares that promptId).
|
|
166
|
-
const carryOverSeenInBatch = carryOverPromptId &&
|
|
167
|
-
[...promptIdByUuid.values()].includes(carryOverPromptId);
|
|
168
|
-
|
|
169
|
-
// ── Group assistant turns by promptId ─────────────────────────────────────
|
|
170
|
-
const promptGroups = new Map(); // promptId → assistant entries[]
|
|
171
|
-
|
|
172
|
-
for (const entry of byUuid.values()) {
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
const isUser = entry.type === 'user' || entry.message?.role === 'user';
|
|
173
176
|
const isAssistant = entry.type === 'assistant' || entry.message?.role === 'assistant';
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
|
|
178
|
+
if (isUser && entry.promptId) {
|
|
179
|
+
if (entry.promptId === carryOverPromptId) carryOverSeenInBatch = true;
|
|
180
|
+
currentPromptId = entry.promptId;
|
|
181
|
+
latestPromptId = entry.promptId;
|
|
182
|
+
ensureGroup(currentPromptId, entry);
|
|
183
|
+
} else if (isAssistant && currentPromptId) {
|
|
184
|
+
// Track ALL assistant entries for file extraction, but only add ones with usage for token counting
|
|
185
|
+
ensureGroup(currentPromptId, entry);
|
|
186
|
+
groupAllEntries.get(currentPromptId).push(entry);
|
|
187
|
+
if (entry.message?.usage) {
|
|
188
|
+
groupTurns.get(currentPromptId).push(entry);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
178
191
|
}
|
|
179
192
|
|
|
180
|
-
// Persist the latest promptId so the next batch can attribute orphaned entries
|
|
181
193
|
if (latestPromptId) state.setLastPromptId(filePath, latestPromptId);
|
|
182
194
|
|
|
183
|
-
// ──
|
|
195
|
+
// ── Emit records ──────────────────────────────────────────────────────────
|
|
184
196
|
const records = [];
|
|
185
197
|
|
|
186
|
-
for (const
|
|
187
|
-
turns
|
|
188
|
-
const
|
|
198
|
+
for (const promptId of groupOrder) {
|
|
199
|
+
const turns = groupTurns.get(promptId);
|
|
200
|
+
const allEntries = groupAllEntries.get(promptId);
|
|
201
|
+
if (!turns.length) continue;
|
|
202
|
+
|
|
203
|
+
const ref = groupRef.get(promptId);
|
|
189
204
|
|
|
190
205
|
// If this group is a pure carry-over (the previous batch already emitted a
|
|
191
|
-
// record with uuid=promptId), suffix
|
|
192
|
-
//
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
for (const
|
|
202
|
-
|
|
203
|
-
const fp = block.input?.file_path || block.input?.path;
|
|
204
|
-
if (fp && typeof fp === 'string') return fp;
|
|
205
|
-
}
|
|
206
|
+
// record with uuid=promptId), suffix :carry so both records accumulate in
|
|
207
|
+
// the DB rather than one being deduplicated away.
|
|
208
|
+
const isCarryOver = promptId === carryOverPromptId && !carryOverSeenInBatch;
|
|
209
|
+
const baseUuid = isCarryOver ? `${promptId}:carry` : promptId;
|
|
210
|
+
|
|
211
|
+
// Collect ALL file paths from ALL entries (including ones without usage),
|
|
212
|
+
// not just from entries with usage
|
|
213
|
+
const allFiles = new Set();
|
|
214
|
+
const turnFiles = turns.map(extractFilePath);
|
|
215
|
+
for (const entry of allEntries) {
|
|
216
|
+
for (const fp of extractAllFilePaths(entry)) {
|
|
217
|
+
allFiles.add(fp);
|
|
206
218
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const hasAnyFile = turnFiles.some(f => f !== null);
|
|
219
|
+
}
|
|
220
|
+
const hasAnyFile = allFiles.size > 0;
|
|
211
221
|
|
|
212
222
|
if (!hasAnyFile) {
|
|
213
|
-
|
|
214
|
-
records.push(makeRecord(first, baseUuid, sumUsage(turns), []));
|
|
223
|
+
records.push(makeRecord(ref, baseUuid, sumUsage(turns), []));
|
|
215
224
|
continue;
|
|
216
225
|
}
|
|
217
226
|
|
|
218
|
-
// Attribute orphan turns (no file) to the
|
|
219
|
-
//
|
|
220
|
-
const fileGroups
|
|
221
|
-
let lastFile = turnFiles.find(f => f !== null);
|
|
227
|
+
// Attribute orphan turns (no file anchor) to the nearest anchor:
|
|
228
|
+
// leading orphans → first anchor; trailing orphans → last anchor seen.
|
|
229
|
+
const fileGroups = new Map();
|
|
222
230
|
const firstAnchorIdx = turnFiles.findIndex(f => f !== null);
|
|
231
|
+
let lastFile = turnFiles[firstAnchorIdx];
|
|
223
232
|
|
|
224
233
|
for (let i = 0; i < turns.length; i++) {
|
|
225
234
|
const file = turnFiles[i] !== null ? turnFiles[i]
|
|
226
235
|
: (i < firstAnchorIdx ? turnFiles[firstAnchorIdx] : lastFile);
|
|
227
|
-
|
|
228
236
|
if (!fileGroups.has(file)) fileGroups.set(file, []);
|
|
229
237
|
fileGroups.get(file).push(turns[i]);
|
|
230
238
|
if (turnFiles[i] !== null) lastFile = turnFiles[i];
|
|
231
239
|
}
|
|
232
240
|
|
|
233
|
-
// Emit one record per file; disambiguate uuid by appending file index
|
|
234
241
|
let idx = 0;
|
|
235
242
|
for (const [fp, fileTurns] of fileGroups) {
|
|
236
243
|
const uuid = idx === 0 ? baseUuid : `${baseUuid}:${idx}`;
|
|
237
|
-
|
|
244
|
+
// Include all files from the turn(s) in this group, not just the grouped file
|
|
245
|
+
const filesForRecord = [...new Set([fp, ...fileTurns.flatMap(extractAllFilePaths)])];
|
|
246
|
+
records.push(makeRecord(ref, uuid, sumUsage(fileTurns), filesForRecord));
|
|
238
247
|
idx++;
|
|
239
248
|
}
|
|
240
249
|
}
|