aicp-tracker 1.2.1 → 1.2.2
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 +148 -56
package/package.json
CHANGED
package/src/log-parser.js
CHANGED
|
@@ -3,23 +3,98 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const state = require('./state');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Extracts all JIRA-style task keys from a git branch name.
|
|
11
|
-
* Handles all combinations per spec:
|
|
12
|
-
* "STORAGE-56: ASD", "STORAGE-56 ASD", "ASD STORAGE-56", "STORAGE-56",
|
|
13
|
-
* "storage-56 asd" (case-insensitive), "asd :storage-56",
|
|
14
|
-
* "STORAGE-56, STORAGE-57: ASD" (multiple tasks → returns both)
|
|
15
|
-
* Returns uppercase deduplicated keys: ["STORAGE-56", "STORAGE-57"]
|
|
16
|
-
*/
|
|
6
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'MultiEdit']);
|
|
7
|
+
|
|
17
8
|
function extractTasksFromBranch(branch) {
|
|
18
9
|
if (!branch || typeof branch !== 'string') return [];
|
|
19
10
|
const matches = branch.match(/[A-Za-z][A-Za-z0-9]*-\d+/g) || [];
|
|
20
11
|
return [...new Set(matches.map(m => m.toUpperCase()))];
|
|
21
12
|
}
|
|
22
13
|
|
|
14
|
+
// Returns true for a genuine user message (not a tool_result injected by the harness)
|
|
15
|
+
function isUserInitiated(entry) {
|
|
16
|
+
if (entry.type !== 'user' && entry.message?.role !== 'user') return false;
|
|
17
|
+
const content = entry.message?.content ?? entry.content;
|
|
18
|
+
if (!content) return false;
|
|
19
|
+
if (typeof content === 'string') return true;
|
|
20
|
+
if (Array.isArray(content)) return content.some(c => c.type !== 'tool_result');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Walk parentUuid upward to find the user-initiated root of this assistant turn.
|
|
25
|
+
// Returns rootUuid, or the entry's own uuid if root isn't in the current batch.
|
|
26
|
+
function findPromptRoot(entry, byUuid) {
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
let cur = entry;
|
|
29
|
+
while (cur && !visited.has(cur.uuid)) {
|
|
30
|
+
visited.add(cur.uuid);
|
|
31
|
+
const parent = byUuid.get(cur.parentUuid);
|
|
32
|
+
if (!parent) break;
|
|
33
|
+
if (isUserInitiated(parent)) return parent.uuid;
|
|
34
|
+
cur = parent;
|
|
35
|
+
}
|
|
36
|
+
return entry.uuid;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sum usage fields from an array of assistant entries
|
|
40
|
+
function sumUsage(entries) {
|
|
41
|
+
let input = 0, cc = 0, cr = 0, out = 0;
|
|
42
|
+
let ephUser = 0, ephAsst = 0, ephTool = 0;
|
|
43
|
+
let webSearch = 0, webFetch = 0;
|
|
44
|
+
let serviceTier = null, speed = null;
|
|
45
|
+
const models = new Set();
|
|
46
|
+
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
const u = e.message?.usage || {};
|
|
49
|
+
input += u.input_tokens || 0;
|
|
50
|
+
cc += u.cache_creation_input_tokens || 0;
|
|
51
|
+
cr += u.cache_read_input_tokens || 0;
|
|
52
|
+
out += u.output_tokens || 0;
|
|
53
|
+
ephUser += u.ephemeral_user_input_tokens || 0;
|
|
54
|
+
ephAsst += u.ephemeral_assistant_input_tokens || 0;
|
|
55
|
+
ephTool += u.ephemeral_tool_input_tokens || 0;
|
|
56
|
+
if (!serviceTier && u.service_tier) serviceTier = u.service_tier;
|
|
57
|
+
if (!speed && u.speed) speed = u.speed;
|
|
58
|
+
|
|
59
|
+
const content = e.message?.content;
|
|
60
|
+
if (Array.isArray(content)) {
|
|
61
|
+
for (const block of content) {
|
|
62
|
+
if (block.type !== 'tool_use') continue;
|
|
63
|
+
if (block.name === 'web_search' || block.name === 'WebSearch') webSearch++;
|
|
64
|
+
if (block.name === 'web_fetch' || block.name === 'WebFetch') webFetch++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (e.message?.model) models.add(e.message.model);
|
|
68
|
+
}
|
|
69
|
+
return { input, cc, cr, out, ephUser, ephAsst, ephTool, webSearch, webFetch, serviceTier, speed, models };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeRecord(first, rootUuid, usage, filePaths) {
|
|
73
|
+
const { input, cc, cr, out, ephUser, ephAsst, ephTool, webSearch, webFetch, serviceTier, speed, models } = usage;
|
|
74
|
+
return {
|
|
75
|
+
sessionId: first.sessionId || null,
|
|
76
|
+
uuid: rootUuid,
|
|
77
|
+
parentUuid: first.parentUuid || null,
|
|
78
|
+
timestamp: first.timestamp || new Date().toISOString(),
|
|
79
|
+
gitBranch: first.gitBranch || null,
|
|
80
|
+
task_from_git_branch: extractTasksFromBranch(first.gitBranch),
|
|
81
|
+
model: [...models].join(',') || null,
|
|
82
|
+
input_tokens: input,
|
|
83
|
+
cache_creation_input_tokens: cc,
|
|
84
|
+
cache_read_input_tokens: cr,
|
|
85
|
+
output_tokens: out,
|
|
86
|
+
ephemeral_user_input_tokens: ephUser,
|
|
87
|
+
ephemeral_assistant_input_tokens: ephAsst,
|
|
88
|
+
ephemeral_tool_input_tokens: ephTool,
|
|
89
|
+
web_search_requests: webSearch,
|
|
90
|
+
web_fetch_requests: webFetch,
|
|
91
|
+
file_paths: filePaths,
|
|
92
|
+
enterprise_usd_per_token: null,
|
|
93
|
+
service_tier: serviceTier,
|
|
94
|
+
speed: speed || (cc > 0 ? 'fast' : 'normal'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
23
98
|
function parseNewLines(filePath) {
|
|
24
99
|
const offset = state.getOffset(filePath);
|
|
25
100
|
let stat;
|
|
@@ -32,64 +107,81 @@ function parseNewLines(filePath) {
|
|
|
32
107
|
fs.closeSync(fd);
|
|
33
108
|
state.setOffset(filePath, offset + read);
|
|
34
109
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
for (const raw of lines) {
|
|
110
|
+
// ── Parse all entries ─────────────────────────────────────────────────────
|
|
111
|
+
const byUuid = new Map();
|
|
112
|
+
for (const raw of buf.slice(0, read).toString('utf8').split('\n')) {
|
|
39
113
|
const line = raw.trim();
|
|
40
114
|
if (!line) continue;
|
|
41
115
|
let entry;
|
|
42
116
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
117
|
+
if (entry.uuid) byUuid.set(entry.uuid, entry);
|
|
118
|
+
}
|
|
43
119
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!usage) continue;
|
|
120
|
+
// ── Group assistant turns by prompt root ──────────────────────────────────
|
|
121
|
+
const promptGroups = new Map(); // rootUuid → assistant entries[]
|
|
47
122
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
123
|
+
for (const entry of byUuid.values()) {
|
|
124
|
+
const isAssistant = entry.type === 'assistant' || entry.message?.role === 'assistant';
|
|
125
|
+
if (!isAssistant || !entry.message?.usage) continue;
|
|
126
|
+
const rootUuid = findPromptRoot(entry, byUuid);
|
|
127
|
+
if (!promptGroups.has(rootUuid)) promptGroups.set(rootUuid, []);
|
|
128
|
+
promptGroups.get(rootUuid).push(entry);
|
|
129
|
+
}
|
|
52
130
|
|
|
53
|
-
|
|
131
|
+
// ── Per prompt: bucket turns by file anchor, emit one record per file ─────
|
|
132
|
+
const records = [];
|
|
133
|
+
|
|
134
|
+
for (const [rootUuid, turns] of promptGroups) {
|
|
135
|
+
turns.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
136
|
+
const first = turns[0];
|
|
137
|
+
|
|
138
|
+
// Label each turn with the file it writes (null if none)
|
|
139
|
+
const turnFiles = turns.map(e => {
|
|
140
|
+
const content = e.message?.content;
|
|
141
|
+
if (!Array.isArray(content)) return null;
|
|
54
142
|
for (const block of content) {
|
|
55
|
-
if (block.type
|
|
56
|
-
if (block.name === 'web_search' || block.name === 'WebSearch') webSearchRequests++;
|
|
57
|
-
if (block.name === 'web_fetch' || block.name === 'WebFetch') webFetchRequests++;
|
|
58
|
-
if (FILE_TOOLS.has(block.name)) {
|
|
143
|
+
if (block.type === 'tool_use' && WRITE_TOOLS.has(block.name)) {
|
|
59
144
|
const fp = block.input?.file_path || block.input?.path;
|
|
60
|
-
if (fp && typeof fp === 'string')
|
|
145
|
+
if (fp && typeof fp === 'string') return fp;
|
|
61
146
|
}
|
|
62
147
|
}
|
|
148
|
+
return null;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const hasAnyFile = turnFiles.some(f => f !== null);
|
|
152
|
+
|
|
153
|
+
if (!hasAnyFile) {
|
|
154
|
+
// Pure conversation or Bash-only — one record, empty file_paths
|
|
155
|
+
records.push(makeRecord(first, rootUuid, sumUsage(turns), []));
|
|
156
|
+
continue;
|
|
63
157
|
}
|
|
64
158
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
speed: usage.speed || (usage.cache_creation_input_tokens > 0 ? 'fast' : 'normal'),
|
|
92
|
-
});
|
|
159
|
+
// Attribute orphan turns (no file) to the NEXT anchor turn.
|
|
160
|
+
// Trailing orphans after the last anchor → attributed to the last file seen.
|
|
161
|
+
// Group: Map of filePath → turns[]
|
|
162
|
+
const fileGroups = new Map();
|
|
163
|
+
let lastFile = turnFiles.find(f => f !== null); // seed with first anchor
|
|
164
|
+
|
|
165
|
+
// First pass: find the first anchor and assign pre-anchor orphans to it
|
|
166
|
+
const firstAnchorIdx = turnFiles.findIndex(f => f !== null);
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < turns.length; i++) {
|
|
169
|
+
const file = turnFiles[i] !== null ? turnFiles[i]
|
|
170
|
+
: (i < firstAnchorIdx ? turnFiles[firstAnchorIdx] // pre-anchor → first anchor
|
|
171
|
+
: lastFile); // post-anchor → last anchor
|
|
172
|
+
|
|
173
|
+
if (!fileGroups.has(file)) fileGroups.set(file, []);
|
|
174
|
+
fileGroups.get(file).push(turns[i]);
|
|
175
|
+
if (turnFiles[i] !== null) lastFile = turnFiles[i];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Emit one record per file, uuid disambiguated by appending file index
|
|
179
|
+
let idx = 0;
|
|
180
|
+
for (const [fp, fileTurns] of fileGroups) {
|
|
181
|
+
const uuid = idx === 0 ? rootUuid : `${rootUuid}:${idx}`;
|
|
182
|
+
records.push(makeRecord(first, uuid, sumUsage(fileTurns), [fp]));
|
|
183
|
+
idx++;
|
|
184
|
+
}
|
|
93
185
|
}
|
|
94
186
|
|
|
95
187
|
return records;
|