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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/dist/index.js +312 -33
  3. 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** — via your API key (read-only endpoints)
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(3),
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 [credits, activity] = await Promise.all([
4344
- get("/credits", key).catch(() => null),
4345
- get("/activity", key).catch(() => null)
4346
- ]);
4347
- const totalCostUsd = credits?.data?.total_usage ?? credits?.data?.usage ?? 0;
4348
- const rows = activity?.data ?? [];
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 ?? 0) + (row.completion_tokens ?? 0) + (row.reasoning_tokens ?? 0);
4355
- requestCount += row.requests ?? 0;
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 && totalCostUsd === 0 && requestCount === 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: Math.round(totalCostUsd * 100) / 100,
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.0";
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
- openrouter?.source.totalCostUsd,
4643
- cursor?.source.totalCostUsd
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
- openrouter = await collectOpenRouter(key);
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
- openrouter ? `OpenRouter: $${openrouter.source.totalCostUsd} all-time, ${fmt(openrouter.source.totalTokens)} tokens (last 30 days)` : "OpenRouter: no usage found \u2014 skipping"
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, or run Claude Code first."
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.0",
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": "./dist/index.js"
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
  }