aicp-tracker 1.2.7 → 1.2.8
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 +113 -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,105 @@ 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
|
-
promptIdByUuid.set(entry.uuid, entry.uuid);
|
|
137
|
-
latestPromptId = entry.uuid;
|
|
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[]
|
|
162
|
+
const groupRef = new Map(); // promptId → reference entry (user msg or first asst turn)
|
|
163
|
+
|
|
164
|
+
function ensureGroup(pid, refEntry) {
|
|
165
|
+
if (!groupTurns.has(pid)) {
|
|
166
|
+
groupTurns.set(pid, []);
|
|
167
|
+
groupRef.set(pid, refEntry);
|
|
168
|
+
groupOrder.push(pid);
|
|
139
169
|
}
|
|
140
170
|
}
|
|
141
171
|
|
|
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()) {
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const isUser = entry.type === 'user' || entry.message?.role === 'user';
|
|
173
174
|
const isAssistant = entry.type === 'assistant' || entry.message?.role === 'assistant';
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
|
|
176
|
+
if (isUser && entry.promptId) {
|
|
177
|
+
if (entry.promptId === carryOverPromptId) carryOverSeenInBatch = true;
|
|
178
|
+
currentPromptId = entry.promptId;
|
|
179
|
+
latestPromptId = entry.promptId;
|
|
180
|
+
ensureGroup(currentPromptId, entry);
|
|
181
|
+
} else if (isAssistant && entry.message?.usage) {
|
|
182
|
+
if (currentPromptId) {
|
|
183
|
+
ensureGroup(currentPromptId, entry);
|
|
184
|
+
groupTurns.get(currentPromptId).push(entry);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
// Persist the latest promptId so the next batch can attribute orphaned entries
|
|
181
189
|
if (latestPromptId) state.setLastPromptId(filePath, latestPromptId);
|
|
182
190
|
|
|
183
|
-
// ──
|
|
191
|
+
// ── Emit records ──────────────────────────────────────────────────────────
|
|
184
192
|
const records = [];
|
|
185
193
|
|
|
186
|
-
for (const
|
|
187
|
-
turns
|
|
188
|
-
|
|
194
|
+
for (const promptId of groupOrder) {
|
|
195
|
+
const turns = groupTurns.get(promptId);
|
|
196
|
+
if (!turns.length) continue;
|
|
197
|
+
|
|
198
|
+
const ref = groupRef.get(promptId);
|
|
189
199
|
|
|
190
200
|
// 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 turnFiles = turns.map(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (block.type === 'tool_use' && WRITE_TOOLS.has(block.name)) {
|
|
203
|
-
const fp = block.input?.file_path || block.input?.path;
|
|
204
|
-
if (fp && typeof fp === 'string') return fp;
|
|
205
|
-
}
|
|
201
|
+
// record with uuid=promptId), suffix :carry so both records accumulate in
|
|
202
|
+
// the DB rather than one being deduplicated away.
|
|
203
|
+
const isCarryOver = promptId === carryOverPromptId && !carryOverSeenInBatch;
|
|
204
|
+
const baseUuid = isCarryOver ? `${promptId}:carry` : promptId;
|
|
205
|
+
|
|
206
|
+
// Collect ALL file paths from all turns (not just the first file)
|
|
207
|
+
const allFiles = new Set();
|
|
208
|
+
const turnFiles = turns.map(extractFilePath);
|
|
209
|
+
for (const turn of turns) {
|
|
210
|
+
for (const fp of extractAllFilePaths(turn)) {
|
|
211
|
+
allFiles.add(fp);
|
|
206
212
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const hasAnyFile = turnFiles.some(f => f !== null);
|
|
213
|
+
}
|
|
214
|
+
const hasAnyFile = allFiles.size > 0;
|
|
211
215
|
|
|
212
216
|
if (!hasAnyFile) {
|
|
213
|
-
|
|
214
|
-
records.push(makeRecord(first, baseUuid, sumUsage(turns), []));
|
|
217
|
+
records.push(makeRecord(ref, baseUuid, sumUsage(turns), []));
|
|
215
218
|
continue;
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
// Attribute orphan turns (no file) to the
|
|
219
|
-
//
|
|
220
|
-
const fileGroups
|
|
221
|
-
let lastFile = turnFiles.find(f => f !== null);
|
|
221
|
+
// Attribute orphan turns (no file anchor) to the nearest anchor:
|
|
222
|
+
// leading orphans → first anchor; trailing orphans → last anchor seen.
|
|
223
|
+
const fileGroups = new Map();
|
|
222
224
|
const firstAnchorIdx = turnFiles.findIndex(f => f !== null);
|
|
225
|
+
let lastFile = turnFiles[firstAnchorIdx];
|
|
223
226
|
|
|
224
227
|
for (let i = 0; i < turns.length; i++) {
|
|
225
228
|
const file = turnFiles[i] !== null ? turnFiles[i]
|
|
226
229
|
: (i < firstAnchorIdx ? turnFiles[firstAnchorIdx] : lastFile);
|
|
227
|
-
|
|
228
230
|
if (!fileGroups.has(file)) fileGroups.set(file, []);
|
|
229
231
|
fileGroups.get(file).push(turns[i]);
|
|
230
232
|
if (turnFiles[i] !== null) lastFile = turnFiles[i];
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
// Emit one record per file; disambiguate uuid by appending file index
|
|
234
235
|
let idx = 0;
|
|
235
236
|
for (const [fp, fileTurns] of fileGroups) {
|
|
236
237
|
const uuid = idx === 0 ? baseUuid : `${baseUuid}:${idx}`;
|
|
237
|
-
|
|
238
|
+
// Include all files from the turn(s) in this group, not just the grouped file
|
|
239
|
+
const filesForRecord = [...new Set([fp, ...fileTurns.flatMap(extractAllFilePaths)])];
|
|
240
|
+
records.push(makeRecord(ref, uuid, sumUsage(fileTurns), filesForRecord));
|
|
238
241
|
idx++;
|
|
239
242
|
}
|
|
240
243
|
}
|