codex-usage-analyzer 0.1.0 → 0.2.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/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +125 -185
- package/SECURITY.md +44 -0
- package/bin/codex-usage-analyzer.js +4 -2
- package/docs/account-usage-contract.md +85 -0
- package/docs/account-usage.schema.json +99 -0
- package/docs/downstream-integration.md +195 -0
- package/package.json +20 -8
- package/src/account-usage.js +115 -0
- package/src/app-server-client.js +229 -0
- package/src/cli.js +64 -51
- package/src/errors.js +46 -0
- package/src/format-account-usage.js +81 -0
- package/src/index.d.ts +63 -33
- package/src/index.js +10 -14
- package/src/analyze.js +0 -239
- package/src/fixtures/sample-v2-snapshot.js +0 -103
- package/src/parser/activity-aggregate.js +0 -264
- package/src/parser/asset-aggregate.js +0 -363
- package/src/parser/codex-home.js +0 -37
- package/src/parser/model-aggregate.js +0 -204
- package/src/parser/session-jsonl.js +0 -231
- package/src/parser/skill-plugin-aggregate.js +0 -228
- package/src/parser/token-aggregate.js +0 -229
- package/src/snapshot/index.js +0 -6
- package/src/snapshot/v2-schema.js +0 -519
- package/src/snapshot/v2-types.d.ts +0 -109
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { USAGE_SNAPSHOT_V2_SCHEMA_VERSION } from "../snapshot/v2-schema.js";
|
|
2
|
-
|
|
3
|
-
export const sampleUsageSnapshotV2 = Object.freeze({
|
|
4
|
-
schemaVersion: USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
|
|
5
|
-
capturedAt: "2026-06-12T00:00:00.000Z",
|
|
6
|
-
producer: {
|
|
7
|
-
name: "codex-usage-analyzer",
|
|
8
|
-
version: "0.1.0"
|
|
9
|
-
},
|
|
10
|
-
codexProfile: {
|
|
11
|
-
displayName: "postmelee",
|
|
12
|
-
username: "meleeisdeveloping",
|
|
13
|
-
planLabel: "Pro"
|
|
14
|
-
},
|
|
15
|
-
usage: {
|
|
16
|
-
totalTokens: 10300000000,
|
|
17
|
-
peakDailyTokens: 703000000,
|
|
18
|
-
tokenBreakdown: {
|
|
19
|
-
inputTokens: 646900000,
|
|
20
|
-
outputTokens: 34500000,
|
|
21
|
-
cacheReadTokens: 10300000000,
|
|
22
|
-
cacheWriteTokens: 11000000,
|
|
23
|
-
reasoningTokens: null
|
|
24
|
-
},
|
|
25
|
-
daily: [
|
|
26
|
-
{
|
|
27
|
-
date: "2026-06-06",
|
|
28
|
-
totalTokens: 158000000,
|
|
29
|
-
inputTokens: null,
|
|
30
|
-
outputTokens: null,
|
|
31
|
-
cacheReadTokens: null,
|
|
32
|
-
cacheWriteTokens: null,
|
|
33
|
-
reasoningTokens: null
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
models: {
|
|
38
|
-
favoriteModel: {
|
|
39
|
-
model: "gpt-5-codex",
|
|
40
|
-
displayName: "GPT-5 Codex",
|
|
41
|
-
totalTokens: 7000000000,
|
|
42
|
-
usageCount: null,
|
|
43
|
-
basis: "tokens",
|
|
44
|
-
inputTokens: null,
|
|
45
|
-
outputTokens: null,
|
|
46
|
-
cacheReadTokens: null,
|
|
47
|
-
cacheWriteTokens: null,
|
|
48
|
-
reasoningTokens: null
|
|
49
|
-
},
|
|
50
|
-
items: [
|
|
51
|
-
{
|
|
52
|
-
model: "gpt-5-codex",
|
|
53
|
-
displayName: "GPT-5 Codex",
|
|
54
|
-
totalTokens: 7000000000,
|
|
55
|
-
usageCount: null,
|
|
56
|
-
basis: "tokens",
|
|
57
|
-
inputTokens: null,
|
|
58
|
-
outputTokens: null,
|
|
59
|
-
cacheReadTokens: null,
|
|
60
|
-
cacheWriteTokens: null,
|
|
61
|
-
reasoningTokens: null
|
|
62
|
-
}
|
|
63
|
-
]
|
|
64
|
-
},
|
|
65
|
-
activity: {
|
|
66
|
-
longestTaskDurationMs: 6780000,
|
|
67
|
-
currentStreakDays: 46,
|
|
68
|
-
longestStreakDays: 46,
|
|
69
|
-
fastModePercent: 55,
|
|
70
|
-
reasoningEffort: "xhigh",
|
|
71
|
-
reasoningEffortPercent: 76,
|
|
72
|
-
totalThreads: 1735
|
|
73
|
-
},
|
|
74
|
-
skills: {
|
|
75
|
-
exploredCount: 49,
|
|
76
|
-
totalUsed: 3144,
|
|
77
|
-
topSkills: [
|
|
78
|
-
{
|
|
79
|
-
id: "pr-merge-cleanup",
|
|
80
|
-
name: "pr-merge-cleanup",
|
|
81
|
-
displayName: "pr-merge-cleanup",
|
|
82
|
-
usageCount: 563
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
},
|
|
86
|
-
plugins: {
|
|
87
|
-
topPlugins: []
|
|
88
|
-
},
|
|
89
|
-
codexAssets: {
|
|
90
|
-
avatar: {
|
|
91
|
-
kind: "remote-url",
|
|
92
|
-
url: "/assets/postmelee-avatar.png",
|
|
93
|
-
assetRef: null,
|
|
94
|
-
contentType: "image/png"
|
|
95
|
-
},
|
|
96
|
-
pet: null
|
|
97
|
-
},
|
|
98
|
-
extensions: {
|
|
99
|
-
"codexUsageAnalyzer.fixture": {
|
|
100
|
-
note: "sample-backed skeleton"
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
});
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { resolveCodexHome } from "./codex-home.js";
|
|
2
|
-
import {
|
|
3
|
-
discoverSessionJsonlFiles,
|
|
4
|
-
normalizeSessionTokenCountEvent,
|
|
5
|
-
readSessionJsonlEntries
|
|
6
|
-
} from "./session-jsonl.js";
|
|
7
|
-
import {
|
|
8
|
-
getTokenUsageTotal,
|
|
9
|
-
getUtcDate
|
|
10
|
-
} from "./token-aggregate.js";
|
|
11
|
-
|
|
12
|
-
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
13
|
-
|
|
14
|
-
export async function aggregateActivityFromCodexHome(options = {}) {
|
|
15
|
-
const { codexHome, source } = resolveCodexHome(options);
|
|
16
|
-
const discovery = await discoverSessionJsonlFiles(codexHome);
|
|
17
|
-
const aggregate = await aggregateActivityFromSessionFiles(discovery.files, options);
|
|
18
|
-
|
|
19
|
-
aggregate.diagnostics.source = source;
|
|
20
|
-
aggregate.diagnostics.discovery = summarizeDiscovery(discovery.diagnostics);
|
|
21
|
-
|
|
22
|
-
if (discovery.files.length === 0) {
|
|
23
|
-
aggregate.diagnostics.status = "unavailable";
|
|
24
|
-
aggregate.diagnostics.reason = "session_jsonl_not_found";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return aggregate;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function aggregateActivityFromSessionFiles(files, options = {}) {
|
|
31
|
-
const state = createAggregateState(normalizeNow(options.now));
|
|
32
|
-
|
|
33
|
-
for await (const entry of readSessionJsonlEntries(files)) {
|
|
34
|
-
state.diagnostics.entriesScanned += 1;
|
|
35
|
-
|
|
36
|
-
if (entry.kind === "malformed_line") {
|
|
37
|
-
state.diagnostics.malformedLines += 1;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (entry.kind === "file_error") {
|
|
42
|
-
state.diagnostics.fileErrors += 1;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const tokenEvent = normalizeSessionTokenCountEvent(entry.event);
|
|
47
|
-
if (tokenEvent === null) {
|
|
48
|
-
state.diagnostics.ignoredEvents += 1;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
state.diagnostics.tokenEvents += 1;
|
|
53
|
-
applyDuration(state, tokenEvent.durationMs);
|
|
54
|
-
applyEffort(state, tokenEvent.effort);
|
|
55
|
-
applyModeDiagnostic(state, tokenEvent.mode);
|
|
56
|
-
applyUsageDate(state, tokenEvent);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return finalizeAggregate(files.length, state);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function createAggregateState(now) {
|
|
63
|
-
return {
|
|
64
|
-
datesWithUsage: new Set(),
|
|
65
|
-
diagnostics: {
|
|
66
|
-
status: "unavailable",
|
|
67
|
-
reason: "no_activity_events",
|
|
68
|
-
source: "session_jsonl",
|
|
69
|
-
filesScanned: 0,
|
|
70
|
-
entriesScanned: 0,
|
|
71
|
-
ignoredEvents: 0,
|
|
72
|
-
tokenEvents: 0,
|
|
73
|
-
tokenEventsWithDateUsage: 0,
|
|
74
|
-
tokenEventsWithDuration: 0,
|
|
75
|
-
tokenEventsWithEffort: 0,
|
|
76
|
-
tokenEventsWithMode: 0,
|
|
77
|
-
malformedLines: 0,
|
|
78
|
-
fileErrors: 0,
|
|
79
|
-
unavailableFields: ["fastModePercent"],
|
|
80
|
-
fastModeReason: "source_unconfirmed",
|
|
81
|
-
streakDateBasis: "utc_date_from_session_token_usage",
|
|
82
|
-
profileParity: "not_guaranteed",
|
|
83
|
-
profileParityReason: "remote_profile_api_not_used",
|
|
84
|
-
discovery: []
|
|
85
|
-
},
|
|
86
|
-
effortCounts: new Map(),
|
|
87
|
-
effortOrder: new Map(),
|
|
88
|
-
longestTaskDurationMs: null,
|
|
89
|
-
now
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function applyDuration(state, value) {
|
|
94
|
-
const durationMs = readNonNegativeInteger(value);
|
|
95
|
-
if (durationMs === null) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
state.diagnostics.tokenEventsWithDuration += 1;
|
|
100
|
-
state.longestTaskDurationMs = Math.max(state.longestTaskDurationMs ?? 0, durationMs);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function applyEffort(state, value) {
|
|
104
|
-
const effort = readNonEmptyString(value);
|
|
105
|
-
if (effort === null) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
state.diagnostics.tokenEventsWithEffort += 1;
|
|
110
|
-
state.effortCounts.set(effort, (state.effortCounts.get(effort) ?? 0) + 1);
|
|
111
|
-
if (!state.effortOrder.has(effort)) {
|
|
112
|
-
state.effortOrder.set(effort, state.effortOrder.size);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function applyModeDiagnostic(state, value) {
|
|
117
|
-
if (readNonEmptyString(value) !== null) {
|
|
118
|
-
state.diagnostics.tokenEventsWithMode += 1;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function applyUsageDate(state, tokenEvent) {
|
|
123
|
-
if (tokenEvent.lastTokenUsage === null) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const totalTokens = getTokenUsageTotal(tokenEvent.lastTokenUsage);
|
|
128
|
-
if (totalTokens === null || totalTokens <= 0) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const date = getUtcDate(tokenEvent.timestamp);
|
|
133
|
-
if (date === null) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
state.diagnostics.tokenEventsWithDateUsage += 1;
|
|
138
|
-
state.datesWithUsage.add(date);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function finalizeAggregate(filesScanned, state) {
|
|
142
|
-
const effort = getDominantEffort(state);
|
|
143
|
-
const streak = calculateStreaks(state.datesWithUsage, state.now);
|
|
144
|
-
const hasActivity = filesScanned > 0 || state.diagnostics.tokenEvents > 0;
|
|
145
|
-
|
|
146
|
-
state.diagnostics.filesScanned = filesScanned;
|
|
147
|
-
state.diagnostics.status = hasActivity ? "ok" : state.diagnostics.status;
|
|
148
|
-
state.diagnostics.reason = hasActivity ? null : state.diagnostics.reason;
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
activity: {
|
|
152
|
-
longestTaskDurationMs: state.longestTaskDurationMs,
|
|
153
|
-
currentStreakDays: streak.currentStreakDays,
|
|
154
|
-
longestStreakDays: streak.longestStreakDays,
|
|
155
|
-
fastModePercent: null,
|
|
156
|
-
reasoningEffort: effort?.name ?? null,
|
|
157
|
-
reasoningEffortPercent: effort?.percent ?? null,
|
|
158
|
-
totalThreads: filesScanned > 0 ? filesScanned : null
|
|
159
|
-
},
|
|
160
|
-
diagnostics: state.diagnostics
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function getDominantEffort(state) {
|
|
165
|
-
const total = state.diagnostics.tokenEventsWithEffort;
|
|
166
|
-
if (total === 0) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const [name, count] = Array.from(state.effortCounts.entries())
|
|
171
|
-
.sort(([leftName, leftCount], [rightName, rightCount]) => {
|
|
172
|
-
const countDiff = rightCount - leftCount;
|
|
173
|
-
if (countDiff !== 0) return countDiff;
|
|
174
|
-
|
|
175
|
-
const orderDiff = state.effortOrder.get(leftName) - state.effortOrder.get(rightName);
|
|
176
|
-
if (orderDiff !== 0) return orderDiff;
|
|
177
|
-
|
|
178
|
-
return leftName.localeCompare(rightName);
|
|
179
|
-
})[0];
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
name,
|
|
183
|
-
percent: roundPercent((count / total) * 100)
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function calculateStreaks(dates, now) {
|
|
188
|
-
if (dates.size === 0) {
|
|
189
|
-
return {
|
|
190
|
-
currentStreakDays: null,
|
|
191
|
-
longestStreakDays: null
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const dayNumbers = Array.from(dates)
|
|
196
|
-
.map(dateToDayNumber)
|
|
197
|
-
.sort((left, right) => left - right);
|
|
198
|
-
|
|
199
|
-
let longest = 0;
|
|
200
|
-
let run = 0;
|
|
201
|
-
let previous = null;
|
|
202
|
-
|
|
203
|
-
for (const dayNumber of dayNumbers) {
|
|
204
|
-
run = previous !== null && dayNumber === previous + 1 ? run + 1 : 1;
|
|
205
|
-
longest = Math.max(longest, run);
|
|
206
|
-
previous = dayNumber;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const nowDay = dateToDayNumber(getUtcDate(now.toISOString()));
|
|
210
|
-
let current = 0;
|
|
211
|
-
for (let day = nowDay; dates.has(dayNumberToDate(day)); day -= 1) {
|
|
212
|
-
current += 1;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
currentStreakDays: current,
|
|
217
|
-
longestStreakDays: longest
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function normalizeNow(value) {
|
|
222
|
-
if (value === undefined || value === null) {
|
|
223
|
-
return new Date();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
227
|
-
if (Number.isNaN(date.getTime())) {
|
|
228
|
-
throw new TypeError("now must be a valid date");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return date;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function dateToDayNumber(date) {
|
|
235
|
-
return Math.floor(Date.parse(`${date}T00:00:00.000Z`) / MS_PER_DAY);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function dayNumberToDate(dayNumber) {
|
|
239
|
-
return new Date(dayNumber * MS_PER_DAY).toISOString().slice(0, 10);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function readNonEmptyString(value) {
|
|
243
|
-
if (typeof value !== "string") {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const trimmed = value.trim();
|
|
248
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function readNonNegativeInteger(value) {
|
|
252
|
-
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function roundPercent(value) {
|
|
256
|
-
return Math.round(value * 100) / 100;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function summarizeDiscovery(diagnostics) {
|
|
260
|
-
return diagnostics.map((diagnostic) => ({
|
|
261
|
-
code: diagnostic.code,
|
|
262
|
-
severity: diagnostic.severity
|
|
263
|
-
}));
|
|
264
|
-
}
|