agent-orchestrator-kit 0.10.0 → 0.11.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/CHANGELOG.md +10 -0
- package/README.md +13 -5
- package/bin/agent-orchestrator.js +200 -59
- package/bin/session-client.js +1 -1
- package/bin/spend-collect.js +197 -2
- package/package.json +1 -1
- package/templates/.agents/rules/session-handoff.mdc +2 -2
- package/templates/.agents/skills/agent-orchestration/SKILL.md +3 -3
- package/templates/.agents/subagents/session-handoff.md +2 -2
- package/templates/.agents/subagents/spec-archiver.md +1 -1
- package/templates/scripts/cursor-spend-collect.cjs +416 -36
- package/templates/scripts/cursor-spend-hook.cjs +8 -2
|
@@ -18,6 +18,73 @@ function addNullable(a, b) {
|
|
|
18
18
|
return (a ?? 0) + (b ?? 0);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const GROK_46 = {
|
|
22
|
+
inputPerM: 2,
|
|
23
|
+
cachedPerM: 0.5,
|
|
24
|
+
outputPerM: 6,
|
|
25
|
+
longInputPerM: 4,
|
|
26
|
+
longCachedPerM: 1,
|
|
27
|
+
longOutputPerM: 12,
|
|
28
|
+
longAt: 200000,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function ratesForModel(model) {
|
|
32
|
+
const id = String(model || '').toLowerCase();
|
|
33
|
+
if (!id) return null;
|
|
34
|
+
let rates = null;
|
|
35
|
+
if (id.includes('grok-4.6') || id.includes('grok-4-6')) rates = { ...GROK_46 };
|
|
36
|
+
else if (id.includes('grok-4.5') || id.includes('grok-4-5')) {
|
|
37
|
+
rates = { ...GROK_46, cachedPerM: 0.3, longCachedPerM: 0.6 };
|
|
38
|
+
} else {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (id.includes('fast')) {
|
|
42
|
+
for (const key of ['inputPerM', 'cachedPerM', 'outputPerM', 'longInputPerM', 'longCachedPerM', 'longOutputPerM']) {
|
|
43
|
+
rates[key] *= 2;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return rates;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function estimateCursorCostUsd({ model, inputTokens, outputTokens, cacheReadTokens, totalTokens } = {}) {
|
|
50
|
+
const rates = ratesForModel(model);
|
|
51
|
+
if (rates) {
|
|
52
|
+
const input = numOrNull(inputTokens);
|
|
53
|
+
const output = numOrNull(outputTokens) ?? 0;
|
|
54
|
+
if (input == null && output == 0) return null;
|
|
55
|
+
const totalInput = input ?? 0;
|
|
56
|
+
const cached = Math.min(numOrNull(cacheReadTokens) ?? 0, totalInput);
|
|
57
|
+
const fresh = Math.max(0, totalInput - cached);
|
|
58
|
+
const long = totalInput >= rates.longAt;
|
|
59
|
+
const inputRate = long ? rates.longInputPerM : rates.inputPerM;
|
|
60
|
+
const cachedRate = long ? rates.longCachedPerM : rates.cachedPerM;
|
|
61
|
+
const outputRate = long ? rates.longOutputPerM : rates.outputPerM;
|
|
62
|
+
const usd = (fresh * inputRate + cached * cachedRate + output * outputRate) / 1e6;
|
|
63
|
+
return Math.round(usd * 10000) / 10000;
|
|
64
|
+
}
|
|
65
|
+
const input = numOrNull(inputTokens);
|
|
66
|
+
const output = numOrNull(outputTokens);
|
|
67
|
+
if (input != null || output != null) {
|
|
68
|
+
const usd = ((input ?? 0) * 3 + (output ?? 0) * 15) / 1e6;
|
|
69
|
+
return Math.round(usd * 10000) / 10000;
|
|
70
|
+
}
|
|
71
|
+
const total = numOrNull(totalTokens);
|
|
72
|
+
if (total != null) {
|
|
73
|
+
const usd = total * 3.5 / 1e6;
|
|
74
|
+
return Math.round(usd * 10000) / 10000;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function describeCursorCostEstimate(args) {
|
|
80
|
+
const usd = estimateCursorCostUsd(args);
|
|
81
|
+
if (usd == null) return null;
|
|
82
|
+
return {
|
|
83
|
+
usd,
|
|
84
|
+
costSource: ratesForModel(args && args.model) != null ? 'api-estimate' : 'api-estimate-fallback',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
21
88
|
function resolveBaseDir(payload) {
|
|
22
89
|
const cwd = process.cwd();
|
|
23
90
|
if (existsSync(join(cwd, 'openspec', 'changes'))) return cwd;
|
|
@@ -28,6 +95,67 @@ function resolveBaseDir(payload) {
|
|
|
28
95
|
return cwd;
|
|
29
96
|
}
|
|
30
97
|
|
|
98
|
+
const CURSOR_LEFTOVER_GRACE_MS = 120000;
|
|
99
|
+
|
|
100
|
+
function cursorSpendFingerprint(row) {
|
|
101
|
+
if (!row || typeof row !== 'object') return null;
|
|
102
|
+
const input = numOrNull(row.inputTokens);
|
|
103
|
+
const output = numOrNull(row.outputTokens);
|
|
104
|
+
if (input == null && output == null) return null;
|
|
105
|
+
const model = String(row.model || row.modelId || '');
|
|
106
|
+
const cache = numOrNull(row.cacheReadTokens) ?? 0;
|
|
107
|
+
return `${model}|${input ?? 0}|${output ?? 0}|${cache}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function preferCursorSource(previous, next) {
|
|
111
|
+
if (!previous) return next;
|
|
112
|
+
if (!next) return previous;
|
|
113
|
+
if (next.event === 'stop' && previous.event !== 'stop') return next;
|
|
114
|
+
if (previous.event === 'stop' && next.event !== 'stop') return previous;
|
|
115
|
+
const prevAt = Date.parse(previous.at);
|
|
116
|
+
const nextAt = Date.parse(next.at);
|
|
117
|
+
if (Number.isFinite(nextAt) && Number.isFinite(prevAt) && nextAt !== prevAt) {
|
|
118
|
+
return nextAt > prevAt ? next : previous;
|
|
119
|
+
}
|
|
120
|
+
return (next.totalTokens ?? 0) >= (previous.totalTokens ?? 0) ? next : previous;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stripCursorCollectMeta(record) {
|
|
124
|
+
if (!record || typeof record !== 'object') return record;
|
|
125
|
+
const { event, ...rest } = record;
|
|
126
|
+
return rest;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function dedupeCursorSources(sources) {
|
|
130
|
+
const best = new Map();
|
|
131
|
+
const rest = [];
|
|
132
|
+
for (const src of sources || []) {
|
|
133
|
+
if (!src || src.platform !== 'cursor') {
|
|
134
|
+
rest.push(src);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const fp = cursorSpendFingerprint(src);
|
|
138
|
+
if (!fp) {
|
|
139
|
+
rest.push(src);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
best.set(fp, preferCursorSource(best.get(fp), src));
|
|
143
|
+
}
|
|
144
|
+
return [...rest, ...[...best.values()].map(stripCursorCollectMeta)];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function leftoverWindowEnd(metrics, last) {
|
|
148
|
+
if (metrics.pending && metrics.pending.startedAt) return metrics.pending.startedAt;
|
|
149
|
+
if (!last || !last.endedAt) return null;
|
|
150
|
+
const end = Date.parse(last.endedAt);
|
|
151
|
+
if (!Number.isFinite(end)) return null;
|
|
152
|
+
return new Date(end + CURSOR_LEFTOVER_GRACE_MS).toISOString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function leftoverEndExclusive(metrics) {
|
|
156
|
+
return Boolean(metrics.pending && metrics.pending.startedAt);
|
|
157
|
+
}
|
|
158
|
+
|
|
31
159
|
function existingIds(metrics) {
|
|
32
160
|
const ids = new Set();
|
|
33
161
|
for (const session of metrics.sessions || []) {
|
|
@@ -38,18 +166,96 @@ function existingIds(metrics) {
|
|
|
38
166
|
return ids;
|
|
39
167
|
}
|
|
40
168
|
|
|
169
|
+
function loadCursorUsageById(cwd) {
|
|
170
|
+
const filePath = join(cwd, '.agents', 'spend', 'cursor-usage.jsonl');
|
|
171
|
+
const bestById = new Map();
|
|
172
|
+
if (!existsSync(filePath)) return bestById;
|
|
173
|
+
let text;
|
|
174
|
+
try {
|
|
175
|
+
text = readFileSync(filePath, 'utf-8');
|
|
176
|
+
} catch {
|
|
177
|
+
return bestById;
|
|
178
|
+
}
|
|
179
|
+
for (const line of text.split('\n')) {
|
|
180
|
+
if (!line.trim()) continue;
|
|
181
|
+
let row;
|
|
182
|
+
try {
|
|
183
|
+
row = JSON.parse(line);
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!row || typeof row !== 'object') continue;
|
|
188
|
+
const id = row.id == null || row.id === '' ? null : String(row.id);
|
|
189
|
+
if (!id) continue;
|
|
190
|
+
const inputTokens = numOrNull(row.inputTokens);
|
|
191
|
+
const outputTokens = numOrNull(row.outputTokens);
|
|
192
|
+
if (inputTokens == null && outputTokens == null) continue;
|
|
193
|
+
const totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
194
|
+
const previous = bestById.get(id);
|
|
195
|
+
const previousTotal = previous
|
|
196
|
+
? (numOrNull(previous.inputTokens) ?? 0) + (numOrNull(previous.outputTokens) ?? 0)
|
|
197
|
+
: -1;
|
|
198
|
+
if (!previous || totalTokens >= previousTotal) bestById.set(id, row);
|
|
199
|
+
}
|
|
200
|
+
return bestById;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function applyCursorEstimate(record) {
|
|
204
|
+
const described = describeCursorCostEstimate({
|
|
205
|
+
model: record.model,
|
|
206
|
+
inputTokens: record.inputTokens,
|
|
207
|
+
outputTokens: record.outputTokens,
|
|
208
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
209
|
+
totalTokens: record.totalTokens,
|
|
210
|
+
});
|
|
211
|
+
if (!described) return false;
|
|
212
|
+
let changed = false;
|
|
213
|
+
if (record.costUsdEstimated !== described.usd) {
|
|
214
|
+
record.costUsdEstimated = described.usd;
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
if (record.costSource !== described.costSource) {
|
|
218
|
+
record.costSource = described.costSource;
|
|
219
|
+
changed = true;
|
|
220
|
+
}
|
|
221
|
+
return changed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function attachCursorEstimates(sources, byId) {
|
|
225
|
+
let changed = false;
|
|
226
|
+
for (const src of sources || []) {
|
|
227
|
+
if (!src || src.platform !== 'cursor') continue;
|
|
228
|
+
const row = src.id ? byId.get(String(src.id)) : null;
|
|
229
|
+
if (row) {
|
|
230
|
+
const cache = numOrNull(row.cacheReadTokens);
|
|
231
|
+
if (src.cacheReadTokens == null && cache != null) {
|
|
232
|
+
src.cacheReadTokens = cache;
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
if (!src.model && (row.model || row.modelId)) {
|
|
236
|
+
src.model = row.model || row.modelId;
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (applyCursorEstimate(src)) changed = true;
|
|
241
|
+
}
|
|
242
|
+
return changed;
|
|
243
|
+
}
|
|
244
|
+
|
|
41
245
|
function sourceTotals(sources) {
|
|
42
246
|
let inputTokens = null;
|
|
43
247
|
let outputTokens = null;
|
|
44
248
|
let totalTokens = null;
|
|
45
249
|
let costUsd = null;
|
|
250
|
+
let costUsdEstimated = null;
|
|
46
251
|
for (const src of sources || []) {
|
|
47
252
|
inputTokens = addNullable(inputTokens, numOrNull(src.inputTokens));
|
|
48
253
|
outputTokens = addNullable(outputTokens, numOrNull(src.outputTokens));
|
|
49
254
|
totalTokens = addNullable(totalTokens, numOrNull(src.totalTokens));
|
|
50
255
|
if (src.costUsd != null) costUsd = addNullable(costUsd, numOrNull(src.costUsd));
|
|
256
|
+
costUsdEstimated = addNullable(costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
51
257
|
}
|
|
52
|
-
return { inputTokens, outputTokens, totalTokens, costUsd };
|
|
258
|
+
return { inputTokens, outputTokens, totalTokens, costUsd, costUsdEstimated };
|
|
53
259
|
}
|
|
54
260
|
|
|
55
261
|
function looksOverridden(session) {
|
|
@@ -70,6 +276,7 @@ function emptyPlatform(source = 'none') {
|
|
|
70
276
|
totalTokens: null,
|
|
71
277
|
costUsd: null,
|
|
72
278
|
ampCredits: null,
|
|
279
|
+
costUsdEstimated: null,
|
|
73
280
|
source,
|
|
74
281
|
};
|
|
75
282
|
}
|
|
@@ -77,7 +284,7 @@ function emptyPlatform(source = 'none') {
|
|
|
77
284
|
function recompute(metrics) {
|
|
78
285
|
const phases = {};
|
|
79
286
|
const totals = { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 };
|
|
80
|
-
const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null };
|
|
287
|
+
const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
81
288
|
const byPlatform = {
|
|
82
289
|
cursor: emptyPlatform(),
|
|
83
290
|
claude: emptyPlatform(),
|
|
@@ -102,12 +309,13 @@ function recompute(metrics) {
|
|
|
102
309
|
outputTokens: null,
|
|
103
310
|
totalTokens: null,
|
|
104
311
|
costUsd: null,
|
|
312
|
+
costUsdEstimated: null,
|
|
105
313
|
agents: [],
|
|
106
314
|
models: [],
|
|
107
315
|
};
|
|
108
316
|
phase.sessions += 1;
|
|
109
317
|
phase.durationMs = addNullable(phase.durationMs, numOrNull(session.durationMs));
|
|
110
|
-
for (const spendKey of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd']) {
|
|
318
|
+
for (const spendKey of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
111
319
|
const fromSession = numOrNull(session[spendKey]);
|
|
112
320
|
let value = fromSession;
|
|
113
321
|
if (value == null) {
|
|
@@ -135,6 +343,8 @@ function recompute(metrics) {
|
|
|
135
343
|
bucket.outputTokens = addNullable(bucket.outputTokens, numOrNull(src.outputTokens));
|
|
136
344
|
bucket.totalTokens = addNullable(bucket.totalTokens, numOrNull(src.totalTokens));
|
|
137
345
|
bucket.costUsd = addNullable(bucket.costUsd, numOrNull(src.costUsd));
|
|
346
|
+
bucket.ampCredits = addNullable(bucket.ampCredits, numOrNull(src.ampCredits));
|
|
347
|
+
bucket.costUsdEstimated = addNullable(bucket.costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
138
348
|
if (platform === 'claude') bucket.source = 'claude-jsonl';
|
|
139
349
|
else if (platform === 'amp') bucket.source = 'amp-thread';
|
|
140
350
|
else if (platform === 'cursor') bucket.source = 'cursor-hook';
|
|
@@ -149,11 +359,14 @@ function recompute(metrics) {
|
|
|
149
359
|
totalTokens: null,
|
|
150
360
|
costUsd: null,
|
|
151
361
|
ampCredits: null,
|
|
362
|
+
costUsdEstimated: null,
|
|
152
363
|
};
|
|
153
364
|
row.inputTokens = addNullable(row.inputTokens, numOrNull(src.inputTokens));
|
|
154
365
|
row.outputTokens = addNullable(row.outputTokens, numOrNull(src.outputTokens));
|
|
155
366
|
row.totalTokens = addNullable(row.totalTokens, numOrNull(src.totalTokens));
|
|
156
367
|
row.costUsd = addNullable(row.costUsd, numOrNull(src.costUsd));
|
|
368
|
+
row.ampCredits = addNullable(row.ampCredits, numOrNull(src.ampCredits));
|
|
369
|
+
row.costUsdEstimated = addNullable(row.costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
157
370
|
byModel.set(modelKey, row);
|
|
158
371
|
}
|
|
159
372
|
}
|
|
@@ -169,49 +382,123 @@ function recompute(metrics) {
|
|
|
169
382
|
metrics.spendByModel = [...byModel.values()];
|
|
170
383
|
}
|
|
171
384
|
|
|
172
|
-
function incomingCursorSources(cwd, existing, windowStart) {
|
|
173
|
-
const filePath = join(cwd, '.agents', 'spend', 'cursor-usage.jsonl');
|
|
174
|
-
if (!existsSync(filePath)) return [];
|
|
385
|
+
function incomingCursorSources(cwd, existing, fingerprints, windowStart, windowEnd, byId, exclusiveEnd) {
|
|
175
386
|
const startMs = windowStart ? Date.parse(windowStart) : NaN;
|
|
387
|
+
const endMs = windowEnd ? Date.parse(windowEnd) : NaN;
|
|
176
388
|
const bestById = new Map();
|
|
177
|
-
for (const
|
|
178
|
-
if (
|
|
179
|
-
let row;
|
|
180
|
-
try {
|
|
181
|
-
row = JSON.parse(line);
|
|
182
|
-
} catch {
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (!row || typeof row !== 'object') continue;
|
|
186
|
-
const id = row.id == null || row.id === '' ? null : String(row.id);
|
|
187
|
-
if (!id || existing.has(id)) continue;
|
|
389
|
+
for (const [id, row] of byId) {
|
|
390
|
+
if (existing.has(id)) continue;
|
|
188
391
|
const atMs = Date.parse(row.at);
|
|
189
392
|
if (Number.isFinite(startMs) && Number.isFinite(atMs) && atMs < startMs) continue;
|
|
393
|
+
if (Number.isFinite(endMs) && Number.isFinite(atMs)) {
|
|
394
|
+
if (exclusiveEnd) {
|
|
395
|
+
if (atMs >= endMs) continue;
|
|
396
|
+
} else if (atMs > endMs) continue;
|
|
397
|
+
}
|
|
190
398
|
const inputTokens = numOrNull(row.inputTokens);
|
|
191
399
|
const outputTokens = numOrNull(row.outputTokens);
|
|
192
400
|
if (inputTokens == null && outputTokens == null) continue;
|
|
193
|
-
const
|
|
401
|
+
const fp = cursorSpendFingerprint(row);
|
|
402
|
+
if (fp && fingerprints.has(fp)) continue;
|
|
403
|
+
const cacheReadTokens = numOrNull(row.cacheReadTokens);
|
|
194
404
|
const record = {
|
|
195
405
|
id,
|
|
196
406
|
platform: 'cursor',
|
|
197
407
|
model: row.model || row.modelId || null,
|
|
198
408
|
inputTokens,
|
|
199
409
|
outputTokens,
|
|
200
|
-
totalTokens,
|
|
410
|
+
totalTokens: (inputTokens ?? 0) + (outputTokens ?? 0),
|
|
201
411
|
costUsd: null,
|
|
202
412
|
ampCredits: null,
|
|
203
413
|
at: row.at == null ? null : String(row.at),
|
|
414
|
+
event: row.event || null,
|
|
204
415
|
};
|
|
416
|
+
if (cacheReadTokens != null) record.cacheReadTokens = cacheReadTokens;
|
|
417
|
+
applyCursorEstimate(record);
|
|
205
418
|
const previous = bestById.get(id);
|
|
206
419
|
if (!previous || (record.totalTokens ?? 0) >= (previous.totalTokens ?? 0)) {
|
|
207
420
|
bestById.set(id, record);
|
|
208
421
|
}
|
|
209
422
|
}
|
|
210
|
-
|
|
423
|
+
const bestByFingerprint = new Map();
|
|
424
|
+
for (const record of bestById.values()) {
|
|
425
|
+
const fp = cursorSpendFingerprint(record) || record.id;
|
|
426
|
+
bestByFingerprint.set(fp, preferCursorSource(bestByFingerprint.get(fp), record));
|
|
427
|
+
}
|
|
428
|
+
return [...bestByFingerprint.values()].map(stripCursorCollectMeta);
|
|
211
429
|
}
|
|
212
430
|
|
|
213
|
-
function
|
|
214
|
-
const
|
|
431
|
+
function existingFingerprints(metrics) {
|
|
432
|
+
const set = new Set();
|
|
433
|
+
for (const session of metrics.sessions || []) {
|
|
434
|
+
for (const src of session.sources || []) {
|
|
435
|
+
const fp = cursorSpendFingerprint(src);
|
|
436
|
+
if (fp) set.add(fp);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return set;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function sessionHasSpendNumbers(session) {
|
|
443
|
+
return (
|
|
444
|
+
numOrNull(session.inputTokens) != null
|
|
445
|
+
|| numOrNull(session.outputTokens) != null
|
|
446
|
+
|| numOrNull(session.totalTokens) != null
|
|
447
|
+
|| numOrNull(session.costUsd) != null
|
|
448
|
+
|| numOrNull(session.ampCredits) != null
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function sessionSpendFrozen(session) {
|
|
453
|
+
if (!session) return false;
|
|
454
|
+
if (session.spendSource === 'flag') return true;
|
|
455
|
+
if (!sessionHasSpendNumbers(session)) return false;
|
|
456
|
+
if (!session.spendSource || session.spendSource === 'adapter' || session.spendSource === 'unreported') return false;
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function syncAdapterSessionTotals(session) {
|
|
461
|
+
if (sessionSpendFrozen(session)) return false;
|
|
462
|
+
const totals = sourceTotals(session.sources || []);
|
|
463
|
+
let changed = false;
|
|
464
|
+
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
465
|
+
if (session[key] !== totals[key]) {
|
|
466
|
+
session[key] = totals[key];
|
|
467
|
+
changed = true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if ((session.sources || []).length > 0 && session.spendSource !== 'adapter') {
|
|
471
|
+
session.spendSource = 'adapter';
|
|
472
|
+
changed = true;
|
|
473
|
+
}
|
|
474
|
+
return changed;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function enrichMetrics(metrics, cwd) {
|
|
478
|
+
const byId = loadCursorUsageById(cwd);
|
|
479
|
+
let changed = false;
|
|
480
|
+
for (const session of metrics.sessions || []) {
|
|
481
|
+
const deduped = dedupeCursorSources(session.sources || []);
|
|
482
|
+
if (deduped.length !== (session.sources || []).length) {
|
|
483
|
+
session.sources = deduped;
|
|
484
|
+
changed = true;
|
|
485
|
+
} else {
|
|
486
|
+
session.sources = deduped;
|
|
487
|
+
}
|
|
488
|
+
if (attachCursorEstimates(session.sources || [], byId)) changed = true;
|
|
489
|
+
if (syncAdapterSessionTotals(session)) changed = true;
|
|
490
|
+
if (
|
|
491
|
+
session.spendSource === 'unreported'
|
|
492
|
+
&& (session.inputTokens != null || session.totalTokens != null || (session.sources || []).length)
|
|
493
|
+
) {
|
|
494
|
+
session.spendSource = 'adapter';
|
|
495
|
+
changed = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return changed;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function backfillMetricsFile(cwd, filePath) {
|
|
215
502
|
if (!existsSync(filePath)) return;
|
|
216
503
|
let metrics;
|
|
217
504
|
try {
|
|
@@ -222,27 +509,99 @@ function backfillChange(cwd, changeName) {
|
|
|
222
509
|
if (!metrics || typeof metrics !== 'object') return;
|
|
223
510
|
const sessions = Array.isArray(metrics.sessions) ? metrics.sessions : [];
|
|
224
511
|
if (!sessions.length) return;
|
|
512
|
+
let collapsed = false;
|
|
513
|
+
for (const session of sessions) {
|
|
514
|
+
const next = dedupeCursorSources(session.sources || []);
|
|
515
|
+
if (next.length !== (session.sources || []).length) collapsed = true;
|
|
516
|
+
session.sources = next;
|
|
517
|
+
}
|
|
225
518
|
const last = sessions[sessions.length - 1];
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
519
|
+
const byId = loadCursorUsageById(cwd);
|
|
520
|
+
const leftoverEnd = leftoverWindowEnd(metrics, last);
|
|
521
|
+
const incoming = last.endedAt && leftoverEnd
|
|
522
|
+
? incomingCursorSources(
|
|
523
|
+
cwd,
|
|
524
|
+
existingIds(metrics),
|
|
525
|
+
existingFingerprints(metrics),
|
|
526
|
+
last.endedAt,
|
|
527
|
+
leftoverEnd,
|
|
528
|
+
byId,
|
|
529
|
+
leftoverEndExclusive(metrics),
|
|
530
|
+
)
|
|
531
|
+
: [];
|
|
532
|
+
if (incoming.length) {
|
|
533
|
+
last.sources = [...(last.sources || []), ...incoming];
|
|
534
|
+
if (!sessionSpendFrozen(last)) {
|
|
535
|
+
const totals = sourceTotals(last.sources);
|
|
536
|
+
last.inputTokens = totals.inputTokens;
|
|
537
|
+
last.outputTokens = totals.outputTokens;
|
|
538
|
+
last.totalTokens = totals.totalTokens;
|
|
539
|
+
last.costUsd = totals.costUsd;
|
|
540
|
+
last.costUsdEstimated = totals.costUsdEstimated;
|
|
541
|
+
last.spendSource = 'adapter';
|
|
542
|
+
}
|
|
240
543
|
}
|
|
544
|
+
const enriched = enrichMetrics(metrics, cwd);
|
|
545
|
+
if (!incoming.length && !enriched && !collapsed) return;
|
|
241
546
|
metrics.updatedAt = new Date().toISOString();
|
|
242
547
|
recompute(metrics);
|
|
243
548
|
writeFileSync(filePath, `${JSON.stringify(metrics, null, 2)}\n`);
|
|
244
549
|
}
|
|
245
550
|
|
|
551
|
+
function backfillChange(cwd, changeName) {
|
|
552
|
+
backfillMetricsFile(cwd, join(cwd, 'openspec', 'changes', changeName, 'metrics.json'));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function archivedChangeName(dirName) {
|
|
556
|
+
const match = /^(\d{4}-\d{2}-\d{2})-(.+)$/.exec(dirName);
|
|
557
|
+
return match ? match[2] : null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function metricsArchivedAtMs(metricsPath) {
|
|
561
|
+
try {
|
|
562
|
+
const metrics = JSON.parse(readFileSync(metricsPath, 'utf-8'));
|
|
563
|
+
if (metrics && metrics.archivedAt) {
|
|
564
|
+
const t = Date.parse(metrics.archivedAt);
|
|
565
|
+
if (Number.isFinite(t)) return t;
|
|
566
|
+
}
|
|
567
|
+
} catch {}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function newestArchiveDirForName(archiveDir, changeName) {
|
|
572
|
+
let best = null;
|
|
573
|
+
let bestKey = -Infinity;
|
|
574
|
+
let names;
|
|
575
|
+
try {
|
|
576
|
+
names = readdirSync(archiveDir);
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
for (const entry of names) {
|
|
581
|
+
if (archivedChangeName(entry) !== changeName) continue;
|
|
582
|
+
const full = join(archiveDir, entry);
|
|
583
|
+
try {
|
|
584
|
+
if (!statSync(full).isDirectory()) continue;
|
|
585
|
+
} catch {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const fromMetrics = metricsArchivedAtMs(join(full, 'metrics.json'));
|
|
589
|
+
let key = fromMetrics;
|
|
590
|
+
if (key == null) {
|
|
591
|
+
try {
|
|
592
|
+
key = statSync(full).mtimeMs;
|
|
593
|
+
} catch {
|
|
594
|
+
key = 0;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (key >= bestKey) {
|
|
598
|
+
bestKey = key;
|
|
599
|
+
best = full;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return best;
|
|
603
|
+
}
|
|
604
|
+
|
|
246
605
|
function main(raw) {
|
|
247
606
|
let payload = {};
|
|
248
607
|
try {
|
|
@@ -253,6 +612,7 @@ function main(raw) {
|
|
|
253
612
|
const cwd = resolveBaseDir(payload && typeof payload === 'object' ? payload : {});
|
|
254
613
|
const changesDir = join(cwd, 'openspec', 'changes');
|
|
255
614
|
if (!existsSync(changesDir)) return;
|
|
615
|
+
const activeNames = new Set();
|
|
256
616
|
for (const name of readdirSync(changesDir)) {
|
|
257
617
|
if (name === 'archive') continue;
|
|
258
618
|
const full = join(changesDir, name);
|
|
@@ -261,8 +621,28 @@ function main(raw) {
|
|
|
261
621
|
} catch {
|
|
262
622
|
continue;
|
|
263
623
|
}
|
|
624
|
+
activeNames.add(name);
|
|
264
625
|
backfillChange(cwd, name);
|
|
265
626
|
}
|
|
627
|
+
const archiveDir = join(changesDir, 'archive');
|
|
628
|
+
if (!existsSync(archiveDir)) return;
|
|
629
|
+
let archiveEntries;
|
|
630
|
+
try {
|
|
631
|
+
archiveEntries = readdirSync(archiveDir);
|
|
632
|
+
} catch {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const archivedNames = new Set();
|
|
636
|
+
for (const entry of archiveEntries) {
|
|
637
|
+
const changeName = archivedChangeName(entry);
|
|
638
|
+
if (!changeName || activeNames.has(changeName)) continue;
|
|
639
|
+
archivedNames.add(changeName);
|
|
640
|
+
}
|
|
641
|
+
for (const changeName of archivedNames) {
|
|
642
|
+
const dir = newestArchiveDirForName(archiveDir, changeName);
|
|
643
|
+
if (!dir) continue;
|
|
644
|
+
backfillMetricsFile(cwd, join(dir, 'metrics.json'));
|
|
645
|
+
}
|
|
266
646
|
}
|
|
267
647
|
|
|
268
648
|
if (process.stdin.isTTY) {
|
|
@@ -41,7 +41,13 @@ function main(raw) {
|
|
|
41
41
|
|
|
42
42
|
const generationId = payload.generation_id ? String(payload.generation_id) : '';
|
|
43
43
|
const conversationId = payload.conversation_id ? String(payload.conversation_id) : '';
|
|
44
|
-
const
|
|
44
|
+
const cacheReadTokens = numOrNull(payload.cache_read_tokens);
|
|
45
|
+
const id = generationId || [
|
|
46
|
+
conversationId || 'none',
|
|
47
|
+
inputTokens ?? 0,
|
|
48
|
+
outputTokens ?? 0,
|
|
49
|
+
cacheReadTokens ?? 0,
|
|
50
|
+
].join(':');
|
|
45
51
|
|
|
46
52
|
const record = {
|
|
47
53
|
id,
|
|
@@ -51,7 +57,7 @@ function main(raw) {
|
|
|
51
57
|
modelId: payload.model_id ? String(payload.model_id) : null,
|
|
52
58
|
inputTokens,
|
|
53
59
|
outputTokens,
|
|
54
|
-
cacheReadTokens
|
|
60
|
+
cacheReadTokens,
|
|
55
61
|
cacheWriteTokens: numOrNull(payload.cache_write_tokens),
|
|
56
62
|
at: new Date().toISOString(),
|
|
57
63
|
};
|