codex-usage-analyzer 0.1.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/README.md +255 -0
- package/bin/codex-usage-analyzer.js +10 -0
- package/package.json +41 -0
- package/src/analyze.js +239 -0
- package/src/cli.js +75 -0
- package/src/fixtures/sample-v2-snapshot.js +103 -0
- package/src/index.d.ts +46 -0
- package/src/index.js +17 -0
- package/src/parser/activity-aggregate.js +264 -0
- package/src/parser/asset-aggregate.js +363 -0
- package/src/parser/codex-home.js +37 -0
- package/src/parser/model-aggregate.js +204 -0
- package/src/parser/session-jsonl.js +231 -0
- package/src/parser/skill-plugin-aggregate.js +228 -0
- package/src/parser/token-aggregate.js +229 -0
- package/src/snapshot/index.js +6 -0
- package/src/snapshot/v2-schema.js +519 -0
- package/src/snapshot/v2-types.d.ts +109 -0
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
});
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { UsageSnapshotV2 } from "./snapshot/v2-types.js";
|
|
2
|
+
|
|
3
|
+
export * from "./snapshot/v2-types.js";
|
|
4
|
+
|
|
5
|
+
export declare const ANALYZER_NAME: "codex-usage-analyzer";
|
|
6
|
+
export declare const ANALYZER_VERSION: string;
|
|
7
|
+
|
|
8
|
+
export interface AnalyzeUsageOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Capture timestamp for the production snapshot. Defaults to the analyzer run time.
|
|
11
|
+
*/
|
|
12
|
+
capturedAt?: string | Date | null;
|
|
13
|
+
/**
|
|
14
|
+
* Local Codex home root for parser source discovery.
|
|
15
|
+
*/
|
|
16
|
+
codexHome?: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic clock value for date and streak calculations in parser tests.
|
|
19
|
+
*/
|
|
20
|
+
now?: string | Date | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CliIo {
|
|
24
|
+
stdout?: {
|
|
25
|
+
write(chunk: string): unknown;
|
|
26
|
+
};
|
|
27
|
+
stderr?: {
|
|
28
|
+
write(chunk: string): unknown;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Analyze usage into a production UsageSnapshot v2.
|
|
34
|
+
*
|
|
35
|
+
* Local session JSONL sources, including allowlisted usage and skill/plugin
|
|
36
|
+
* invocation events, are parsed when available. Unavailable fields are
|
|
37
|
+
* represented with zero/null/empty values plus a namespaced diagnostic
|
|
38
|
+
* extension. This function does not return the sample fixture.
|
|
39
|
+
*/
|
|
40
|
+
export declare function analyzeUsage(options?: AnalyzeUsageOptions): Promise<UsageSnapshotV2>;
|
|
41
|
+
|
|
42
|
+
export declare function createSampleUsageSnapshotV2(
|
|
43
|
+
overrides?: Record<string, unknown>
|
|
44
|
+
): UsageSnapshotV2;
|
|
45
|
+
|
|
46
|
+
export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ANALYZER_NAME,
|
|
3
|
+
ANALYZER_VERSION,
|
|
4
|
+
analyzeUsage,
|
|
5
|
+
createSampleUsageSnapshotV2
|
|
6
|
+
} from "./analyze.js";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
|
|
10
|
+
assertUsageSnapshotV2,
|
|
11
|
+
isUsageSnapshotV2,
|
|
12
|
+
validateUsageSnapshotV2
|
|
13
|
+
} from "./snapshot/v2-schema.js";
|
|
14
|
+
|
|
15
|
+
export { sampleUsageSnapshotV2 } from "./fixtures/sample-v2-snapshot.js";
|
|
16
|
+
|
|
17
|
+
export { runCli } from "./cli.js";
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
}
|