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.
@@ -0,0 +1,519 @@
1
+ export const USAGE_SNAPSHOT_V2_SCHEMA_VERSION = 2;
2
+
3
+ const TOP_LEVEL_REQUIRED_KEYS = [
4
+ "schemaVersion",
5
+ "capturedAt",
6
+ "usage",
7
+ "models",
8
+ "activity",
9
+ "skills",
10
+ "plugins"
11
+ ];
12
+ const TOP_LEVEL_OPTIONAL_KEYS = [
13
+ "producer",
14
+ "codexProfile",
15
+ "codexAssets",
16
+ "extensions"
17
+ ];
18
+ const PRODUCER_KEYS = ["name", "version"];
19
+ const CODEX_PROFILE_KEYS = ["displayName", "username", "planLabel"];
20
+ const USAGE_KEYS = ["totalTokens", "peakDailyTokens", "tokenBreakdown", "daily"];
21
+ const TOKEN_BREAKDOWN_KEYS = [
22
+ "inputTokens",
23
+ "outputTokens",
24
+ "cacheReadTokens",
25
+ "cacheWriteTokens",
26
+ "reasoningTokens"
27
+ ];
28
+ const DAILY_USAGE_KEYS = ["date", "totalTokens", ...TOKEN_BREAKDOWN_KEYS];
29
+ const MODELS_KEYS = ["favoriteModel", "items"];
30
+ const MODEL_USAGE_KEYS = [
31
+ "model",
32
+ "displayName",
33
+ "totalTokens",
34
+ "usageCount",
35
+ "basis",
36
+ ...TOKEN_BREAKDOWN_KEYS
37
+ ];
38
+ const MODEL_USAGE_BASIS = new Set(["tokens", "usage_count", "duration", "unknown"]);
39
+ const ACTIVITY_KEYS = [
40
+ "longestTaskDurationMs",
41
+ "currentStreakDays",
42
+ "longestStreakDays",
43
+ "fastModePercent",
44
+ "reasoningEffort",
45
+ "reasoningEffortPercent",
46
+ "totalThreads"
47
+ ];
48
+ const SKILLS_KEYS = ["exploredCount", "totalUsed", "topSkills"];
49
+ const PLUGINS_KEYS = ["topPlugins"];
50
+ const RANKING_ITEM_KEYS = ["id", "name", "displayName", "usageCount"];
51
+ const CODEX_ASSETS_KEYS = ["avatar", "pet"];
52
+ const ASSET_KEYS = ["kind", "url", "assetRef", "contentType"];
53
+ const ASSET_KINDS = new Set([
54
+ "remote-url",
55
+ "data-url",
56
+ "uploaded-asset",
57
+ "codex-asset",
58
+ "spritesheet"
59
+ ]);
60
+ const SAFE_SECRET_SUFFIXES = new Set(["digest", "hash", "fingerprint"]);
61
+ const SECRET_WORDS = new Set(["password", "secret"]);
62
+ const KEY_PREFIX_WORDS = new Set([
63
+ "access",
64
+ "api",
65
+ "auth",
66
+ "codex",
67
+ "github",
68
+ "openai",
69
+ "private",
70
+ "upload"
71
+ ]);
72
+ const TOKEN_PREFIX_WORDS = new Set([
73
+ "access",
74
+ "api",
75
+ "auth",
76
+ "bearer",
77
+ "cli",
78
+ "codex",
79
+ "github",
80
+ "id",
81
+ "openai",
82
+ "refresh",
83
+ "upload"
84
+ ]);
85
+ const SECRET_VALUE_PATTERNS = [
86
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/i,
87
+ /\bCODEX_ACCESS_TOKEN\s*=/i,
88
+ /"access_token"\s*:/i,
89
+ /"refresh_token"\s*:/i,
90
+ /\bsk-[A-Za-z0-9_-]{10,}\b/,
91
+ /\bgh[opsu]_[A-Za-z0-9_]{20,}\b/,
92
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
93
+ /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/
94
+ ];
95
+ const LOCAL_PRIVATE_PATH_RE = /^(?:\/Users\/|\/home\/|\/var\/|\/private\/var\/|[A-Za-z]:\\)/;
96
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
97
+
98
+ export function validateUsageSnapshotV2(value) {
99
+ const errors = [];
100
+
101
+ collectForbiddenSnapshotFields(value, "$", errors, new WeakSet());
102
+
103
+ if (!isRecord(value)) {
104
+ errors.push("$: expected object");
105
+ return { ok: false, errors };
106
+ }
107
+
108
+ validateKeys("$", value, TOP_LEVEL_REQUIRED_KEYS, TOP_LEVEL_OPTIONAL_KEYS, errors);
109
+ validateLiteral("$.schemaVersion", value.schemaVersion, USAGE_SNAPSHOT_V2_SCHEMA_VERSION, errors);
110
+ validateIsoDateTime("$.capturedAt", value.capturedAt, errors);
111
+ validateOptionalProducer("$.producer", value.producer, errors);
112
+ validateOptionalCodexProfile("$.codexProfile", value.codexProfile, errors);
113
+ validateUsage("$.usage", value.usage, errors);
114
+ validateModels("$.models", value.models, errors);
115
+ validateActivity("$.activity", value.activity, errors);
116
+ validateSkills("$.skills", value.skills, errors);
117
+ validatePlugins("$.plugins", value.plugins, errors);
118
+ validateOptionalCodexAssets("$.codexAssets", value.codexAssets, errors);
119
+ validateOptionalExtensions("$.extensions", value.extensions, errors);
120
+
121
+ return { ok: errors.length === 0, errors };
122
+ }
123
+
124
+ export function assertUsageSnapshotV2(value) {
125
+ const result = validateUsageSnapshotV2(value);
126
+
127
+ if (!result.ok) {
128
+ throw new TypeError(`Invalid usage snapshot v2:\n${result.errors.join("\n")}`);
129
+ }
130
+
131
+ return value;
132
+ }
133
+
134
+ export function isUsageSnapshotV2(value) {
135
+ return validateUsageSnapshotV2(value).ok;
136
+ }
137
+
138
+ function validateOptionalProducer(path, value, errors) {
139
+ if (value === undefined) return;
140
+ if (!expectRecord(path, value, errors)) return;
141
+
142
+ validateKeys(path, value, PRODUCER_KEYS, [], errors);
143
+ validateNonEmptyString(`${path}.name`, value.name, errors);
144
+ validateNonEmptyString(`${path}.version`, value.version, errors);
145
+ }
146
+
147
+ function validateOptionalCodexProfile(path, value, errors) {
148
+ if (value === undefined) return;
149
+ if (!expectRecord(path, value, errors)) return;
150
+
151
+ validateKeys(path, value, CODEX_PROFILE_KEYS, [], errors);
152
+ validateNullableString(`${path}.displayName`, value.displayName, errors);
153
+ validateNullableString(`${path}.username`, value.username, errors);
154
+ validateNullableString(`${path}.planLabel`, value.planLabel, errors);
155
+ }
156
+
157
+ function validateUsage(path, value, errors) {
158
+ if (!expectRecord(path, value, errors)) return;
159
+
160
+ validateKeys(path, value, USAGE_KEYS, [], errors);
161
+ validateNonNegativeInteger(`${path}.totalTokens`, value.totalTokens, errors);
162
+ validateNullableNonNegativeInteger(`${path}.peakDailyTokens`, value.peakDailyTokens, errors);
163
+ validateTokenBreakdown(`${path}.tokenBreakdown`, value.tokenBreakdown, errors);
164
+ validateDailyUsage(`${path}.daily`, value.daily, errors);
165
+ }
166
+
167
+ function validateTokenBreakdown(path, value, errors) {
168
+ if (!expectRecord(path, value, errors)) return;
169
+
170
+ validateKeys(path, value, TOKEN_BREAKDOWN_KEYS, [], errors);
171
+ validateTokenBreakdownFields(path, value, errors);
172
+ }
173
+
174
+ function validateTokenBreakdownFields(path, value, errors) {
175
+ for (const key of TOKEN_BREAKDOWN_KEYS) {
176
+ validateNullableNonNegativeInteger(`${path}.${key}`, value[key], errors);
177
+ }
178
+ }
179
+
180
+ function validateDailyUsage(path, value, errors) {
181
+ if (!Array.isArray(value)) {
182
+ errors.push(`${path}: expected array`);
183
+ return;
184
+ }
185
+
186
+ value.forEach((bucket, index) => {
187
+ const bucketPath = `${path}[${index}]`;
188
+ if (!expectRecord(bucketPath, bucket, errors)) return;
189
+
190
+ validateKeys(bucketPath, bucket, DAILY_USAGE_KEYS, [], errors);
191
+ validateIsoDate(`${bucketPath}.date`, bucket.date, errors);
192
+ validateNonNegativeInteger(`${bucketPath}.totalTokens`, bucket.totalTokens, errors);
193
+ validateTokenBreakdownFields(bucketPath, bucket, errors);
194
+ });
195
+ }
196
+
197
+ function validateModels(path, value, errors) {
198
+ if (!expectRecord(path, value, errors)) return;
199
+
200
+ validateKeys(path, value, MODELS_KEYS, [], errors);
201
+ validateNullableModelUsage(`${path}.favoriteModel`, value.favoriteModel, errors);
202
+ validateModelUsageArray(`${path}.items`, value.items, errors);
203
+ }
204
+
205
+ function validateNullableModelUsage(path, value, errors) {
206
+ if (value === null) return;
207
+ validateModelUsage(path, value, errors);
208
+ }
209
+
210
+ function validateModelUsageArray(path, value, errors) {
211
+ if (!Array.isArray(value)) {
212
+ errors.push(`${path}: expected array`);
213
+ return;
214
+ }
215
+
216
+ value.forEach((item, index) => {
217
+ validateModelUsage(`${path}[${index}]`, item, errors);
218
+ });
219
+ }
220
+
221
+ function validateModelUsage(path, value, errors) {
222
+ if (!expectRecord(path, value, errors)) return;
223
+
224
+ validateKeys(path, value, MODEL_USAGE_KEYS, [], errors);
225
+ validateNonEmptyString(`${path}.model`, value.model, errors);
226
+ validateNullableString(`${path}.displayName`, value.displayName, errors);
227
+ validateNullableNonNegativeInteger(`${path}.totalTokens`, value.totalTokens, errors);
228
+ validateNullableNonNegativeInteger(`${path}.usageCount`, value.usageCount, errors);
229
+ validateEnum(`${path}.basis`, value.basis, MODEL_USAGE_BASIS, errors);
230
+ validateTokenBreakdownFields(path, value, errors);
231
+ }
232
+
233
+ function validateActivity(path, value, errors) {
234
+ if (!expectRecord(path, value, errors)) return;
235
+
236
+ validateKeys(path, value, ACTIVITY_KEYS, [], errors);
237
+ validateNullableNonNegativeInteger(`${path}.longestTaskDurationMs`, value.longestTaskDurationMs, errors);
238
+ validateNullableNonNegativeInteger(`${path}.currentStreakDays`, value.currentStreakDays, errors);
239
+ validateNullableNonNegativeInteger(`${path}.longestStreakDays`, value.longestStreakDays, errors);
240
+ validateNullablePercent(`${path}.fastModePercent`, value.fastModePercent, errors);
241
+ validateNullableString(`${path}.reasoningEffort`, value.reasoningEffort, errors);
242
+ validateNullablePercent(`${path}.reasoningEffortPercent`, value.reasoningEffortPercent, errors);
243
+ validateNullableNonNegativeInteger(`${path}.totalThreads`, value.totalThreads, errors);
244
+ }
245
+
246
+ function validateSkills(path, value, errors) {
247
+ if (!expectRecord(path, value, errors)) return;
248
+
249
+ validateKeys(path, value, SKILLS_KEYS, [], errors);
250
+ validateNullableNonNegativeInteger(`${path}.exploredCount`, value.exploredCount, errors);
251
+ validateNullableNonNegativeInteger(`${path}.totalUsed`, value.totalUsed, errors);
252
+ validateRankingItems(`${path}.topSkills`, value.topSkills, errors);
253
+ }
254
+
255
+ function validatePlugins(path, value, errors) {
256
+ if (!expectRecord(path, value, errors)) return;
257
+
258
+ validateKeys(path, value, PLUGINS_KEYS, [], errors);
259
+ validateRankingItems(`${path}.topPlugins`, value.topPlugins, errors);
260
+ }
261
+
262
+ function validateRankingItems(path, value, errors) {
263
+ if (!Array.isArray(value)) {
264
+ errors.push(`${path}: expected array`);
265
+ return;
266
+ }
267
+
268
+ value.forEach((item, index) => {
269
+ const itemPath = `${path}[${index}]`;
270
+ if (!expectRecord(itemPath, item, errors)) return;
271
+
272
+ validateKeys(itemPath, item, RANKING_ITEM_KEYS, [], errors);
273
+ validateNonEmptyString(`${itemPath}.id`, item.id, errors);
274
+ validateNullableString(`${itemPath}.name`, item.name, errors);
275
+ validateNullableString(`${itemPath}.displayName`, item.displayName, errors);
276
+ validateNonNegativeInteger(`${itemPath}.usageCount`, item.usageCount, errors);
277
+ });
278
+ }
279
+
280
+ function validateOptionalCodexAssets(path, value, errors) {
281
+ if (value === undefined) return;
282
+ if (!expectRecord(path, value, errors)) return;
283
+
284
+ validateKeys(path, value, CODEX_ASSETS_KEYS, [], errors);
285
+ validateNullableAsset(`${path}.avatar`, value.avatar, errors);
286
+ validateNullableAsset(`${path}.pet`, value.pet, errors);
287
+ }
288
+
289
+ function validateNullableAsset(path, value, errors) {
290
+ if (value === null) return;
291
+ if (!expectRecord(path, value, errors)) return;
292
+
293
+ validateKeys(path, value, ASSET_KEYS, [], errors);
294
+ validateEnum(`${path}.kind`, value.kind, ASSET_KINDS, errors);
295
+ validateNullableString(`${path}.url`, value.url, errors);
296
+ validateNullableString(`${path}.assetRef`, value.assetRef, errors);
297
+ validateNullableString(`${path}.contentType`, value.contentType, errors);
298
+
299
+ if (value.url === null && value.assetRef === null) {
300
+ errors.push(`${path}: expected url or assetRef`);
301
+ }
302
+ }
303
+
304
+ function validateOptionalExtensions(path, value, errors) {
305
+ if (value === undefined) return;
306
+ if (!expectRecord(path, value, errors)) return;
307
+
308
+ for (const key of Object.keys(value)) {
309
+ if (!key.includes(".")) {
310
+ errors.push(`${path}.${key}: expected namespaced extension key`);
311
+ }
312
+ }
313
+ }
314
+
315
+ function validateKeys(path, value, requiredKeys, optionalKeys, errors) {
316
+ const allowed = new Set([...requiredKeys, ...optionalKeys]);
317
+
318
+ for (const key of Object.keys(value)) {
319
+ if (!allowed.has(key)) {
320
+ errors.push(`${path}.${key}: unknown field`);
321
+ }
322
+ }
323
+
324
+ for (const key of requiredKeys) {
325
+ if (!Object.hasOwn(value, key)) {
326
+ errors.push(`${path}.${key}: missing field`);
327
+ }
328
+ }
329
+ }
330
+
331
+ function validateLiteral(path, value, expected, errors) {
332
+ if (value !== expected) {
333
+ errors.push(`${path}: expected ${expected}`);
334
+ }
335
+ }
336
+
337
+ function validateNonEmptyString(path, value, errors) {
338
+ if (typeof value !== "string" || value.trim().length === 0) {
339
+ errors.push(`${path}: expected non-empty string`);
340
+ }
341
+ }
342
+
343
+ function validateNullableString(path, value, errors) {
344
+ if (value === null) return;
345
+ if (typeof value !== "string") {
346
+ errors.push(`${path}: expected string or null`);
347
+ }
348
+ }
349
+
350
+ function validateNonNegativeInteger(path, value, errors) {
351
+ if (!Number.isInteger(value) || value < 0) {
352
+ errors.push(`${path}: expected non-negative integer`);
353
+ }
354
+ }
355
+
356
+ function validateNullableNonNegativeInteger(path, value, errors) {
357
+ if (value === null) return;
358
+ validateNonNegativeInteger(path, value, errors);
359
+ }
360
+
361
+ function validateNullablePercent(path, value, errors) {
362
+ if (value === null) return;
363
+ if (!Number.isFinite(value) || value < 0 || value > 100) {
364
+ errors.push(`${path}: expected percent between 0 and 100 or null`);
365
+ }
366
+ }
367
+
368
+ function validateEnum(path, value, allowedValues, errors) {
369
+ if (typeof value !== "string" || !allowedValues.has(value)) {
370
+ errors.push(`${path}: expected one of ${Array.from(allowedValues).join(", ")}`);
371
+ }
372
+ }
373
+
374
+ function validateIsoDate(path, value, errors) {
375
+ if (typeof value !== "string" || !ISO_DATE_RE.test(value)) {
376
+ errors.push(`${path}: expected YYYY-MM-DD date`);
377
+ return;
378
+ }
379
+
380
+ const date = new Date(`${value}T00:00:00.000Z`);
381
+ if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
382
+ errors.push(`${path}: expected valid UTC date`);
383
+ }
384
+ }
385
+
386
+ function validateIsoDateTime(path, value, errors) {
387
+ if (typeof value !== "string") {
388
+ errors.push(`${path}: expected ISO date-time string`);
389
+ return;
390
+ }
391
+
392
+ const date = new Date(value);
393
+ if (Number.isNaN(date.getTime()) || date.toISOString() !== value) {
394
+ errors.push(`${path}: expected valid ISO date-time string`);
395
+ }
396
+ }
397
+
398
+ function expectRecord(path, value, errors) {
399
+ if (!isRecord(value)) {
400
+ errors.push(`${path}: expected object`);
401
+ return false;
402
+ }
403
+
404
+ return true;
405
+ }
406
+
407
+ function isRecord(value) {
408
+ return value !== null && typeof value === "object" && !Array.isArray(value);
409
+ }
410
+
411
+ function collectForbiddenSnapshotFields(value, path, errors, seen) {
412
+ if (typeof value === "string") {
413
+ if (isForbiddenSecretValue(value)) {
414
+ errors.push(`${path}: forbidden credential-like value`);
415
+ }
416
+
417
+ if (LOCAL_PRIVATE_PATH_RE.test(value)) {
418
+ errors.push(`${path}: forbidden private local path`);
419
+ }
420
+
421
+ return;
422
+ }
423
+
424
+ if (value === null || typeof value !== "object") {
425
+ return;
426
+ }
427
+
428
+ if (seen.has(value)) {
429
+ return;
430
+ }
431
+ seen.add(value);
432
+
433
+ if (Array.isArray(value)) {
434
+ value.forEach((item, index) => {
435
+ collectForbiddenSnapshotFields(item, `${path}[${index}]`, errors, seen);
436
+ });
437
+ return;
438
+ }
439
+
440
+ for (const [key, child] of Object.entries(value)) {
441
+ const childPath = `${path}.${key}`;
442
+ const keyReason = getForbiddenKeyReason(key);
443
+
444
+ if (keyReason) {
445
+ errors.push(`${childPath}: forbidden ${keyReason}`);
446
+ }
447
+
448
+ collectForbiddenSnapshotFields(child, childPath, errors, seen);
449
+ }
450
+ }
451
+
452
+ function getForbiddenKeyReason(key) {
453
+ const words = keyToWords(key);
454
+
455
+ if (words.includes("github")) {
456
+ return "GitHub-facing field";
457
+ }
458
+
459
+ if (isForbiddenSecretKey(words, key)) {
460
+ return "credential-like field";
461
+ }
462
+
463
+ return null;
464
+ }
465
+
466
+ function isForbiddenSecretKey(words, key) {
467
+ const lowerKey = String(key).toLowerCase();
468
+ if (lowerKey === "auth.json" || lowerKey === "authjson") {
469
+ return true;
470
+ }
471
+
472
+ if (words.length === 0) {
473
+ return false;
474
+ }
475
+
476
+ if (words.some((word) => SECRET_WORDS.has(word))) {
477
+ return !words.some((word) => SAFE_SECRET_SUFFIXES.has(word));
478
+ }
479
+
480
+ if (words.includes("authorization") || words.includes("bearer")) {
481
+ return true;
482
+ }
483
+
484
+ const keyIndex = words.indexOf("key");
485
+ if (keyIndex !== -1) {
486
+ const nextWord = words[keyIndex + 1];
487
+ if (SAFE_SECRET_SUFFIXES.has(nextWord)) {
488
+ return false;
489
+ }
490
+
491
+ const previousWord = words[keyIndex - 1];
492
+ return KEY_PREFIX_WORDS.has(previousWord);
493
+ }
494
+
495
+ const tokenIndex = words.indexOf("token");
496
+ if (tokenIndex === -1) {
497
+ return false;
498
+ }
499
+
500
+ const nextWord = words[tokenIndex + 1];
501
+ if (SAFE_SECRET_SUFFIXES.has(nextWord)) {
502
+ return false;
503
+ }
504
+
505
+ const previousWord = words[tokenIndex - 1];
506
+ return words.length === 1 || TOKEN_PREFIX_WORDS.has(previousWord);
507
+ }
508
+
509
+ function isForbiddenSecretValue(value) {
510
+ return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
511
+ }
512
+
513
+ function keyToWords(key) {
514
+ return String(key)
515
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
516
+ .toLowerCase()
517
+ .split(/[^a-z0-9]+/)
518
+ .filter(Boolean);
519
+ }
@@ -0,0 +1,109 @@
1
+ export declare const USAGE_SNAPSHOT_V2_SCHEMA_VERSION: 2;
2
+
3
+ export interface UsageSnapshotV2 {
4
+ schemaVersion: typeof USAGE_SNAPSHOT_V2_SCHEMA_VERSION;
5
+ capturedAt: string;
6
+ producer?: UsageSnapshotProducerV2;
7
+ codexProfile?: UsageSnapshotCodexProfileV2;
8
+ usage: UsageSummaryV2;
9
+ models: UsageModelsV2;
10
+ activity: UsageActivityV2;
11
+ skills: UsageSkillsV2;
12
+ plugins: UsagePluginsV2;
13
+ codexAssets?: UsageCodexAssetsV2;
14
+ extensions?: Record<string, unknown>;
15
+ }
16
+
17
+ export interface UsageSnapshotProducerV2 {
18
+ name: string;
19
+ version: string;
20
+ }
21
+
22
+ export interface UsageSnapshotCodexProfileV2 {
23
+ displayName: string | null;
24
+ username: string | null;
25
+ planLabel: string | null;
26
+ }
27
+
28
+ export interface UsageSummaryV2 {
29
+ totalTokens: number;
30
+ peakDailyTokens: number | null;
31
+ tokenBreakdown: UsageTokenBreakdownV2;
32
+ daily: UsageDailyV2[];
33
+ }
34
+
35
+ export interface UsageTokenBreakdownV2 {
36
+ inputTokens: number | null;
37
+ outputTokens: number | null;
38
+ cacheReadTokens: number | null;
39
+ cacheWriteTokens: number | null;
40
+ reasoningTokens: number | null;
41
+ }
42
+
43
+ export interface UsageDailyV2 extends UsageTokenBreakdownV2 {
44
+ date: string;
45
+ totalTokens: number;
46
+ }
47
+
48
+ export interface UsageModelsV2 {
49
+ favoriteModel: UsageModelV2 | null;
50
+ items: UsageModelV2[];
51
+ }
52
+
53
+ export interface UsageModelV2 extends UsageTokenBreakdownV2 {
54
+ model: string;
55
+ displayName: string | null;
56
+ totalTokens: number | null;
57
+ usageCount: number | null;
58
+ basis: "tokens" | "usage_count" | "duration" | "unknown";
59
+ }
60
+
61
+ export interface UsageActivityV2 {
62
+ longestTaskDurationMs: number | null;
63
+ currentStreakDays: number | null;
64
+ longestStreakDays: number | null;
65
+ fastModePercent: number | null;
66
+ reasoningEffort: string | null;
67
+ reasoningEffortPercent: number | null;
68
+ totalThreads: number | null;
69
+ }
70
+
71
+ export interface UsageSkillsV2 {
72
+ exploredCount: number | null;
73
+ totalUsed: number | null;
74
+ topSkills: UsageRankingItemV2[];
75
+ }
76
+
77
+ export interface UsagePluginsV2 {
78
+ topPlugins: UsageRankingItemV2[];
79
+ }
80
+
81
+ export interface UsageRankingItemV2 {
82
+ id: string;
83
+ name: string | null;
84
+ displayName: string | null;
85
+ usageCount: number;
86
+ }
87
+
88
+ export interface UsageCodexAssetsV2 {
89
+ avatar: UsageSnapshotAssetV2 | null;
90
+ pet: UsageSnapshotAssetV2 | null;
91
+ }
92
+
93
+ export interface UsageSnapshotAssetV2 {
94
+ kind: "remote-url" | "data-url" | "uploaded-asset" | "codex-asset" | "spritesheet";
95
+ url: string | null;
96
+ assetRef: string | null;
97
+ contentType: string | null;
98
+ }
99
+
100
+ export interface UsageSnapshotV2ValidationResult {
101
+ ok: boolean;
102
+ errors: string[];
103
+ }
104
+
105
+ export declare function validateUsageSnapshotV2(value: unknown): UsageSnapshotV2ValidationResult;
106
+
107
+ export declare function assertUsageSnapshotV2(value: unknown): UsageSnapshotV2;
108
+
109
+ export declare function isUsageSnapshotV2(value: unknown): value is UsageSnapshotV2;