Sign In

codex-usage-analyzer

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

codex-usage-analyzer - npm Package Compare versions

Comparing version
0.4.0
to
0.4.1
+470
docs/experimental-full-profile-v2.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Codex Usage Analyzer Experimental Full Profile Contract v2",
"description": "Experimental opt-in profile output with selected custom pet data. This is separate from the stable Account Usage Contract.",
"type": "object",
"additionalProperties": false,
"required": [
"fullProfileContractVersion",
"kind",
"stability",
"status",
"usage",
"profile",
"activityInsights",
"pet"
],
"properties": {
"fullProfileContractVersion": {
"const": 2
},
"kind": {
"const": "codex-usage-analyzer.fullProfile"
},
"stability": {
"const": "experimental"
},
"status": {
"enum": [
"ok",
"partial",
"unavailable"
]
},
"usage": {
"$ref": "#/definitions/accountUsage"
},
"profile": {
"oneOf": [
{
"$ref": "#/definitions/profile"
},
{
"type": "null"
}
]
},
"activityInsights": {
"oneOf": [
{
"$ref": "#/definitions/activityInsights"
},
{
"type": "null"
}
]
},
"pet": {
"$ref": "#/definitions/pet"
}
},
"definitions": {
"nullableNonNegativeInteger": {
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"nullablePercentage": {
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 100
},
{
"type": "null"
}
]
},
"nullableString64": {
"oneOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[^\\u0000-\\u001F\\u007F]*$"
},
{
"type": "null"
}
]
},
"accountUsage": {
"type": "object",
"additionalProperties": false,
"required": [
"contractVersion",
"capturedAt",
"summary",
"dailyUsageBuckets"
],
"properties": {
"contractVersion": {
"const": 1
},
"capturedAt": {
"type": "string",
"format": "date-time"
},
"summary": {
"$ref": "#/definitions/accountUsageSummary"
},
"dailyUsageBuckets": {
"oneOf": [
{
"type": "array",
"items": {
"$ref": "#/definitions/accountUsageDailyBucket"
}
},
{
"type": "null"
}
]
}
}
},
"accountUsageSummary": {
"type": "object",
"additionalProperties": false,
"required": [
"lifetimeTokens",
"peakDailyTokens",
"longestRunningTurnSec",
"currentStreakDays",
"longestStreakDays"
],
"properties": {
"lifetimeTokens": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"peakDailyTokens": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"longestRunningTurnSec": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"currentStreakDays": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"longestStreakDays": {
"$ref": "#/definitions/nullableNonNegativeInteger"
}
}
},
"accountUsageDailyBucket": {
"type": "object",
"additionalProperties": false,
"required": [
"startDate",
"tokens"
],
"properties": {
"startDate": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
},
"tokens": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
}
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": [
"displayName",
"username",
"avatarUrl",
"planType"
],
"properties": {
"displayName": {
"oneOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001F\\u007F]*$"
},
{
"type": "null"
}
]
},
"username": {
"oneOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 100,
"pattern": "^[^\\u0000-\\u001F\\u007F]*$"
},
{
"type": "null"
}
]
},
"avatarUrl": {
"oneOf": [
{
"type": "string",
"format": "uri",
"pattern": "^https://",
"maxLength": 2048
},
{
"type": "null"
}
]
},
"planType": {
"$ref": "#/definitions/nullableString64"
}
}
},
"activityInsights": {
"type": "object",
"additionalProperties": false,
"required": [
"fastModePercent",
"reasoningEffort",
"reasoningEffortPercent",
"skillsExplored",
"totalSkillsUsed",
"totalThreads",
"topInvocations"
],
"properties": {
"fastModePercent": {
"$ref": "#/definitions/nullablePercentage"
},
"reasoningEffort": {
"$ref": "#/definitions/nullableString64"
},
"reasoningEffortPercent": {
"$ref": "#/definitions/nullablePercentage"
},
"skillsExplored": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"totalSkillsUsed": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"totalThreads": {
"$ref": "#/definitions/nullableNonNegativeInteger"
},
"topInvocations": {
"oneOf": [
{
"type": "array",
"maxItems": 100,
"items": {
"$ref": "#/definitions/topInvocation"
}
},
{
"type": "null"
}
]
}
}
},
"topInvocation": {
"type": "object",
"additionalProperties": false,
"required": [
"type",
"name",
"usageCount"
],
"properties": {
"type": {
"enum": [
"plugin",
"skill"
]
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[^\\u0000-\\u001F\\u007F]*$"
},
"usageCount": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
}
},
"pet": {
"type": "object",
"additionalProperties": false,
"required": [
"status",
"reason",
"kind",
"image"
],
"properties": {
"status": {
"enum": [
"ok",
"unavailable"
]
},
"reason": {
"oneOf": [
{
"enum": [
"selected_pet_state_unavailable",
"selected_pet_not_custom",
"selected_pet_selection_unavailable",
"selected_pet_manifest_unavailable",
"selected_pet_image_unavailable",
"selected_pet_image_invalid",
"selected_pet_image_too_large"
]
},
{
"type": "null"
}
]
},
"kind": {
"oneOf": [
{
"const": "custom"
},
{
"type": "null"
}
]
},
"image": {
"oneOf": [
{
"$ref": "#/definitions/petImage"
},
{
"type": "null"
}
]
}
},
"allOf": [
{
"if": {
"properties": {
"status": {
"const": "ok"
}
}
},
"then": {
"properties": {
"reason": {
"type": "null"
},
"kind": {
"const": "custom"
},
"image": {
"$ref": "#/definitions/petImage"
}
}
}
},
{
"if": {
"properties": {
"status": {
"const": "unavailable"
}
}
},
"then": {
"properties": {
"reason": {
"enum": [
"selected_pet_state_unavailable",
"selected_pet_not_custom",
"selected_pet_selection_unavailable",
"selected_pet_manifest_unavailable",
"selected_pet_image_unavailable",
"selected_pet_image_invalid",
"selected_pet_image_too_large"
]
},
"kind": {
"type": "null"
},
"image": {
"type": "null"
}
}
}
}
]
},
"petImage": {
"type": "object",
"additionalProperties": false,
"required": [
"role",
"contentType",
"width",
"height",
"byteLength",
"sha256",
"base64"
],
"properties": {
"role": {
"const": "spritesheet"
},
"contentType": {
"enum": [
"image/webp",
"image/png"
]
},
"width": {
"type": "integer",
"minimum": 1,
"maximum": 8192
},
"height": {
"type": "integer",
"minimum": 1,
"maximum": 8192
},
"byteLength": {
"type": "integer",
"minimum": 1,
"maximum": 8388608
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"base64": {
"type": "string",
"minLength": 4,
"maxLength": 11184812,
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
}
}
}
}
}
import { emitKeypressEvents } from "node:readline";
const MAX_SELECTOR_DISPLAY_NAME_LENGTH = 128;
export async function selectExperimentalPet(catalog, options = {}) {
const input = options.input ?? process.stdin;
const output = options.output ?? process.stderr;
const items = normalizeCatalog(catalog);
if (items.length === 0
|| input?.isTTY !== true
|| output?.isTTY !== true
|| typeof input.setRawMode !== "function"
|| typeof input.on !== "function"
|| typeof input.off !== "function") {
return null;
}
const initialIndex = items.findIndex((item) => item.selected);
let cursor = initialIndex < 0 ? 0 : initialIndex;
return new Promise((resolve) => {
const previousRawMode = input.isRaw === true;
const wasFlowing = input.readableFlowing === true;
let active = true;
let rendered = false;
function cleanup() {
if (!active) return;
active = false;
input.off("keypress", onKeypress);
try {
input.setRawMode(previousRawMode);
} catch {
// Terminal cleanup is best effort after selection has settled.
}
if (!wasFlowing && typeof input.pause === "function") input.pause();
try {
output.write("\x1b[?25h");
} catch {
// Output cleanup is best effort after selection has settled.
}
}
function settle(value) {
cleanup();
try {
output.write("\n");
} catch {
// The caller receives the selection even if the final newline fails.
}
resolve(value);
}
function render() {
const lineCount = items.length + 2;
if (rendered) output.write(`\x1b[${lineCount}A`);
output.write("\r\x1b[2KSelect a custom Codex pet\n");
for (let index = 0; index < items.length; index += 1) {
const marker = index === cursor ? ">" : " ";
output.write(
`\r\x1b[2K${marker} [${items[index].key}] ${items[index].label}\n`
);
}
output.write("\r\x1b[2KUse Up/Down and Enter; Esc cancels\n");
rendered = true;
}
function onKeypress(_text, key = {}) {
if (!active) return;
if (key.name === "up") {
cursor = (cursor - 1 + items.length) % items.length;
} else if (key.name === "down") {
cursor = (cursor + 1) % items.length;
} else if (key.name === "return" || key.name === "enter") {
settle(items[cursor].key);
return;
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
settle(null);
return;
} else {
return;
}
try {
render();
} catch {
settle(null);
}
}
try {
input.setRawMode(true);
emitKeypressEvents(input);
input.on("keypress", onKeypress);
if (typeof input.resume === "function") input.resume();
output.write("\x1b[?25l");
render();
} catch {
settle(null);
}
});
}
function normalizeCatalog(value) {
if (!Array.isArray(value)) return [];
return value.flatMap((item) => {
if (!isRecord(item)
|| !Number.isSafeInteger(item.key)
|| item.key < 1
|| typeof item.selected !== "boolean") {
return [];
}
return [{
key: item.key,
selected: item.selected,
label: normalizeDisplayName(item.displayName) ?? `Custom pet ${item.key}`
}];
});
}
function normalizeDisplayName(value) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (trimmed.length < 1
|| trimmed.length > MAX_SELECTOR_DISPLAY_NAME_LENGTH
|| /[\u0000-\u001f\u007f]/u.test(trimmed)) {
return null;
}
return trimmed;
}
function isRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
import { createHash } from "node:crypto";
import {
lstat,
open,
readdir,
realpath
} from "node:fs/promises";
import { homedir } from "node:os";
import {
extname,
isAbsolute,
join,
normalize,
relative,
resolve,
sep
} from "node:path";
export const EXPERIMENTAL_PET_STATUSES = Object.freeze([
"ok",
"unavailable"
]);
export const EXPERIMENTAL_PET_REASONS = Object.freeze([
"selected_pet_state_unavailable",
"selected_pet_not_custom",
"selected_pet_selection_unavailable",
"selected_pet_manifest_unavailable",
"selected_pet_image_unavailable",
"selected_pet_image_invalid",
"selected_pet_image_too_large"
]);
export const EXPERIMENTAL_PET_IMAGE_CONTENT_TYPES = Object.freeze([
"image/webp",
"image/png"
]);
export const EXPERIMENTAL_PET_CATALOG_FIELDS = Object.freeze([
"key",
"displayName",
"selected"
]);
export const MAX_EXPERIMENTAL_PET_STATE_BYTES = 1_048_576;
export const MAX_EXPERIMENTAL_PET_MANIFEST_BYTES = 65_536;
export const MAX_EXPERIMENTAL_PET_IMAGE_BYTES = 8_388_608;
export const MAX_EXPERIMENTAL_PET_IMAGE_DIMENSION = 8_192;
export const MAX_EXPERIMENTAL_PET_IMAGE_PIXELS = 16_777_216;
const STATE_FILE_NAME = ".codex-global-state.json";
const PERSISTED_ATOM_KEY = "electron-persisted-atom-state";
const SELECTED_AVATAR_KEY = "selected-avatar-id";
const CUSTOM_AVATAR_PREFIX = "custom:";
const PETS_DIRECTORY_NAME = "pets";
const PET_MANIFEST_NAME = "pet.json";
const MAX_SELECTED_ID_LENGTH = 256;
const MAX_DISPLAY_NAME_LENGTH = 128;
const MAX_SPRITESHEET_PATH_LENGTH = 1_024;
const PROTOTYPE_SENSITIVE_KEYS = Object.freeze([
"__proto__",
"constructor",
"prototype"
]);
const CONTENT_TYPE_BY_EXTENSION = Object.freeze({
".png": "image/png",
".webp": "image/webp"
});
export async function readExperimentalPet(options = {}) {
const codexHome = resolveCodexHome(options);
let manifestResult;
if (Object.hasOwn(options, "petKey")) {
if (!isPositiveSafeInteger(options.petKey)) {
return unavailablePet("selected_pet_selection_unavailable");
}
const catalog = await readPetCatalog(codexHome, null);
const selectedEntry = catalog.find((entry) => entry.key === options.petKey);
if (selectedEntry === undefined) {
return unavailablePet("selected_pet_selection_unavailable");
}
manifestResult = selectedEntry.manifest;
} else {
const selected = await readSelectedCustomPet(codexHome);
if (selected.reason !== null) {
return unavailablePet(selected.reason);
}
manifestResult = await readSelectedManifest(
codexHome,
selected.customId
);
selected.customId = null;
if (manifestResult.reason !== null) {
return unavailablePet(manifestResult.reason);
}
}
const imageResult = await readPetImage(manifestResult);
manifestResult.petDirectory = null;
manifestResult.spritesheetPath = null;
if (imageResult.reason !== null) {
return unavailablePet(imageResult.reason);
}
return {
status: "ok",
reason: null,
kind: "custom",
image: imageResult.image
};
}
export async function listExperimentalPets(options = {}) {
const codexHome = resolveCodexHome(options);
const selected = await readSelectedCustomPet(codexHome);
const selectedCustomId = selected.reason === null ? selected.customId : null;
const catalog = await readPetCatalog(codexHome, selectedCustomId);
selected.customId = null;
return catalog.map((entry) => ({
key: entry.key,
displayName: entry.displayName,
selected: entry.selected
}));
}
function resolveCodexHome(options) {
if (typeof options.codexHome === "string"
&& options.codexHome.trim().length > 0) {
return resolve(options.codexHome);
}
const env = isRecord(options.env) ? options.env : process.env;
if (typeof env.CODEX_HOME === "string"
&& env.CODEX_HOME.trim().length > 0) {
return resolve(env.CODEX_HOME);
}
const homeDir = typeof options.homeDir === "string"
&& options.homeDir.trim().length > 0
? options.homeDir
: homedir();
return resolve(homeDir, ".codex");
}
async function readSelectedCustomPet(codexHome) {
const result = await readLimitedJson(
join(codexHome, STATE_FILE_NAME),
MAX_EXPERIMENTAL_PET_STATE_BYTES
);
if (!result.ok || !isRecord(result.value)) {
return selectedFailure("selected_pet_state_unavailable");
}
const persistedState = result.value[PERSISTED_ATOM_KEY];
if (!isRecord(persistedState)
|| hasPrototypeSensitiveKey(persistedState)) {
return selectedFailure("selected_pet_state_unavailable");
}
const selectedAvatarId = persistedState[SELECTED_AVATAR_KEY];
if (typeof selectedAvatarId !== "string"
|| selectedAvatarId.length < 1
|| selectedAvatarId.length > MAX_SELECTED_ID_LENGTH
|| selectedAvatarId.trim() !== selectedAvatarId
|| hasControlCharacter(selectedAvatarId)) {
return selectedFailure("selected_pet_state_unavailable");
}
if (!selectedAvatarId.startsWith(CUSTOM_AVATAR_PREFIX)) {
return selectedFailure("selected_pet_not_custom");
}
const customId = selectedAvatarId.slice(CUSTOM_AVATAR_PREFIX.length);
if (customId.length < 1 || hasControlCharacter(customId)) {
return selectedFailure("selected_pet_state_unavailable");
}
return {
reason: null,
customId
};
}
async function readSelectedManifest(codexHome, customId) {
const petsDirectory = join(codexHome, PETS_DIRECTORY_NAME);
let entries;
try {
entries = await readdir(petsDirectory, { withFileTypes: true });
} catch {
return manifestFailure();
}
const selectedEntry = entries.find((entry) => (
entry.isDirectory() && entry.name === customId
));
if (selectedEntry === undefined) {
return manifestFailure();
}
const petDirectory = join(petsDirectory, selectedEntry.name);
const manifest = await readCatalogManifest(petDirectory);
return manifest ?? manifestFailure();
}
async function readPetCatalog(codexHome, selectedCustomId) {
const petsDirectory = join(codexHome, PETS_DIRECTORY_NAME);
let entries;
try {
entries = await readdir(petsDirectory, { withFileTypes: true });
} catch {
return [];
}
const directories = entries
.filter((entry) => entry.isDirectory())
.sort(compareDirectoryEntries);
const catalog = [];
for (const entry of directories) {
const manifest = await readCatalogManifest(
join(petsDirectory, entry.name)
);
if (manifest === null) continue;
catalog.push({
key: catalog.length + 1,
displayName: manifest.displayName,
selected: selectedCustomId !== null && entry.name === selectedCustomId,
manifest
});
}
return catalog;
}
async function readCatalogManifest(petDirectory) {
const manifestResult = await readLimitedJson(
join(petDirectory, PET_MANIFEST_NAME),
MAX_EXPERIMENTAL_PET_MANIFEST_BYTES
);
if (!manifestResult.ok
|| !isRecord(manifestResult.value)
|| hasPrototypeSensitiveKey(manifestResult.value)) {
return null;
}
const spritesheetPath = normalizeRelativeImagePath(
manifestResult.value.spritesheetPath
);
if (spritesheetPath === null) {
return null;
}
return {
reason: null,
petDirectory,
spritesheetPath,
displayName: normalizeDisplayName(manifestResult.value.displayName)
};
}
async function readPetImage(manifestResult) {
const extension = extname(manifestResult.spritesheetPath).toLowerCase();
const contentType = CONTENT_TYPE_BY_EXTENSION[extension];
if (contentType === undefined) {
return imageFailure("selected_pet_image_invalid");
}
const imagePath = join(
manifestResult.petDirectory,
manifestResult.spritesheetPath
);
const containedPath = await resolveContainedFile(
manifestResult.petDirectory,
imagePath
);
if (containedPath === null) {
return imageFailure("selected_pet_image_unavailable");
}
const readResult = await readBoundedFile(
containedPath,
MAX_EXPERIMENTAL_PET_IMAGE_BYTES
);
if (!readResult.ok) {
return imageFailure(readResult.tooLarge
? "selected_pet_image_too_large"
: "selected_pet_image_unavailable");
}
const dimensions = contentType === "image/png"
? readPngDimensions(readResult.bytes)
: readWebpDimensions(readResult.bytes);
if (!isAllowedDimensions(dimensions)) {
return imageFailure("selected_pet_image_invalid");
}
return {
reason: null,
image: {
role: "spritesheet",
contentType,
width: dimensions.width,
height: dimensions.height,
byteLength: readResult.bytes.byteLength,
sha256: createHash("sha256").update(readResult.bytes).digest("hex"),
base64: readResult.bytes.toString("base64")
}
};
}
async function readLimitedJson(filePath, maximumBytes) {
const result = await readBoundedFile(filePath, maximumBytes);
if (!result.ok) return { ok: false, value: null };
try {
const text = new TextDecoder("utf-8", { fatal: true })
.decode(result.bytes);
return { ok: true, value: JSON.parse(text) };
} catch {
return { ok: false, value: null };
}
}
async function readBoundedFile(filePath, maximumBytes) {
let handle;
try {
const metadata = await lstat(filePath);
if (!metadata.isFile() || metadata.isSymbolicLink()) {
return { ok: false, tooLarge: false, bytes: null };
}
if (!Number.isSafeInteger(metadata.size) || metadata.size > maximumBytes) {
return { ok: false, tooLarge: true, bytes: null };
}
handle = await open(filePath, "r");
const openedMetadata = await handle.stat();
if (!openedMetadata.isFile()
|| !Number.isSafeInteger(openedMetadata.size)
|| openedMetadata.size !== metadata.size
|| openedMetadata.size > maximumBytes) {
return {
ok: false,
tooLarge: openedMetadata.size > maximumBytes,
bytes: null
};
}
const buffer = Buffer.alloc(openedMetadata.size + 1);
let offset = 0;
while (offset < buffer.byteLength) {
const { bytesRead } = await handle.read(
buffer,
offset,
buffer.byteLength - offset,
offset
);
if (bytesRead === 0) break;
offset += bytesRead;
}
const finalMetadata = await handle.stat();
if (offset !== openedMetadata.size
|| finalMetadata.size !== openedMetadata.size) {
return {
ok: false,
tooLarge: offset > maximumBytes || finalMetadata.size > maximumBytes,
bytes: null
};
}
return {
ok: true,
tooLarge: false,
bytes: buffer.subarray(0, offset)
};
} catch {
return { ok: false, tooLarge: false, bytes: null };
} finally {
await handle?.close().catch(() => {});
}
}
async function resolveContainedFile(baseDirectory, candidatePath) {
try {
const candidateMetadata = await lstat(candidatePath);
if (!candidateMetadata.isFile() || candidateMetadata.isSymbolicLink()) {
return null;
}
const [baseRealPath, candidateRealPath] = await Promise.all([
realpath(baseDirectory),
realpath(candidatePath)
]);
const relativePath = relative(baseRealPath, candidateRealPath);
if (relativePath.length === 0
|| relativePath === ".."
|| relativePath.startsWith(`..${sep}`)
|| isAbsolute(relativePath)) {
return null;
}
return candidateRealPath;
} catch {
return null;
}
}
function normalizeRelativeImagePath(value) {
if (typeof value !== "string"
|| value.length < 1
|| value.length > MAX_SPRITESHEET_PATH_LENGTH
|| value.trim() !== value
|| hasControlCharacter(value)
|| isAbsolute(value)) {
return null;
}
const normalizedPath = normalize(value);
if (normalizedPath === "."
|| normalizedPath === ".."
|| normalizedPath.startsWith(`..${sep}`)) {
return null;
}
return normalizedPath;
}
function normalizeDisplayName(value) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (trimmed.length < 1
|| trimmed.length > MAX_DISPLAY_NAME_LENGTH
|| hasControlCharacter(trimmed)) {
return null;
}
return trimmed;
}
function compareDirectoryEntries(left, right) {
if (left.name < right.name) return -1;
if (left.name > right.name) return 1;
return 0;
}
function readPngDimensions(bytes) {
const signature = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
]);
if (bytes.byteLength < 33
|| !bytes.subarray(0, signature.byteLength).equals(signature)
|| bytes.readUInt32BE(8) !== 13
|| bytes.toString("ascii", 12, 16) !== "IHDR") {
return null;
}
return {
width: bytes.readUInt32BE(16),
height: bytes.readUInt32BE(20)
};
}
function readWebpDimensions(bytes) {
if (bytes.byteLength < 20
|| bytes.toString("ascii", 0, 4) !== "RIFF"
|| bytes.toString("ascii", 8, 12) !== "WEBP"
|| bytes.readUInt32LE(4) + 8 !== bytes.byteLength) {
return null;
}
let offset = 12;
while (offset + 8 <= bytes.byteLength) {
const chunkType = bytes.toString("ascii", offset, offset + 4);
const chunkLength = bytes.readUInt32LE(offset + 4);
const payloadOffset = offset + 8;
const payloadEnd = payloadOffset + chunkLength;
if (payloadEnd > bytes.byteLength) return null;
if (chunkType === "VP8X") {
if (chunkLength < 10) return null;
return {
width: 1 + readUInt24LE(bytes, payloadOffset + 4),
height: 1 + readUInt24LE(bytes, payloadOffset + 7)
};
}
if (chunkType === "VP8L") {
if (chunkLength < 5 || bytes[payloadOffset] !== 0x2f) return null;
const bits = bytes.readUInt32LE(payloadOffset + 1);
return {
width: 1 + (bits & 0x3fff),
height: 1 + ((bits >>> 14) & 0x3fff)
};
}
if (chunkType === "VP8 ") {
if (chunkLength < 10
|| bytes[payloadOffset + 3] !== 0x9d
|| bytes[payloadOffset + 4] !== 0x01
|| bytes[payloadOffset + 5] !== 0x2a) {
return null;
}
return {
width: bytes.readUInt16LE(payloadOffset + 6) & 0x3fff,
height: bytes.readUInt16LE(payloadOffset + 8) & 0x3fff
};
}
offset = payloadEnd + (chunkLength % 2);
}
return null;
}
function readUInt24LE(bytes, offset) {
return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16);
}
function isAllowedDimensions(value) {
return value !== null
&& Number.isInteger(value.width)
&& Number.isInteger(value.height)
&& value.width >= 1
&& value.height >= 1
&& value.width <= MAX_EXPERIMENTAL_PET_IMAGE_DIMENSION
&& value.height <= MAX_EXPERIMENTAL_PET_IMAGE_DIMENSION
&& value.width * value.height <= MAX_EXPERIMENTAL_PET_IMAGE_PIXELS;
}
function hasPrototypeSensitiveKey(value) {
return PROTOTYPE_SENSITIVE_KEYS.some((key) => Object.hasOwn(value, key));
}
function hasControlCharacter(value) {
return /[\u0000-\u001f\u007f]/u.test(value);
}
function isRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isPositiveSafeInteger(value) {
return Number.isSafeInteger(value) && value >= 1;
}
function selectedFailure(reason) {
return { reason, customId: null };
}
function manifestFailure() {
return {
reason: "selected_pet_manifest_unavailable",
petDirectory: null,
spritesheetPath: null
};
}
function imageFailure(reason) {
return { reason, image: null };
}
function unavailablePet(reason) {
return {
status: "unavailable",
reason,
kind: null,
image: null
};
}
import type {
ExperimentalFullProfileV1,
ExperimentalFullProfileV2,
ExperimentalPetCatalogItem
} from "./index.js";
export type {
ExperimentalFullProfileActivityInsights,
ExperimentalFullProfileInvocation,
ExperimentalFullProfileProfile,
ExperimentalFullProfileStatus,
ExperimentalFullProfileV1,
ExperimentalFullProfileV2,
ExperimentalPet,
ExperimentalPetAvailable,
ExperimentalPetCatalogItem,
ExperimentalPetImage,
ExperimentalPetReason,
ExperimentalPetUnavailable
} from "./index.js";
export interface ExperimentalPetSourceOptions {
codexHome?: string;
env?: Record<string, string | undefined>;
homeDir?: string;
}
export type ExperimentalPetSelector = (
catalog: readonly ExperimentalPetCatalogItem[]
) => number | null | Promise<number | null>;
export interface ExperimentalProfileV1Options {
timeoutMs?: number;
includePet?: false;
}
export interface ExperimentalProfileV2Options
extends ExperimentalPetSourceOptions {
timeoutMs?: number;
includePet: true;
petKey?: number;
selectPet?: ExperimentalPetSelector;
forcePetSelection?: boolean;
}
export type ExperimentalProfileOptions =
| ExperimentalProfileV1Options
| ExperimentalProfileV2Options;
export declare function listExperimentalPets(
options?: ExperimentalPetSourceOptions
): Promise<ExperimentalPetCatalogItem[]>;
export declare function readExperimentalProfile(
options?: ExperimentalProfileV1Options
): Promise<ExperimentalFullProfileV1>;
export declare function readExperimentalProfile(
options: ExperimentalProfileV2Options
): Promise<ExperimentalFullProfileV2>;
export { listExperimentalPets } from "./experimental-pet.js";
export { readExperimentalProfile } from "./experimental-profile-client.js";
+43
-4

@@ -5,3 +5,3 @@ # Downstream Integration Guide

The stable CLI and SDK own one artifact: the identity-free [Account Usage Contract](account-usage-contract.md). An explicit CLI-only experiment can emit a separate [Full Profile Envelope](experimental-full-profile.md). A downstream service owns identity, authorization, submission, persistence, rendering, cache behavior, privacy controls, and deletion.
The stable CLI and SDK own one artifact: the identity-free [Account Usage Contract](account-usage-contract.md). An explicit CLI or module experiment can emit a separate [Full Profile Envelope](experimental-full-profile.md). A downstream service owns identity, authorization, submission, persistence, rendering, cache behavior, privacy controls, and deletion.

@@ -78,3 +78,3 @@ ## Recommended architecture

POST /v1/account-usage
Authorization: Bearer <downstream-issued-submit-token>
Authorization: bearer <downstream-issued-submit-token>
Content-Type: application/json

@@ -183,2 +183,41 @@ ```

## Experimental custom pet
Full Profile v2 adds a required `pet` result when the caller explicitly uses
`--include-pet` or `readExperimentalProfile({ includePet: true })`. The default
Full Profile v1 path never reads pet files. A Tokenmon-style downstream can use
an available custom pet as a cosmetic portrait image, but it must not treat the
catalog key, display name, digest, or image as identity or ownership proof.
The default selection is the custom pet already selected in Codex Desktop.
Callers can explicitly choose a deterministic one-based catalog key, use an
interactive selector, or provide a module callback that returns a key. Keys are
valid only for the catalog snapshot in which they were listed. A downstream must
not persist them as stable pet ids or infer a first/only-pet fallback.
`pet.image.base64` contains the complete submitted spritesheet. Before accepting
or rendering it, a downstream should:
1. Validate the complete Full Profile v2 document against
[`experimental-full-profile-v2.schema.json`](experimental-full-profile-v2.schema.json).
2. Strictly decode base64 and reject non-canonical or trailing data.
3. Match decoded bytes to `byteLength` and lowercase SHA-256.
4. Enforce independent encoded-body, decoded-byte, dimension, and pixel limits.
5. Verify the declared PNG or WebP content type from decoded bytes and fully
decode the image with a maintained image library.
6. Safely re-encode and re-host the accepted image under a downstream-owned URL.
Never publish the submitted base64 directly, embed it unchanged in a public data
URL, or log the full envelope. Strip metadata and animation behavior the product
does not intentionally support. Treat image parsers as an attack surface and
perform decoding in a resource-bounded worker or sandbox where practical.
Pet retention and publication require a consent control separate from profile
identity and activity-insight consent. Explain that the local image bytes will be
uploaded, how they will be rendered, whether they will be public, and how long
source and derived assets remain. Replacing a pet should advance the profile
revision and invalidate derived assets. Revocation or deletion must remove the
source bytes, re-encoded assets, rendered cards, cached variants where possible,
and any content-addressed references that would keep the image reachable.
Handle envelope status explicitly:

@@ -190,5 +229,5 @@

| `partial` | Preserve nested official usage; render or store only non-null fields allowed by policy. |
| `unavailable` | Accept nested official usage only if the endpoint and product policy allow it; profile and activity are `null`. |
| `unavailable` | Accept nested official usage only if the endpoint and product policy allow it; v1 profile/activity are unavailable, while v2 may also report pet unavailability. |
Keep the experimental envelope authenticated and non-public by default. Validate it against [`experimental-full-profile.schema.json`](experimental-full-profile.schema.json), preserve null/empty semantics, and isolate stored records by `fullProfileContractVersion` rather than treating it as `contractVersion` 1.
Keep the experimental envelope authenticated and non-public by default. Validate v1 against [`experimental-full-profile.schema.json`](experimental-full-profile.schema.json) and v2 against [`experimental-full-profile-v2.schema.json`](experimental-full-profile-v2.schema.json), preserve null/empty semantics, and isolate stored records by `fullProfileContractVersion` rather than treating either envelope as `contractVersion` 1.

@@ -195,0 +234,0 @@ ## Versioning

# Experimental Full Profile Contract
The Full Profile Envelope is an opt-in, CLI-only contract for identity and
The Full Profile Envelope is an opt-in CLI and module contract for identity and
activity fields shown by Codex. It combines one stable, identity-free Account
Usage Contract document with allowlisted fields from an unsupported private
profile endpoint.
profile endpoint. Full Profile v2 can additionally carry one explicitly opted-in
local custom Codex pet.
This contract is experimental. It is not part of the public JavaScript SDK and
This contract is experimental. It is isolated from the root JavaScript SDK and
does not change the default command or the stable
[`account/usage/read`](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md)
path. The machine-readable source of truth is
[`experimental-full-profile.schema.json`](experimental-full-profile.schema.json).
path. The machine-readable sources of truth are the
[v1 schema](experimental-full-profile.schema.json) and
[v2 schema](experimental-full-profile-v2.schema.json).

@@ -19,2 +21,5 @@ ## CLI

codex-usage-analyzer profile --json
codex-usage-analyzer profile --json --include-pet
codex-usage-analyzer profile --include-pet --pet-key 2
codex-usage-analyzer profile --include-pet --select-pet
```

@@ -29,2 +34,25 @@

`--include-pet` is a second, independent opt-in and writes another warning:
```text
codex-usage-analyzer: Warning: --include-pet reads local custom pet metadata and image bytes.
```
Pet options are order-independent. `--pet-key` accepts one positive safe integer;
`--pet-key` and `--select-pet` are mutually exclusive and valid only with
`--include-pet`. Invalid combinations fail before app-server, profile, catalog,
or pet-file access.
Selection follows this order:
1. `--pet-key N` reads catalog key `N`.
2. `--select-pet` forces an arrow-key selector and requires stdin and stderr TTYs.
3. Otherwise, the custom pet selected in Codex Desktop is preferred.
4. If that selection is unavailable, interactive human output offers the selector.
Default JSON output never prompts. Cancellation, an invalid key, an empty catalog,
or selection failure returns `selected_pet_selection_unavailable`; no first or
only catalog entry is selected implicitly. Catalog keys are deterministic and
one-based for a catalog snapshot, but are not persistent pet identifiers.
`profile --help` and `profile -h` do not start app-server or make a network

@@ -34,3 +62,3 @@ request. There is no environment variable, configuration file, implicit mode,

## Envelope
## Full Profile v1 envelope

@@ -87,3 +115,3 @@ ```json

## Root fields
## Full Profile v1 root fields

@@ -100,2 +128,90 @@ | Field | Type | Meaning |

## Full Profile v2 pet extension
When pet inclusion is requested, the envelope version is `2` and the required
root `pet` field is added. All v1 fields retain their meanings.
```json
{
"fullProfileContractVersion": 2,
"kind": "codex-usage-analyzer.fullProfile",
"stability": "experimental",
"status": "partial",
"usage": {
"contractVersion": 1,
"capturedAt": "2026-01-01T00:00:00.000Z",
"summary": {
"lifetimeTokens": 1000,
"peakDailyTokens": 200,
"longestRunningTurnSec": 300,
"currentStreakDays": 4,
"longestStreakDays": 5
},
"dailyUsageBuckets": []
},
"profile": null,
"activityInsights": null,
"pet": {
"status": "ok",
"reason": null,
"kind": "custom",
"image": {
"role": "spritesheet",
"contentType": "image/png",
"width": 1,
"height": 1,
"byteLength": 68,
"sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460",
"base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
}
}
}
```
All values are synthetic. For an available pet, `image` contains the complete
spritesheet bytes encoded as strict base64 plus validated content type,
dimensions, byte length, and lowercase SHA-256. `role` is always `spritesheet`
and `kind` is always `custom`. The human renderer prints only status, kind,
content type, dimensions, and byte length; it never prints base64 or the digest.
An unavailable pet has `status: "unavailable"`, `kind: null`, and `image: null`.
Its `reason` is one of:
- `selected_pet_state_unavailable`
- `selected_pet_not_custom`
- `selected_pet_selection_unavailable`
- `selected_pet_manifest_unavailable`
- `selected_pet_image_unavailable`
- `selected_pet_image_invalid`
- `selected_pet_image_too_large`
The v2 root status combines remote profile and pet availability: both available
is `ok`, both unavailable is `unavailable`, and every mixed case is `partial`.
The CLI exits `1` only when the combined root status is `unavailable`.
Without pet opt-in, the reader does not access the pet state, catalog, manifest,
or image and continues to emit Full Profile v1.
## Module API
```js
import {
listExperimentalPets,
readExperimentalProfile
} from "codex-usage-analyzer/experimental-profile";
const catalog = await listExperimentalPets();
const profile = await readExperimentalProfile({
includePet: true,
selectPet(items) {
return items.find((item) => item.selected)?.key ?? null;
}
});
```
`listExperimentalPets()` returns only `{ key, displayName, selected }` and does
not read image bytes. `readExperimentalProfile()` returns v1 unless
`includePet: true` is supplied. Module selection uses a numeric key returned by
`selectPet`; `forcePetSelection: true` invokes it before Desktop-state lookup.
The envelope has no separate observation timestamp. `usage.capturedAt` is the

@@ -206,3 +322,3 @@ canonical time at which the official usage result was received.

Bearer and account context references exist only in process memory for the
Bearer-token and account context references exist only in process memory for the
request. They are never returned, logged, written to a file, or passed to the

@@ -217,2 +333,9 @@ normalizer. JavaScript memory zeroization is not guaranteed, so the implementation

Pet opt-in is a separate local-file boundary. The reader uses bounded state,
manifest, and image reads; rejects unsafe paths, symlinks, unsupported formats,
invalid dimensions, excessive pixels, and oversized files; and returns only
normalized fields. It supports PNG and WebP custom-pet spritesheets. It never
returns local paths or raw parser errors. The base64 value is the complete image
and must be treated as private binary data.
## Unsupported endpoint and drift

@@ -243,2 +366,5 @@

names
- obtain separate consent before retaining or publishing `pet.image.base64`
- verify strict base64, decoded byte length, SHA-256, dimensions, content type,
and a full image decode before safely re-encoding and re-hosting a pet
- validate, decode, safely re-encode, and re-host an allowed avatar instead of

@@ -245,0 +371,0 @@ exposing the source URL

{
"name": "codex-usage-analyzer",
"version": "0.4.0",
"version": "0.4.1",
"description": "Read Codex account usage through the official app-server protocol.",

@@ -30,2 +30,6 @@ "keywords": [

"import": "./src/index.js"
},
"./experimental-profile": {
"types": "./src/experimental-profile-api.d.ts",
"import": "./src/experimental-profile-api.js"
}

@@ -41,2 +45,6 @@ },

"src/errors.js",
"src/experimental-pet.js",
"src/experimental-pet-selector.js",
"src/experimental-profile-api.d.ts",
"src/experimental-profile-api.js",
"src/experimental-profile-client.js",

@@ -43,0 +51,0 @@ "src/experimental-profile.js",

+219
-33

@@ -68,32 +68,9 @@ # codex-usage-analyzer

## Experimental profile
> **Experimental: `profile` command.** It adds identity, a 52-week token activity
> map, activity insights, and top invocations (most-used skills and plugins with
> usage counts). It uses an unsupported private endpoint and may expose private
> data. A separate `--include-pet` opt-in can also include the selected local
> custom Codex pet spritesheet.
> [Learn more about the experimental `profile` command.](#experimental-profile-command)
The default command and `--json` continue to use the documented
[`account/usage/read`](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md)
method and remain identity-free. A separate explicit command can also request the
profile and activity fields shown by Codex:
```bash
npx codex-usage-analyzer@latest profile
npx codex-usage-analyzer@latest profile --json
```
> **Experimental and unsupported:** `profile` uses the private
> `/wham/profiles/me` endpoint. It can change or stop working without notice.
> The command prints a warning to stderr and may emit a display name, username,
> avatar URL, plan type, activity insights, and top invocation names.
The command starts a dedicated app-server session, reads canonical usage through
`account/usage/read`, obtains the minimum ChatGPT auth context from that process,
and performs one fixed HTTPS request. The bearer token and account context stay in
process memory for that request; the CLI does not directly read authentication
files, cookies, or keychains. JavaScript cannot guarantee memory zeroization.
There is no retry, alternate endpoint, Desktop-client impersonation, or silent
fallback from the default command. `profile --json` returns a separate Full
Profile Envelope rather than extending the stable Account Usage Contract or SDK.
See the [Experimental Full Profile Contract](docs/experimental-full-profile.md)
and [JSON Schema](docs/experimental-full-profile.schema.json) before integrating
it or making identity and activity public.
## Why this CLI

@@ -146,3 +123,4 @@

codex-usage-analyzer [usage] [--json]
codex-usage-analyzer profile [--json] (experimental)
codex-usage-analyzer profile [--json] [--include-pet] (experimental)
codex-usage-analyzer profile --include-pet [--pet-key N | --select-pet]
codex-usage-analyzer [usage|profile] --help

@@ -160,6 +138,9 @@ codex-usage-analyzer --version

| `codex-usage-analyzer profile --json` | Experimental Full Profile Envelope JSON |
| `codex-usage-analyzer profile --include-pet` | Full Profile v2 with the Desktop-selected custom pet; interactive human output offers a selector when that selection is unavailable |
| `codex-usage-analyzer profile --include-pet --pet-key 2` | Full Profile v2 with catalog key `2` |
| `codex-usage-analyzer profile --include-pet --select-pet` | Force the interactive arrow-key selector; stdin and stderr must be TTYs |
| `codex-usage-analyzer --help` | Help without starting app-server |
| `codex-usage-analyzer --version` | Package version without starting app-server |
Command output is written to stdout. Failures are written to stderr as a stable error code and a safe message, without raw RPC data or app-server stderr. The experimental profile warning is always written to stderr, including successful profile calls. An unavailable private profile still emits an envelope and exits with status `1`; canonical usage remains nested when the official read succeeded.
Command output is written to stdout. Failures are written to stderr as a stable error code and a safe message, without raw RPC data or app-server stderr. The experimental profile warning is always written to stderr, including successful profile calls. Pet opt-in adds a second warning. An unavailable private profile still emits an envelope and exits with status `1`; canonical usage remains nested when the official read succeeded.

@@ -187,2 +168,22 @@ ## SDK

The experimental profile and pet APIs are isolated in an explicit subpath:
```js
import {
listExperimentalPets,
readExperimentalProfile
} from "codex-usage-analyzer/experimental-profile";
const pets = await listExperimentalPets();
const selectedKey = pets.find((pet) => pet.selected)?.key;
const profile = await readExperimentalProfile({
includePet: true,
...(selectedKey === undefined ? {} : { petKey: selectedKey })
});
```
`listExperimentalPets()` returns deterministic, one-based keys and safe display
metadata; it does not read image bytes. Module callers can instead pass
`selectPet(catalog)` and return a key. The root SDK surface remains unchanged.
## How it works

@@ -206,3 +207,3 @@

Downstreams that explicitly accept the Full Profile Envelope must keep GitHub identity as the ownership proof, treat remote profile identity as cosmetic untrusted input, and opt in separately before storing activity insights.
Downstreams that explicitly accept the Full Profile Envelope must keep GitHub identity as the ownership proof, treat remote profile identity as cosmetic untrusted input, and opt in separately before storing activity insights or pet image bytes.

@@ -220,6 +221,191 @@ ## Privacy and Security

The experimental `profile` command has a different privacy boundary: it can emit identity, an avatar source URL, plan information, activity insights, and invocation names. Inspect its JSON and the downstream privacy policy before storing, publishing, or submitting it.
The experimental `profile` command has a different privacy boundary: it can emit identity, an avatar source URL, plan information, activity insights, and invocation names. With `--include-pet`, it also reads a local custom-pet manifest and embeds the complete spritesheet as base64 plus a digest and image metadata. The base64 is the image, not a harmless identifier. Inspect its JSON and the downstream privacy policy before storing, publishing, or submitting it.
For vulnerability reporting and supported versions, see [SECURITY.md](SECURITY.md).
## Experimental: `profile` command
The default command and `--json` continue to use the documented
[`account/usage/read`](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md)
method and remain identity-free. The explicit experimental `profile` command
includes the same canonical usage and adds:
- **Profile:** display name, username, avatar information, and plan type
- **Token activity:** a 52-week map derived from canonical daily usage
- **Activity insights:** fast mode share, reasoning effort, skills explored,
total skill uses, and total threads
- **Top invocations:** most-used skills and plugins with usage counts
- **Optional custom pet:** the complete local spritesheet and validated metadata,
only when `--include-pet` is present
> **Experimental and unsupported:** `profile` uses the private
> `/wham/profiles/me` endpoint. It can change or stop working without notice.
> The command prints a warning to stderr and may emit account identity and
> private activity information.
Human-readable output:
```bash
npx codex-usage-analyzer@latest profile
```
```text
Codex profile (experimental)
Status ok
Profile
Display name Example Name
Username @example-user
Avatar Available
Plan example-plan
Usage
Codex account usage
Lifetime tokens 1.23B
Peak daily tokens 45.6M
Longest turn 12m 34s
Current streak 3 days
Longest streak 21 days
Daily buckets 4 days
Captured at 2026-07-11T00:00:00.000Z
Token activity
Sun |....................................................|
Mon |...................................................:|
Tue |...................................................*|
Wed |...................................................O|
Thu |...................................................#|
Fri |....................................................|
Sat |....................................................|
Legend . 0 : 1 * 2 O 3 # 4
Activity insights
Fast mode 25%
Reasoning effort high (50%)
Skills explored 6
Total skill uses 42
Total threads 128
Top invocations
$example-skill 9
@example-plugin 4
```
For machine-readable output:
```bash
npx codex-usage-analyzer@latest profile --json
```
To include a custom pet, use one of these explicit forms:
```bash
# Prefer the custom pet already selected in Codex Desktop.
npx codex-usage-analyzer@latest profile --json --include-pet
# Choose by the deterministic one-based catalog key.
npx codex-usage-analyzer@latest profile --json --include-pet --pet-key 2
# Force the interactive arrow-key selector (TTY only).
npx codex-usage-analyzer@latest profile --include-pet --select-pet
```
Human TTY output automatically offers the selector only when the Desktop-selected
custom pet cannot be resolved. JSON output never prompts unless `--select-pet`
is explicitly present. Cancellation or an invalid selection produces a safe
unavailable `pet` result rather than silently choosing the first pet.
```json
{
"fullProfileContractVersion": 1,
"kind": "codex-usage-analyzer.fullProfile",
"stability": "experimental",
"status": "ok",
"usage": {
"contractVersion": 1,
"capturedAt": "2026-07-11T00:00:00.000Z",
"summary": {
"lifetimeTokens": 1234567890,
"peakDailyTokens": 45600000,
"longestRunningTurnSec": 754,
"currentStreakDays": 3,
"longestStreakDays": 21
},
"dailyUsageBuckets": [
{
"startDate": "2026-07-06",
"tokens": 11400000
},
{
"startDate": "2026-07-07",
"tokens": 22800000
},
{
"startDate": "2026-07-08",
"tokens": 34200000
},
{
"startDate": "2026-07-09",
"tokens": 45600000
}
]
},
"profile": {
"displayName": "Example Name",
"username": "example-user",
"avatarUrl": "https://example.invalid/avatar.png",
"planType": "example-plan"
},
"activityInsights": {
"fastModePercent": 25,
"reasoningEffort": "high",
"reasoningEffortPercent": 50,
"skillsExplored": 6,
"totalSkillsUsed": 42,
"totalThreads": 128,
"topInvocations": [
{
"type": "skill",
"name": "example-skill",
"usageCount": 9
},
{
"type": "plugin",
"name": "example-plugin",
"usageCount": 4
}
]
}
}
```
> The examples above use synthetic values. An actual `profile` command returns
> the data available to the currently signed-in Codex account.
Key limitations:
- Human-readable output reports the avatar only as `Available` or `Unavailable`;
JSON includes the validated source URL, which consumers must treat as untrusted.
- The experiment makes one fixed request to an unsupported private endpoint,
without retries, alternate endpoints, Desktop-client impersonation, or fallback
from the default command.
- Authentication context remains in process memory for the request. The CLI does
not directly read authentication files, cookies, or keychains, and JavaScript
cannot guarantee memory zeroization.
- `profile --json` returns a separate experimental Full Profile Envelope; it does
not extend the stable Account Usage Contract or root SDK.
- Pet inclusion changes the envelope from v1 to v2 and adds the required `pet`
field. Without `--include-pet`, output remains Full Profile v1 compatible.
- Catalog keys are local, one-based selectors for the current catalog snapshot;
they are not persistent pet identifiers.
- Pet base64, digest, dimensions, and byte length are present in JSON. Human
output shows only safe metadata and never prints the base64 or digest.
See the [Experimental Full Profile Contract](docs/experimental-full-profile.md)
and the JSON Schemas for [v1](docs/experimental-full-profile.schema.json) and
[v2](docs/experimental-full-profile-v2.schema.json) before integrating, storing,
or publishing this output.
## Troubleshooting

@@ -226,0 +412,0 @@

+1
-1

@@ -196,3 +196,3 @@ import { spawn } from "node:child_process";

title: "Codex Usage Analyzer",
version: "0.4.0"
version: "0.4.1"
}

@@ -199,0 +199,0 @@ }

@@ -6,5 +6,7 @@ import { readAccountUsage } from "./account-usage.js";

export const PACKAGE_NAME = "codex-usage-analyzer";
export const PACKAGE_VERSION = "0.4.0";
export const PACKAGE_VERSION = "0.4.1";
export const EXPERIMENTAL_PROFILE_WARNING =
"codex-usage-analyzer: Warning: profile uses an unsupported experimental endpoint and may expose account identity fields.";
export const EXPERIMENTAL_PET_WARNING =
"codex-usage-analyzer: Warning: --include-pet reads local custom pet metadata and image bytes.";

@@ -16,3 +18,4 @@ const USAGE = [

" codex-usage-analyzer [usage] [--json]",
" codex-usage-analyzer profile [--json] (experimental)",
" codex-usage-analyzer profile [--json] [--include-pet] (experimental)",
" codex-usage-analyzer profile --include-pet [--pet-key N | --select-pet]",
" codex-usage-analyzer [usage|profile] --help",

@@ -25,2 +28,3 @@ " codex-usage-analyzer --version"

const stderr = io.stderr ?? process.stderr;
const stdin = io.stdin ?? process.stdin;
const parsed = parseArguments(argv);

@@ -43,9 +47,41 @@

if (parsed.action === "profile"
&& parsed.selectPet
&& (stdin?.isTTY !== true || stderr?.isTTY !== true)) {
stderr.write(`${USAGE}\n`);
return 1;
}
try {
if (parsed.action === "profile") {
stderr.write(`${EXPERIMENTAL_PROFILE_WARNING}\n`);
if (parsed.includePet) stderr.write(`${EXPERIMENTAL_PET_WARNING}\n`);
const readProfile = dependencies.readExperimentalProfile
?? (await import("./experimental-profile-client.js"))
.readExperimentalProfile;
const profile = await readProfile();
let profile;
if (parsed.includePet) {
const readOptions = { includePet: true };
if (parsed.petKey !== null) readOptions.petKey = parsed.petKey;
const useSelector = parsed.petKey === null && (
parsed.selectPet
|| (!parsed.json && stdin?.isTTY === true && stderr?.isTTY === true)
);
if (useSelector) {
const selectPet = dependencies.selectExperimentalPet
?? (await import("./experimental-pet-selector.js"))
.selectExperimentalPet;
readOptions.selectPet = (catalog) => selectPet(catalog, {
input: stdin,
output: stderr
});
}
if (parsed.selectPet) readOptions.forcePetSelection = true;
profile = await readProfile(readOptions);
} else {
profile = await readProfile();
}
let output;

@@ -95,23 +131,65 @@

if (args.length === 0) {
return { action, json: false };
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
return { action: "help" };
}
if (args.length !== 1) {
return { action: "invalid" };
if (args.length === 1 && (args[0] === "--version" || args[0] === "-v")) {
return { action: "version" };
}
if (args[0] === "--json") {
return { action, json: true };
if (action === "usage") {
if (args.length === 0) return { action, json: false };
if (args.length === 1 && args[0] === "--json") {
return { action, json: true };
}
return { action: "invalid" };
}
if (args[0] === "--help" || args[0] === "-h") {
return { action: "help" };
let json = false;
let includePet = false;
let selectPet = false;
let petKey = null;
for (let index = 0; index < args.length; index += 1) {
const argument = args[index];
if (argument === "--json") {
if (json) return { action: "invalid" };
json = true;
continue;
}
if (argument === "--include-pet") {
if (includePet) return { action: "invalid" };
includePet = true;
continue;
}
if (argument === "--select-pet") {
if (selectPet) return { action: "invalid" };
selectPet = true;
continue;
}
if (argument === "--pet-key") {
if (petKey !== null || index + 1 >= args.length) {
return { action: "invalid" };
}
const value = args[index + 1];
if (!/^[1-9][0-9]*$/u.test(value)) return { action: "invalid" };
petKey = Number(value);
if (!Number.isSafeInteger(petKey)) return { action: "invalid" };
index += 1;
continue;
}
return { action: "invalid" };
}
if (args[0] === "--version" || args[0] === "-v") {
return { action: "version" };
if ((petKey !== null || selectPet) && !includePet) {
return { action: "invalid" };
}
if (petKey !== null && selectPet) return { action: "invalid" };
return { action: "invalid" };
return { action, json, includePet, petKey, selectPet };
}

@@ -9,3 +9,5 @@ import { spawn } from "node:child_process";

createUnavailableFullProfile,
normalizeFullProfileResult
createUnavailableFullProfileV2,
normalizeFullProfileResult,
normalizeFullProfileV2Result
} from "./experimental-profile.js";

@@ -25,4 +27,5 @@

const AUTH_STATUS_REQUEST_ID = 3;
const CLIENT_VERSION = "0.4.0";
const CLIENT_VERSION = "0.4.1";
const ACCOUNT_CLAIM = "https://api.openai.com/auth";
const AUTHORIZATION_SCHEME = "Bearer";

@@ -39,2 +42,5 @@ export async function readExperimentalProfile(options = {}) {

});
const pet = options.includePet === true
? await readRequestedPet(options)
: null;
const planType = extractPlanType(session.accountResult);

@@ -50,3 +56,5 @@ let authToken = extractAuthToken(session.authResult);

accountId = null;
return createUnavailableFullProfile(usage);
return options.includePet === true
? createUnavailableFullProfileV2(usage, pet)
: createUnavailableFullProfile(usage);
}

@@ -63,6 +71,10 @@

if (remoteResult === null) {
return createUnavailableFullProfile(usage);
return options.includePet === true
? createUnavailableFullProfileV2(usage, pet)
: createUnavailableFullProfile(usage);
}
return normalizeFullProfileResult(usage, remoteResult, { planType });
return options.includePet === true
? normalizeFullProfileV2Result(usage, remoteResult, pet, { planType })
: normalizeFullProfileResult(usage, remoteResult, { planType });
} finally {

@@ -74,2 +86,83 @@ authToken = null;

async function readRequestedPet(options) {
let petModule;
async function loadPetModule() {
petModule ??= await import("./experimental-pet.js");
return petModule;
}
async function readPet(petOptions) {
const reader = options.readExperimentalPet
?? (await loadPetModule()).readExperimentalPet;
return reader(petOptions);
}
async function listPets(petOptions) {
const reader = options.listExperimentalPets
?? (await loadPetModule()).listExperimentalPets;
return reader(petOptions);
}
const petOptions = copyPetSourceOptions(options);
try {
if (options.forcePetSelection === true) {
return await selectRequestedPet(options, petOptions, listPets, readPet);
}
if (Object.hasOwn(options, "petKey")) {
return await readPet({ ...petOptions, petKey: options.petKey });
}
const selected = await readPet(petOptions);
if (selected?.status === "ok" || typeof options.selectPet !== "function") {
return selected;
}
return await selectRequestedPet(options, petOptions, listPets, readPet);
} catch {
return selectionUnavailablePet();
}
}
async function selectRequestedPet(
options,
petOptions,
listPets,
readPet
) {
if (typeof options.selectPet !== "function") {
return selectionUnavailablePet();
}
try {
const catalog = await listPets(petOptions);
const petKey = await options.selectPet(catalog);
if (!Number.isSafeInteger(petKey) || petKey < 1) {
return selectionUnavailablePet();
}
return await readPet({ ...petOptions, petKey });
} catch {
return selectionUnavailablePet();
}
}
function copyPetSourceOptions(options) {
const copied = {};
for (const key of ["codexHome", "env", "homeDir"]) {
if (Object.hasOwn(options, key)) copied[key] = options[key];
}
return copied;
}
function selectionUnavailablePet() {
return {
status: "unavailable",
reason: "selected_pet_selection_unavailable",
kind: null,
image: null
};
}
async function requestExperimentalSession(options) {

@@ -367,3 +460,3 @@ const spawnProcess = options.spawnProcess ?? spawn;

Accept: "application/json",
Authorization: `Bearer ${options.authToken}`,
Authorization: `${AUTHORIZATION_SCHEME} ${options.authToken}`,
"ChatGPT-Account-Id": options.accountId,

@@ -370,0 +463,0 @@ originator: "codex-usage-analyzer",

@@ -0,4 +1,14 @@

import { createHash } from "node:crypto";
import { ACCOUNT_USAGE_SUMMARY_FIELDS } from "./account-usage.js";
import {
EXPERIMENTAL_PET_IMAGE_CONTENT_TYPES,
EXPERIMENTAL_PET_REASONS,
MAX_EXPERIMENTAL_PET_IMAGE_BYTES,
MAX_EXPERIMENTAL_PET_IMAGE_DIMENSION,
MAX_EXPERIMENTAL_PET_IMAGE_PIXELS
} from "./experimental-pet.js";
export const FULL_PROFILE_CONTRACT_VERSION = 1;
export const FULL_PROFILE_V2_CONTRACT_VERSION = 2;
export const FULL_PROFILE_KIND = "codex-usage-analyzer.fullProfile";

@@ -36,2 +46,30 @@ export const FULL_PROFILE_STABILITY = "experimental";

export const FULL_PROFILE_V2_FIELDS = Object.freeze([
"fullProfileContractVersion",
"kind",
"stability",
"status",
"usage",
"profile",
"activityInsights",
"pet"
]);
export const EXPERIMENTAL_PET_FIELDS = Object.freeze([
"status",
"reason",
"kind",
"image"
]);
export const EXPERIMENTAL_PET_IMAGE_FIELDS = Object.freeze([
"role",
"contentType",
"width",
"height",
"byteLength",
"sha256",
"base64"
]);
const PROFILE_STRING_LIMITS = Object.freeze({

@@ -96,2 +134,28 @@ displayName: 256,

export function normalizeFullProfileV2Result(
usageDocument,
remoteResult,
petResult,
options = {}
) {
const fullProfileV1 = normalizeFullProfileResult(
usageDocument,
remoteResult,
options
);
const pet = normalizeExperimentalPet(petResult);
return createV2Envelope(fullProfileV1, pet);
}
export function createUnavailableFullProfileV2(
usageDocument,
petResult = null
) {
return createV2Envelope(
createUnavailableFullProfile(usageDocument),
normalizeExperimentalPet(petResult)
);
}
function createEnvelope(status, usage, profile, activityInsights) {

@@ -109,2 +173,117 @@ return {

function createV2Envelope(fullProfileV1, pet) {
return {
fullProfileContractVersion: FULL_PROFILE_V2_CONTRACT_VERSION,
kind: fullProfileV1.kind,
stability: fullProfileV1.stability,
status: resolveFullProfileV2Status(fullProfileV1.status, pet.status),
usage: fullProfileV1.usage,
profile: fullProfileV1.profile,
activityInsights: fullProfileV1.activityInsights,
pet
};
}
function resolveFullProfileV2Status(remoteStatus, petStatus) {
if (remoteStatus === "ok" && petStatus === "ok") return "ok";
if (remoteStatus === "unavailable" && petStatus === "unavailable") {
return "unavailable";
}
return "partial";
}
function normalizeExperimentalPet(value) {
if (!isRecord(value)) return unavailableExperimentalPet();
if (value.status === "unavailable"
&& EXPERIMENTAL_PET_REASONS.includes(value.reason)
&& value.kind === null
&& value.image === null) {
return {
status: "unavailable",
reason: value.reason,
kind: null,
image: null
};
}
if (value.status !== "ok"
|| value.reason !== null
|| value.kind !== "custom") {
return unavailableExperimentalPet();
}
const image = normalizeExperimentalPetImage(value.image);
if (image === null) return unavailableExperimentalPet();
return {
status: "ok",
reason: null,
kind: "custom",
image
};
}
function normalizeExperimentalPetImage(value) {
if (!isRecord(value)
|| value.role !== "spritesheet"
|| !EXPERIMENTAL_PET_IMAGE_CONTENT_TYPES.includes(value.contentType)
|| !isPositiveBoundedInteger(
value.width,
MAX_EXPERIMENTAL_PET_IMAGE_DIMENSION
)
|| !isPositiveBoundedInteger(
value.height,
MAX_EXPERIMENTAL_PET_IMAGE_DIMENSION
)
|| value.width * value.height > MAX_EXPERIMENTAL_PET_IMAGE_PIXELS
|| !isPositiveBoundedInteger(
value.byteLength,
MAX_EXPERIMENTAL_PET_IMAGE_BYTES
)
|| typeof value.sha256 !== "string"
|| !/^[0-9a-f]{64}$/u.test(value.sha256)
|| typeof value.base64 !== "string"
|| !isStrictBase64(value.base64)) {
return null;
}
const bytes = Buffer.from(value.base64, "base64");
if (bytes.byteLength !== value.byteLength
|| createHash("sha256").update(bytes).digest("hex") !== value.sha256) {
return null;
}
return {
role: "spritesheet",
contentType: value.contentType,
width: value.width,
height: value.height,
byteLength: value.byteLength,
sha256: value.sha256,
base64: value.base64
};
}
function unavailableExperimentalPet() {
return {
status: "unavailable",
reason: "selected_pet_state_unavailable",
kind: null,
image: null
};
}
function isPositiveBoundedInteger(value, maximum) {
return Number.isSafeInteger(value) && value >= 1 && value <= maximum;
}
function isStrictBase64(value) {
return value.length >= 4
&& value.length <= 11_184_812
&& value.length % 4 === 0
&& /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u
.test(value);
}
function normalizeProfile(value, planType) {

@@ -111,0 +290,0 @@ const normalizedPlanType = normalizeOptionalString(

@@ -20,2 +20,6 @@ import { formatAccountUsage } from "./format-account-usage.js";

export function formatExperimentalProfile(envelope) {
const petSection = Object.hasOwn(envelope, "pet")
? ["", ...formatPet(envelope.pet)]
: [];
return [

@@ -26,2 +30,3 @@ "Codex profile (experimental)",

...formatProfile(envelope.profile),
...petSection,
"",

@@ -39,2 +44,19 @@ "Usage",

function formatPet(pet) {
if (pet?.status !== "ok" || pet.kind !== "custom" || pet.image === null) {
return ["Pet", UNAVAILABLE];
}
return [
"Pet",
...formatRows([
["Status", "Available"],
["Kind", "custom"],
["Content type", pet.image.contentType],
["Dimensions", `${pet.image.width} × ${pet.image.height}`],
["Byte length", formatInteger(pet.image.byteLength)]
])
];
}
function formatProfile(profile) {

@@ -41,0 +63,0 @@ if (profile === null) return ["Profile", UNAVAILABLE];

export declare const PACKAGE_NAME: "codex-usage-analyzer";
export declare const PACKAGE_VERSION: "0.4.0";
export declare const PACKAGE_VERSION: "0.4.1";
export declare const ACCOUNT_USAGE_CONTRACT_VERSION: 1;

@@ -32,2 +32,94 @@ export declare const ACCOUNT_USAGE_SUMMARY_FIELDS: readonly [

export type ExperimentalFullProfileStatus =
| "ok"
| "partial"
| "unavailable";
export interface ExperimentalFullProfileProfile {
displayName: string | null;
username: string | null;
avatarUrl: string | null;
planType: string | null;
}
export interface ExperimentalFullProfileInvocation {
type: "plugin" | "skill";
name: string;
usageCount: number;
}
export interface ExperimentalFullProfileActivityInsights {
fastModePercent: number | null;
reasoningEffort: string | null;
reasoningEffortPercent: number | null;
skillsExplored: number | null;
totalSkillsUsed: number | null;
totalThreads: number | null;
topInvocations: ExperimentalFullProfileInvocation[] | null;
}
export interface ExperimentalFullProfileV1 {
fullProfileContractVersion: 1;
kind: "codex-usage-analyzer.fullProfile";
stability: "experimental";
status: ExperimentalFullProfileStatus;
usage: AccountUsageDocument;
profile: ExperimentalFullProfileProfile | null;
activityInsights: ExperimentalFullProfileActivityInsights | null;
}
export type ExperimentalPetReason =
| "selected_pet_state_unavailable"
| "selected_pet_not_custom"
| "selected_pet_selection_unavailable"
| "selected_pet_manifest_unavailable"
| "selected_pet_image_unavailable"
| "selected_pet_image_invalid"
| "selected_pet_image_too_large";
export interface ExperimentalPetImage {
role: "spritesheet";
contentType: "image/webp" | "image/png";
width: number;
height: number;
byteLength: number;
sha256: string;
base64: string;
}
export interface ExperimentalPetAvailable {
status: "ok";
reason: null;
kind: "custom";
image: ExperimentalPetImage;
}
export interface ExperimentalPetUnavailable {
status: "unavailable";
reason: ExperimentalPetReason;
kind: null;
image: null;
}
export type ExperimentalPet =
| ExperimentalPetAvailable
| ExperimentalPetUnavailable;
export interface ExperimentalPetCatalogItem {
key: number;
displayName: string | null;
selected: boolean;
}
export interface ExperimentalFullProfileV2 {
fullProfileContractVersion: 2;
kind: "codex-usage-analyzer.fullProfile";
stability: "experimental";
status: ExperimentalFullProfileStatus;
usage: AccountUsageDocument;
profile: ExperimentalFullProfileProfile | null;
activityInsights: ExperimentalFullProfileActivityInsights | null;
pet: ExperimentalPet;
}
export interface ReadAccountUsageOptions {

@@ -65,6 +157,11 @@ timeoutMs?: number;

export interface CliIo {
stdin?: {
isTTY?: boolean;
};
stdout?: {
isTTY?: boolean;
write(chunk: string): unknown;
};
stderr?: {
isTTY?: boolean;
write(chunk: string): unknown;

@@ -71,0 +168,0 @@ };