aieracard 0.1.1 → 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 +3 -2
  2. package/dist/index.js +249 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,9 +9,10 @@ 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** — management API key (read-only `/credits` + `/activity`).
15
+ - **OpenRouter** — optional management API key (`/credits` + `/activity`).
15
16
  Tokens are last 30 days; all-time spend stays on the source object and is
16
17
  **not** rolled into the card's aggregate "compute spent"
17
18
 
@@ -33,7 +34,7 @@ that unfurls into a stats card when shared.
33
34
  --openrouter-key <k> OpenRouter API key (or OPENROUTER_API_KEY env)
34
35
  --cursor-cookie <t> Cursor web session token (auto-detected if omitted)
35
36
  --cursor-csv <path> usage CSV export (fallback if the API path fails)
36
- --no-claude-code / --no-openrouter / --no-cursor
37
+ --no-claude-code / --no-codex / --no-openrouter / --no-cursor
37
38
  --handle <name> display name on the card (unverified)
38
39
  --open open the card URL in your browser
39
40
  ```
package/dist/index.js CHANGED
@@ -4102,13 +4102,28 @@ var cursorSourceSchema = external_exports.object({
4102
4102
  dateRange: external_exports.object({ from: dateOnly, to: dateOnly }),
4103
4103
  models
4104
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
+ });
4105
4120
  var aggregateSchema = external_exports.object({
4106
4121
  totalTokens: tokens,
4107
4122
  totalCostUsd: costUsd.nullable(),
4108
4123
  totalActiveDays: count,
4109
4124
  longestStreakDays: count,
4110
4125
  distinctModels: models,
4111
- sourceCount: external_exports.number().int().min(1).max(3),
4126
+ sourceCount: external_exports.number().int().min(1).max(4),
4112
4127
  firstActivityDate: dateOnly,
4113
4128
  lastActivityDate: dateOnly
4114
4129
  });
@@ -4119,9 +4134,10 @@ var snapshotPayloadSchema = external_exports.object({
4119
4134
  sources: external_exports.object({
4120
4135
  claudeCode: claudeCodeSourceSchema.optional(),
4121
4136
  openrouter: openRouterSourceSchema.optional(),
4122
- cursor: cursorSourceSchema.optional()
4137
+ cursor: cursorSourceSchema.optional(),
4138
+ codex: codexSourceSchema.optional()
4123
4139
  }).refine(
4124
- (s) => s.claudeCode || s.openrouter || s.cursor,
4140
+ (s) => s.claudeCode || s.openrouter || s.cursor || s.codex,
4125
4141
  "at least one source is required"
4126
4142
  ),
4127
4143
  aggregate: aggregateSchema,
@@ -4163,11 +4179,28 @@ var MODEL_PRICING = {
4163
4179
  "claude-3-5-sonnet": rates(3, 15),
4164
4180
  "claude-haiku-4-5": rates(1, 5),
4165
4181
  "claude-3-5-haiku": rates(0.8, 4),
4166
- "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)
4167
4200
  };
4168
4201
  var sortedKeys = Object.keys(MODEL_PRICING).sort((a, b) => b.length - a.length);
4169
4202
  function ratesFor(modelId) {
4170
- const normalized = modelId.replace(/^(anthropic\.|us\.anthropic\.)/, "");
4203
+ const normalized = modelId.replace(/^(anthropic\.|us\.anthropic\.|openai\/|openai\.)/, "").replace(/^models\//, "");
4171
4204
  for (const key of sortedKeys) {
4172
4205
  if (normalized.startsWith(key)) return MODEL_PRICING[key];
4173
4206
  }
@@ -4659,24 +4692,204 @@ async function collectCursorApi(cookie, onProgress) {
4659
4692
  };
4660
4693
  }
4661
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
+
4662
4873
  // src/merge.ts
4663
- var CLI_VERSION = "0.1.1";
4874
+ var CLI_VERSION = "0.1.2";
4664
4875
  function buildPayload(opts) {
4665
- const { claudeCode, openrouter, cursor, handle } = opts;
4876
+ const { claudeCode, openrouter, cursor, codex, handle } = opts;
4666
4877
  const allDates = /* @__PURE__ */ new Set();
4667
- for (const r of [claudeCode, openrouter, cursor]) {
4878
+ for (const r of [claudeCode, openrouter, cursor, codex]) {
4668
4879
  if (r) for (const d of r.activeDates) allDates.add(d);
4669
4880
  }
4670
- 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);
4671
4882
  const costs = [
4672
4883
  claudeCode?.source.estimatedCostUsd,
4673
- cursor?.source.totalCostUsd
4884
+ cursor?.source.totalCostUsd,
4885
+ codex?.source.estimatedCostUsd
4674
4886
  ].filter((c) => typeof c === "number");
4675
4887
  const totalCostUsd = costs.length > 0 ? Math.round(costs.reduce((a, b) => a + b, 0) * 100) / 100 : null;
4676
4888
  const distinctModels = /* @__PURE__ */ new Set([
4677
4889
  ...claudeCode?.source.models ?? [],
4678
4890
  ...openrouter?.source.models ?? [],
4679
- ...cursor?.source.models ?? []
4891
+ ...cursor?.source.models ?? [],
4892
+ ...codex?.source.models ?? []
4680
4893
  ]);
4681
4894
  let first = null;
4682
4895
  let last = null;
@@ -4684,6 +4897,10 @@ function buildPayload(opts) {
4684
4897
  first = minDate(first, claudeCode.source.firstActivityDate);
4685
4898
  last = maxDate(last, claudeCode.source.lastActivityDate);
4686
4899
  }
4900
+ if (codex) {
4901
+ first = minDate(first, codex.source.firstActivityDate);
4902
+ last = maxDate(last, codex.source.lastActivityDate);
4903
+ }
4687
4904
  if (cursor) {
4688
4905
  first = minDate(first, cursor.source.dateRange.from);
4689
4906
  last = maxDate(last, cursor.source.dateRange.to);
@@ -4700,7 +4917,8 @@ function buildPayload(opts) {
4700
4917
  sources: {
4701
4918
  ...claudeCode ? { claudeCode: claudeCode.source } : {},
4702
4919
  ...openrouter ? { openrouter: openrouter.source } : {},
4703
- ...cursor ? { cursor: cursor.source } : {}
4920
+ ...cursor ? { cursor: cursor.source } : {},
4921
+ ...codex ? { codex: codex.source } : {}
4704
4922
  },
4705
4923
  aggregate: {
4706
4924
  totalTokens,
@@ -4708,7 +4926,7 @@ function buildPayload(opts) {
4708
4926
  totalActiveDays: allDates.size,
4709
4927
  longestStreakDays: longestStreak(allDates),
4710
4928
  distinctModels: [...distinctModels].sort().slice(0, 50),
4711
- sourceCount: [claudeCode, openrouter, cursor].filter(Boolean).length,
4929
+ sourceCount: [claudeCode, openrouter, cursor, codex].filter(Boolean).length,
4712
4930
  firstActivityDate: first ?? today,
4713
4931
  lastActivityDate: last ?? today
4714
4932
  },
@@ -4730,6 +4948,7 @@ var { values: args } = parseArgs({
4730
4948
  "no-claude-code": { type: "boolean", default: false },
4731
4949
  "no-openrouter": { type: "boolean", default: false },
4732
4950
  "no-cursor": { type: "boolean", default: false },
4951
+ "no-codex": { type: "boolean", default: false },
4733
4952
  handle: { type: "string" },
4734
4953
  endpoint: { type: "string" },
4735
4954
  open: { type: "boolean", default: false },
@@ -4764,6 +4983,7 @@ Options:
4764
4983
  --cursor-cookie <t> Cursor web session token (auto-detected if omitted)
4765
4984
  --cursor-csv <path> usage CSV export (fallback if the API path fails)
4766
4985
  --no-claude-code skip Claude Code logs
4986
+ --no-codex skip OpenAI Codex CLI logs
4767
4987
  --no-openrouter skip OpenRouter
4768
4988
  --no-cursor skip Cursor
4769
4989
  --handle <name> display name on the card (unverified)
@@ -4789,6 +5009,19 @@ Options:
4789
5009
  s2.stop("Claude Code: no local logs found \u2014 skipping");
4790
5010
  }
4791
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
+ }
4792
5025
  let openrouter = null;
4793
5026
  if (!args["no-openrouter"]) {
4794
5027
  let key = args["openrouter-key"] ?? process.env.OPENROUTER_API_KEY ?? "";
@@ -4876,9 +5109,9 @@ Options:
4876
5109
  }
4877
5110
  }
4878
5111
  }
4879
- if (!claudeCode && !openrouter && !cursor) {
5112
+ if (!claudeCode && !openrouter && !cursor && !codex) {
4880
5113
  bail(
4881
- "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."
4882
5115
  );
4883
5116
  }
4884
5117
  let handle = args.handle ?? null;
@@ -4893,6 +5126,7 @@ Options:
4893
5126
  claudeCode,
4894
5127
  openrouter,
4895
5128
  cursor,
5129
+ codex,
4896
5130
  handle
4897
5131
  });
4898
5132
  const parsed = snapshotPayloadSchema.safeParse(payload);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aieracard",
3
- "version": "0.1.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": {