tokentracker-cli 0.81.2 → 0.82.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.
- package/dashboard/dist/assets/AchievementBadge-D6iPEEbz.js +1 -0
- package/dashboard/dist/assets/AchievementsPage-Jry355YE.js +2 -0
- package/dashboard/dist/assets/AchievementsSection-ah-kG00E.js +1 -0
- package/dashboard/dist/assets/BadgeDetailModal-FyvUrVvF.js +1 -0
- package/dashboard/dist/assets/Card-C3nLScju.js +1 -0
- package/dashboard/dist/assets/ClawdAnimated-CLHhE26z.js +1 -0
- package/dashboard/dist/assets/CommunityStatsModal-DA3PHLV5.js +1 -0
- package/dashboard/dist/assets/ConfirmModal-CTDarLrD.js +1 -0
- package/dashboard/dist/assets/DashboardPage-DZIiWc3Y.js +1 -0
- package/dashboard/dist/assets/DevicePage-CyXjUO8W.js +1 -0
- package/dashboard/dist/assets/DialogDescription-ahxSNUcB.js +1 -0
- package/dashboard/dist/assets/DialogTitle-z86PHHyk.js +1 -0
- package/dashboard/dist/assets/FadeIn-BDWtfIsO.js +1 -0
- package/dashboard/dist/assets/HeaderGithubStar-DE1USQbb.js +1 -0
- package/dashboard/dist/assets/{IpCheckPage-CMf9CHFr.js → IpCheckPage-B2SkgT0P.js} +3 -3
- package/dashboard/dist/assets/{LandingPage-BT61_TqT.js → LandingPage-PU5gxsfY.js} +165 -165
- package/dashboard/dist/assets/LeaderboardAvatar-CaOV_hFE.js +1 -0
- package/dashboard/dist/assets/LeaderboardPage-B0JtEyRP.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-C_kxqOMn.js → LeaderboardProfileModal-DtNEbVH8.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-zgNsE0EQ.js → LeaderboardProfilePage-DRklax3-.js} +3 -3
- package/dashboard/dist/assets/LimitsPage-Dg0cJa40.js +4 -0
- package/dashboard/dist/assets/LocalOnlyNotice-si9PMKya.js +1 -0
- package/dashboard/dist/assets/LoginPage-DgLwhXKo.js +1 -0
- package/dashboard/dist/assets/PetPage-DEZbjk99.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-DX7v1XfE.js +1 -0
- package/dashboard/dist/assets/ResetPasswordPage-C6NvRO-Q.js +1 -0
- package/dashboard/dist/assets/Select-CH2xVU8X.js +1 -0
- package/dashboard/dist/assets/SelectItemText-CCW6JeeN.js +1 -0
- package/dashboard/dist/assets/SettingsPage-Cm3uT11D.js +1 -0
- package/dashboard/dist/assets/SkillsPage-CljRL6yF.js +1 -0
- package/dashboard/dist/assets/TrendMonitor-DM1_WNfw.js +19 -0
- package/dashboard/dist/assets/WidgetsPage-mJ1j6hly.js +1 -0
- package/dashboard/dist/assets/WrappedPage-Dz71ln4t.js +1 -0
- package/dashboard/dist/assets/__vite-browser-external-C7JwykID.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-Eoyj4eG-.js +1341 -0
- package/dashboard/dist/assets/agent-logos-D3K_ZRUj.js +1 -0
- package/dashboard/dist/assets/arrow-up-right-CT4-gmOS.js +1 -0
- package/dashboard/dist/assets/check-B87_qvAg.js +1 -0
- package/dashboard/dist/assets/copy-Dtr68epm.js +1 -0
- package/dashboard/dist/assets/download-DaWsA_Gj.js +1 -0
- package/dashboard/dist/assets/flame-vLBOq0X0.js +1 -0
- package/dashboard/dist/assets/format-tokens-C6Pidz3s.js +1 -0
- package/dashboard/dist/assets/icons-BaaYJxUN.js +1 -0
- package/dashboard/dist/assets/index-lKnLA6Pd.js +1 -0
- package/dashboard/dist/assets/info-B46Qt16h.js +1 -0
- package/dashboard/dist/assets/limits-providers-13af4PEg.js +1 -0
- package/dashboard/dist/assets/link-2-DeN6MJpT.js +1 -0
- package/dashboard/dist/assets/loader-circle-ByEuaaSw.js +1 -0
- package/dashboard/dist/assets/provider-display-CKbOKeV4.js +42 -0
- package/dashboard/dist/assets/terminal-DyicX7lG.js +1 -0
- package/dashboard/dist/assets/trash-2-CVCC3DtQ.js +1 -0
- package/dashboard/dist/assets/use-community-stats-CExk3D6G.js +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-CRl4uU95.js +1 -0
- package/dashboard/dist/assets/use-native-settings-Bi6E9dsE.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-DPbiKnPc.js → use-ordered-list-Dk0p2otw.js} +2 -2
- package/dashboard/dist/assets/use-session-efficiency-pref-YAG-OYIh.js +1 -0
- package/dashboard/dist/assets/use-transform-lflyghWN.js +1 -0
- package/dashboard/dist/assets/use-usage-limits-DJXvJ25Q.js +1 -0
- package/dashboard/dist/assets/useCurrency-BtHtcsO5.js +1 -0
- package/dashboard/dist/assets/useScrollLock-C2C-Yazq.js +1 -0
- package/dashboard/dist/assets/useTokenFormat-BdD057ih.js +1 -0
- package/dashboard/dist/assets/zap-DWWma29E.js +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/serve.js +8 -6
- package/src/commands/sessions.js +29 -0
- package/src/commands/status.js +2 -1
- package/src/commands/sync.js +164 -36
- package/src/lib/codex-context-breakdown.js +330 -13
- package/src/lib/codex-rollout-parser.js +323 -42
- package/src/lib/context-health.js +124 -0
- package/src/lib/cursor-store.js +1289 -0
- package/src/lib/diagnostics.js +8 -5
- package/src/lib/fs.js +254 -16
- package/src/lib/git-outcomes.js +147 -0
- package/src/lib/local-api.js +151 -20
- package/src/lib/outcomes-engine.js +23 -0
- package/src/lib/pricing/index.js +8 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +101 -9
- package/src/lib/session-analytics.js +612 -0
- package/src/lib/usage-limits.js +21 -0
- package/dashboard/dist/assets/AchievementBadge-DXWHZrAL.js +0 -1
- package/dashboard/dist/assets/AchievementsPage-BhrQXniL.js +0 -2
- package/dashboard/dist/assets/AchievementsSection-gzP3m1Ml.js +0 -1
- package/dashboard/dist/assets/BadgeDetailModal-DGKPuvDU.js +0 -1
- package/dashboard/dist/assets/Card-BRtYQ-Ot.js +0 -1
- package/dashboard/dist/assets/ClawdAnimated-UsNG-qDv.js +0 -1
- package/dashboard/dist/assets/CommunityStatsModal-CJQw-P4t.js +0 -1
- package/dashboard/dist/assets/ConfirmModal-ByY4GDsy.js +0 -1
- package/dashboard/dist/assets/DashboardPage-CuLVnW98.js +0 -19
- package/dashboard/dist/assets/DevicePage-D67CxpmR.js +0 -1
- package/dashboard/dist/assets/DialogClose-Ci8hkTw5.js +0 -1
- package/dashboard/dist/assets/DialogDescription-C9vXUZU0.js +0 -1
- package/dashboard/dist/assets/DialogTitle-sLJxKWjv.js +0 -1
- package/dashboard/dist/assets/FadeIn-xVds37w6.js +0 -1
- package/dashboard/dist/assets/HeaderGithubStar-kK3omgmB.js +0 -1
- package/dashboard/dist/assets/LeaderboardAvatar-DyKz6NdT.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-DJ4zihJJ.js +0 -2
- package/dashboard/dist/assets/LimitsPage-BiQpqxj8.js +0 -4
- package/dashboard/dist/assets/LocalOnlyNotice-DTrrUVbZ.js +0 -1
- package/dashboard/dist/assets/LoginPage-BOM9Q2sI.js +0 -1
- package/dashboard/dist/assets/PetPage-BURk7RG-.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-BV_aoNBd.js +0 -1
- package/dashboard/dist/assets/ResetPasswordPage-SBL2LvXC.js +0 -1
- package/dashboard/dist/assets/Select-BsRdHde3.js +0 -1
- package/dashboard/dist/assets/SelectItemText-DgkrZxIq.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DqxVsfnB.js +0 -1
- package/dashboard/dist/assets/SkillsPage-B0VfWkOh.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-4QQ8wI_g.js +0 -1
- package/dashboard/dist/assets/WrappedPage-BK53khVY.js +0 -1
- package/dashboard/dist/assets/agent-logos-DXpW_Wmz.js +0 -1
- package/dashboard/dist/assets/arrow-up-right-DCc--V5G.js +0 -1
- package/dashboard/dist/assets/check-DqxuosV2.js +0 -1
- package/dashboard/dist/assets/copy-qChAT7MY.js +0 -1
- package/dashboard/dist/assets/download-W9R3sBph.js +0 -1
- package/dashboard/dist/assets/flame-0eyhMnfU.js +0 -1
- package/dashboard/dist/assets/format-tokens-CkfBUcoa.js +0 -1
- package/dashboard/dist/assets/info-DY1O85-U.js +0 -1
- package/dashboard/dist/assets/link-2-B_Z66hFT.js +0 -1
- package/dashboard/dist/assets/loader-circle-xH40lon2.js +0 -1
- package/dashboard/dist/assets/main-CeaB5N36.js +0 -1259
- package/dashboard/dist/assets/main-DAc77vwF.css +0 -1
- package/dashboard/dist/assets/provider-display-C_XKZQcF.js +0 -42
- package/dashboard/dist/assets/trash-2-DVhNJ74r.js +0 -1
- package/dashboard/dist/assets/use-community-stats-CO-nr2Vf.js +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-RRqmyoxt.js +0 -1
- package/dashboard/dist/assets/use-native-settings-79piPeET.js +0 -1
- package/dashboard/dist/assets/use-quality-per-dollar-pref-DyF49QRF.js +0 -1
- package/dashboard/dist/assets/use-transform-BvXyGAtb.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-BiZ_I-KE.js +0 -1
- package/dashboard/dist/assets/useCurrency-D-C1e7eE.js +0 -1
- package/dashboard/dist/assets/useScrollLock-BfU2EMvG.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-Cm4EvrQp.js +0 -1
- package/dashboard/dist/assets/zap-DlAOTwFs.js +0 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
"use strict";
|
|
7
7
|
|
|
8
|
+
const crypto = require("node:crypto");
|
|
8
9
|
const fs = require("node:fs");
|
|
9
10
|
const path = require("node:path");
|
|
10
11
|
const readline = require("node:readline");
|
|
@@ -23,6 +24,7 @@ const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
|
|
|
23
24
|
const MAX_DISCOVERY_INVENTORIES = 32;
|
|
24
25
|
const DISCOVERY_INVENTORIES = new Map();
|
|
25
26
|
const ZONED_DAY_FORMATTERS = new Map();
|
|
27
|
+
const CODEX_RESUME_INVALIDATED = "TOKENTRACKER_CODEX_RESUME_INVALIDATED";
|
|
26
28
|
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
// Timezone helpers
|
|
@@ -555,20 +557,62 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
555
557
|
timeZoneContext = null,
|
|
556
558
|
diagnostics = null,
|
|
557
559
|
seenTokenEvents = null,
|
|
560
|
+
startOffset = 0,
|
|
561
|
+
endOffset = null,
|
|
562
|
+
resumeState = null,
|
|
563
|
+
captureResumeState = false,
|
|
564
|
+
captureContentHash = false,
|
|
565
|
+
contentHashState = null,
|
|
566
|
+
sourceHandle = null,
|
|
558
567
|
} = {}) {
|
|
559
568
|
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
560
569
|
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
570
|
+
const isResuming = Boolean(resumeState && filePaths.length === 1);
|
|
571
|
+
const initialOffset = Math.max(0, Number(startOffset) || 0);
|
|
572
|
+
const readProgress = {
|
|
573
|
+
endOffset: initialOffset,
|
|
574
|
+
lastByte: initialOffset > 0 ? 0x0a : null,
|
|
575
|
+
};
|
|
576
|
+
let contentHasher = null;
|
|
577
|
+
if (captureResumeState && captureContentHash && filePaths.length === 1) {
|
|
578
|
+
if (contentHashState && typeof contentHashState.copy === "function") {
|
|
579
|
+
try {
|
|
580
|
+
contentHasher = contentHashState.copy();
|
|
581
|
+
} catch {
|
|
582
|
+
contentHasher = null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (initialOffset > 0 && !contentHasher) {
|
|
586
|
+
const error = new Error("Codex rollout append is missing its prefix hash state");
|
|
587
|
+
error.code = CODEX_RESUME_INVALIDATED;
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
if (!contentHasher) contentHasher = crypto.createHash("sha256");
|
|
591
|
+
}
|
|
561
592
|
|
|
562
|
-
let sessionId = null;
|
|
563
|
-
let cwd = null;
|
|
564
|
-
let model = null;
|
|
565
|
-
let provider = null;
|
|
566
|
-
let cliVersion = null;
|
|
567
|
-
|
|
568
|
-
let prevTotals = null;
|
|
569
|
-
let
|
|
570
|
-
|
|
571
|
-
|
|
593
|
+
let sessionId = isResuming ? resumeState.sessionId || null : null;
|
|
594
|
+
let cwd = isResuming ? resumeState.cwd || null : null;
|
|
595
|
+
let model = isResuming ? resumeState.model || null : null;
|
|
596
|
+
let provider = isResuming ? resumeState.provider || null : null;
|
|
597
|
+
let cliVersion = isResuming ? resumeState.cliVersion || null : null;
|
|
598
|
+
|
|
599
|
+
let prevTotals = isResuming ? resumeState.prevTotals || null : null;
|
|
600
|
+
let pendingToolNames = isResuming
|
|
601
|
+
? Array.from(resumeState.pendingToolNames || [])
|
|
602
|
+
: [];
|
|
603
|
+
let pendingSkills = isResuming ? Array.from(resumeState.pendingSkills || []) : [];
|
|
604
|
+
let pendingExecDetails = isResuming
|
|
605
|
+
? Array.from(resumeState.pendingExecDetails || [])
|
|
606
|
+
: [];
|
|
607
|
+
let lastTokenTimestamp = isResuming
|
|
608
|
+
? String(resumeState.lastTokenTimestamp || "") || null
|
|
609
|
+
: null;
|
|
610
|
+
let lastTimestampEventSignatures = new Set(
|
|
611
|
+
isResuming ? resumeState.lastTimestampEventSignatures || [] : [],
|
|
612
|
+
);
|
|
613
|
+
let lastTimestampLastUsage = null;
|
|
614
|
+
let lastTimestampTotalUsage = null;
|
|
615
|
+
let hasUnmaterializedLastTimestampUsage = false;
|
|
572
616
|
|
|
573
617
|
const totals = emptyTotals();
|
|
574
618
|
const byTool = new Map(); // tool_name -> {name,calls,totals}
|
|
@@ -639,9 +683,7 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
639
683
|
if (details.failed) row.failures += 1;
|
|
640
684
|
}
|
|
641
685
|
|
|
642
|
-
function absorbExecEnd(
|
|
643
|
-
const details = getExecKeys(p);
|
|
644
|
-
if (!details) return;
|
|
686
|
+
function absorbExecEnd(details) {
|
|
645
687
|
absorbExecStats(byExecKind, details.kind, details);
|
|
646
688
|
absorbExecStats(byExecExit, details.exitKey, details);
|
|
647
689
|
absorbExecStats(byExecExecutable, details.executable, details);
|
|
@@ -652,18 +694,15 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
652
694
|
|
|
653
695
|
function attributeTurn(delta) {
|
|
654
696
|
if (!delta || delta.total_tokens <= 0) {
|
|
655
|
-
for (const
|
|
656
|
-
|
|
697
|
+
for (const details of pendingExecDetails) absorbExecEnd(details);
|
|
698
|
+
pendingToolNames = [];
|
|
657
699
|
pendingSkills = [];
|
|
658
|
-
|
|
700
|
+
pendingExecDetails = [];
|
|
659
701
|
return;
|
|
660
702
|
}
|
|
661
703
|
turnCount += 1;
|
|
662
704
|
|
|
663
|
-
const
|
|
664
|
-
.map((c) => normalizeToolName(c))
|
|
665
|
-
.filter(Boolean);
|
|
666
|
-
const unique = [...new Set(toolNames)];
|
|
705
|
+
const unique = [...new Set(pendingToolNames.filter(Boolean))];
|
|
667
706
|
const tools = unique.length > 0 ? unique : ["text_response"];
|
|
668
707
|
const share = 1 / tools.length;
|
|
669
708
|
|
|
@@ -710,11 +749,9 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
710
749
|
total_tokens: delta.total_tokens * execToolShare,
|
|
711
750
|
} : null;
|
|
712
751
|
|
|
713
|
-
if (execDelta &&
|
|
714
|
-
const perExecShare = 1 /
|
|
715
|
-
for (const
|
|
716
|
-
const details = getExecKeys(p);
|
|
717
|
-
if (!details) continue;
|
|
752
|
+
if (execDelta && pendingExecDetails.length > 0) {
|
|
753
|
+
const perExecShare = 1 / pendingExecDetails.length;
|
|
754
|
+
for (const details of pendingExecDetails) {
|
|
718
755
|
const attributed = {
|
|
719
756
|
input_tokens: execDelta.input_tokens * perExecShare,
|
|
720
757
|
cached_input_tokens: execDelta.cached_input_tokens * perExecShare,
|
|
@@ -731,20 +768,76 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
731
768
|
addInto(ensureExec(byExecDuration, details.duration).totals, attributed);
|
|
732
769
|
addInto(ensureExec(byExecOutput, details.output).totals, attributed);
|
|
733
770
|
|
|
734
|
-
absorbExecEnd(
|
|
771
|
+
absorbExecEnd(details);
|
|
735
772
|
}
|
|
736
773
|
} else {
|
|
737
774
|
// Still ingest exec end stats without token attribution so the drill-down works.
|
|
738
|
-
for (const
|
|
775
|
+
for (const details of pendingExecDetails) absorbExecEnd(details);
|
|
739
776
|
}
|
|
740
777
|
|
|
741
778
|
addInto(totals, delta);
|
|
742
|
-
|
|
779
|
+
pendingToolNames = [];
|
|
743
780
|
pendingSkills = [];
|
|
744
|
-
|
|
781
|
+
pendingExecDetails = [];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function usageEventSignature(lastUsage, totalUsage) {
|
|
785
|
+
return JSON.stringify({ lastUsage, totalUsage });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function materializeLastTimestampSignatures() {
|
|
789
|
+
if (hasUnmaterializedLastTimestampUsage) {
|
|
790
|
+
lastTimestampEventSignatures.add(
|
|
791
|
+
usageEventSignature(lastTimestampLastUsage, lastTimestampTotalUsage),
|
|
792
|
+
);
|
|
793
|
+
hasUnmaterializedLastTimestampUsage = false;
|
|
794
|
+
}
|
|
795
|
+
return lastTimestampEventSignatures;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function noteTokenEvent(timestamp, lastUsage, totalUsage) {
|
|
799
|
+
if (!isResuming) {
|
|
800
|
+
if (timestamp !== lastTokenTimestamp) {
|
|
801
|
+
lastTokenTimestamp = timestamp;
|
|
802
|
+
lastTimestampEventSignatures = new Set();
|
|
803
|
+
hasUnmaterializedLastTimestampUsage = false;
|
|
804
|
+
} else {
|
|
805
|
+
materializeLastTimestampSignatures();
|
|
806
|
+
}
|
|
807
|
+
lastTimestampLastUsage = lastUsage;
|
|
808
|
+
lastTimestampTotalUsage = totalUsage;
|
|
809
|
+
hasUnmaterializedLastTimestampUsage = true;
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
if (isResuming && lastTokenTimestamp && timestamp < lastTokenTimestamp) {
|
|
813
|
+
const error = new Error("Codex rollout append is not timestamp-monotonic");
|
|
814
|
+
error.code = CODEX_RESUME_INVALIDATED;
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
if (!lastTokenTimestamp || timestamp > lastTokenTimestamp) {
|
|
818
|
+
lastTokenTimestamp = timestamp;
|
|
819
|
+
lastTimestampEventSignatures = new Set();
|
|
820
|
+
lastTimestampLastUsage = lastUsage;
|
|
821
|
+
lastTimestampTotalUsage = totalUsage;
|
|
822
|
+
hasUnmaterializedLastTimestampUsage = true;
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
if (timestamp === lastTokenTimestamp) {
|
|
826
|
+
const signature = usageEventSignature(lastUsage, totalUsage);
|
|
827
|
+
const signatures = materializeLastTimestampSignatures();
|
|
828
|
+
if (signatures.has(signature)) return false;
|
|
829
|
+
signatures.add(signature);
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
745
832
|
}
|
|
746
833
|
|
|
747
|
-
for await (const obj of iterateCodexObjects(filePaths, diagnostics
|
|
834
|
+
for await (const obj of iterateCodexObjects(filePaths, diagnostics, {
|
|
835
|
+
startOffset,
|
|
836
|
+
endOffset,
|
|
837
|
+
readProgress,
|
|
838
|
+
contentHasher,
|
|
839
|
+
sourceHandle,
|
|
840
|
+
})) {
|
|
748
841
|
const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
|
|
749
842
|
if (!ts) continue;
|
|
750
843
|
const inRequestedRange = isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext });
|
|
@@ -765,14 +858,15 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
765
858
|
}
|
|
766
859
|
|
|
767
860
|
if (obj.type === "response_item" && obj.payload?.type === "function_call") {
|
|
768
|
-
|
|
861
|
+
pendingToolNames.push(normalizeToolName(obj.payload));
|
|
769
862
|
const skill = extractSkillNameFromFunctionCall(obj.payload, diagnostics);
|
|
770
863
|
if (skill) pendingSkills.push(skill);
|
|
771
864
|
continue;
|
|
772
865
|
}
|
|
773
866
|
|
|
774
867
|
if (obj.type === "event_msg" && obj.payload?.type === "exec_command_end") {
|
|
775
|
-
|
|
868
|
+
const details = getExecKeys(obj.payload);
|
|
869
|
+
if (details) pendingExecDetails.push(details);
|
|
776
870
|
continue;
|
|
777
871
|
}
|
|
778
872
|
|
|
@@ -783,25 +877,26 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
783
877
|
const totalUsage = info?.total_token_usage;
|
|
784
878
|
const delta = pickDelta(lastUsage, totalUsage, prevTotals);
|
|
785
879
|
if (totalUsage && typeof totalUsage === "object") prevTotals = totalUsage;
|
|
880
|
+
const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
|
|
786
881
|
if (inRequestedRange) {
|
|
787
882
|
const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
|
|
788
|
-
const eventKey = `${eventSessionId}:${ts}:${
|
|
789
|
-
if (seenTokenEvents && seenTokenEvents.has(eventKey)) {
|
|
883
|
+
const eventKey = `${eventSessionId}:${ts}:${usageEventSignature(lastUsage, totalUsage)}`;
|
|
884
|
+
if (!isNewResumeEvent || (seenTokenEvents && seenTokenEvents.has(eventKey))) {
|
|
790
885
|
attributeTurn(null);
|
|
791
886
|
} else {
|
|
792
887
|
if (seenTokenEvents && delta?.total_tokens > 0) seenTokenEvents.add(eventKey);
|
|
793
888
|
attributeTurn(delta);
|
|
794
889
|
}
|
|
795
890
|
} else {
|
|
796
|
-
|
|
891
|
+
pendingToolNames = [];
|
|
797
892
|
pendingSkills = [];
|
|
798
|
-
|
|
893
|
+
pendingExecDetails = [];
|
|
799
894
|
}
|
|
800
895
|
continue;
|
|
801
896
|
}
|
|
802
897
|
}
|
|
803
898
|
|
|
804
|
-
|
|
899
|
+
const result = {
|
|
805
900
|
sessionId: sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath,
|
|
806
901
|
cwd,
|
|
807
902
|
model: model || provider,
|
|
@@ -825,11 +920,178 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
825
920
|
byOutput: finalizeExecRows(byExecOutput),
|
|
826
921
|
},
|
|
827
922
|
};
|
|
923
|
+
if (captureResumeState && filePaths.length === 1) {
|
|
924
|
+
result.resumeState = {
|
|
925
|
+
sessionId,
|
|
926
|
+
cwd,
|
|
927
|
+
model,
|
|
928
|
+
provider,
|
|
929
|
+
cliVersion,
|
|
930
|
+
prevTotals,
|
|
931
|
+
pendingToolNames,
|
|
932
|
+
pendingSkills,
|
|
933
|
+
pendingExecDetails,
|
|
934
|
+
lastTokenTimestamp,
|
|
935
|
+
lastTimestampEventSignatures: Array.from(materializeLastTimestampSignatures()),
|
|
936
|
+
};
|
|
937
|
+
result.endOffset = readProgress.endOffset;
|
|
938
|
+
result.appendable = Boolean(contentHasher) && (
|
|
939
|
+
readProgress.endOffset === 0 || readProgress.lastByte === 0x0a
|
|
940
|
+
);
|
|
941
|
+
result.contentHashState = contentHasher;
|
|
942
|
+
}
|
|
943
|
+
return result;
|
|
828
944
|
}
|
|
829
945
|
|
|
830
|
-
|
|
946
|
+
function parseCodexLine(lineBuffer, diagnostics) {
|
|
947
|
+
let content = lineBuffer;
|
|
948
|
+
if (content.length > 0 && content[content.length - 1] === 0x0d) {
|
|
949
|
+
content = content.subarray(0, content.length - 1);
|
|
950
|
+
}
|
|
951
|
+
if (content.length === 0) return null;
|
|
952
|
+
try {
|
|
953
|
+
if (diagnostics) diagnostics.json_parse_calls += 1;
|
|
954
|
+
return JSON.parse(content.toString("utf8"));
|
|
955
|
+
} catch {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function* readCodexObjectsIncremental(filePath, diagnostics, fileIndex, {
|
|
961
|
+
startOffset = 0,
|
|
962
|
+
endOffset = null,
|
|
963
|
+
readProgress = null,
|
|
964
|
+
contentHasher = null,
|
|
965
|
+
sourceHandle = null,
|
|
966
|
+
} = {}) {
|
|
967
|
+
const start = Math.max(0, Number(startOffset) || 0);
|
|
968
|
+
const requestedEnd = Number(endOffset);
|
|
969
|
+
const hasBoundedEnd = endOffset !== null && endOffset !== undefined &&
|
|
970
|
+
Number.isFinite(requestedEnd) && requestedEnd >= 0;
|
|
971
|
+
if (hasBoundedEnd && requestedEnd <= start) {
|
|
972
|
+
if (readProgress) readProgress.endOffset = start;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
|
|
831
976
|
if (diagnostics) diagnostics.opened_files += 1;
|
|
832
|
-
const stream =
|
|
977
|
+
const stream = sourceHandle && typeof sourceHandle.read === "function"
|
|
978
|
+
? null
|
|
979
|
+
: fs.createReadStream(filePath, {
|
|
980
|
+
start,
|
|
981
|
+
...(hasBoundedEnd ? { end: requestedEnd - 1 } : {}),
|
|
982
|
+
});
|
|
983
|
+
const chunks = stream || (async function* readStableHandleChunks() {
|
|
984
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
985
|
+
const limit = hasBoundedEnd ? requestedEnd : Number.MAX_SAFE_INTEGER;
|
|
986
|
+
let offset = start;
|
|
987
|
+
while (offset < limit) {
|
|
988
|
+
const length = Math.min(buffer.length, limit - offset);
|
|
989
|
+
const result = await sourceHandle.read(buffer, 0, length, offset);
|
|
990
|
+
if (!result.bytesRead) break;
|
|
991
|
+
offset += result.bytesRead;
|
|
992
|
+
yield buffer.subarray(0, result.bytesRead);
|
|
993
|
+
}
|
|
994
|
+
})();
|
|
995
|
+
const fragments = [];
|
|
996
|
+
let fragmentsBytes = 0;
|
|
997
|
+
let nextChunkOffset = start;
|
|
998
|
+
let lineStartOffset = start;
|
|
999
|
+
let lineIndex = 0;
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
for await (const value of chunks) {
|
|
1003
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
1004
|
+
const chunkStart = nextChunkOffset;
|
|
1005
|
+
nextChunkOffset += chunk.length;
|
|
1006
|
+
if (contentHasher) contentHasher.update(chunk);
|
|
1007
|
+
if (readProgress && chunk.length > 0) {
|
|
1008
|
+
readProgress.lastByte = chunk[chunk.length - 1];
|
|
1009
|
+
}
|
|
1010
|
+
if (diagnostics) {
|
|
1011
|
+
diagnostics.bytes_read = Number(diagnostics.bytes_read || 0) + chunk.length;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
let cursor = 0;
|
|
1015
|
+
while (cursor < chunk.length) {
|
|
1016
|
+
const newline = chunk.indexOf(0x0a, cursor);
|
|
1017
|
+
if (newline < 0) {
|
|
1018
|
+
const tail = chunk.subarray(cursor);
|
|
1019
|
+
if (tail.length > 0) {
|
|
1020
|
+
// Stable-handle reads reuse their buffer, so retained fragments
|
|
1021
|
+
// must own their bytes before the next read overwrites it.
|
|
1022
|
+
fragments.push(Buffer.from(tail));
|
|
1023
|
+
fragmentsBytes += tail.length;
|
|
1024
|
+
}
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const tail = chunk.subarray(cursor, newline);
|
|
1029
|
+
const lineBuffer = fragments.length > 0
|
|
1030
|
+
? Buffer.concat([...fragments, tail], fragmentsBytes + tail.length)
|
|
1031
|
+
: tail;
|
|
1032
|
+
fragments.length = 0;
|
|
1033
|
+
fragmentsBytes = 0;
|
|
1034
|
+
|
|
1035
|
+
const absoluteEnd = chunkStart + newline + 1;
|
|
1036
|
+
if (readProgress) readProgress.endOffset = absoluteEnd;
|
|
1037
|
+
lineStartOffset = absoluteEnd;
|
|
1038
|
+
const obj = parseCodexLine(lineBuffer, diagnostics);
|
|
1039
|
+
if (obj) {
|
|
1040
|
+
yield { obj, fileIndex, lineIndex };
|
|
1041
|
+
lineIndex += 1;
|
|
1042
|
+
}
|
|
1043
|
+
cursor = newline + 1;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (fragmentsBytes > 0) {
|
|
1048
|
+
const lineBuffer = fragments.length === 1
|
|
1049
|
+
? fragments[0]
|
|
1050
|
+
: Buffer.concat(fragments, fragmentsBytes);
|
|
1051
|
+
const obj = parseCodexLine(lineBuffer, diagnostics);
|
|
1052
|
+
if (obj) {
|
|
1053
|
+
if (readProgress) readProgress.endOffset = lineStartOffset + fragmentsBytes;
|
|
1054
|
+
yield { obj, fileIndex, lineIndex };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} finally {
|
|
1058
|
+
if (readProgress) readProgress.endOffset = nextChunkOffset;
|
|
1059
|
+
stream?.destroy();
|
|
1060
|
+
if (diagnostics) diagnostics.parsed_files += 1;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function* readCodexObjectsFull(filePath, diagnostics, fileIndex, {
|
|
1065
|
+
startOffset = 0,
|
|
1066
|
+
endOffset = null,
|
|
1067
|
+
readProgress = null,
|
|
1068
|
+
contentHasher = null,
|
|
1069
|
+
} = {}) {
|
|
1070
|
+
const start = Math.max(0, Number(startOffset) || 0);
|
|
1071
|
+
const requestedEnd = Number(endOffset);
|
|
1072
|
+
const hasBoundedEnd = endOffset !== null && endOffset !== undefined &&
|
|
1073
|
+
Number.isFinite(requestedEnd) && requestedEnd >= 0;
|
|
1074
|
+
if (hasBoundedEnd && requestedEnd <= start) {
|
|
1075
|
+
if (readProgress) readProgress.endOffset = start;
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (diagnostics) diagnostics.opened_files += 1;
|
|
1080
|
+
const stream = fs.createReadStream(filePath, {
|
|
1081
|
+
...(contentHasher ? {} : { encoding: "utf8" }),
|
|
1082
|
+
start,
|
|
1083
|
+
...(hasBoundedEnd ? { end: requestedEnd - 1 } : {}),
|
|
1084
|
+
});
|
|
1085
|
+
const trackChunk = contentHasher
|
|
1086
|
+
? (value) => {
|
|
1087
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
1088
|
+
contentHasher.update(chunk);
|
|
1089
|
+
if (readProgress && chunk.length > 0) {
|
|
1090
|
+
readProgress.lastByte = chunk[chunk.length - 1];
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
: null;
|
|
1094
|
+
if (trackChunk) stream.on("data", trackChunk);
|
|
833
1095
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
834
1096
|
let lineIndex = 0;
|
|
835
1097
|
try {
|
|
@@ -846,15 +1108,29 @@ async function* readCodexObjects(filePath, diagnostics, fileIndex) {
|
|
|
846
1108
|
lineIndex += 1;
|
|
847
1109
|
}
|
|
848
1110
|
} finally {
|
|
1111
|
+
const bytesRead = Number(stream.bytesRead || 0);
|
|
1112
|
+
if (readProgress) readProgress.endOffset = start + bytesRead;
|
|
1113
|
+
if (diagnostics) {
|
|
1114
|
+
diagnostics.bytes_read = Number(diagnostics.bytes_read || 0) + bytesRead;
|
|
1115
|
+
diagnostics.parsed_files += 1;
|
|
1116
|
+
}
|
|
1117
|
+
if (trackChunk) stream.off("data", trackChunk);
|
|
849
1118
|
rl.close();
|
|
850
1119
|
stream.close?.();
|
|
851
|
-
if (diagnostics) diagnostics.parsed_files += 1;
|
|
852
1120
|
}
|
|
853
1121
|
}
|
|
854
1122
|
|
|
855
|
-
async function*
|
|
1123
|
+
async function* readCodexObjects(filePath, diagnostics, fileIndex, options = {}) {
|
|
1124
|
+
const start = Math.max(0, Number(options.startOffset) || 0);
|
|
1125
|
+
const reader = start > 0 || options.sourceHandle
|
|
1126
|
+
? readCodexObjectsIncremental
|
|
1127
|
+
: readCodexObjectsFull;
|
|
1128
|
+
yield* reader(filePath, diagnostics, fileIndex, options);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
|
|
856
1132
|
if (filePaths.length <= 1) {
|
|
857
|
-
for await (const record of readCodexObjects(filePaths[0], diagnostics, 0)) {
|
|
1133
|
+
for await (const record of readCodexObjects(filePaths[0], diagnostics, 0, options)) {
|
|
858
1134
|
yield record.obj;
|
|
859
1135
|
}
|
|
860
1136
|
return;
|
|
@@ -892,6 +1168,10 @@ function rolloutSessionIdFromPath(filePath) {
|
|
|
892
1168
|
return match ? match[1] : null;
|
|
893
1169
|
}
|
|
894
1170
|
|
|
1171
|
+
function isCodexResumeInvalidated(error) {
|
|
1172
|
+
return error?.code === CODEX_RESUME_INVALIDATED;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
895
1175
|
module.exports = {
|
|
896
1176
|
parseCodexRolloutFile,
|
|
897
1177
|
extractTokenCount,
|
|
@@ -913,6 +1193,7 @@ module.exports = {
|
|
|
913
1193
|
timestampDayKey,
|
|
914
1194
|
isTimestampInRequestedDayRange,
|
|
915
1195
|
rolloutSessionIdFromPath,
|
|
1196
|
+
isCodexResumeInvalidated,
|
|
916
1197
|
finalizeToolRows,
|
|
917
1198
|
finalizeSkillRows,
|
|
918
1199
|
finalizeExecRows,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Estimates fixed context overhead without returning file contents, prompts,
|
|
4
|
+
// tool schemas, secrets, or command arguments.
|
|
5
|
+
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const contextCache = new Map();
|
|
10
|
+
|
|
11
|
+
function estimateTokens(text) {
|
|
12
|
+
const value = String(text || "");
|
|
13
|
+
let cjk = 0;
|
|
14
|
+
for (const char of value) if (/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/u.test(char)) cjk += 1;
|
|
15
|
+
return Math.ceil(cjk + (value.length - cjk) / 4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readMetadata(filePath, kind, root) {
|
|
19
|
+
try {
|
|
20
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
21
|
+
return { kind, name: path.relative(root, filePath) || path.basename(filePath), bytes: Buffer.byteLength(text), estimated_tokens: estimateTokens(text) };
|
|
22
|
+
} catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walkNamedFiles(root, targetName, limit = 500) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const stack = [root];
|
|
28
|
+
while (stack.length && out.length < limit) {
|
|
29
|
+
const dir = stack.pop();
|
|
30
|
+
let entries;
|
|
31
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if ([".git", "node_modules", "dist", "build"].includes(entry.name)) continue;
|
|
34
|
+
const filePath = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) stack.push(filePath);
|
|
36
|
+
else if (entry.isFile() && entry.name === targetName) out.push(filePath);
|
|
37
|
+
if (out.length >= limit) break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function countMcpServers(home, cwd) {
|
|
44
|
+
const files = [
|
|
45
|
+
path.join(home, ".claude.json"),
|
|
46
|
+
path.join(home, ".claude", "settings.json"),
|
|
47
|
+
path.join(cwd, ".mcp.json"),
|
|
48
|
+
];
|
|
49
|
+
const names = new Set();
|
|
50
|
+
for (const filePath of files) {
|
|
51
|
+
try {
|
|
52
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
53
|
+
const servers = data.mcpServers || data.mcp_servers || {};
|
|
54
|
+
for (const name of Object.keys(servers)) names.add(name);
|
|
55
|
+
} catch { /* absent or not JSON */ }
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const toml = fs.readFileSync(path.join(home, ".codex", "config.toml"), "utf8");
|
|
59
|
+
for (const match of toml.matchAll(/^\s*\[mcp_servers\.([^\]]+)\]/gm)) names.add(match[1]);
|
|
60
|
+
} catch { /* absent */ }
|
|
61
|
+
return names.size;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function computeContextHealth({ home = os.homedir(), cwd = process.cwd(), env = process.env, cacheTtlMs = 5 * 60_000 } = {}) {
|
|
65
|
+
const cacheKey = `${home}\0${cwd}\0${env.TOKENTRACKER_MCP_TOOL_SCHEMA_TOKENS || ""}\0${env.TOKENTRACKER_MCP_TOOLS_PER_SERVER || ""}`;
|
|
66
|
+
const cached = contextCache.get(cacheKey);
|
|
67
|
+
if (cached && Date.now() - cached.at < cacheTtlMs) return cached.value;
|
|
68
|
+
const files = [];
|
|
69
|
+
const known = [
|
|
70
|
+
[path.join(home, ".claude", "CLAUDE.md"), "instruction", home],
|
|
71
|
+
[path.join(home, ".codex", "AGENTS.md"), "instruction", home],
|
|
72
|
+
[path.join(cwd, "CLAUDE.md"), "instruction", cwd],
|
|
73
|
+
[path.join(cwd, "AGENTS.md"), "instruction", cwd],
|
|
74
|
+
];
|
|
75
|
+
for (const [filePath, kind, root] of known) {
|
|
76
|
+
const item = readMetadata(filePath, kind, root);
|
|
77
|
+
if (item) files.push(item);
|
|
78
|
+
}
|
|
79
|
+
const skillRoots = [
|
|
80
|
+
path.join(home, ".claude", "skills"),
|
|
81
|
+
path.join(home, ".codex", "skills"),
|
|
82
|
+
path.join(cwd, ".claude", "skills"),
|
|
83
|
+
path.join(cwd, ".agents", "skills"),
|
|
84
|
+
];
|
|
85
|
+
for (const root of skillRoots) {
|
|
86
|
+
for (const filePath of walkNamedFiles(root, "SKILL.md")) {
|
|
87
|
+
const item = readMetadata(filePath, "skill", root);
|
|
88
|
+
if (item) files.push(item);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const instructionTokens = files.filter((row) => row.kind === "instruction").reduce((sum, row) => sum + row.estimated_tokens, 0);
|
|
92
|
+
const skillTokens = files.filter((row) => row.kind === "skill").reduce((sum, row) => sum + row.estimated_tokens, 0);
|
|
93
|
+
const mcpServers = countMcpServers(home, cwd);
|
|
94
|
+
const perToolSchemaTokens = Math.max(0, Number(env.TOKENTRACKER_MCP_TOOL_SCHEMA_TOKENS || 400));
|
|
95
|
+
const assumedToolsPerServer = Math.max(1, Number(env.TOKENTRACKER_MCP_TOOLS_PER_SERVER || 5));
|
|
96
|
+
const mcpSchemaTokens = mcpServers * perToolSchemaTokens * assumedToolsPerServer;
|
|
97
|
+
const total = instructionTokens + skillTokens + mcpSchemaTokens;
|
|
98
|
+
const severity = total >= 50_000 ? "high" : total >= 20_000 ? "medium" : "low";
|
|
99
|
+
const result = {
|
|
100
|
+
generated_at: new Date().toISOString(),
|
|
101
|
+
estimated_fixed_tokens: total,
|
|
102
|
+
severity,
|
|
103
|
+
breakdown: {
|
|
104
|
+
instruction_files: files.filter((row) => row.kind === "instruction").length,
|
|
105
|
+
instruction_tokens: instructionTokens,
|
|
106
|
+
skills: files.filter((row) => row.kind === "skill").length,
|
|
107
|
+
skill_tokens: skillTokens,
|
|
108
|
+
mcp_servers: mcpServers,
|
|
109
|
+
mcp_schema_tokens: mcpSchemaTokens,
|
|
110
|
+
},
|
|
111
|
+
largest_items: files.sort((a, b) => b.estimated_tokens - a.estimated_tokens).slice(0, 10),
|
|
112
|
+
provenance: {
|
|
113
|
+
confidence: "inferred",
|
|
114
|
+
source: "local-config-metadata",
|
|
115
|
+
mcp_assumption: { tools_per_server: assumedToolsPerServer, tokens_per_tool_schema: perToolSchemaTokens },
|
|
116
|
+
content_retained: false,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
contextCache.set(cacheKey, { at: Date.now(), value: result });
|
|
120
|
+
if (contextCache.size > 8) contextCache.delete(contextCache.keys().next().value);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { estimateTokens, computeContextHealth, countMcpServers };
|