nomnomtokens 0.1.0 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +10 -9
  2. package/dist/index.js +283 -13
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/web/nitro.json +1 -1
  6. package/web/public/_nuxt/{CC85Q8DD.js → 075nz0Lp.js} +1 -1
  7. package/web/public/_nuxt/{CoCv6Hmx.js → BGnU88BC.js} +1 -1
  8. package/web/public/_nuxt/{DvKiMgoh.js → BnfUOzcc.js} +1 -1
  9. package/web/public/_nuxt/{Dw4si2jw.js → CiWIPYVi.js} +1 -1
  10. package/web/public/_nuxt/{Cay8LZ9u.js → CqSNCP5f.js} +1 -1
  11. package/web/public/_nuxt/{CNp1wm92.js → Cu3rIkF2.js} +1 -1
  12. package/web/public/_nuxt/Cutk9Ewh.js +1 -0
  13. package/web/public/_nuxt/DCVp7cZB.js +96 -0
  14. package/web/public/_nuxt/builds/latest.json +1 -1
  15. package/web/public/_nuxt/builds/meta/63ad5f37-5b40-456d-8f53-69ff57b4903c.json +1 -0
  16. package/web/public/_nuxt/entry.DTNjKx6y.css +2 -0
  17. package/web/public/_nuxt/{C-xeWIVA.js → fjfS4ymU.js} +1 -1
  18. package/web/public/_nuxt/limits.DDC9tNkw.css +1 -0
  19. package/web/public/_nuxt/{Cy-tzONg.js → moFy8lON.js} +2 -2
  20. package/web/server/chunks/_/filters.mjs +1 -1
  21. package/web/server/chunks/build/{Sparkline-DDDyGBE4.mjs → Sparkline-Cxifh2uK.mjs} +2 -2
  22. package/web/server/chunks/build/{Sparkline-DDDyGBE4.mjs.map → Sparkline-Cxifh2uK.mjs.map} +1 -1
  23. package/web/server/chunks/build/{_id_-HbVNBXK4.mjs → _id_-BKmH0aHH.mjs} +3 -3
  24. package/web/server/chunks/build/{_id_-HbVNBXK4.mjs.map → _id_-BKmH0aHH.mjs.map} +1 -1
  25. package/web/server/chunks/build/{limits-B44okAo1.mjs → limits-CnRLojCD.mjs} +25 -20
  26. package/web/server/chunks/build/limits-CnRLojCD.mjs.map +1 -0
  27. package/web/server/chunks/build/limits-styles.CKdzkgZ8.mjs +8 -0
  28. package/web/server/chunks/build/limits-styles.CKdzkgZ8.mjs.map +1 -0
  29. package/web/server/chunks/build/nuxt-link-mYiLFsJs.mjs +1 -1
  30. package/web/server/chunks/build/{pages-CffxtNMi.mjs → pages-DlIWqsLr.mjs} +3 -3
  31. package/web/server/chunks/build/{pages-CffxtNMi.mjs.map → pages-DlIWqsLr.mjs.map} +1 -1
  32. package/web/server/chunks/build/{providers-CUjXlgDS.mjs → providers-DOMeWSDZ.mjs} +2 -2
  33. package/web/server/chunks/build/{providers-CUjXlgDS.mjs.map → providers-DOMeWSDZ.mjs.map} +1 -1
  34. package/web/server/chunks/build/{scopes-CQokoqM9.mjs → scopes-Bagepvnf.mjs} +15 -9
  35. package/web/server/chunks/build/scopes-Bagepvnf.mjs.map +1 -0
  36. package/web/server/chunks/build/{sessions-BvRUAKPl.mjs → sessions-peyhKv2_.mjs} +2 -2
  37. package/web/server/chunks/build/{sessions-BvRUAKPl.mjs.map → sessions-peyhKv2_.mjs.map} +1 -1
  38. package/web/server/chunks/build/{timeline-BbSoi8xO.mjs → timeline-P2RrZyBs.mjs} +2 -2
  39. package/web/server/chunks/build/{timeline-BbSoi8xO.mjs.map → timeline-P2RrZyBs.mjs.map} +1 -1
  40. package/web/server/chunks/build/{useFormat-D8in50HT.mjs → useFormat-BfI0x0lv.mjs} +6 -3
  41. package/web/server/chunks/build/{useFormat-D8in50HT.mjs.map → useFormat-BfI0x0lv.mjs.map} +1 -1
  42. package/web/server/chunks/nitro/nitro.mjs +430 -152
  43. package/web/server/chunks/nitro/nitro.mjs.map +1 -1
  44. package/web/server/chunks/routes/api/stats/limits.get.mjs +4 -3
  45. package/web/server/chunks/routes/api/stats/limits.get.mjs.map +1 -1
  46. package/web/server/chunks/routes/api/stats/providers.get.mjs +1 -1
  47. package/web/server/chunks/routes/api/stats/scopes.get.mjs +1 -1
  48. package/web/server/chunks/routes/api/stats/series.get.mjs +1 -1
  49. package/web/server/chunks/routes/api/stats/session/_id_.get.mjs +1 -1
  50. package/web/server/chunks/routes/api/stats/sessions.get.mjs +1 -1
  51. package/web/server/chunks/routes/api/stats/summary.get.mjs +1 -1
  52. package/web/server/chunks/routes/renderer.mjs +2 -2
  53. package/web/server/chunks/virtual/entry.mjs +8 -8
  54. package/web/server/chunks/virtual/precomputed.mjs +1 -1
  55. package/web/server/chunks/virtual/precomputed.mjs.map +1 -1
  56. package/web/server/chunks/virtual/styles.mjs +3 -1
  57. package/web/server/chunks/virtual/styles.mjs.map +1 -1
  58. package/web/server/index.mjs +1 -1
  59. package/web/public/_nuxt/CYcD169V.js +0 -1
  60. package/web/public/_nuxt/TnCE9lfg.js +0 -86
  61. package/web/public/_nuxt/builds/meta/57c20bc9-f17f-4623-94bd-ea8746650367.json +0 -1
  62. package/web/public/_nuxt/entry.CwLI8Fh7.css +0 -2
  63. package/web/server/chunks/build/limits-B44okAo1.mjs.map +0 -1
  64. package/web/server/chunks/build/scopes-CQokoqM9.mjs.map +0 -1
package/README.md CHANGED
@@ -12,9 +12,10 @@ npx nomnomtokens
12
12
 
13
13
  ![nomnomtokens dashboard — Timeline with spend chart and activity heatmap](docs/dashboard.png)
14
14
 
15
- Claude Code today. Cursor when its local IDE database has token fields. The
16
- event contract is deliberately not AI-shaped, so other agents — and eventually
17
- CI minutes and cloud bills — plug in as adapters without touching the core.
15
+ Claude Code, Cursor (when its local IDE database has token fields), and Codex
16
+ CLI/IDE session rollouts. The event contract is deliberately not AI-shaped, so
17
+ other agents — and eventually CI minutes and cloud bills — plug in as adapters
18
+ without touching the core.
18
19
 
19
20
  ## What you get
20
21
 
@@ -134,9 +135,10 @@ the core, the contract is wrong and that's a bug worth reporting.
134
135
 
135
136
  ## Roadmap
136
137
 
137
- Shipped: Claude Code adapter, Cursor adapter (local `state.vscdb`), all six
138
- screens, limits with forecasting, live updates. CSV helpers exist in
139
- `packages/adapters` but are not wired into `scan` / the UI yet.
138
+ Shipped: Claude Code adapter, Cursor adapter (local `state.vscdb`), Codex
139
+ adapter (`~/.codex/sessions/**/rollout-*.jsonl`), all six screens, limits with
140
+ forecasting, live updates. CSV helpers exist in `packages/adapters` but are not
141
+ wired into `scan` / the UI yet.
140
142
 
141
143
  Cursor caveat: the IDE often stores zero token counts in local bubbles. We only
142
144
  emit events when numbers are present (exact `tokenCount`, or
@@ -144,9 +146,8 @@ emit events when numbers are present (exact `tokenCount`, or
144
146
  local counts will not appear until Cursor writes them — or until a future
145
147
  optional CSV enrich from the Cursor dashboard.
146
148
 
147
- Next: wire up CSV import, OTLP receiver, Codex adapter, export, alerts. Then
148
- cloud mode — a second sink for the same events, never a rewrite, and never in
149
- the critical path.
149
+ Next: wire up CSV import, OTLP receiver, export, alerts. Then cloud mode — a
150
+ second sink for the same events, never a rewrite, and never in the critical path.
150
151
 
151
152
  ## License
152
153
 
package/dist/index.js CHANGED
@@ -50,7 +50,18 @@ var PRICES = {
50
50
  "claude-haiku-4-5": { input: 1, output: 5 },
51
51
  "claude-3-5-haiku": { input: 0.8, output: 4 },
52
52
  "claude-3-haiku": { input: 0.25, output: 1.25 },
53
- // Cursor-routed third-party ids (public list prices; Composer itself is unpriced).
53
+ // Cursor / Codex-routed OpenAI ids (public list prices where known; Composer is unpriced).
54
+ // Dotted and dashed forms: resolveModelKey normalises `.` → `-`.
55
+ "gpt-5.6": { input: 1.75, output: 14 },
56
+ "gpt-5-6": { input: 1.75, output: 14 },
57
+ "gpt-5.6-terra": { input: 1.75, output: 14 },
58
+ "gpt-5-6-terra": { input: 1.75, output: 14 },
59
+ "gpt-5.6-luna": { input: 1.75, output: 14 },
60
+ "gpt-5-6-luna": { input: 1.75, output: 14 },
61
+ "gpt-5.5": { input: 1.25, output: 10 },
62
+ "gpt-5-5": { input: 1.25, output: 10 },
63
+ "gpt-5.4-mini": { input: 0.25, output: 2 },
64
+ "gpt-5-4-mini": { input: 0.25, output: 2 },
54
65
  "gpt-5.2": { input: 1.75, output: 14 },
55
66
  "gpt-5-2": { input: 1.75, output: 14 },
56
67
  "gpt-5.2-codex": { input: 1.75, output: 14 },
@@ -261,7 +272,7 @@ function parseLimits(payload, now = Date.now()) {
261
272
  window,
262
273
  usedPct: raw.used_percentage,
263
274
  // the payload uses unix seconds; everything downstream is unix ms
264
- resetsAt: typeof raw.resets_at === "number" ? raw.resets_at * 1e3 : null
275
+ resetsAt: typeof raw.resets_at === "number" && Number.isFinite(raw.resets_at) ? raw.resets_at * 1e3 : null
265
276
  });
266
277
  }
267
278
  return out;
@@ -440,6 +451,263 @@ var ClaudeCodeAdapter = class {
440
451
  };
441
452
  }
442
453
  };
454
+
455
+ // packages/adapters/src/codex/parse.ts
456
+ function num2(v) {
457
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
458
+ }
459
+ function windowLabel(minutes) {
460
+ if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes <= 0) return null;
461
+ if (minutes === 300) return "5h";
462
+ if (minutes === 10080) return "7d";
463
+ if (minutes === 43200) return "30d";
464
+ if (minutes % (24 * 60) === 0) return `${minutes / (24 * 60)}d`;
465
+ if (minutes % 60 === 0) return `${minutes / 60}h`;
466
+ return `${minutes}m`;
467
+ }
468
+ function labelFromCwd(cwd) {
469
+ if (!cwd) return "unknown";
470
+ const parts = cwd.replace(/\\/g, "/").split("/").filter(Boolean);
471
+ return parts[parts.length - 1] ?? "unknown";
472
+ }
473
+ function qtyFromUsage(usage) {
474
+ const input = num2(usage.input_tokens);
475
+ const cached = num2(usage.cached_input_tokens);
476
+ return {
477
+ in: Math.max(0, input - cached),
478
+ out: num2(usage.output_tokens),
479
+ cacheCreate: num2(usage.cache_write_input_tokens),
480
+ cacheCreate1h: 0,
481
+ cacheRead: cached
482
+ };
483
+ }
484
+ function parseLimits2(rate, provider, ts) {
485
+ if (!rate) return [];
486
+ const out = [];
487
+ for (const key of ["primary", "secondary"]) {
488
+ const win = rate[key];
489
+ if (!win || typeof win.used_percent !== "number") continue;
490
+ const window = windowLabel(win.window_minutes) ?? key;
491
+ out.push({
492
+ ts,
493
+ provider,
494
+ window,
495
+ usedPct: win.used_percent,
496
+ resetsAt: typeof win.resets_at === "number" && Number.isFinite(win.resets_at) ? win.resets_at * 1e3 : null
497
+ });
498
+ }
499
+ return out;
500
+ }
501
+ var CodexRolloutParser = class {
502
+ prices;
503
+ provider;
504
+ sessionId = null;
505
+ cwd = null;
506
+ model = null;
507
+ /** Last emitted limit per window — Codex repeats the same % on every token_count. */
508
+ lastLimits = /* @__PURE__ */ new Map();
509
+ constructor(opts = {}) {
510
+ this.prices = opts.prices ?? defaultPrices;
511
+ this.provider = opts.provider ?? "codex";
512
+ }
513
+ reset() {
514
+ this.sessionId = null;
515
+ this.cwd = null;
516
+ this.model = null;
517
+ this.lastLimits.clear();
518
+ }
519
+ /** Last path segment of the session cwd — safe to show as a project label. */
520
+ scopeLabel() {
521
+ return labelFromCwd(this.cwd);
522
+ }
523
+ /** @returns zero or more ingest records for this line. */
524
+ async parseLine(line) {
525
+ const trimmed = line.trim();
526
+ if (!trimmed) return [];
527
+ let raw;
528
+ try {
529
+ raw = JSON.parse(trimmed);
530
+ } catch {
531
+ return [];
532
+ }
533
+ const payload = raw.payload;
534
+ if (!payload) return [];
535
+ if (raw.type === "session_meta") {
536
+ this.sessionId = payload.session_id ?? payload.id ?? this.sessionId;
537
+ if (typeof payload.cwd === "string" && payload.cwd) this.cwd = payload.cwd;
538
+ return [];
539
+ }
540
+ if (raw.type === "turn_context") {
541
+ if (typeof payload.model === "string" && payload.model) this.model = payload.model;
542
+ if (typeof payload.cwd === "string" && payload.cwd) this.cwd = payload.cwd;
543
+ return [];
544
+ }
545
+ if (raw.type !== "event_msg" || payload.type !== "token_count") return [];
546
+ const ts = raw.timestamp ? Date.parse(raw.timestamp) : Number.NaN;
547
+ if (!Number.isFinite(ts)) return [];
548
+ const records = [];
549
+ for (const limit of parseLimits2(payload.rate_limits, this.provider, ts)) {
550
+ const prev = this.lastLimits.get(limit.window);
551
+ if (prev && prev.usedPct === limit.usedPct && prev.resetsAt === limit.resetsAt) {
552
+ continue;
553
+ }
554
+ this.lastLimits.set(limit.window, { usedPct: limit.usedPct, resetsAt: limit.resetsAt });
555
+ records.push({ type: "limit", limit });
556
+ }
557
+ const last = payload.info?.last_token_usage;
558
+ const total = payload.info?.total_token_usage;
559
+ if (!last) return records;
560
+ const qty = qtyFromUsage(last);
561
+ if (qty.in + qty.out + qty.cacheCreate + qty.cacheRead === 0) return records;
562
+ const sessionId = this.sessionId;
563
+ const totalIn = num2(total?.input_tokens);
564
+ const totalOut = num2(total?.output_tokens);
565
+ const id = `${this.provider}:${sessionId ?? "nosession"}:${totalIn}:${totalOut}`;
566
+ const hash = this.cwd ? await scopeHash(this.cwd) : "unknown";
567
+ const event = {
568
+ id,
569
+ ts,
570
+ provider: this.provider,
571
+ kind: "tokens",
572
+ sessionId,
573
+ scopeHash: hash,
574
+ unitLabel: this.model,
575
+ qty,
576
+ costUsd: costOf(this.model, qty, this.prices),
577
+ meta: {
578
+ reasoningOutputTokens: num2(last.reasoning_output_tokens)
579
+ }
580
+ };
581
+ records.push({ type: "event", event });
582
+ return records;
583
+ }
584
+ };
585
+ function codexHome() {
586
+ return process.env.CODEX_HOME ?? join(homedir(), ".codex");
587
+ }
588
+ function codexSessionsDir(home = codexHome()) {
589
+ return join(home, "sessions");
590
+ }
591
+
592
+ // packages/adapters/src/codex/index.ts
593
+ async function* readLinesFrom2(path, start) {
594
+ const stream = createReadStream(path, { start });
595
+ const decoder = new StringDecoder("utf8");
596
+ let buffer = "";
597
+ let consumed = start;
598
+ for await (const chunk of stream) {
599
+ buffer += decoder.write(chunk);
600
+ let nl = buffer.indexOf("\n");
601
+ while (nl !== -1) {
602
+ const line = buffer.slice(0, nl);
603
+ buffer = buffer.slice(nl + 1);
604
+ consumed += Buffer.byteLength(line, "utf8") + 1;
605
+ yield { line, offset: consumed };
606
+ nl = buffer.indexOf("\n");
607
+ }
608
+ }
609
+ }
610
+ async function* walkRollouts(root) {
611
+ let entries;
612
+ try {
613
+ entries = await readdir(root, { withFileTypes: true });
614
+ } catch {
615
+ return;
616
+ }
617
+ for (const entry of entries) {
618
+ const path = join(root, entry.name);
619
+ if (entry.isDirectory()) {
620
+ yield* walkRollouts(path);
621
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.startsWith("rollout-")) {
622
+ yield path;
623
+ }
624
+ }
625
+ }
626
+ var CodexAdapter = class {
627
+ name = "codex";
628
+ home;
629
+ sessions;
630
+ prices;
631
+ constructor(opts = {}) {
632
+ this.home = opts.home ?? codexHome();
633
+ this.sessions = codexSessionsDir(this.home);
634
+ this.prices = opts.prices;
635
+ }
636
+ async detect() {
637
+ return existsSync(this.sessions) || existsSync(this.home);
638
+ }
639
+ async *scan(state) {
640
+ const seenScopes = /* @__PURE__ */ new Set();
641
+ for await (const path of walkRollouts(this.sessions)) {
642
+ const info = await stat(path).catch(() => null);
643
+ if (!info) continue;
644
+ const cursor = state.get(path);
645
+ if (cursor && cursor.size === info.size && cursor.mtime === info.mtimeMs) continue;
646
+ const start = cursor && info.size >= cursor.size ? cursor.offset : 0;
647
+ const parser = new CodexRolloutParser(this.prices ? { prices: this.prices } : {});
648
+ let offset = start;
649
+ for await (const { line, offset: next } of readLinesFrom2(path, start)) {
650
+ offset = next;
651
+ for (const record of await parser.parseLine(line)) {
652
+ if (record.type === "event" && !seenScopes.has(record.event.scopeHash)) {
653
+ seenScopes.add(record.event.scopeHash);
654
+ yield {
655
+ type: "scope",
656
+ scopeHash: record.event.scopeHash,
657
+ label: parser.scopeLabel(),
658
+ provider: this.name
659
+ };
660
+ }
661
+ yield record;
662
+ }
663
+ }
664
+ state.set({ sourcePath: path, mtime: info.mtimeMs, size: info.size, offset });
665
+ }
666
+ }
667
+ watch(emit) {
668
+ const offsets = /* @__PURE__ */ new Map();
669
+ const inFlight = /* @__PURE__ */ new Set();
670
+ const parsers = /* @__PURE__ */ new Map();
671
+ let closed = false;
672
+ const pump = async (path) => {
673
+ if (closed || inFlight.has(path) || !path.endsWith(".jsonl")) return;
674
+ inFlight.add(path);
675
+ try {
676
+ const info = await stat(path).catch(() => null);
677
+ if (!info) return;
678
+ const known = offsets.get(path);
679
+ let offset = known === void 0 ? info.size : info.size < known ? 0 : known;
680
+ let parser = parsers.get(path);
681
+ if (!parser || offset === 0) {
682
+ parser = new CodexRolloutParser(this.prices ? { prices: this.prices } : {});
683
+ parsers.set(path, parser);
684
+ }
685
+ for await (const { line, offset: next } of readLinesFrom2(path, offset)) {
686
+ offset = next;
687
+ for (const record of await parser.parseLine(line)) emit(record);
688
+ }
689
+ offsets.set(path, offset);
690
+ } finally {
691
+ inFlight.delete(path);
692
+ }
693
+ };
694
+ const watcherPromise = import('chokidar').then(({ watch }) => {
695
+ const w = watch(this.sessions, {
696
+ // sessions / YYYY / MM / DD / rollout-*.jsonl
697
+ depth: 4,
698
+ ignoreInitial: true,
699
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
700
+ });
701
+ w.on("add", (p) => void pump(p));
702
+ w.on("change", (p) => void pump(p));
703
+ return w;
704
+ });
705
+ return () => {
706
+ closed = true;
707
+ void watcherPromise.then((w) => w.close());
708
+ };
709
+ }
710
+ };
443
711
  function cursorUserDataDir() {
444
712
  if (process.env.CURSOR_USER_DATA_DIR) return process.env.CURSOR_USER_DATA_DIR;
445
713
  const home = homedir();
@@ -640,7 +908,7 @@ function readCursorSnapshot(stateDbPath, workspaces) {
640
908
  }
641
909
 
642
910
  // packages/adapters/src/cursor/parse.ts
643
- function num2(v) {
911
+ function num3(v) {
644
912
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
645
913
  }
646
914
  function parseTs(raw) {
@@ -655,10 +923,10 @@ function parseTs(raw) {
655
923
  }
656
924
  function hasExactTokens(b) {
657
925
  const tc = b.tokenCount;
658
- return num2(tc?.inputTokens) > 0 || num2(tc?.outputTokens) > 0;
926
+ return num3(tc?.inputTokens) > 0 || num3(tc?.outputTokens) > 0;
659
927
  }
660
928
  function tokensUsedOf(b) {
661
- return num2(b.contextWindowStatusAtCreation?.tokensUsed);
929
+ return num3(b.contextWindowStatusAtCreation?.tokensUsed);
662
930
  }
663
931
  function eventsFromComposer(composer, bubbles, scopeHash2, opts = {}) {
664
932
  const prices = opts.prices ?? defaultPrices;
@@ -687,8 +955,8 @@ function eventsFromComposer(composer, bubbles, scopeHash2, opts = {}) {
687
955
  if (ts === null) continue;
688
956
  const qty = {
689
957
  ...EMPTY_TOKEN_QTY,
690
- in: num2(b.tokenCount?.inputTokens),
691
- out: num2(b.tokenCount?.outputTokens)
958
+ in: num3(b.tokenCount?.inputTokens),
959
+ out: num3(b.tokenCount?.outputTokens)
692
960
  };
693
961
  events2.push({
694
962
  id: `${provider}:${b.composerId}:${b.bubbleId}`,
@@ -837,7 +1105,7 @@ var CursorAdapter = class {
837
1105
 
838
1106
  // packages/adapters/src/index.ts
839
1107
  function allAdapters() {
840
- return [new ClaudeCodeAdapter(), new CursorAdapter()];
1108
+ return [new ClaudeCodeAdapter(), new CursorAdapter(), new CodexAdapter()];
841
1109
  }
842
1110
  async function detectAdapters() {
843
1111
  const found = [];
@@ -1446,7 +1714,7 @@ function sql(strings, ...params) {
1446
1714
  return new SQL([new StringChunk(str)]);
1447
1715
  }
1448
1716
  sql2.raw = raw;
1449
- function join9(chunks, separator) {
1717
+ function join11(chunks, separator) {
1450
1718
  const result = [];
1451
1719
  for (const [i, chunk] of chunks.entries()) {
1452
1720
  if (i > 0 && separator !== void 0) {
@@ -1456,7 +1724,7 @@ function sql(strings, ...params) {
1456
1724
  }
1457
1725
  return new SQL(result);
1458
1726
  }
1459
- sql2.join = join9;
1727
+ sql2.join = join11;
1460
1728
  function identifier(value) {
1461
1729
  return new Name(value);
1462
1730
  }
@@ -3915,7 +4183,7 @@ var SQLiteSelectQueryBuilderBase = class extends TypedQueryBuilder {
3915
4183
  const baseTableName = this.tableName;
3916
4184
  const tableName = getTableLikeName(table);
3917
4185
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
3918
- if (typeof tableName === "string" && this.config.joins?.some((join9) => join9.alias === tableName)) {
4186
+ if (typeof tableName === "string" && this.config.joins?.some((join11) => join11.alias === tableName)) {
3919
4187
  throw new Error(`Alias "${tableName}" is already used in this query`);
3920
4188
  }
3921
4189
  if (!this.isPartialSelect) {
@@ -4774,7 +5042,7 @@ var SQLiteUpdateBase = class extends QueryPromise {
4774
5042
  createJoin(joinType) {
4775
5043
  return (table, on) => {
4776
5044
  const tableName = getTableLikeName(table);
4777
- if (typeof tableName === "string" && this.config.joins.some((join9) => join9.alias === tableName)) {
5045
+ if (typeof tableName === "string" && this.config.joins.some((join11) => join11.alias === tableName)) {
4778
5046
  throw new Error(`Alias "${tableName}" is already used in this query`);
4779
5047
  }
4780
5048
  if (typeof on === "function") {
@@ -6325,6 +6593,8 @@ async function doctor(opts = {}) {
6325
6593
  check(existsSync(projects), "Claude Code transcripts", projects);
6326
6594
  const cursorDb = cursorStateDbPath();
6327
6595
  check(existsSync(cursorDb), "Cursor state database", cursorDb);
6596
+ const codexSessions = codexSessionsDir();
6597
+ check(existsSync(codexSessions), "Codex session rollouts", codexSessions);
6328
6598
  const adapters = await detectAdapters();
6329
6599
  check(adapters.length > 0, `${adapters.length} adapter(s) detected`, adapters.map((a) => a.name).join(", ") || "none");
6330
6600
  console.log();
@@ -6433,7 +6703,7 @@ async function scan(opts = {}) {
6433
6703
  const adapters = await detectAdapters();
6434
6704
  if (adapters.length === 0) {
6435
6705
  console.error(c.yellow("No sources detected on this machine."));
6436
- console.error(c.dim("Looks for ~/.claude/projects and Cursor state.vscdb. Run `nnt doctor`."));
6706
+ console.error(c.dim("Looks for ~/.claude/projects, Cursor state.vscdb, and ~/.codex/sessions. Run `nnt doctor`."));
6437
6707
  process.exitCode = 1;
6438
6708
  return;
6439
6709
  }