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
package/src/cli.js
CHANGED
|
@@ -1,75 +1,88 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { readAccountUsage } from "./account-usage.js";
|
|
2
|
+
import { CodexUsageError } from "./errors.js";
|
|
3
|
+
import { formatAccountUsage } from "./format-account-usage.js";
|
|
4
|
+
|
|
5
|
+
export const PACKAGE_NAME = "codex-usage-analyzer";
|
|
6
|
+
export const PACKAGE_VERSION = "0.2.0";
|
|
5
7
|
|
|
6
8
|
const USAGE = [
|
|
9
|
+
"codex-usage-analyzer - Read your Codex account usage",
|
|
10
|
+
"",
|
|
7
11
|
"Usage:",
|
|
8
|
-
" codex-usage-analyzer
|
|
9
|
-
" codex-usage-analyzer
|
|
12
|
+
" codex-usage-analyzer [usage] [--json]",
|
|
13
|
+
" codex-usage-analyzer [usage] --help",
|
|
14
|
+
" codex-usage-analyzer --version"
|
|
10
15
|
].join("\n");
|
|
11
16
|
|
|
12
|
-
export async function runCli(argv, io = {}) {
|
|
17
|
+
export async function runCli(argv, io = {}, dependencies = {}) {
|
|
13
18
|
const stdout = io.stdout ?? process.stdout;
|
|
14
19
|
const stderr = io.stderr ?? process.stderr;
|
|
20
|
+
const parsed = parseArguments(argv);
|
|
15
21
|
|
|
16
|
-
if (
|
|
22
|
+
if (parsed.action === "help") {
|
|
17
23
|
stdout.write(`${USAGE}\n`);
|
|
18
24
|
return 0;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
stderr.write(`${USAGE}\n`);
|
|
25
|
-
return 1;
|
|
27
|
+
if (parsed.action === "version") {
|
|
28
|
+
stdout.write(`${PACKAGE_VERSION}\n`);
|
|
29
|
+
return 0;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
if (!parsedFlags.ok) {
|
|
32
|
+
if (parsed.action === "invalid") {
|
|
30
33
|
stderr.write(`${USAGE}\n`);
|
|
31
34
|
return 1;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
: await analyzeUsage({
|
|
37
|
-
codexHome: parsedFlags.codexHome
|
|
38
|
-
});
|
|
39
|
-
stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
37
|
+
const readUsage = dependencies.readAccountUsage ?? readAccountUsage;
|
|
38
|
+
const formatUsage = dependencies.formatAccountUsage ?? formatAccountUsage;
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
40
|
+
try {
|
|
41
|
+
const usage = await readUsage();
|
|
42
|
+
const output = parsed.json
|
|
43
|
+
? JSON.stringify(usage, null, 2)
|
|
44
|
+
: formatUsage(usage);
|
|
45
|
+
stdout.write(`${output}\n`);
|
|
46
|
+
return 0;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof CodexUsageError) {
|
|
49
|
+
stderr.write(`${PACKAGE_NAME}: ${error.message} [${error.code}]\n`);
|
|
66
50
|
} else {
|
|
67
|
-
|
|
51
|
+
stderr.write(`${PACKAGE_NAME}: Unexpected failure. [UNEXPECTED_ERROR]\n`);
|
|
68
52
|
}
|
|
53
|
+
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArguments(argv) {
|
|
59
|
+
const args = [...argv];
|
|
60
|
+
|
|
61
|
+
if (args[0] === "usage") {
|
|
62
|
+
args.shift();
|
|
63
|
+
} else if (args[0] !== undefined && !args[0].startsWith("-")) {
|
|
64
|
+
return { action: "invalid" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (args.length === 0) {
|
|
68
|
+
return { action: "usage", json: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (args.length !== 1) {
|
|
72
|
+
return { action: "invalid" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (args[0] === "--json") {
|
|
76
|
+
return { action: "usage", json: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
80
|
+
return { action: "help" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
84
|
+
return { action: "version" };
|
|
69
85
|
}
|
|
70
86
|
|
|
71
|
-
return {
|
|
72
|
-
...parsed,
|
|
73
|
-
ok: parsed.hasJson
|
|
74
|
-
};
|
|
87
|
+
return { action: "invalid" };
|
|
75
88
|
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const CODEX_USAGE_ERROR_CODES = Object.freeze({
|
|
2
|
+
INVALID_TIMEOUT: "INVALID_TIMEOUT",
|
|
3
|
+
CODEX_NOT_FOUND: "CODEX_NOT_FOUND",
|
|
4
|
+
APP_SERVER_START_FAILED: "APP_SERVER_START_FAILED",
|
|
5
|
+
APP_SERVER_EXITED: "APP_SERVER_EXITED",
|
|
6
|
+
APP_SERVER_TIMEOUT: "APP_SERVER_TIMEOUT",
|
|
7
|
+
APP_SERVER_PROTOCOL_ERROR: "APP_SERVER_PROTOCOL_ERROR",
|
|
8
|
+
APP_SERVER_RPC_ERROR: "APP_SERVER_RPC_ERROR",
|
|
9
|
+
INVALID_ACCOUNT_USAGE_RESPONSE: "INVALID_ACCOUNT_USAGE_RESPONSE"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const ERROR_MESSAGES = Object.freeze({
|
|
13
|
+
[CODEX_USAGE_ERROR_CODES.INVALID_TIMEOUT]:
|
|
14
|
+
"timeoutMs must be an integer between 1 and 120000.",
|
|
15
|
+
[CODEX_USAGE_ERROR_CODES.CODEX_NOT_FOUND]:
|
|
16
|
+
"Codex CLI was not found. Install or update Codex, then sign in with ChatGPT.",
|
|
17
|
+
[CODEX_USAGE_ERROR_CODES.APP_SERVER_START_FAILED]:
|
|
18
|
+
"Codex app-server could not be started.",
|
|
19
|
+
[CODEX_USAGE_ERROR_CODES.APP_SERVER_EXITED]:
|
|
20
|
+
"Codex app-server exited before returning account usage.",
|
|
21
|
+
[CODEX_USAGE_ERROR_CODES.APP_SERVER_TIMEOUT]:
|
|
22
|
+
"Codex app-server timed out before returning account usage.",
|
|
23
|
+
[CODEX_USAGE_ERROR_CODES.APP_SERVER_PROTOCOL_ERROR]:
|
|
24
|
+
"Codex app-server returned an invalid protocol message.",
|
|
25
|
+
[CODEX_USAGE_ERROR_CODES.APP_SERVER_RPC_ERROR]:
|
|
26
|
+
"Codex app-server could not read account usage. Check your Codex version and ChatGPT sign-in.",
|
|
27
|
+
[CODEX_USAGE_ERROR_CODES.INVALID_ACCOUNT_USAGE_RESPONSE]:
|
|
28
|
+
"Codex app-server returned an invalid account usage response."
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export class CodexUsageError extends Error {
|
|
32
|
+
constructor(code, options = {}) {
|
|
33
|
+
const message = ERROR_MESSAGES[code];
|
|
34
|
+
if (message === undefined) {
|
|
35
|
+
throw new TypeError("Unknown Codex usage error code");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "CodexUsageError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
|
|
42
|
+
if (Number.isInteger(options.rpcCode)) {
|
|
43
|
+
this.rpcCode = options.rpcCode;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const UNAVAILABLE = "Unavailable";
|
|
2
|
+
|
|
3
|
+
export function formatAccountUsage(usage) {
|
|
4
|
+
const rows = [
|
|
5
|
+
["Lifetime tokens", formatCompactInteger(usage.summary.lifetimeTokens)],
|
|
6
|
+
["Peak daily tokens", formatCompactInteger(usage.summary.peakDailyTokens)],
|
|
7
|
+
["Longest turn", formatDuration(usage.summary.longestRunningTurnSec)],
|
|
8
|
+
["Current streak", formatDays(usage.summary.currentStreakDays)],
|
|
9
|
+
["Longest streak", formatDays(usage.summary.longestStreakDays)],
|
|
10
|
+
["Daily buckets", formatBucketCount(usage.dailyUsageBuckets)]
|
|
11
|
+
];
|
|
12
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
13
|
+
|
|
14
|
+
return [
|
|
15
|
+
"Codex account usage",
|
|
16
|
+
"",
|
|
17
|
+
...rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`),
|
|
18
|
+
"",
|
|
19
|
+
`Captured at ${usage.capturedAt}`
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatCompactInteger(value) {
|
|
24
|
+
if (value === null) return UNAVAILABLE;
|
|
25
|
+
|
|
26
|
+
const units = [
|
|
27
|
+
[1_000_000_000_000, "T"],
|
|
28
|
+
[1_000_000_000, "B"],
|
|
29
|
+
[1_000_000, "M"],
|
|
30
|
+
[1_000, "K"]
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const [divisor, suffix] of units) {
|
|
34
|
+
if (value >= divisor) {
|
|
35
|
+
const scaled = value / divisor;
|
|
36
|
+
return `${trimTrailingZeros(scaled.toFixed(2))}${suffix}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatDuration(value) {
|
|
44
|
+
if (value === null) return UNAVAILABLE;
|
|
45
|
+
if (value === 0) return "0s";
|
|
46
|
+
|
|
47
|
+
let remaining = value;
|
|
48
|
+
const units = [
|
|
49
|
+
[86_400, "d"],
|
|
50
|
+
[3_600, "h"],
|
|
51
|
+
[60, "m"],
|
|
52
|
+
[1, "s"]
|
|
53
|
+
];
|
|
54
|
+
const parts = [];
|
|
55
|
+
|
|
56
|
+
for (const [seconds, suffix] of units) {
|
|
57
|
+
const amount = Math.floor(remaining / seconds);
|
|
58
|
+
if (amount > 0) {
|
|
59
|
+
parts.push(`${amount}${suffix}`);
|
|
60
|
+
remaining -= amount * seconds;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (parts.length === 2) break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return parts.join(" ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatDays(value) {
|
|
70
|
+
if (value === null) return UNAVAILABLE;
|
|
71
|
+
return `${value} ${value === 1 ? "day" : "days"}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatBucketCount(value) {
|
|
75
|
+
if (value === null) return UNAVAILABLE;
|
|
76
|
+
return `${value.length} ${value.length === 1 ? "day" : "days"}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function trimTrailingZeros(value) {
|
|
80
|
+
return value.replace(/\.0+$/u, "").replace(/(\.\d*?)0+$/u, "$1");
|
|
81
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,23 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
export declare const PACKAGE_NAME: "codex-usage-analyzer";
|
|
2
|
+
export declare const PACKAGE_VERSION: "0.2.0";
|
|
3
|
+
export declare const ACCOUNT_USAGE_CONTRACT_VERSION: 1;
|
|
4
|
+
export declare const ACCOUNT_USAGE_SUMMARY_FIELDS: readonly [
|
|
5
|
+
"lifetimeTokens",
|
|
6
|
+
"peakDailyTokens",
|
|
7
|
+
"longestRunningTurnSec",
|
|
8
|
+
"currentStreakDays",
|
|
9
|
+
"longestStreakDays"
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export interface AccountUsageSummary {
|
|
13
|
+
lifetimeTokens: number | null;
|
|
14
|
+
peakDailyTokens: number | null;
|
|
15
|
+
longestRunningTurnSec: number | null;
|
|
16
|
+
currentStreakDays: number | null;
|
|
17
|
+
longestStreakDays: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AccountUsageDailyBucket {
|
|
21
|
+
startDate: string;
|
|
22
|
+
tokens: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AccountUsageDocument {
|
|
26
|
+
contractVersion: 1;
|
|
27
|
+
capturedAt: string;
|
|
28
|
+
summary: AccountUsageSummary;
|
|
29
|
+
dailyUsageBuckets: AccountUsageDailyBucket[] | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ReadAccountUsageOptions {
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type CodexUsageErrorCode =
|
|
37
|
+
| "INVALID_TIMEOUT"
|
|
38
|
+
| "CODEX_NOT_FOUND"
|
|
39
|
+
| "APP_SERVER_START_FAILED"
|
|
40
|
+
| "APP_SERVER_EXITED"
|
|
41
|
+
| "APP_SERVER_TIMEOUT"
|
|
42
|
+
| "APP_SERVER_PROTOCOL_ERROR"
|
|
43
|
+
| "APP_SERVER_RPC_ERROR"
|
|
44
|
+
| "INVALID_ACCOUNT_USAGE_RESPONSE";
|
|
45
|
+
|
|
46
|
+
export declare const CODEX_USAGE_ERROR_CODES: Readonly<{
|
|
47
|
+
INVALID_TIMEOUT: "INVALID_TIMEOUT";
|
|
48
|
+
CODEX_NOT_FOUND: "CODEX_NOT_FOUND";
|
|
49
|
+
APP_SERVER_START_FAILED: "APP_SERVER_START_FAILED";
|
|
50
|
+
APP_SERVER_EXITED: "APP_SERVER_EXITED";
|
|
51
|
+
APP_SERVER_TIMEOUT: "APP_SERVER_TIMEOUT";
|
|
52
|
+
APP_SERVER_PROTOCOL_ERROR: "APP_SERVER_PROTOCOL_ERROR";
|
|
53
|
+
APP_SERVER_RPC_ERROR: "APP_SERVER_RPC_ERROR";
|
|
54
|
+
INVALID_ACCOUNT_USAGE_RESPONSE: "INVALID_ACCOUNT_USAGE_RESPONSE";
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
export declare class CodexUsageError extends Error {
|
|
58
|
+
readonly code: CodexUsageErrorCode;
|
|
59
|
+
readonly rpcCode?: number;
|
|
60
|
+
constructor(code: CodexUsageErrorCode, options?: { rpcCode?: number });
|
|
21
61
|
}
|
|
22
62
|
|
|
23
63
|
export interface CliIo {
|
|
@@ -29,18 +69,8 @@ export interface CliIo {
|
|
|
29
69
|
};
|
|
30
70
|
}
|
|
31
71
|
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
72
|
+
export declare function readAccountUsage(
|
|
73
|
+
options?: ReadAccountUsageOptions
|
|
74
|
+
): Promise<AccountUsageDocument>;
|
|
45
75
|
|
|
46
76
|
export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
|
package/src/index.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "./analyze.js";
|
|
2
|
+
ACCOUNT_USAGE_CONTRACT_VERSION,
|
|
3
|
+
ACCOUNT_USAGE_SUMMARY_FIELDS,
|
|
4
|
+
readAccountUsage
|
|
5
|
+
} from "./account-usage.js";
|
|
7
6
|
|
|
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";
|
|
7
|
+
export { CODEX_USAGE_ERROR_CODES, CodexUsageError } from "./errors.js";
|
|
16
8
|
|
|
17
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
PACKAGE_NAME,
|
|
11
|
+
PACKAGE_VERSION,
|
|
12
|
+
runCli
|
|
13
|
+
} from "./cli.js";
|
package/src/analyze.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
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
|
-
}
|