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.
@@ -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
- }