aieracard 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.js +312 -33
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -9,9 +9,12 @@ npx aieracard
|
|
|
9
9
|
The CLI collects usage **locally** from:
|
|
10
10
|
|
|
11
11
|
- **Claude Code** — local JSONL logs (`~/.claude/projects`), fully automatic
|
|
12
|
+
- **Codex** — local JSONL under `~/.codex/sessions` (override with `CODEX_HOME`)
|
|
12
13
|
- **Cursor** — all-time usage via your own cursor.com session (token resolved
|
|
13
14
|
locally, sent only to cursor.com); CSV export as fallback
|
|
14
|
-
- **OpenRouter** —
|
|
15
|
+
- **OpenRouter** — optional management API key (`/credits` + `/activity`).
|
|
16
|
+
Tokens are last 30 days; all-time spend stays on the source object and is
|
|
17
|
+
**not** rolled into the card's aggregate "compute spent"
|
|
15
18
|
|
|
16
19
|
It then shows you the **exact JSON** that would be uploaded — aggregate
|
|
17
20
|
numbers only: token counts, cost totals, active days, streaks, model names.
|
|
@@ -31,7 +34,7 @@ that unfurls into a stats card when shared.
|
|
|
31
34
|
--openrouter-key <k> OpenRouter API key (or OPENROUTER_API_KEY env)
|
|
32
35
|
--cursor-cookie <t> Cursor web session token (auto-detected if omitted)
|
|
33
36
|
--cursor-csv <path> usage CSV export (fallback if the API path fails)
|
|
34
|
-
--no-claude-code / --no-openrouter / --no-cursor
|
|
37
|
+
--no-claude-code / --no-codex / --no-openrouter / --no-cursor
|
|
35
38
|
--handle <name> display name on the card (unverified)
|
|
36
39
|
--open open the card URL in your browser
|
|
37
40
|
```
|
package/dist/index.js
CHANGED
|
@@ -4084,8 +4084,11 @@ var claudeCodeSourceSchema = external_exports.object({
|
|
|
4084
4084
|
models
|
|
4085
4085
|
});
|
|
4086
4086
|
var openRouterSourceSchema = external_exports.object({
|
|
4087
|
+
// Tokens/requests/models are last-30-day (/activity). totalCostUsd is
|
|
4088
|
+
// all-time spend from /credits — nullable if that endpoint fails.
|
|
4089
|
+
// Aggregate card totals intentionally omit this cost (mixed windows).
|
|
4087
4090
|
totalTokens: tokens,
|
|
4088
|
-
totalCostUsd: costUsd,
|
|
4091
|
+
totalCostUsd: costUsd.nullable(),
|
|
4089
4092
|
requestCount: count,
|
|
4090
4093
|
activeDays: count,
|
|
4091
4094
|
windowDays: external_exports.literal(30),
|
|
@@ -4099,13 +4102,28 @@ var cursorSourceSchema = external_exports.object({
|
|
|
4099
4102
|
dateRange: external_exports.object({ from: dateOnly, to: dateOnly }),
|
|
4100
4103
|
models
|
|
4101
4104
|
});
|
|
4105
|
+
var codexSourceSchema = external_exports.object({
|
|
4106
|
+
tokensIn: tokens,
|
|
4107
|
+
tokensOut: tokens,
|
|
4108
|
+
cacheReadTokens: tokens,
|
|
4109
|
+
reasoningTokens: tokens,
|
|
4110
|
+
totalTokens: tokens,
|
|
4111
|
+
estimatedCostUsd: costUsd.nullable(),
|
|
4112
|
+
costConfidence: external_exports.enum(["estimated", "partial"]),
|
|
4113
|
+
sessionCount: count,
|
|
4114
|
+
activeDays: count,
|
|
4115
|
+
longestStreakDays: count,
|
|
4116
|
+
firstActivityDate: dateOnly,
|
|
4117
|
+
lastActivityDate: dateOnly,
|
|
4118
|
+
models
|
|
4119
|
+
});
|
|
4102
4120
|
var aggregateSchema = external_exports.object({
|
|
4103
4121
|
totalTokens: tokens,
|
|
4104
4122
|
totalCostUsd: costUsd.nullable(),
|
|
4105
4123
|
totalActiveDays: count,
|
|
4106
4124
|
longestStreakDays: count,
|
|
4107
4125
|
distinctModels: models,
|
|
4108
|
-
sourceCount: external_exports.number().int().min(1).max(
|
|
4126
|
+
sourceCount: external_exports.number().int().min(1).max(4),
|
|
4109
4127
|
firstActivityDate: dateOnly,
|
|
4110
4128
|
lastActivityDate: dateOnly
|
|
4111
4129
|
});
|
|
@@ -4116,9 +4134,10 @@ var snapshotPayloadSchema = external_exports.object({
|
|
|
4116
4134
|
sources: external_exports.object({
|
|
4117
4135
|
claudeCode: claudeCodeSourceSchema.optional(),
|
|
4118
4136
|
openrouter: openRouterSourceSchema.optional(),
|
|
4119
|
-
cursor: cursorSourceSchema.optional()
|
|
4137
|
+
cursor: cursorSourceSchema.optional(),
|
|
4138
|
+
codex: codexSourceSchema.optional()
|
|
4120
4139
|
}).refine(
|
|
4121
|
-
(s) => s.claudeCode || s.openrouter || s.cursor,
|
|
4140
|
+
(s) => s.claudeCode || s.openrouter || s.cursor || s.codex,
|
|
4122
4141
|
"at least one source is required"
|
|
4123
4142
|
),
|
|
4124
4143
|
aggregate: aggregateSchema,
|
|
@@ -4160,11 +4179,28 @@ var MODEL_PRICING = {
|
|
|
4160
4179
|
"claude-3-5-sonnet": rates(3, 15),
|
|
4161
4180
|
"claude-haiku-4-5": rates(1, 5),
|
|
4162
4181
|
"claude-3-5-haiku": rates(0.8, 4),
|
|
4163
|
-
"claude-3-haiku": rates(0.25, 1.25)
|
|
4182
|
+
"claude-3-haiku": rates(0.25, 1.25),
|
|
4183
|
+
// OpenAI / Codex CLI (USD per 1M; cache read ≈ 0.1× input via rates())
|
|
4184
|
+
"gpt-5.4": rates(2.5, 15),
|
|
4185
|
+
"gpt-5.3": rates(2.5, 15),
|
|
4186
|
+
"gpt-5.2": rates(2.5, 15),
|
|
4187
|
+
"gpt-5.1": rates(2.5, 15),
|
|
4188
|
+
"gpt-5-mini": rates(0.4, 1.6),
|
|
4189
|
+
"gpt-5-nano": rates(0.1, 0.4),
|
|
4190
|
+
"gpt-5-codex": rates(2.5, 15),
|
|
4191
|
+
"gpt-5": rates(2.5, 15),
|
|
4192
|
+
"gpt-4.1-mini": rates(0.4, 1.6),
|
|
4193
|
+
"gpt-4.1-nano": rates(0.1, 0.4),
|
|
4194
|
+
"gpt-4.1": rates(2, 8),
|
|
4195
|
+
"gpt-4o-mini": rates(0.15, 0.6),
|
|
4196
|
+
"gpt-4o": rates(2.5, 10),
|
|
4197
|
+
"o4-mini": rates(1.1, 4.4),
|
|
4198
|
+
"o3-mini": rates(1.1, 4.4),
|
|
4199
|
+
"o3": rates(2, 8)
|
|
4164
4200
|
};
|
|
4165
4201
|
var sortedKeys = Object.keys(MODEL_PRICING).sort((a, b) => b.length - a.length);
|
|
4166
4202
|
function ratesFor(modelId) {
|
|
4167
|
-
const normalized = modelId.replace(/^(anthropic\.|us\.anthropic\.)/, "");
|
|
4203
|
+
const normalized = modelId.replace(/^(anthropic\.|us\.anthropic\.|openai\/|openai\.)/, "").replace(/^models\//, "");
|
|
4168
4204
|
for (const key of sortedKeys) {
|
|
4169
4205
|
if (normalized.startsWith(key)) return MODEL_PRICING[key];
|
|
4170
4206
|
}
|
|
@@ -4339,41 +4375,69 @@ async function validateOpenRouterKey(key) {
|
|
|
4339
4375
|
return false;
|
|
4340
4376
|
}
|
|
4341
4377
|
}
|
|
4378
|
+
function parseCostUsd(credits) {
|
|
4379
|
+
if (!credits || typeof credits !== "object") return null;
|
|
4380
|
+
const data = credits.data;
|
|
4381
|
+
const raw = data?.total_usage;
|
|
4382
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? Math.round(raw * 100) / 100 : null;
|
|
4383
|
+
}
|
|
4384
|
+
function parseActivityRows(activity) {
|
|
4385
|
+
if (!activity || typeof activity !== "object") return [];
|
|
4386
|
+
const data = activity.data;
|
|
4387
|
+
return Array.isArray(data) ? data : [];
|
|
4388
|
+
}
|
|
4342
4389
|
async function collectOpenRouter(key) {
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4390
|
+
const warnings = [];
|
|
4391
|
+
let credits = null;
|
|
4392
|
+
try {
|
|
4393
|
+
credits = await get("/credits", key);
|
|
4394
|
+
} catch (e) {
|
|
4395
|
+
warnings.push(
|
|
4396
|
+
`/credits failed (${e.message}) \u2014 all-time spend unknown; tokens still from last 30 days`
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
let activity;
|
|
4400
|
+
try {
|
|
4401
|
+
activity = await get("/activity", key);
|
|
4402
|
+
} catch (e) {
|
|
4403
|
+
throw new Error(
|
|
4404
|
+
`OpenRouter /activity failed (${e.message}). Need a management API key with activity access.`
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
const totalCostUsd = parseCostUsd(credits);
|
|
4408
|
+
if (credits != null && totalCostUsd == null) {
|
|
4409
|
+
warnings.push("/credits returned an unexpected shape \u2014 spend left unknown");
|
|
4410
|
+
}
|
|
4411
|
+
const rows = parseActivityRows(activity);
|
|
4349
4412
|
let totalTokens = 0;
|
|
4350
4413
|
let requestCount = 0;
|
|
4351
4414
|
const activeDates = /* @__PURE__ */ new Set();
|
|
4352
4415
|
const models2 = /* @__PURE__ */ new Set();
|
|
4353
4416
|
for (const row of rows) {
|
|
4354
|
-
totalTokens += (row.prompt_tokens
|
|
4355
|
-
requestCount += row.requests
|
|
4417
|
+
totalTokens += (Number(row.prompt_tokens) || 0) + (Number(row.completion_tokens) || 0) + (Number(row.reasoning_tokens) || 0);
|
|
4418
|
+
requestCount += Number(row.requests) || 0;
|
|
4356
4419
|
if (typeof row.date === "string") {
|
|
4357
4420
|
const d = row.date.slice(0, 10);
|
|
4358
4421
|
if (/^\d{4}-\d{2}-\d{2}$/.test(d)) activeDates.add(d);
|
|
4359
4422
|
}
|
|
4360
4423
|
if (typeof row.model === "string" && row.model) models2.add(row.model);
|
|
4361
|
-
if (typeof row.model_permaslug === "string" && row.model_permaslug)
|
|
4424
|
+
else if (typeof row.model_permaslug === "string" && row.model_permaslug)
|
|
4362
4425
|
models2.add(row.model_permaslug);
|
|
4363
4426
|
}
|
|
4364
|
-
if (totalTokens === 0 &&
|
|
4427
|
+
if (totalTokens === 0 && requestCount === 0 && totalCostUsd == null) {
|
|
4365
4428
|
return null;
|
|
4366
4429
|
}
|
|
4367
4430
|
return {
|
|
4368
4431
|
source: {
|
|
4369
4432
|
totalTokens: Math.round(totalTokens),
|
|
4370
|
-
totalCostUsd
|
|
4433
|
+
totalCostUsd,
|
|
4371
4434
|
requestCount,
|
|
4372
4435
|
activeDays: activeDates.size,
|
|
4373
4436
|
windowDays: 30,
|
|
4374
4437
|
models: [...models2].sort().slice(0, 50)
|
|
4375
4438
|
},
|
|
4376
|
-
activeDates
|
|
4439
|
+
activeDates,
|
|
4440
|
+
warnings
|
|
4377
4441
|
};
|
|
4378
4442
|
}
|
|
4379
4443
|
|
|
@@ -4628,25 +4692,204 @@ async function collectCursorApi(cookie, onProgress) {
|
|
|
4628
4692
|
};
|
|
4629
4693
|
}
|
|
4630
4694
|
|
|
4695
|
+
// src/collectors/codex.ts
|
|
4696
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
4697
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
4698
|
+
import { createInterface as createInterface2 } from "readline";
|
|
4699
|
+
import { homedir as homedir3 } from "os";
|
|
4700
|
+
import { join as join3, relative } from "path";
|
|
4701
|
+
function codexHome() {
|
|
4702
|
+
return process.env.CODEX_HOME?.split(",")[0]?.trim() || join3(homedir3(), ".codex");
|
|
4703
|
+
}
|
|
4704
|
+
function codexSessionsRoots(home = codexHome()) {
|
|
4705
|
+
return [join3(home, "sessions"), join3(home, "archived_sessions")];
|
|
4706
|
+
}
|
|
4707
|
+
function asBucket(v) {
|
|
4708
|
+
if (!v || typeof v !== "object") return null;
|
|
4709
|
+
const o = v;
|
|
4710
|
+
const n = (k) => typeof o[k] === "number" && Number.isFinite(o[k]) ? o[k] : 0;
|
|
4711
|
+
return {
|
|
4712
|
+
input_tokens: n("input_tokens"),
|
|
4713
|
+
cached_input_tokens: n("cached_input_tokens"),
|
|
4714
|
+
output_tokens: n("output_tokens"),
|
|
4715
|
+
reasoning_output_tokens: n("reasoning_output_tokens"),
|
|
4716
|
+
total_tokens: n("total_tokens")
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
function delta(cur, prev) {
|
|
4720
|
+
if (!prev) return cur;
|
|
4721
|
+
return {
|
|
4722
|
+
input_tokens: Math.max(0, cur.input_tokens - prev.input_tokens),
|
|
4723
|
+
cached_input_tokens: Math.max(
|
|
4724
|
+
0,
|
|
4725
|
+
cur.cached_input_tokens - prev.cached_input_tokens
|
|
4726
|
+
),
|
|
4727
|
+
output_tokens: Math.max(0, cur.output_tokens - prev.output_tokens),
|
|
4728
|
+
reasoning_output_tokens: Math.max(
|
|
4729
|
+
0,
|
|
4730
|
+
cur.reasoning_output_tokens - prev.reasoning_output_tokens
|
|
4731
|
+
),
|
|
4732
|
+
total_tokens: Math.max(0, cur.total_tokens - prev.total_tokens)
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
function isZero(b) {
|
|
4736
|
+
return b.input_tokens === 0 && b.cached_input_tokens === 0 && b.output_tokens === 0 && b.reasoning_output_tokens === 0 && b.total_tokens === 0;
|
|
4737
|
+
}
|
|
4738
|
+
async function listJsonlFiles(root) {
|
|
4739
|
+
const out = [];
|
|
4740
|
+
async function walk(dir) {
|
|
4741
|
+
let entries;
|
|
4742
|
+
try {
|
|
4743
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
4744
|
+
} catch {
|
|
4745
|
+
return;
|
|
4746
|
+
}
|
|
4747
|
+
for (const e of entries) {
|
|
4748
|
+
const p2 = join3(dir, e.name);
|
|
4749
|
+
if (e.isDirectory()) await walk(p2);
|
|
4750
|
+
else if (e.isFile() && e.name.endsWith(".jsonl")) out.push(p2);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
await walk(root);
|
|
4754
|
+
return out;
|
|
4755
|
+
}
|
|
4756
|
+
async function collectCodex(home = codexHome()) {
|
|
4757
|
+
const sessionRoot = join3(home, "sessions");
|
|
4758
|
+
const archivedRoot = join3(home, "archived_sessions");
|
|
4759
|
+
const activeFiles = await listJsonlFiles(sessionRoot);
|
|
4760
|
+
const archivedFiles = await listJsonlFiles(archivedRoot);
|
|
4761
|
+
const seenRel = new Set(
|
|
4762
|
+
activeFiles.map((f) => relative(sessionRoot, f).replace(/\\/g, "/"))
|
|
4763
|
+
);
|
|
4764
|
+
const files = [
|
|
4765
|
+
...activeFiles,
|
|
4766
|
+
...archivedFiles.filter((f) => {
|
|
4767
|
+
const rel = relative(archivedRoot, f).replace(/\\/g, "/");
|
|
4768
|
+
return !seenRel.has(rel);
|
|
4769
|
+
})
|
|
4770
|
+
];
|
|
4771
|
+
if (files.length === 0) return null;
|
|
4772
|
+
let tokensIn = 0;
|
|
4773
|
+
let tokensOut = 0;
|
|
4774
|
+
let cacheRead = 0;
|
|
4775
|
+
let reasoning = 0;
|
|
4776
|
+
let costUsd2 = 0;
|
|
4777
|
+
let hasUnpricedModel = false;
|
|
4778
|
+
const sessions = /* @__PURE__ */ new Set();
|
|
4779
|
+
const activeDates = /* @__PURE__ */ new Set();
|
|
4780
|
+
const models2 = /* @__PURE__ */ new Set();
|
|
4781
|
+
let firstDate = null;
|
|
4782
|
+
let lastDate = null;
|
|
4783
|
+
let filesParsed = 0;
|
|
4784
|
+
for (const file of files) {
|
|
4785
|
+
filesParsed++;
|
|
4786
|
+
sessions.add(file);
|
|
4787
|
+
let prevTotals = null;
|
|
4788
|
+
let currentModel = null;
|
|
4789
|
+
const rl = createInterface2({
|
|
4790
|
+
input: createReadStream2(file),
|
|
4791
|
+
crlfDelay: Infinity
|
|
4792
|
+
});
|
|
4793
|
+
for await (const line of rl) {
|
|
4794
|
+
if (!line.includes("token_count") && !line.includes("turn_context") && !line.includes("session_meta")) {
|
|
4795
|
+
continue;
|
|
4796
|
+
}
|
|
4797
|
+
let entry;
|
|
4798
|
+
try {
|
|
4799
|
+
entry = JSON.parse(line);
|
|
4800
|
+
} catch {
|
|
4801
|
+
continue;
|
|
4802
|
+
}
|
|
4803
|
+
const ts = typeof entry.timestamp === "string" ? toDateOnly(entry.timestamp) : null;
|
|
4804
|
+
if (entry.type === "turn_context") {
|
|
4805
|
+
const model = entry.payload?.model ?? entry.payload?.info?.model ?? entry.model;
|
|
4806
|
+
if (typeof model === "string" && model) currentModel = model;
|
|
4807
|
+
continue;
|
|
4808
|
+
}
|
|
4809
|
+
if (entry.type === "session_meta") {
|
|
4810
|
+
const id = entry.payload?.id ?? entry.payload?.session_id;
|
|
4811
|
+
if (typeof id === "string" && id) {
|
|
4812
|
+
sessions.delete(file);
|
|
4813
|
+
sessions.add(id);
|
|
4814
|
+
}
|
|
4815
|
+
continue;
|
|
4816
|
+
}
|
|
4817
|
+
if (entry.type !== "event_msg") continue;
|
|
4818
|
+
if (entry.payload?.type !== "token_count") continue;
|
|
4819
|
+
const total = asBucket(entry.payload?.info?.total_token_usage);
|
|
4820
|
+
if (!total) continue;
|
|
4821
|
+
const d = delta(total, prevTotals);
|
|
4822
|
+
prevTotals = total;
|
|
4823
|
+
if (isZero(d)) continue;
|
|
4824
|
+
const cached = d.cached_input_tokens;
|
|
4825
|
+
const nonCachedIn = Math.max(0, d.input_tokens - cached);
|
|
4826
|
+
const outTok = d.output_tokens;
|
|
4827
|
+
const reasonTok = d.reasoning_output_tokens;
|
|
4828
|
+
tokensIn += d.input_tokens;
|
|
4829
|
+
cacheRead += cached;
|
|
4830
|
+
tokensOut += outTok;
|
|
4831
|
+
reasoning += reasonTok;
|
|
4832
|
+
if (currentModel) {
|
|
4833
|
+
models2.add(currentModel);
|
|
4834
|
+
const r = ratesFor(currentModel);
|
|
4835
|
+
if (r) {
|
|
4836
|
+
costUsd2 += (nonCachedIn * r.input + cached * r.cacheRead + outTok * r.output) / 1e6;
|
|
4837
|
+
} else {
|
|
4838
|
+
hasUnpricedModel = true;
|
|
4839
|
+
}
|
|
4840
|
+
} else {
|
|
4841
|
+
hasUnpricedModel = true;
|
|
4842
|
+
}
|
|
4843
|
+
if (ts) {
|
|
4844
|
+
activeDates.add(ts);
|
|
4845
|
+
if (!firstDate || ts < firstDate) firstDate = ts;
|
|
4846
|
+
if (!lastDate || ts > lastDate) lastDate = ts;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
const totalTokens = tokensIn + tokensOut;
|
|
4851
|
+
if (totalTokens === 0 || !firstDate || !lastDate) return null;
|
|
4852
|
+
return {
|
|
4853
|
+
source: {
|
|
4854
|
+
tokensIn,
|
|
4855
|
+
tokensOut,
|
|
4856
|
+
cacheReadTokens: cacheRead,
|
|
4857
|
+
reasoningTokens: reasoning,
|
|
4858
|
+
totalTokens,
|
|
4859
|
+
estimatedCostUsd: Math.round(costUsd2 * 100) / 100,
|
|
4860
|
+
costConfidence: hasUnpricedModel ? "partial" : "estimated",
|
|
4861
|
+
sessionCount: sessions.size,
|
|
4862
|
+
activeDays: activeDates.size,
|
|
4863
|
+
longestStreakDays: longestStreak(activeDates),
|
|
4864
|
+
firstActivityDate: firstDate,
|
|
4865
|
+
lastActivityDate: lastDate,
|
|
4866
|
+
models: [...models2].sort().slice(0, 50)
|
|
4867
|
+
},
|
|
4868
|
+
activeDates,
|
|
4869
|
+
filesParsed
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4631
4873
|
// src/merge.ts
|
|
4632
|
-
var CLI_VERSION = "0.1.
|
|
4874
|
+
var CLI_VERSION = "0.1.2";
|
|
4633
4875
|
function buildPayload(opts) {
|
|
4634
|
-
const { claudeCode, openrouter, cursor, handle } = opts;
|
|
4876
|
+
const { claudeCode, openrouter, cursor, codex, handle } = opts;
|
|
4635
4877
|
const allDates = /* @__PURE__ */ new Set();
|
|
4636
|
-
for (const r of [claudeCode, openrouter, cursor]) {
|
|
4878
|
+
for (const r of [claudeCode, openrouter, cursor, codex]) {
|
|
4637
4879
|
if (r) for (const d of r.activeDates) allDates.add(d);
|
|
4638
4880
|
}
|
|
4639
|
-
const totalTokens = (claudeCode?.source.totalTokens ?? 0) + (openrouter?.source.totalTokens ?? 0) + (cursor?.source.totalTokens ?? 0);
|
|
4881
|
+
const totalTokens = (claudeCode?.source.totalTokens ?? 0) + (openrouter?.source.totalTokens ?? 0) + (cursor?.source.totalTokens ?? 0) + (codex?.source.totalTokens ?? 0);
|
|
4640
4882
|
const costs = [
|
|
4641
4883
|
claudeCode?.source.estimatedCostUsd,
|
|
4642
|
-
|
|
4643
|
-
|
|
4884
|
+
cursor?.source.totalCostUsd,
|
|
4885
|
+
codex?.source.estimatedCostUsd
|
|
4644
4886
|
].filter((c) => typeof c === "number");
|
|
4645
4887
|
const totalCostUsd = costs.length > 0 ? Math.round(costs.reduce((a, b) => a + b, 0) * 100) / 100 : null;
|
|
4646
4888
|
const distinctModels = /* @__PURE__ */ new Set([
|
|
4647
4889
|
...claudeCode?.source.models ?? [],
|
|
4648
4890
|
...openrouter?.source.models ?? [],
|
|
4649
|
-
...cursor?.source.models ?? []
|
|
4891
|
+
...cursor?.source.models ?? [],
|
|
4892
|
+
...codex?.source.models ?? []
|
|
4650
4893
|
]);
|
|
4651
4894
|
let first = null;
|
|
4652
4895
|
let last = null;
|
|
@@ -4654,6 +4897,10 @@ function buildPayload(opts) {
|
|
|
4654
4897
|
first = minDate(first, claudeCode.source.firstActivityDate);
|
|
4655
4898
|
last = maxDate(last, claudeCode.source.lastActivityDate);
|
|
4656
4899
|
}
|
|
4900
|
+
if (codex) {
|
|
4901
|
+
first = minDate(first, codex.source.firstActivityDate);
|
|
4902
|
+
last = maxDate(last, codex.source.lastActivityDate);
|
|
4903
|
+
}
|
|
4657
4904
|
if (cursor) {
|
|
4658
4905
|
first = minDate(first, cursor.source.dateRange.from);
|
|
4659
4906
|
last = maxDate(last, cursor.source.dateRange.to);
|
|
@@ -4670,7 +4917,8 @@ function buildPayload(opts) {
|
|
|
4670
4917
|
sources: {
|
|
4671
4918
|
...claudeCode ? { claudeCode: claudeCode.source } : {},
|
|
4672
4919
|
...openrouter ? { openrouter: openrouter.source } : {},
|
|
4673
|
-
...cursor ? { cursor: cursor.source } : {}
|
|
4920
|
+
...cursor ? { cursor: cursor.source } : {},
|
|
4921
|
+
...codex ? { codex: codex.source } : {}
|
|
4674
4922
|
},
|
|
4675
4923
|
aggregate: {
|
|
4676
4924
|
totalTokens,
|
|
@@ -4678,7 +4926,7 @@ function buildPayload(opts) {
|
|
|
4678
4926
|
totalActiveDays: allDates.size,
|
|
4679
4927
|
longestStreakDays: longestStreak(allDates),
|
|
4680
4928
|
distinctModels: [...distinctModels].sort().slice(0, 50),
|
|
4681
|
-
sourceCount: [claudeCode, openrouter, cursor].filter(Boolean).length,
|
|
4929
|
+
sourceCount: [claudeCode, openrouter, cursor, codex].filter(Boolean).length,
|
|
4682
4930
|
firstActivityDate: first ?? today,
|
|
4683
4931
|
lastActivityDate: last ?? today
|
|
4684
4932
|
},
|
|
@@ -4700,6 +4948,7 @@ var { values: args } = parseArgs({
|
|
|
4700
4948
|
"no-claude-code": { type: "boolean", default: false },
|
|
4701
4949
|
"no-openrouter": { type: "boolean", default: false },
|
|
4702
4950
|
"no-cursor": { type: "boolean", default: false },
|
|
4951
|
+
"no-codex": { type: "boolean", default: false },
|
|
4703
4952
|
handle: { type: "string" },
|
|
4704
4953
|
endpoint: { type: "string" },
|
|
4705
4954
|
open: { type: "boolean", default: false },
|
|
@@ -4734,6 +4983,7 @@ Options:
|
|
|
4734
4983
|
--cursor-cookie <t> Cursor web session token (auto-detected if omitted)
|
|
4735
4984
|
--cursor-csv <path> usage CSV export (fallback if the API path fails)
|
|
4736
4985
|
--no-claude-code skip Claude Code logs
|
|
4986
|
+
--no-codex skip OpenAI Codex CLI logs
|
|
4737
4987
|
--no-openrouter skip OpenRouter
|
|
4738
4988
|
--no-cursor skip Cursor
|
|
4739
4989
|
--handle <name> display name on the card (unverified)
|
|
@@ -4759,6 +5009,19 @@ Options:
|
|
|
4759
5009
|
s2.stop("Claude Code: no local logs found \u2014 skipping");
|
|
4760
5010
|
}
|
|
4761
5011
|
}
|
|
5012
|
+
let codex = null;
|
|
5013
|
+
if (!args["no-codex"]) {
|
|
5014
|
+
const s2 = p.spinner();
|
|
5015
|
+
s2.start(`Scanning Codex logs (${codexSessionsRoots().join(", ")})`);
|
|
5016
|
+
codex = await collectCodex();
|
|
5017
|
+
if (codex) {
|
|
5018
|
+
s2.stop(
|
|
5019
|
+
`Codex: ${fmt(codex.source.totalTokens)} tokens across ${codex.source.sessionCount} sessions (${codex.filesParsed} log files)`
|
|
5020
|
+
);
|
|
5021
|
+
} else {
|
|
5022
|
+
s2.stop("Codex: no local session logs found \u2014 skipping");
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
4762
5025
|
let openrouter = null;
|
|
4763
5026
|
if (!args["no-openrouter"]) {
|
|
4764
5027
|
let key = args["openrouter-key"] ?? process.env.OPENROUTER_API_KEY ?? "";
|
|
@@ -4775,12 +5038,27 @@ Options:
|
|
|
4775
5038
|
s2.start("Validating OpenRouter key");
|
|
4776
5039
|
if (await validateOpenRouterKey(key)) {
|
|
4777
5040
|
s2.message("Fetching OpenRouter usage");
|
|
4778
|
-
|
|
5041
|
+
try {
|
|
5042
|
+
openrouter = await collectOpenRouter(key);
|
|
5043
|
+
for (const w of openrouter?.warnings ?? []) {
|
|
5044
|
+
p.log.warn(`OpenRouter: ${w}`);
|
|
5045
|
+
}
|
|
5046
|
+
if (openrouter) {
|
|
5047
|
+
const spend = openrouter.source.totalCostUsd != null ? `$${openrouter.source.totalCostUsd} all-time spend (not rolled into card total)` : "spend unknown";
|
|
5048
|
+
s2.stop(
|
|
5049
|
+
`OpenRouter: ${fmt(openrouter.source.totalTokens)} tokens (last 30 days), ${spend}`
|
|
5050
|
+
);
|
|
5051
|
+
} else {
|
|
5052
|
+
s2.stop("OpenRouter: no usage found \u2014 skipping");
|
|
5053
|
+
}
|
|
5054
|
+
} catch (e) {
|
|
5055
|
+
openrouter = null;
|
|
5056
|
+
s2.stop(`OpenRouter: ${e.message} \u2014 skipping`);
|
|
5057
|
+
}
|
|
5058
|
+
} else {
|
|
4779
5059
|
s2.stop(
|
|
4780
|
-
|
|
5060
|
+
"OpenRouter: key invalid or not a management key \u2014 skipping"
|
|
4781
5061
|
);
|
|
4782
|
-
} else {
|
|
4783
|
-
s2.stop("OpenRouter: key invalid \u2014 skipping");
|
|
4784
5062
|
}
|
|
4785
5063
|
}
|
|
4786
5064
|
}
|
|
@@ -4831,9 +5109,9 @@ Options:
|
|
|
4831
5109
|
}
|
|
4832
5110
|
}
|
|
4833
5111
|
}
|
|
4834
|
-
if (!claudeCode && !openrouter && !cursor) {
|
|
5112
|
+
if (!claudeCode && !openrouter && !cursor && !codex) {
|
|
4835
5113
|
bail(
|
|
4836
|
-
"No usage sources found or supplied. Nothing to build a card from.\nTry --openrouter-key or --cursor-csv
|
|
5114
|
+
"No usage sources found or supplied. Nothing to build a card from.\nTry Claude Code, Codex CLI, --openrouter-key, or --cursor-csv."
|
|
4837
5115
|
);
|
|
4838
5116
|
}
|
|
4839
5117
|
let handle = args.handle ?? null;
|
|
@@ -4848,6 +5126,7 @@ Options:
|
|
|
4848
5126
|
claudeCode,
|
|
4849
5127
|
openrouter,
|
|
4850
5128
|
cursor,
|
|
5129
|
+
codex,
|
|
4851
5130
|
handle
|
|
4852
5131
|
});
|
|
4853
5132
|
const parsed = snapshotPayloadSchema.safeParse(payload);
|
package/package.json
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aieracard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Generate a shareable card from your AI tool usage. Local parsing, aggregate numbers only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"aieracard": "
|
|
7
|
+
"aieracard": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
|
+
"homepage": "https://ai-era-card.vercel.app",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/sinsmilex/ai-era-card.git",
|
|
18
|
+
"directory": "apps/cli"
|
|
19
|
+
},
|
|
20
|
+
"bugs": "https://github.com/sinsmilex/ai-era-card/issues",
|
|
14
21
|
"keywords": [
|
|
15
22
|
"ai",
|
|
16
23
|
"usage",
|
|
@@ -27,7 +34,8 @@
|
|
|
27
34
|
"scripts": {
|
|
28
35
|
"build": "tsup",
|
|
29
36
|
"start": "node dist/index.js",
|
|
30
|
-
"dev": "tsup --watch"
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run"
|
|
31
39
|
},
|
|
32
40
|
"dependencies": {
|
|
33
41
|
"@clack/prompts": "^0.11.0",
|
|
@@ -37,6 +45,7 @@
|
|
|
37
45
|
"@aieracard/schema": "workspace:*",
|
|
38
46
|
"@types/node": "^22",
|
|
39
47
|
"tsup": "^8.3.5",
|
|
40
|
-
"typescript": "^5.6.3"
|
|
48
|
+
"typescript": "^5.6.3",
|
|
49
|
+
"vitest": "^2.1.9"
|
|
41
50
|
}
|
|
42
51
|
}
|