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
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# codex-usage-analyzer
|
|
2
|
+
|
|
3
|
+
`codex-usage-analyzer` is the local usage analysis package that emits `UsageSnapshot v2` JSON.
|
|
4
|
+
|
|
5
|
+
The package is designed to be reused by product-specific CLIs and web services. It analyzes local usage sources and returns a validated snapshot; account identity, submit tokens, public profile URLs, and rendered cards belong to the product that wraps it.
|
|
6
|
+
|
|
7
|
+
## CLI
|
|
8
|
+
|
|
9
|
+
The published CLI entry point is:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx codex-usage-analyzer@latest analyze --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The `--json` mode writes a single `UsageSnapshot v2` object to stdout. Errors and usage text are written to stderr.
|
|
16
|
+
|
|
17
|
+
The production `analyze --json` path reads local Codex session JSONL files from the Codex home directory. It uses `--codex-home <path>` when provided, otherwise `CODEX_HOME`, otherwise the default Codex home. When the session source is missing or unreadable, the command still emits a valid snapshot: required numeric totals are zero, unavailable usage details are `null` or empty arrays, and `extensions["codexUsageAnalyzer.diagnostics"]` explains the unavailable source. The production path does not return the sample fixture.
|
|
18
|
+
|
|
19
|
+
The parser currently derives token totals, daily token buckets, model ranking, skill/plugin ranking, longest task duration, streaks, reasoning effort, and total thread count from allowlisted session event fields. Skill/plugin rankings are counted only from actual invocation events that can be classified by session tool catalog metadata; catalog or enabled-tool lists alone do not increment usage. Custom/local skill and plugin names may appear in `topSkills` and `topPlugins`, but the analyzer does not emit raw local file paths, raw JSONL lines, session ids, prompts, responses, tool input, or tool output.
|
|
20
|
+
|
|
21
|
+
Streak fields are local-only analyzer results. The current parser treats a UTC date as active when local session JSONL contains positive `last_token_usage` for that date.
|
|
22
|
+
|
|
23
|
+
Codex Desktop's profile screen is backed by its remote profile data and may include account-level usage that is no longer present in local session files, usage from another device, or data retained after local cleanup. For that reason, `activity.currentStreakDays` and `activity.longestStreakDays` are not guaranteed to match the Codex Desktop profile. The diagnostic extension includes `profileComparison.parity: "not_guaranteed"` when the analyzer has not compared against a remote profile baseline.
|
|
24
|
+
|
|
25
|
+
The analyzer does not call Codex Desktop remote profile APIs or plugin-store APIs. `skills` and `plugins` are local session-derived fields, so they are unavailable when actual invocation source events are absent.
|
|
26
|
+
|
|
27
|
+
`codexAssets.pet` is a safe logical reference, not an image export. The analyzer can report the Codex Desktop built-in pet catalog and the selected pet id when that setting is available. If no selected pet setting is persisted, it follows Codex Desktop's default and reports the built-in `codex` pet as `codex-built-in:pet:codex`. When a selected custom pet is found under `pets/<id>/pet.json` with an allowlisted spritesheet extension, the analyzer reports `codex-local:pet:custom-selected`.
|
|
28
|
+
|
|
29
|
+
The default analyzer output does not include local file paths, custom pet directory names, image bytes, data URLs, or generated image artifacts. Files under `generated_images/` are treated as private generated artifacts and are not promoted to `codexAssets`. A wrapper that wants to render custom pet images in a web application must provide its own opt-in asset export, upload, or local serving layer.
|
|
30
|
+
|
|
31
|
+
Local repository smoke command:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node bin/codex-usage-analyzer.js analyze --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Parser fixture smoke command:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node bin/codex-usage-analyzer.js analyze --json --codex-home src/__tests__/fixtures/parser
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Asset fixture smoke command:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node bin/codex-usage-analyzer.js analyze --json --codex-home src/__tests__/fixtures/assets
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Development fixture command:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node bin/codex-usage-analyzer.js analyze --json --fixture-sample
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `--fixture-sample` mode is for tests, examples, and contract inspection only. It returns the packaged sample snapshot and must not be treated as real local Codex usage.
|
|
56
|
+
|
|
57
|
+
## Profile Parity Smoke (Repository Only)
|
|
58
|
+
|
|
59
|
+
Use the profile smoke helper from a repository checkout when you want to compare
|
|
60
|
+
a local analyzer result with values manually copied from Codex Desktop's profile
|
|
61
|
+
UI. The comparison uses a redacted baseline file; do not commit a baseline copied
|
|
62
|
+
from a real account.
|
|
63
|
+
|
|
64
|
+
The helper is not published as an npm package binary. It is a maintainer QA
|
|
65
|
+
tool for release and parser parity checks.
|
|
66
|
+
|
|
67
|
+
Create a production snapshot:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node bin/codex-usage-analyzer.js analyze --json > <local-snapshot.json>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Create a redacted baseline using
|
|
74
|
+
`src/__tests__/fixtures/profile-baseline/redacted-baseline.json` as the shape
|
|
75
|
+
reference, then compare:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node scripts/profile-smoke.js --baseline <redacted-baseline.json> --snapshot <local-snapshot.json>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The smoke output is a field-level summary. Result statuses mean:
|
|
82
|
+
|
|
83
|
+
- `match`: expected and actual values are equal.
|
|
84
|
+
- `within_tolerance`: numeric values differ only within the baseline tolerance.
|
|
85
|
+
- `mismatch`: the field is comparable and differs outside tolerance.
|
|
86
|
+
- `not_comparable`: the baseline intentionally marks the field as visible in
|
|
87
|
+
profile UI but not comparable to local analyzer data.
|
|
88
|
+
- `skipped`: the baseline did not include that expected field.
|
|
89
|
+
|
|
90
|
+
Mismatch reasons distinguish parser-bug candidates from expected source
|
|
91
|
+
differences:
|
|
92
|
+
|
|
93
|
+
- `numeric_mismatch`, `value_mismatch`, `actual_field_absent`, and ranking
|
|
94
|
+
shape reasons mean a comparable field differed.
|
|
95
|
+
- `source_mismatch` means the baseline author marked that field as comparing
|
|
96
|
+
different sources, such as remote profile data versus local analyzer data.
|
|
97
|
+
- `profile_parity_not_guaranteed` means the local snapshot itself reports that
|
|
98
|
+
remote profile parity is not guaranteed, and the field is source-sensitive.
|
|
99
|
+
- `remote_profile_source_differs` is used with `not_comparable` fields that
|
|
100
|
+
should be visible in the profile baseline but intentionally not compared.
|
|
101
|
+
|
|
102
|
+
Use optional baseline `sourcePolicy` metadata to keep a field comparable while
|
|
103
|
+
labeling source-driven differences:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"sourcePolicy": {
|
|
108
|
+
"activity.totalThreads": "source_mismatch",
|
|
109
|
+
"skills.topSkills": "source_mismatch",
|
|
110
|
+
"plugins.topPlugins": "source_mismatch"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`sourcePolicy` belongs only to the redacted smoke baseline. It is not part of
|
|
116
|
+
`UsageSnapshot v2`, and the analyzer output schema does not change. A
|
|
117
|
+
source-aware mismatch still makes the smoke command exit nonzero; the field
|
|
118
|
+
reason is what separates source differences from likely parser regressions.
|
|
119
|
+
|
|
120
|
+
Known mismatch reasons:
|
|
121
|
+
|
|
122
|
+
- Codex Desktop profile values can come from a remote account-level source,
|
|
123
|
+
while this analyzer reads local session files and local Codex metadata.
|
|
124
|
+
- Local cleanup, migration, archiving, deletion, or another device can make the
|
|
125
|
+
profile UI and local analyzer cover different source ranges.
|
|
126
|
+
- Streaks use the analyzer's UTC date buckets from local token events; profile
|
|
127
|
+
UI can use remote activity data.
|
|
128
|
+
- Activity insights and thread counts can differ when the profile UI aggregates
|
|
129
|
+
remote account-level data and the analyzer reads retained local sessions.
|
|
130
|
+
- Top skills/plugins require actual local invocation events. Catalog, enabled
|
|
131
|
+
tool lists, or remote profile rankings alone are not counted.
|
|
132
|
+
- `--fixture-sample` snapshots are rejected by the profile smoke helper, so a
|
|
133
|
+
packaged example cannot pass as a real profile parity check.
|
|
134
|
+
|
|
135
|
+
Redaction rules for real local baselines:
|
|
136
|
+
|
|
137
|
+
- Keep only profile-visible numbers, model ids, ranking ids, and explicit
|
|
138
|
+
tolerance values needed for comparison.
|
|
139
|
+
- Do not include real account handles, emails, local private paths, credential
|
|
140
|
+
material, conversation identifiers, conversation titles, prompts, responses,
|
|
141
|
+
tool input/output bodies, image captures, or unredacted session JSONL.
|
|
142
|
+
|
|
143
|
+
## SDK
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
import {
|
|
147
|
+
analyzeUsage,
|
|
148
|
+
assertUsageSnapshotV2,
|
|
149
|
+
createSampleUsageSnapshotV2,
|
|
150
|
+
validateUsageSnapshotV2
|
|
151
|
+
} from "codex-usage-analyzer";
|
|
152
|
+
|
|
153
|
+
const snapshot = await analyzeUsage();
|
|
154
|
+
assertUsageSnapshotV2(snapshot);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`analyzeUsage()` returns the production analyzer result. You can pass `codexHome` for deterministic tests or custom Codex home discovery:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const snapshot = await analyzeUsage({
|
|
161
|
+
codexHome: "/path/to/codex-home"
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When a local source is unavailable, fields use zero, `null`, empty arrays, and a namespaced diagnostic extension rather than sample values.
|
|
166
|
+
|
|
167
|
+
Use `createSampleUsageSnapshotV2()` only for examples, tests, and contract inspection.
|
|
168
|
+
|
|
169
|
+
Public exports:
|
|
170
|
+
|
|
171
|
+
- `analyzeUsage(options?)`
|
|
172
|
+
- `createSampleUsageSnapshotV2(overrides?)`
|
|
173
|
+
- `validateUsageSnapshotV2(value)`
|
|
174
|
+
- `assertUsageSnapshotV2(value)`
|
|
175
|
+
- `isUsageSnapshotV2(value)`
|
|
176
|
+
- `USAGE_SNAPSHOT_V2_SCHEMA_VERSION`
|
|
177
|
+
- `sampleUsageSnapshotV2`
|
|
178
|
+
|
|
179
|
+
## Package Contents
|
|
180
|
+
|
|
181
|
+
The npm package includes the CLI entry point, runtime analyzer source, parser
|
|
182
|
+
modules, snapshot validators, type declarations, and the sample snapshot fixture
|
|
183
|
+
used by the SDK. It excludes repository tests, parser fixtures, working docs,
|
|
184
|
+
and repository-only smoke helper scripts.
|
|
185
|
+
|
|
186
|
+
## Ownership Boundary
|
|
187
|
+
|
|
188
|
+
The analyzer owns local usage fields such as token totals, token breakdown, model usage, skill usage, plugin usage, activity statistics, and safe Codex pet logical references.
|
|
189
|
+
|
|
190
|
+
Web products own GitHub login, display name, avatar URL, bio, profile visibility, submit tokens, devices, public URLs, rendered cards, and any uploaded or web-served pet image assets.
|
|
191
|
+
|
|
192
|
+
Product-specific wrappers can call this SDK and submit the resulting snapshot to their own service:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import {
|
|
196
|
+
analyzeUsage,
|
|
197
|
+
assertUsageSnapshotV2
|
|
198
|
+
} from "codex-usage-analyzer";
|
|
199
|
+
|
|
200
|
+
const snapshot = assertUsageSnapshotV2(await analyzeUsage());
|
|
201
|
+
await submitToProductService({ snapshot });
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Wrapper metadata such as bearer tokens, device ids, account handles, visibility, GitHub bio, GitHub avatar URLs, custom pet upload URLs, and card-only rendering hints must stay outside the analyzer snapshot.
|
|
205
|
+
|
|
206
|
+
## Tests
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npm test
|
|
210
|
+
node bin/codex-usage-analyzer.js analyze --json
|
|
211
|
+
node bin/codex-usage-analyzer.js analyze --json --codex-home src/__tests__/fixtures/parser
|
|
212
|
+
node bin/codex-usage-analyzer.js analyze --json --codex-home src/__tests__/fixtures/assets
|
|
213
|
+
node bin/codex-usage-analyzer.js analyze --json --fixture-sample
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The test suite validates the SDK exports, production parser behavior, asset safe output behavior, fixture-only CLI behavior, and `UsageSnapshot v2` schema rules.
|
|
217
|
+
|
|
218
|
+
## Release Checklist
|
|
219
|
+
|
|
220
|
+
Before publishing:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npm test
|
|
224
|
+
npm pack --dry-run
|
|
225
|
+
node bin/codex-usage-analyzer.js analyze --json
|
|
226
|
+
npx --yes github:postmelee/codex-usage-analyzer analyze --json
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
After the package is published:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npx --yes codex-usage-analyzer@latest analyze --json
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Do not paste raw production snapshot output into release notes, PR bodies, or
|
|
236
|
+
issue comments. Record only structural pass/fail results, exit codes, and
|
|
237
|
+
package metadata needed for release verification.
|
|
238
|
+
|
|
239
|
+
## Non-Goals
|
|
240
|
+
|
|
241
|
+
This package does not:
|
|
242
|
+
|
|
243
|
+
- perform GitHub OAuth
|
|
244
|
+
- use private Codex Desktop profile endpoints
|
|
245
|
+
- export custom pet image files by default
|
|
246
|
+
- issue or store submit tokens
|
|
247
|
+
- own public profile handles
|
|
248
|
+
- render cards or images
|
|
249
|
+
- update GitHub README files
|
|
250
|
+
- upload raw credential files
|
|
251
|
+
- expose local private paths
|
|
252
|
+
|
|
253
|
+
## Status
|
|
254
|
+
|
|
255
|
+
This repository is the standalone home for the analyzer package. The production analyzer path is separated from the packaged sample fixture, and the release checklist above is the maintainer path for npm publishing and npx verification. Broader parity work against Codex Desktop profile data remains outside the npm release flow.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from "../src/cli.js";
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
6
|
+
process.exitCode = exitCode;
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-usage-analyzer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Analyze local Codex usage and emit UsageSnapshot v2 JSON.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://github.com/postmelee/codex-usage-analyzer#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/postmelee/codex-usage-analyzer.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/postmelee/codex-usage-analyzer/issues"
|
|
13
|
+
},
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"codex-usage-analyzer": "bin/codex-usage-analyzer.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./src/index.d.ts",
|
|
21
|
+
"import": "./src/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"src/analyze.js",
|
|
27
|
+
"src/cli.js",
|
|
28
|
+
"src/fixtures/sample-v2-snapshot.js",
|
|
29
|
+
"src/index.d.ts",
|
|
30
|
+
"src/index.js",
|
|
31
|
+
"src/parser",
|
|
32
|
+
"src/snapshot",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/analyze.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { sampleUsageSnapshotV2 } from "./fixtures/sample-v2-snapshot.js";
|
|
2
|
+
import { aggregateActivityFromCodexHome } from "./parser/activity-aggregate.js";
|
|
3
|
+
import { aggregateCodexAssetsFromCodexHome } from "./parser/asset-aggregate.js";
|
|
4
|
+
import { aggregateModelUsageFromCodexHome } from "./parser/model-aggregate.js";
|
|
5
|
+
import { aggregateSkillPluginUsageFromCodexHome } from "./parser/skill-plugin-aggregate.js";
|
|
6
|
+
import { aggregateTokenUsageFromCodexHome } from "./parser/token-aggregate.js";
|
|
7
|
+
import {
|
|
8
|
+
USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
|
|
9
|
+
assertUsageSnapshotV2
|
|
10
|
+
} from "./snapshot/v2-schema.js";
|
|
11
|
+
|
|
12
|
+
export const ANALYZER_NAME = "codex-usage-analyzer";
|
|
13
|
+
export const ANALYZER_VERSION = "0.1.0";
|
|
14
|
+
|
|
15
|
+
export async function analyzeUsage(options = {}) {
|
|
16
|
+
const [
|
|
17
|
+
usageAggregate,
|
|
18
|
+
modelAggregate,
|
|
19
|
+
activityAggregate,
|
|
20
|
+
skillPluginAggregate,
|
|
21
|
+
assetAggregate
|
|
22
|
+
] = await Promise.all([
|
|
23
|
+
aggregateTokenUsageFromCodexHome(options),
|
|
24
|
+
aggregateModelUsageFromCodexHome(options),
|
|
25
|
+
aggregateActivityFromCodexHome(options),
|
|
26
|
+
aggregateSkillPluginUsageFromCodexHome(options),
|
|
27
|
+
aggregateCodexAssetsFromCodexHome(options)
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const snapshot = createUnavailableUsageSnapshotV2({
|
|
31
|
+
capturedAt: normalizeCapturedAt(options.capturedAt),
|
|
32
|
+
producer: {
|
|
33
|
+
name: ANALYZER_NAME,
|
|
34
|
+
version: ANALYZER_VERSION
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (usageAggregate.diagnostics.status === "ok") {
|
|
39
|
+
snapshot.usage = usageAggregate.usage;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (modelAggregate.diagnostics.status === "ok") {
|
|
43
|
+
snapshot.models = modelAggregate.models;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (activityAggregate.diagnostics.status === "ok") {
|
|
47
|
+
snapshot.activity = activityAggregate.activity;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (skillPluginAggregate.diagnostics.status === "ok") {
|
|
51
|
+
snapshot.skills = skillPluginAggregate.skills;
|
|
52
|
+
snapshot.plugins = skillPluginAggregate.plugins;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (assetAggregate.diagnostics.status === "ok") {
|
|
56
|
+
snapshot.codexAssets = assetAggregate.codexAssets;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
snapshot.extensions["codexUsageAnalyzer.diagnostics"] = createAnalyzerDiagnostics({
|
|
60
|
+
activity: activityAggregate,
|
|
61
|
+
assets: assetAggregate,
|
|
62
|
+
models: modelAggregate,
|
|
63
|
+
skillPlugin: skillPluginAggregate,
|
|
64
|
+
usage: usageAggregate
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return assertUsageSnapshotV2(snapshot);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createUnavailableUsageSnapshotV2(overrides = {}) {
|
|
71
|
+
return mergeSnapshot({
|
|
72
|
+
schemaVersion: USAGE_SNAPSHOT_V2_SCHEMA_VERSION,
|
|
73
|
+
capturedAt: new Date().toISOString(),
|
|
74
|
+
producer: {
|
|
75
|
+
name: ANALYZER_NAME,
|
|
76
|
+
version: ANALYZER_VERSION
|
|
77
|
+
},
|
|
78
|
+
usage: {
|
|
79
|
+
totalTokens: 0,
|
|
80
|
+
peakDailyTokens: null,
|
|
81
|
+
tokenBreakdown: createUnavailableTokenBreakdown(),
|
|
82
|
+
daily: []
|
|
83
|
+
},
|
|
84
|
+
models: {
|
|
85
|
+
favoriteModel: null,
|
|
86
|
+
items: []
|
|
87
|
+
},
|
|
88
|
+
activity: {
|
|
89
|
+
longestTaskDurationMs: null,
|
|
90
|
+
currentStreakDays: null,
|
|
91
|
+
longestStreakDays: null,
|
|
92
|
+
fastModePercent: null,
|
|
93
|
+
reasoningEffort: null,
|
|
94
|
+
reasoningEffortPercent: null,
|
|
95
|
+
totalThreads: null
|
|
96
|
+
},
|
|
97
|
+
skills: {
|
|
98
|
+
exploredCount: null,
|
|
99
|
+
totalUsed: null,
|
|
100
|
+
topSkills: []
|
|
101
|
+
},
|
|
102
|
+
plugins: {
|
|
103
|
+
topPlugins: []
|
|
104
|
+
},
|
|
105
|
+
extensions: {
|
|
106
|
+
"codexUsageAnalyzer.diagnostics": {
|
|
107
|
+
status: "unavailable",
|
|
108
|
+
reason: "local_sources_unavailable",
|
|
109
|
+
unavailableFields: ["usage", "models", "activity", "skills", "plugins"]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}, overrides);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createUnavailableTokenBreakdown() {
|
|
116
|
+
return {
|
|
117
|
+
inputTokens: null,
|
|
118
|
+
outputTokens: null,
|
|
119
|
+
cacheReadTokens: null,
|
|
120
|
+
cacheWriteTokens: null,
|
|
121
|
+
reasoningTokens: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createAnalyzerDiagnostics(aggregates) {
|
|
126
|
+
const skillPluginDiagnostics = aggregates.skillPlugin.diagnostics;
|
|
127
|
+
const diagnostics = {
|
|
128
|
+
usage: aggregates.usage.diagnostics,
|
|
129
|
+
models: aggregates.models.diagnostics,
|
|
130
|
+
activity: aggregates.activity.diagnostics,
|
|
131
|
+
skills: skillPluginDiagnostics,
|
|
132
|
+
plugins: skillPluginDiagnostics,
|
|
133
|
+
codexAssets: aggregates.assets.diagnostics
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const parsedFields = [];
|
|
137
|
+
const unavailableFields = [];
|
|
138
|
+
|
|
139
|
+
for (const field of ["usage", "models", "activity"]) {
|
|
140
|
+
if (diagnostics[field].status === "ok") {
|
|
141
|
+
parsedFields.push(field);
|
|
142
|
+
} else {
|
|
143
|
+
unavailableFields.push(field);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (skillPluginDiagnostics.status === "ok") {
|
|
148
|
+
parsedFields.push("skills", "plugins");
|
|
149
|
+
} else {
|
|
150
|
+
unavailableFields.push("skills", "plugins");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (diagnostics.codexAssets.status === "ok") {
|
|
154
|
+
parsedFields.push("codexAssets");
|
|
155
|
+
} else {
|
|
156
|
+
unavailableFields.push("codexAssets");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (diagnostics.activity.status === "ok") {
|
|
160
|
+
for (const field of diagnostics.activity.unavailableFields ?? []) {
|
|
161
|
+
unavailableFields.push(`activity.${field}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const status = parsedFields.length === 0
|
|
166
|
+
? "unavailable"
|
|
167
|
+
: unavailableFields.length > 0
|
|
168
|
+
? "partial"
|
|
169
|
+
: "ok";
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
status,
|
|
173
|
+
reason: getAnalyzerDiagnosticReason(status, diagnostics),
|
|
174
|
+
parser: "session_jsonl",
|
|
175
|
+
source: diagnostics.usage.source,
|
|
176
|
+
profileComparison: {
|
|
177
|
+
status: "not_performed",
|
|
178
|
+
reason: "remote_profile_api_not_used",
|
|
179
|
+
localStreakBasis: "session_jsonl_utc_dates",
|
|
180
|
+
remoteProfileBasis: "codex_desktop_remote_profile_api",
|
|
181
|
+
parity: "not_guaranteed"
|
|
182
|
+
},
|
|
183
|
+
parsedFields,
|
|
184
|
+
unavailableFields,
|
|
185
|
+
...diagnostics
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getAnalyzerDiagnosticReason(status, diagnostics) {
|
|
190
|
+
if (status === "ok") {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (status === "partial") {
|
|
195
|
+
return "local_sources_partially_available";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return diagnostics.usage.reason
|
|
199
|
+
?? diagnostics.models.reason
|
|
200
|
+
?? diagnostics.activity.reason
|
|
201
|
+
?? diagnostics.skills.reason
|
|
202
|
+
?? diagnostics.plugins.reason
|
|
203
|
+
?? diagnostics.codexAssets.reason
|
|
204
|
+
?? "local_sources_unavailable";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createSampleUsageSnapshotV2(overrides = {}) {
|
|
208
|
+
return mergeSnapshot(structuredClone(sampleUsageSnapshotV2), overrides);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeCapturedAt(value) {
|
|
212
|
+
if (value === undefined || value === null) {
|
|
213
|
+
return new Date().toISOString();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
217
|
+
|
|
218
|
+
if (Number.isNaN(date.getTime())) {
|
|
219
|
+
throw new TypeError("capturedAt must be a valid date");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return date.toISOString();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function mergeSnapshot(target, source) {
|
|
226
|
+
for (const [key, value] of Object.entries(source)) {
|
|
227
|
+
if (isPlainObject(value) && isPlainObject(target[key])) {
|
|
228
|
+
target[key] = mergeSnapshot(target[key], value);
|
|
229
|
+
} else {
|
|
230
|
+
target[key] = value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return target;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isPlainObject(value) {
|
|
238
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
239
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeUsage,
|
|
3
|
+
createSampleUsageSnapshotV2
|
|
4
|
+
} from "./analyze.js";
|
|
5
|
+
|
|
6
|
+
const USAGE = [
|
|
7
|
+
"Usage:",
|
|
8
|
+
" codex-usage-analyzer analyze --json [--codex-home <path>]",
|
|
9
|
+
" codex-usage-analyzer analyze --json --fixture-sample"
|
|
10
|
+
].join("\n");
|
|
11
|
+
|
|
12
|
+
export async function runCli(argv, io = {}) {
|
|
13
|
+
const stdout = io.stdout ?? process.stdout;
|
|
14
|
+
const stderr = io.stderr ?? process.stderr;
|
|
15
|
+
|
|
16
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
17
|
+
stdout.write(`${USAGE}\n`);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [command, ...flags] = argv;
|
|
22
|
+
|
|
23
|
+
if (command !== "analyze") {
|
|
24
|
+
stderr.write(`${USAGE}\n`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsedFlags = parseAnalyzeFlags(flags);
|
|
29
|
+
if (!parsedFlags.ok) {
|
|
30
|
+
stderr.write(`${USAGE}\n`);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const snapshot = parsedFlags.fixtureSample
|
|
35
|
+
? createSampleUsageSnapshotV2()
|
|
36
|
+
: await analyzeUsage({
|
|
37
|
+
codexHome: parsedFlags.codexHome
|
|
38
|
+
});
|
|
39
|
+
stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseAnalyzeFlags(flags) {
|
|
44
|
+
const parsed = {
|
|
45
|
+
codexHome: null,
|
|
46
|
+
fixtureSample: false,
|
|
47
|
+
hasJson: false,
|
|
48
|
+
ok: true
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
52
|
+
const flag = flags[index];
|
|
53
|
+
|
|
54
|
+
if (flag === "--json") {
|
|
55
|
+
parsed.hasJson = true;
|
|
56
|
+
} else if (flag === "--fixture-sample") {
|
|
57
|
+
parsed.fixtureSample = true;
|
|
58
|
+
} else if (flag === "--codex-home") {
|
|
59
|
+
const value = flags[index + 1];
|
|
60
|
+
if (value === undefined || value.startsWith("--")) {
|
|
61
|
+
return { ok: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
parsed.codexHome = value;
|
|
65
|
+
index += 1;
|
|
66
|
+
} else {
|
|
67
|
+
return { ok: false };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...parsed,
|
|
73
|
+
ok: parsed.hasJson
|
|
74
|
+
};
|
|
75
|
+
}
|