@node9/proxy 2.13.0 → 2.14.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/dist/cli.js CHANGED
@@ -1146,9 +1146,7 @@ function unwrapCommandHead(words) {
1146
1146
  while (i < words.length) {
1147
1147
  const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1148
1148
  if (head === "find") {
1149
- const x = words.findIndex(
1150
- (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1151
- );
1149
+ const x = words.findIndex((w, k) => k > i && w !== null && FIND_EXEC_FLAGS.has(w));
1152
1150
  if (x < 0) break;
1153
1151
  i = x + 1;
1154
1152
  continue;
@@ -1170,7 +1168,11 @@ function unwrapCommandHead(words) {
1170
1168
  if (t.startsWith("-")) {
1171
1169
  i++;
1172
1170
  const nxt = words[i];
1173
- if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1171
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()) && // A reader is a command, never a flag's operand: `env - cat X`,
1172
+ // `stdbuf -o0 cat X`, `ionice -c3 cat X`. Without this the head was
1173
+ // swallowed and the jail needed a looser fallback whose cost was a
1174
+ // false positive on `sudo echo cat X`.
1175
+ !FS_READ_TOOLS.has(nxt.split("/").pop()?.toLowerCase() ?? ""))
1174
1176
  i++;
1175
1177
  continue;
1176
1178
  }
@@ -1289,36 +1291,32 @@ function isProtectedHomePath(rawPath) {
1289
1291
  }
1290
1292
  return true;
1291
1293
  }
1292
- function extractLiteralArgs(callExpr) {
1293
- const args = callExpr.Args || [];
1294
- if (args.length === 0) return { name: "", flags: [], paths: [] };
1295
- const litFromWord = (w) => {
1296
- const parts = w?.Parts || [];
1297
- let s = "";
1298
- for (const p of parts) {
1299
- const t = syntax.NodeType(p);
1300
- if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1301
- else if (t === "SglQuoted") s += p.Value ?? "";
1302
- else if (t === "DblQuoted") {
1303
- const inner = p.Parts || [];
1304
- if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1305
- s += inner.map((ip) => ip.Value ?? "").join("");
1306
- } else {
1307
- return null;
1308
- }
1294
+ function positionedArgs(words, from = 1, to = words.length) {
1295
+ const out = [];
1296
+ let afterFlag = null;
1297
+ for (let i = from; i < to; i++) {
1298
+ const v = words[i];
1299
+ if (v === null) {
1300
+ afterFlag = null;
1301
+ continue;
1309
1302
  }
1310
- return s;
1311
- };
1312
- const name = (litFromWord(args[0]) || "").toLowerCase();
1313
- const flags = [];
1314
- const paths = [];
1315
- for (let i = 1; i < args.length; i++) {
1316
- const v = litFromWord(args[i]);
1317
- if (v === null) continue;
1318
- if (v.startsWith("-")) flags.push(v);
1319
- else paths.push(v);
1303
+ if (v.startsWith("-")) {
1304
+ afterFlag = v;
1305
+ continue;
1306
+ }
1307
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1308
+ afterFlag = null;
1320
1309
  }
1321
- return { name, flags, paths };
1310
+ return out;
1311
+ }
1312
+ function extractLiteralArgs(callExpr) {
1313
+ const rawArgs = callExpr.Args || [];
1314
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1315
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1316
+ const name = baseWord(words[0]);
1317
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1318
+ const args = positionedArgs(words);
1319
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1322
1320
  }
1323
1321
  function resolveWordLiteral(w) {
1324
1322
  const parts = w?.Parts || [];
@@ -1576,16 +1574,21 @@ function isRmCreatedInCommandCleanup(command) {
1576
1574
  }
1577
1575
  return sawRm && ok2;
1578
1576
  }
1579
- function analyzeFsOperationImpl(command) {
1577
+ function analyzeFsOperationImpl(command, depth = 0) {
1580
1578
  const f = parseShared(command);
1581
1579
  if (f === PARSE_FAIL) return null;
1582
1580
  let result = null;
1583
1581
  try {
1584
1582
  syntax.Walk(f, (node) => {
1585
- if (!node || result) return false;
1583
+ if (!node || result?.verdict === "block") return false;
1586
1584
  const n = node;
1587
- if (syntax.NodeType(n) !== "CallExpr") return true;
1588
- const { name, flags, paths } = extractLiteralArgs(n);
1585
+ const nodeType = syntax.NodeType(n);
1586
+ if (nodeType === "Stmt") {
1587
+ result = stricter(result, jailedRedirectRead(n));
1588
+ return result?.verdict !== "block";
1589
+ }
1590
+ if (nodeType !== "CallExpr") return true;
1591
+ const { name, flags, paths, words } = extractLiteralArgs(n);
1589
1592
  if (!name) return true;
1590
1593
  if (name === "rm") {
1591
1594
  const flagStr = flags.join("").toLowerCase();
@@ -1614,21 +1617,27 @@ function analyzeFsOperationImpl(command) {
1614
1617
  }
1615
1618
  }
1616
1619
  }
1617
- if (FS_READ_TOOLS.has(name)) {
1618
- for (const p of paths) {
1619
- for (const sp of SENSITIVE_PATH_RULES) {
1620
- if (sp.match(p)) {
1621
- result = {
1622
- ruleName: sp.rule,
1623
- verdict: sp.verdict ?? "block",
1624
- reason: sp.reason,
1625
- path: p
1626
- };
1627
- return false;
1628
- }
1620
+ if (depth < 1) {
1621
+ const payload = literalShellPayload(words, name);
1622
+ if (payload !== null) {
1623
+ const inner = analyzeFsOperationImpl(payload, depth + 1);
1624
+ if (inner) {
1625
+ result = inner;
1626
+ return false;
1629
1627
  }
1628
+ return true;
1629
+ }
1630
+ }
1631
+ const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
1632
+ if (readPaths) {
1633
+ for (const p of readPaths) {
1634
+ result = stricter(result, matchSensitivePath2(p));
1635
+ if (result?.verdict === "block") return false;
1630
1636
  }
1631
1637
  }
1638
+ for (const p of copySourcePaths(words)) {
1639
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
1640
+ }
1632
1641
  return true;
1633
1642
  });
1634
1643
  return result;
@@ -1636,6 +1645,183 @@ function analyzeFsOperationImpl(command) {
1636
1645
  return null;
1637
1646
  }
1638
1647
  }
1648
+ function stricter(a, b) {
1649
+ if (!a) return b;
1650
+ if (!b) return a;
1651
+ return b.verdict === "block" && a.verdict !== "block" ? b : a;
1652
+ }
1653
+ function flagInfo(w) {
1654
+ if (w.startsWith("--")) {
1655
+ const eq = w.indexOf("=");
1656
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
1657
+ }
1658
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
1659
+ if (!m) return { letter: null, long: null, attached: null };
1660
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
1661
+ }
1662
+ function flagIs(w, names) {
1663
+ if (w === null) return false;
1664
+ const f = flagInfo(w);
1665
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1666
+ }
1667
+ function operandOf(a, names) {
1668
+ if (!names || a.afterFlag === null) return false;
1669
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
1670
+ }
1671
+ function resolveCopyShape(words, h) {
1672
+ const verb = baseWord(words[h]);
1673
+ if (!verb) return null;
1674
+ const direct = COPY_VERBS[verb];
1675
+ if (direct) return { shape: direct, last: h };
1676
+ const slots = positionedArgs(words, h + 1);
1677
+ for (let i = 0; i < slots.length; i++) {
1678
+ for (let n = 3; n >= 1; n--) {
1679
+ const part = slots.slice(i, i + n);
1680
+ if (part.length < n) continue;
1681
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
1682
+ const shape = COPY_VERBS[key];
1683
+ if (shape) return { shape, last: part[n - 1].argv };
1684
+ }
1685
+ if (slots[i].afterFlag === null) return null;
1686
+ }
1687
+ return null;
1688
+ }
1689
+ function findStartPoints(words, h) {
1690
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1691
+ if (k < 0) return { k, starts: [] };
1692
+ const firstPredicate = words.findIndex(
1693
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
1694
+ );
1695
+ const end = firstPredicate > h ? firstPredicate : k;
1696
+ return { k, starts: positionalAfter(words, h + 1, end) };
1697
+ }
1698
+ function copySourcePaths(words) {
1699
+ const h = unwrapCommandHead(words);
1700
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
1701
+ if (fi >= 0) {
1702
+ const { k, starts } = findStartPoints(words, fi);
1703
+ if (k < 0) return [];
1704
+ const action = unwrapCommandHead(words.slice(k + 1));
1705
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
1706
+ }
1707
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
1708
+ const r = resolveCopyShape(words, h);
1709
+ if (!r) return [];
1710
+ const { shape, last } = r;
1711
+ const args = positionedArgs(words, last + 1);
1712
+ const tail = words.slice(last + 1);
1713
+ const skipped = (a) => operandOf(a, shape.skipFlags);
1714
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
1715
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
1716
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1717
+ const dynamicDest = lastOperand === null;
1718
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1719
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
1720
+ return [];
1721
+ let src;
1722
+ switch (shape.source) {
1723
+ case "all":
1724
+ src = args;
1725
+ break;
1726
+ case "first":
1727
+ src = targetDir ? args : args.slice(0, 1);
1728
+ break;
1729
+ case "flagOperand": {
1730
+ const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
1731
+ return [
1732
+ ...args.filter(
1733
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
1734
+ ).map((a) => a.value),
1735
+ ...inline
1736
+ ];
1737
+ }
1738
+ case "archive":
1739
+ src = archiveInputs(shape.archive, args, tail);
1740
+ break;
1741
+ case "allButLast":
1742
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
1743
+ break;
1744
+ }
1745
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1746
+ }
1747
+ function archiveInputs(kind, args, tail) {
1748
+ const first = args[0];
1749
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
1750
+ if (kind === "tar") {
1751
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
1752
+ const mode = (bareKey ? first.value : "") + flagsText;
1753
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
1754
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1755
+ if (extracting && !writing) return [];
1756
+ void mode;
1757
+ let i = 0;
1758
+ if (bareKey) {
1759
+ i = 1;
1760
+ const next = args[1];
1761
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
1762
+ }
1763
+ return args.slice(i);
1764
+ }
1765
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1766
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
1767
+ return args.slice(2);
1768
+ }
1769
+ function copyVerdictOf(hit) {
1770
+ if (!hit) return null;
1771
+ const ruleName = COPY_RULE_OF[hit.ruleName];
1772
+ if (!ruleName) return null;
1773
+ return {
1774
+ ruleName,
1775
+ verdict: "review",
1776
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
1777
+ path: hit.path
1778
+ };
1779
+ }
1780
+ function matchSensitivePath2(p) {
1781
+ for (const sp of SENSITIVE_PATH_RULES) {
1782
+ if (sp.match(p))
1783
+ return { ruleName: sp.rule, verdict: sp.verdict ?? "block", reason: sp.reason, path: p };
1784
+ }
1785
+ return null;
1786
+ }
1787
+ function baseWord(w) {
1788
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
1789
+ }
1790
+ function wrappedReadPaths(words, name) {
1791
+ if (name === "find") {
1792
+ const { k, starts } = findStartPoints(words, 0);
1793
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
1794
+ }
1795
+ if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1796
+ const h = unwrapCommandHead(words);
1797
+ return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
1798
+ }
1799
+ function literalShellPayload(words, name) {
1800
+ const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
1801
+ const head = (words[h] ?? "").split("/").pop()?.toLowerCase() ?? "";
1802
+ if (head === "eval") {
1803
+ const rest = words.slice(h + 1);
1804
+ if (rest.length === 0 || rest.some((w) => w === null)) return null;
1805
+ return rest.join(" ");
1806
+ }
1807
+ if (SHELL_INTERPRETERS.has(head)) {
1808
+ const c = words.findIndex((w, i) => i > h && w !== null && isInlineCodeFlag(head, w));
1809
+ if (c < 0) return null;
1810
+ return words[c + 1] ?? null;
1811
+ }
1812
+ return null;
1813
+ }
1814
+ function jailedRedirectRead(stmt) {
1815
+ const redirs = stmt.Redirs || [];
1816
+ for (const r of redirs) {
1817
+ if (!r || !REDIR_FILE_IN_OPS.has(r.Op)) continue;
1818
+ const p = resolveWordLiteral(r.Word);
1819
+ if (p === null || p === "") continue;
1820
+ const hit = matchSensitivePath2(p);
1821
+ if (hit) return hit;
1822
+ }
1823
+ return null;
1824
+ }
1639
1825
  function analyzeShellCommand(command) {
1640
1826
  const actions = [];
1641
1827
  const paths = [];
@@ -1771,8 +1957,8 @@ function splitOnPipe(cmd) {
1771
1957
  if (current.trim()) segments2.push(current.trim());
1772
1958
  return segments2.filter(Boolean);
1773
1959
  }
1774
- function positionalTokens(segment) {
1775
- return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
1960
+ function positionalTokens(tokens) {
1961
+ return tokens.slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
1776
1962
  }
1777
1963
  function analyzePipeChain(command) {
1778
1964
  const segments2 = splitOnPipe(command);
@@ -1795,8 +1981,10 @@ function analyzePipeChain(command) {
1795
1981
  for (const segment of segments2) {
1796
1982
  const tokens = segment.split(/\s+/).filter(Boolean);
1797
1983
  if (tokens.length === 0) continue;
1798
- const binary = tokens[0].toLowerCase();
1799
- const args = positionalTokens(segment);
1984
+ const h = unwrapCommandHead(tokens);
1985
+ const head = h < tokens.length ? h : 0;
1986
+ const binary = tokens[head].toLowerCase();
1987
+ const args = positionalTokens(tokens.slice(head));
1800
1988
  if (SOURCE_COMMANDS.has(binary)) {
1801
1989
  sourceFiles.push(...args);
1802
1990
  if (args.some(isSensitivePath)) hasSensitiveSource = true;
@@ -2274,6 +2462,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2274
2462
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2275
2463
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2276
2464
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2465
+ let pendingAstReview;
2277
2466
  if (bashCommand !== null) {
2278
2467
  const pipeVerdict = pipeChainVerdict(
2279
2468
  bashCommand,
@@ -2285,7 +2474,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2285
2474
  if (fsVerdict) {
2286
2475
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
2287
2476
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
2288
- return {
2477
+ const astVerdict = {
2289
2478
  decision: fsVerdict.verdict,
2290
2479
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
2291
2480
  reason: fsVerdict.reason,
@@ -2293,6 +2482,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2293
2482
  ruleName: fsVerdict.ruleName,
2294
2483
  ruleDescription: fsVerdict.reason
2295
2484
  };
2485
+ if (fsVerdict.verdict === "block") return astVerdict;
2486
+ pendingAstReview = astVerdict;
2296
2487
  }
2297
2488
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2298
2489
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -2335,7 +2526,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2335
2526
  const matchedRule = resolvePinned(matches);
2336
2527
  if (matchedRule) {
2337
2528
  if (matchedRule.verdict === "allow")
2338
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
2529
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
2339
2530
  return {
2340
2531
  decision: matchedRule.verdict,
2341
2532
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -2362,6 +2553,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2362
2553
  allTokens = analyzed.allTokens;
2363
2554
  pathTokens = analyzed.paths;
2364
2555
  const candidates2 = [];
2556
+ if (pendingAstReview) candidates2.push(pendingAstReview);
2365
2557
  const evalVerdict = detectDangerousShellExec(shellCommand);
2366
2558
  if (evalVerdict === "block") {
2367
2559
  return {
@@ -2658,6 +2850,12 @@ function classifyRuleSeverity(name, verdict) {
2658
2850
  "read-ssh",
2659
2851
  "read-gcp",
2660
2852
  "read-cred",
2853
+ // Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
2854
+ // read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
2855
+ // (below), so copy-env joins the high list, not this one (/code-review).
2856
+ "copy-ssh",
2857
+ "copy-aws",
2858
+ "copy-cred",
2661
2859
  "delete-repo",
2662
2860
  "helm-uninstall",
2663
2861
  "drop-table",
@@ -2670,6 +2868,7 @@ function classifyRuleSeverity(name, verdict) {
2670
2868
  ];
2671
2869
  if (criticalPatterns.some((p) => n.includes(p))) return "critical";
2672
2870
  const highPatterns = [
2871
+ "copy-env",
2673
2872
  "force-push",
2674
2873
  "force_push",
2675
2874
  "git-destructive",
@@ -2687,6 +2886,11 @@ function narrativeRuleLabel(name) {
2687
2886
  const map = {
2688
2887
  "read-aws": "AWS credentials read",
2689
2888
  "read-ssh": "SSH private key read",
2889
+ // Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
2890
+ "copy-ssh": "SSH private key copied out",
2891
+ "copy-aws": "AWS credentials copied out",
2892
+ "copy-env": ".env file copied out",
2893
+ "copy-cred": "credential file copied out",
2690
2894
  "read-gcp": "GCP credentials read",
2691
2895
  "read-cred": "credential file read",
2692
2896
  "delete-repo": "GitHub repository deletion",
@@ -3276,7 +3480,7 @@ function* stringValues(obj, depth = 0) {
3276
3480
  }
3277
3481
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3278
3482
  }
3279
- var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3483
+ var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, SCP_VALUE_FLAGS, RSYNC_SKIP, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3280
3484
  var init_dist = __esm({
3281
3485
  "packages/policy-engine/dist/index.mjs"() {
3282
3486
  "use strict";
@@ -3901,13 +4105,32 @@ var init_dist = __esm({
3901
4105
  })
3902
4106
  );
3903
4107
  SENSITIVE_PATH_PATTERNS = [
3904
- /[/\\]\.ssh[/\\]/i,
3905
- /[/\\]\.aws[/\\]/i,
4108
+ /[/\\]\.ssh([/\\]|$)/i,
4109
+ /[/\\]\.aws([/\\]|$)/i,
3906
4110
  /[/\\]\.config[/\\]gcloud[/\\]/i,
3907
4111
  /[/\\]\.azure[/\\]/i,
3908
4112
  /[/\\]\.kube[/\\]config$/i,
3909
- /[/\\]\.env($|\.)/i,
3910
- // .env, .env.local, .env.production — not .envoy
4113
+ // ⚠️ ONE SEMANTIC, FOUR COPIES. This is the AST tier's `.env` rule verbatim
4114
+ // (shell/index.ts SENSITIVE_PATH_RULES), whose reasoning is documented there:
4115
+ // structural suffix chain rather than a hand-written list, `example|sample|
4116
+ // template` exempt because a fixture stays a fixture whatever follows, and
4117
+ // `.test` anchored because `test` names an ENVIRONMENT -- `.env.test` is the
4118
+ // committed template, `.env.test.local` is gitignored and holds real values.
4119
+ //
4120
+ // It was previously `[/\\]\.env($|\.)` with NO exemptions, so `Read .env.example`
4121
+ // blocked while `cat .env.example` allowed: the same file, opposite verdicts,
4122
+ // decided only by which tool asked. See src/__tests__/jail-both-doors.test.ts,
4123
+ // which is the contract that now holds these copies in step, and stage 5 of
4124
+ // doc/credential-jail-architecture.md, which replaces them with one generated
4125
+ // source.
4126
+ // ⚠️ The `.local` branch comes FIRST and takes no exemption. A fixture stays a
4127
+ // fixture whatever follows it -- `.env.example.md` is documentation -- but
4128
+ // `.env.example.local` is gitignored by the `.env*.local` convention and holds
4129
+ // real values, exactly the reasoning that anchors `(?!\.test$)` rather than
4130
+ // using `\b`. Without this branch the fixture exemption also bought a two-step
4131
+ // bypass: `cp .env .env.sample`, then read the copy.
4132
+ /[/\\]\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
4133
+ // .env + any suffix chain; fixtures exempt unless .local
3911
4134
  /[/\\]\.git-credentials$/i,
3912
4135
  /[/\\]\.npmrc$/i,
3913
4136
  /[/\\]\.docker[/\\]config\.json$/i,
@@ -3989,6 +4212,10 @@ var init_dist = __esm({
3989
4212
  "od",
3990
4213
  "xxd",
3991
4214
  "hexdump",
4215
+ // Emits the file's bytes, re-encoded, so it is a read by the set's own test
4216
+ // ("does it emit file contents"). Absent until 2026-09-10, which is why
4217
+ // `base64 ~/.ssh/id_rsa` printed a private key with no verdict.
4218
+ "base64",
3992
4219
  "strings",
3993
4220
  "sort",
3994
4221
  "uniq",
@@ -3996,8 +4223,56 @@ var init_dist = __esm({
3996
4223
  "nl",
3997
4224
  "dd"
3998
4225
  ]);
4226
+ SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
4227
+ RSYNC_SKIP = [
4228
+ "e",
4229
+ "--rsh",
4230
+ "--exclude",
4231
+ "--exclude-from",
4232
+ "--include",
4233
+ "--include-from",
4234
+ "--files-from",
4235
+ "f",
4236
+ "--filter"
4237
+ ];
4238
+ COPY_VERBS = {
4239
+ cp: { source: "allButLast", targetDirFlag: true },
4240
+ mv: { source: "allButLast", targetDirFlag: true },
4241
+ install: { source: "allButLast", targetDirFlag: true },
4242
+ ln: { source: "first", targetDirFlag: true },
4243
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
4244
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
4245
+ tar: {
4246
+ source: "archive",
4247
+ archive: "tar",
4248
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
4249
+ },
4250
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
4251
+ ar: { source: "archive", archive: "ar" },
4252
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
4253
+ gzip: { source: "all" },
4254
+ bzip2: { source: "all" },
4255
+ xz: { source: "all" },
4256
+ "docker cp": { source: "allButLast" },
4257
+ "kubectl cp": { source: "allButLast" },
4258
+ "gsutil cp": { source: "allButLast" },
4259
+ "gsutil rsync": { source: "allButLast" },
4260
+ "rclone copy": { source: "allButLast" },
4261
+ "rclone sync": { source: "allButLast" },
4262
+ "aws s3 cp": { source: "allButLast" },
4263
+ "aws s3 mv": { source: "allButLast" },
4264
+ "aws s3 sync": { source: "allButLast" },
4265
+ "gcloud storage cp": { source: "allButLast" },
4266
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
4267
+ };
4268
+ TAR_MODE_WORD = /^[a-zA-Z]+$/;
4269
+ COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
3999
4270
  FS_OP_PRESCREEN_RE = new RegExp(
4000
- `(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
4271
+ // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
4272
+ // reader right after `"` / `'`, and without these two characters the
4273
+ // prescreen rejected every string-wrapped read before the parser ran.
4274
+ // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
4275
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
4001
4276
  );
4002
4277
  HOME_CACHE_ALLOWLIST = [
4003
4278
  ".cache",
@@ -4018,12 +4293,12 @@ var init_dist = __esm({
4018
4293
  {
4019
4294
  rule: "shield:project-jail:block-read-ssh",
4020
4295
  reason: "Reading SSH private keys is blocked by project-jail shield",
4021
- match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
4296
+ match: (p) => /([\\/]\.ssh[\\/]|^\.ssh[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.ssh$)/i.test(p)
4022
4297
  },
4023
4298
  {
4024
4299
  rule: "shield:project-jail:block-read-aws",
4025
4300
  reason: "Reading AWS credentials is blocked by project-jail shield",
4026
- match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
4301
+ match: (p) => /([\\/]\.aws[\\/]|^\.aws[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.aws$)/i.test(p)
4027
4302
  },
4028
4303
  {
4029
4304
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
@@ -4067,7 +4342,9 @@ var init_dist = __esm({
4067
4342
  // symmetry — silently exempts every `.env.test.*` file.
4068
4343
  //
4069
4344
  // shields.test.ts:983-995 is the canonical contract; keep both in step.
4070
- match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
4345
+ match: (p) => /(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i.test(
4346
+ p
4347
+ )
4071
4348
  },
4072
4349
  {
4073
4350
  // verdict: 'review' (not 'block') is a deliberate design choice
@@ -4178,8 +4455,18 @@ var init_dist = __esm({
4178
4455
  _redirStdinOps = null;
4179
4456
  _listOps = null;
4180
4457
  WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
4458
+ FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
4181
4459
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
4182
- NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
4460
+ NET_BINARIES = /* @__PURE__ */ new Set([
4461
+ "curl",
4462
+ "wget",
4463
+ "scp",
4464
+ "ssh",
4465
+ "nc",
4466
+ "ncat",
4467
+ "netcat",
4468
+ "rsync"
4469
+ ]);
4183
4470
  VALUE_FLAGS = {
4184
4471
  curl: /* @__PURE__ */ new Set([
4185
4472
  "-d",
@@ -4268,10 +4555,22 @@ var init_dist = __esm({
4268
4555
  fsOpCache = /* @__PURE__ */ new Map();
4269
4556
  stripDotSlash = (p) => p.replace(/^\.\//, "");
4270
4557
  REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
4558
+ REDIR_FILE_IN_OPS = new Set(
4559
+ [deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
4560
+ );
4271
4561
  REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
4272
4562
  deriveRedirOp("cat <<X\nX"),
4273
4563
  deriveRedirOp("cat <<-X\nX")
4274
4564
  ]);
4565
+ FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
4566
+ COPY_RULE_OF = {
4567
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
4568
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
4569
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
4570
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
4571
+ };
4572
+ isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
4573
+ positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
4275
4574
  DEFAULT_EGRESS_ALLOWLIST = [
4276
4575
  // node9's own control plane (api, app, dev-api, staging and the apex).
4277
4576
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -4295,21 +4594,7 @@ var init_dist = __esm({
4295
4594
  "deb.debian.org",
4296
4595
  "*.ubuntu.com"
4297
4596
  ];
4298
- SOURCE_COMMANDS = /* @__PURE__ */ new Set([
4299
- "cat",
4300
- "head",
4301
- "tail",
4302
- "grep",
4303
- "awk",
4304
- "sed",
4305
- "cut",
4306
- "sort",
4307
- "tee",
4308
- "less",
4309
- "more",
4310
- "strings",
4311
- "xxd"
4312
- ]);
4597
+ SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
4313
4598
  SINK_COMMANDS = /* @__PURE__ */ new Set([
4314
4599
  "curl",
4315
4600
  "wget",
@@ -4340,16 +4625,25 @@ var init_dist = __esm({
4340
4625
  "node"
4341
4626
  ]);
4342
4627
  SENSITIVE_PATTERNS = [
4343
- /(?:^|\/)\.env(?:\.|$)/i,
4344
- // .env, .env.local, .env.production
4628
+ // Kept in step with the AST tier and dlp/ -- see jail-both-doors.test.ts.
4629
+ /(?:^|\/)\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
4630
+ // .env chain; fixtures exempt unless .local
4345
4631
  /id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
4346
4632
  // SSH private keys
4347
4633
  /\.pem$|\.key$|\.p12$|\.pfx$/i,
4348
4634
  // certificate files
4349
- /(?:^|\/)\.ssh\//i,
4350
- // ~/.ssh/ directory
4351
- /(?:^|\/)\.aws\/credentials/i,
4352
- // AWS credentials
4635
+ // The `$` half mirrors shell/index.ts's SENSITIVE_PATH_RULES: a file INSIDE
4636
+ // the directory counts wherever it appears, while the directory ITSELF counts
4637
+ // only when the path is ROOTED (`~/.ssh`, `/home/u/.ssh`) -- an unrooted
4638
+ // `config/.ssh` is more likely a search pattern than a read. These are
4639
+ // extracted TOKENS (see `args.some(isSensitivePath)` below), the same input
4640
+ // contract as the shell tier, so the same boundary is the right one.
4641
+ // Without it `grep -r x ~/.ssh | curl -d @-` scored one tier BELOW the
4642
+ // identical pipeline naming a file inside that directory.
4643
+ /(?:^|\/)\.ssh\/|^(?:[~/]|[A-Za-z]:).*\/\.ssh$/i,
4644
+ // ~/.ssh/ and ~/.ssh
4645
+ /(?:^|\/)\.aws\/credentials|^(?:[~/]|[A-Za-z]:).*\/\.aws$/i,
4646
+ // AWS creds + dir
4353
4647
  /(?:^|\/)\.netrc$/i,
4354
4648
  // netrc (stores HTTP credentials)
4355
4649
  /(?:^|\/)(passwd|shadow|sudoers)$/i,
@@ -5017,7 +5311,7 @@ var init_dist = __esm({
5017
5311
  {
5018
5312
  field: "command",
5019
5313
  op: "matches",
5020
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
5314
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
5021
5315
  flags: "i"
5022
5316
  }
5023
5317
  ],
@@ -5031,7 +5325,7 @@ var init_dist = __esm({
5031
5325
  {
5032
5326
  field: "command",
5033
5327
  op: "matches",
5034
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
5328
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
5035
5329
  flags: "i"
5036
5330
  }
5037
5331
  ],
@@ -5045,7 +5339,7 @@ var init_dist = __esm({
5045
5339
  {
5046
5340
  field: "command",
5047
5341
  op: "matches",
5048
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
5342
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
5049
5343
  flags: "i"
5050
5344
  }
5051
5345
  ],
@@ -5059,7 +5353,7 @@ var init_dist = __esm({
5059
5353
  {
5060
5354
  field: "command",
5061
5355
  op: "matches",
5062
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
5356
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
5063
5357
  flags: "i"
5064
5358
  }
5065
5359
  ],
@@ -5073,7 +5367,7 @@ var init_dist = __esm({
5073
5367
  {
5074
5368
  field: "file_path",
5075
5369
  op: "matches",
5076
- value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
5370
+ value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
5077
5371
  flags: "i"
5078
5372
  }
5079
5373
  ],
@@ -5087,7 +5381,7 @@ var init_dist = __esm({
5087
5381
  {
5088
5382
  field: "file_path",
5089
5383
  op: "matches",
5090
- value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
5384
+ value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
5091
5385
  flags: "i"
5092
5386
  }
5093
5387
  ],
@@ -5101,7 +5395,7 @@ var init_dist = __esm({
5101
5395
  {
5102
5396
  field: "file_path",
5103
5397
  op: "matches",
5104
- value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
5398
+ value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
5105
5399
  flags: "i"
5106
5400
  }
5107
5401
  ],
@@ -5244,7 +5538,7 @@ var init_dist = __esm({
5244
5538
  };
5245
5539
  LOOP_THRESHOLD_FOR_WASTE = 3;
5246
5540
  DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
5247
- SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
5541
+ SENSITIVE_PATH_RE = /[\\/]\.aws(?:[\\/]|$)|^\.aws[\\/]|[\\/]\.ssh(?:[\\/]|$)|^\.ssh[\\/]|(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
5248
5542
  FILE_TOOLS = /* @__PURE__ */ new Set([
5249
5543
  "read",
5250
5544
  "read_file",
@@ -5321,7 +5615,7 @@ var init_dist = __esm({
5321
5615
  { view: "separators-stripped", decoder: "separators", stripped: true }
5322
5616
  ];
5323
5617
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
5324
- CANONICAL_EXTRACTOR_VERSION = "canonical-v10";
5618
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
5325
5619
  DEDUPE_PREVIEW_LEN = 120;
5326
5620
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
5327
5621
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
@@ -8640,7 +8934,7 @@ function isNetworkTool(toolName, args) {
8640
8934
  if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
8641
8935
  const a = args;
8642
8936
  const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
8643
- return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
8937
+ return NETWORK_COMMAND_RE.test(cmd);
8644
8938
  }
8645
8939
  return false;
8646
8940
  }
@@ -9515,7 +9809,7 @@ function canaryRecordById(id) {
9515
9809
  return null;
9516
9810
  }
9517
9811
  }
9518
- var import_crypto7, WRITE_TOOLS;
9812
+ var import_crypto7, WRITE_TOOLS, NETWORK_COMMAND_RE;
9519
9813
  var init_orchestrator = __esm({
9520
9814
  "src/auth/orchestrator.ts"() {
9521
9815
  "use strict";
@@ -9546,6 +9840,7 @@ var init_orchestrator = __esm({
9546
9840
  "notebook_edit",
9547
9841
  "notebookedit"
9548
9842
  ]);
9843
+ NETWORK_COMMAND_RE = new RegExp(`(?<![.\\w-])(${[...NET_BINARIES].join("|")})\\b`);
9549
9844
  }
9550
9845
  });
9551
9846
 
@@ -12609,9 +12904,10 @@ async function ensurePricingLoaded() {
12609
12904
  memCacheAt = Date.now();
12610
12905
  lookupCache.clear();
12611
12906
  }
12612
- function pricingFor(model) {
12907
+ function pricingFor(model, options = {}) {
12613
12908
  const norm = normalizeModel(model);
12614
- const cached = lookupCache.get(norm);
12909
+ const lookupKey = options.exact ? `exact:${norm}` : norm;
12910
+ const cached = lookupCache.get(lookupKey);
12615
12911
  if (cached !== void 0) return cached;
12616
12912
  if (memCache === null && !diskChecked) {
12617
12913
  diskChecked = true;
@@ -12631,6 +12927,7 @@ function pricingFor(model) {
12631
12927
  resolved = exact;
12632
12928
  break;
12633
12929
  }
12930
+ if (options.exact) continue;
12634
12931
  let best = null;
12635
12932
  for (const key of Object.keys(source)) {
12636
12933
  if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
@@ -12642,7 +12939,7 @@ function pricingFor(model) {
12642
12939
  break;
12643
12940
  }
12644
12941
  }
12645
- lookupCache.set(norm, resolved);
12942
+ lookupCache.set(lookupKey, resolved);
12646
12943
  return resolved;
12647
12944
  }
12648
12945
  var import_fs18, import_path20, import_os17, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, diskChecked, lookupCache;
@@ -12679,6 +12976,18 @@ var init_litellm = __esm({
12679
12976
  "gpt-5": [125e-8, 1e-5, 0, 125e-9],
12680
12977
  "gpt-5-codex": [125e-8, 1e-5, 0, 125e-9],
12681
12978
  "gpt-5-mini": [25e-8, 2e-6, 0, 25e-9],
12979
+ // Codex offline rates checked against official OpenAI model pages, 2026-09-11.
12980
+ "gpt-5.1-codex": [125e-8, 1e-5, 0, 125e-9],
12981
+ "gpt-5.1-codex-max": [125e-8, 1e-5, 0, 125e-9],
12982
+ "gpt-5.1-codex-mini": [25e-8, 2e-6, 0, 25e-9],
12983
+ "gpt-5.2-codex": [175e-8, 14e-6, 0, 175e-9],
12984
+ "gpt-5.3-codex": [175e-8, 14e-6, 0, 175e-9],
12985
+ "gpt-5.4": [25e-7, 15e-6, 0, 25e-8],
12986
+ "gpt-5.4-mini": [75e-8, 45e-7, 0, 75e-9],
12987
+ "gpt-5.5": [5e-6, 3e-5, 0, 5e-7],
12988
+ "gpt-5.6-sol": [4e-6, 2e-5, 5e-6, 4e-7],
12989
+ "gpt-5.6-terra": [2e-6, 12e-6, 25e-7, 2e-7],
12990
+ "gpt-6-astra": [1e-5, 5e-5, 125e-7, 1e-6],
12682
12991
  o3: [2e-6, 8e-6, 0, 5e-7],
12683
12992
  "o4-mini": [11e-7, 44e-7, 0, 275e-9],
12684
12993
  // Google. Values copied from the live LiteLLM table (verified 2026-06-14)
@@ -12847,103 +13156,259 @@ var init_cost_gemini = __esm({
12847
13156
 
12848
13157
  // src/cost-codex.ts
12849
13158
  function codexSessionsDir() {
12850
- return import_path22.default.join(import_os19.default.homedir(), ".codex", "sessions");
13159
+ return import_path22.default.join(process.env.CODEX_HOME?.trim() || import_path22.default.join(import_os19.default.homedir(), ".codex"), "sessions");
12851
13160
  }
12852
13161
  function codexPriceFor(model) {
12853
- return pricingFor(model) ?? CODEX_FALLBACK;
13162
+ return pricingFor(codexModel(model), { exact: true }) ?? CODEX_FALLBACK;
12854
13163
  }
12855
- function codexSessionCost(model, tokens) {
12856
- const nonCached = Math.max(0, tokens.input - tokens.cached);
12857
- const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
12858
- return nonCached * pin + tokens.cached * pcr + tokens.output * pout;
13164
+ function codexModel(model) {
13165
+ return normalizeModel(model.replace(/^openai\//i, "").replace(/-\d{4}-\d{2}-\d{2}$/, ""));
12859
13166
  }
12860
- function listCodexSessionFiles(base) {
12861
- const out = [];
12862
- for (const y of safeReaddir2(base)) {
12863
- const yp = import_path22.default.join(base, y);
12864
- if (!isDir2(yp)) continue;
12865
- for (const m of safeReaddir2(yp)) {
12866
- const mp = import_path22.default.join(yp, m);
12867
- if (!isDir2(mp)) continue;
12868
- for (const d of safeReaddir2(mp)) {
12869
- const dp = import_path22.default.join(mp, d);
12870
- if (!isDir2(dp)) continue;
12871
- for (const f of safeReaddir2(dp)) {
12872
- if (f.endsWith(".jsonl")) out.push(import_path22.default.join(dp, f));
12873
- }
13167
+ function addTokens(previous, delta) {
13168
+ return {
13169
+ input: (previous?.input ?? 0) + delta.input,
13170
+ cached: (previous?.cached ?? 0) + delta.cached,
13171
+ output: (previous?.output ?? 0) + delta.output,
13172
+ cacheWrite: (previous?.cacheWrite ?? 0) + delta.cacheWrite
13173
+ };
13174
+ }
13175
+ function codexSessionCost(model, tokens, request2) {
13176
+ const input = tokenNumber(tokens.input);
13177
+ const cached = Math.min(input, tokenNumber(tokens.cached));
13178
+ const written = Math.min(input - cached, tokenNumber(tokens.cacheWrite));
13179
+ const [pin, pout, pcw, pcr] = codexPriceFor(model || "gpt-5");
13180
+ const longContext = request2 && request2.inputTokens > 272e3 && ["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra"].includes(
13181
+ codexModel(model)
13182
+ );
13183
+ const inputMultiplier = longContext ? 2 : 1;
13184
+ const outputMultiplier = longContext ? 1.5 : 1;
13185
+ const tierMultiplier = request2?.serviceTier === "flex" ? 0.5 : codexModel(model) === "gpt-6-astra" && (request2?.serviceTier === "fast" || request2?.serviceTier === "priority") ? 2 : 1;
13186
+ return (((input - cached - written) * pin + cached * pcr + written * (pcw || pin)) * inputMultiplier + tokenNumber(tokens.output) * pout * outputMultiplier) * tierMultiplier;
13187
+ }
13188
+ function statAndFirstLine(file) {
13189
+ const CAP = 4 * 1024 * 1024;
13190
+ const CHUNK = 64 * 1024;
13191
+ const fd = import_fs20.default.openSync(file, "r");
13192
+ try {
13193
+ const stat = import_fs20.default.fstatSync(fd);
13194
+ const limit = Math.min(stat.size, CAP);
13195
+ const parts = [];
13196
+ for (let pos = 0; pos < limit; pos += CHUNK) {
13197
+ const buf = Buffer.alloc(Math.min(CHUNK, limit - pos));
13198
+ const read2 = import_fs20.default.readSync(fd, buf, 0, buf.length, pos);
13199
+ if (read2 <= 0) break;
13200
+ const slice = buf.subarray(0, read2);
13201
+ const nl = slice.indexOf(10);
13202
+ parts.push(nl >= 0 ? slice.subarray(0, nl) : slice);
13203
+ if (nl >= 0) break;
13204
+ }
13205
+ return { stat, first: Buffer.concat(parts).toString("utf8") };
13206
+ } finally {
13207
+ import_fs20.default.closeSync(fd);
13208
+ }
13209
+ }
13210
+ function listCodexSessionFiles(base = codexSessionsDir()) {
13211
+ const files = [];
13212
+ const walk = (dir) => {
13213
+ try {
13214
+ for (const entry of import_fs20.default.readdirSync(dir, { withFileTypes: true })) {
13215
+ const file = import_path22.default.join(dir, entry.name);
13216
+ if (entry.isDirectory()) walk(file);
13217
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(file);
13218
+ }
13219
+ } catch {
13220
+ }
13221
+ };
13222
+ walk(base);
13223
+ if (import_path22.default.basename(base) === "sessions") walk(import_path22.default.join(import_path22.default.dirname(base), "archived_sessions"));
13224
+ const sessions = /* @__PURE__ */ new Map();
13225
+ for (const file of files.sort()) {
13226
+ try {
13227
+ const { stat, first: head } = statAndFirstLine(file);
13228
+ let id = "";
13229
+ try {
13230
+ const first = JSON.parse(head);
13231
+ if (first?.type === "session_meta" && typeof first.payload?.id === "string")
13232
+ id = first.payload.id;
13233
+ } catch {
12874
13234
  }
13235
+ const key = id ? `session:${id}` : `file:${file}`;
13236
+ const prior = sessions.get(key);
13237
+ if (!prior || stat.mtimeMs > prior.mtime || stat.mtimeMs === prior.mtime && stat.size > prior.size) {
13238
+ sessions.set(key, { file, mtime: stat.mtimeMs, size: stat.size });
13239
+ }
13240
+ } catch {
12875
13241
  }
12876
13242
  }
12877
- return out;
13243
+ return [...sessions.values()].map((s) => s.file);
12878
13244
  }
12879
- function safeReaddir2(dir) {
12880
- try {
12881
- return import_fs20.default.readdirSync(dir);
12882
- } catch {
12883
- return [];
12884
- }
13245
+ function record(value) {
13246
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
12885
13247
  }
12886
- function isDir2(p) {
12887
- try {
12888
- return import_fs20.default.statSync(p).isDirectory();
12889
- } catch {
12890
- return false;
13248
+ function tokenNumber(value) {
13249
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
13250
+ }
13251
+ function usage(value, fallback) {
13252
+ const u = record(value);
13253
+ if (!["input_tokens", "output_tokens"].some((k) => typeof u[k] === "number")) return null;
13254
+ for (const key of [
13255
+ "input_tokens",
13256
+ "cached_input_tokens",
13257
+ "cache_read_input_tokens",
13258
+ "output_tokens",
13259
+ "cache_write_input_tokens"
13260
+ ]) {
13261
+ if (u[key] !== void 0 && (typeof u[key] !== "number" || !Number.isFinite(u[key]) || u[key] < 0))
13262
+ return null;
12891
13263
  }
13264
+ return {
13265
+ input: tokenNumber(u.input_tokens ?? fallback?.input),
13266
+ cached: tokenNumber(u.cached_input_tokens ?? u.cache_read_input_tokens ?? fallback?.cached),
13267
+ output: tokenNumber(u.output_tokens ?? fallback?.output),
13268
+ cacheWrite: tokenNumber(u.cache_write_input_tokens ?? fallback?.cacheWrite)
13269
+ };
12892
13270
  }
12893
- function parseCodexSession(lines) {
12894
- let sessionStart = "";
12895
- let runId = "";
12896
- let cwd = "";
12897
- let model = "";
12898
- let input = 0;
12899
- let cached = 0;
12900
- let output = 0;
12901
- let sawUsage = false;
13271
+ function timestamp(value) {
13272
+ return typeof value === "string" && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : "";
13273
+ }
13274
+ function parseCodexUsage(lines) {
13275
+ const result = {
13276
+ events: [],
13277
+ sessionStart: "",
13278
+ runId: "",
13279
+ workingDir: "",
13280
+ legacyModels: []
13281
+ };
13282
+ let model = "gpt-5";
13283
+ let serviceTier;
13284
+ let previous = null;
13285
+ const legacyModels = /* @__PURE__ */ new Set();
13286
+ const seenStandalone = /* @__PURE__ */ new Set();
12902
13287
  for (const raw of lines) {
12903
- if (!raw.trim()) continue;
12904
13288
  let entry;
12905
13289
  try {
12906
- entry = JSON.parse(raw);
13290
+ entry = record(JSON.parse(raw));
12907
13291
  } catch {
12908
13292
  continue;
12909
13293
  }
12910
- const p = entry.payload ?? {};
13294
+ const p = record(entry.payload);
12911
13295
  if (entry.type === "session_meta") {
12912
- if (!sessionStart && typeof p["timestamp"] === "string") sessionStart = p["timestamp"];
12913
- if (!runId && typeof p["id"] === "string") runId = p["id"];
12914
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
13296
+ result.sessionStart ||= timestamp(p.timestamp ?? entry.timestamp);
13297
+ if (!result.runId && typeof p.id === "string") result.runId = p.id;
13298
+ if (!result.workingDir && typeof p.cwd === "string") result.workingDir = p.cwd;
12915
13299
  continue;
12916
13300
  }
12917
13301
  if (entry.type === "turn_context") {
12918
- if (typeof p["model"] === "string") model = p["model"];
12919
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
13302
+ if (typeof p.model === "string" && p.model) {
13303
+ model = p.model;
13304
+ legacyModels.add(normalizeModel(model));
13305
+ }
13306
+ if (!result.workingDir && typeof p.cwd === "string") result.workingDir = p.cwd;
13307
+ serviceTier = typeof p.service_tier === "string" ? p.service_tier : void 0;
12920
13308
  continue;
12921
13309
  }
12922
- if (entry.type === "event_msg" && p["type"] === "token_count") {
12923
- const info = p["info"] ?? {};
12924
- const usage = info["total_token_usage"] ?? {};
12925
- if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
12926
- if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
12927
- if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
12928
- sawUsage = true;
13310
+ if (entry.type !== "event_msg" || p.type !== "token_count") continue;
13311
+ const info = record(p.info);
13312
+ const total = usage(info.total_token_usage, previous);
13313
+ const last = usage(info.last_token_usage);
13314
+ if (!total && !last) continue;
13315
+ const eventModel = [info.model, info.model_name, p.model].find(
13316
+ (v) => typeof v === "string" && v
13317
+ );
13318
+ if (typeof eventModel === "string") model = eventModel;
13319
+ let delta;
13320
+ if (total) {
13321
+ if (previous && Object.keys(total).every(
13322
+ (k) => total[k] === previous[k]
13323
+ ))
13324
+ continue;
13325
+ const reset = previous && (total.input < previous.input || total.output < previous.output);
13326
+ delta = reset ? last ?? total : {
13327
+ input: Math.max(0, total.input - (previous?.input ?? 0)),
13328
+ cached: Math.max(0, total.cached - (previous?.cached ?? 0)),
13329
+ output: Math.max(0, total.output - (previous?.output ?? 0)),
13330
+ cacheWrite: Math.max(0, total.cacheWrite - (previous?.cacheWrite ?? 0))
13331
+ };
13332
+ previous = total;
13333
+ } else {
13334
+ delta = last;
13335
+ const key = JSON.stringify([entry.timestamp, model, delta]);
13336
+ if (entry.timestamp && seenStandalone.has(key)) continue;
13337
+ if (entry.timestamp) seenStandalone.add(key);
13338
+ previous = addTokens(previous, delta);
13339
+ }
13340
+ if (delta.input === 0 && delta.output === 0) continue;
13341
+ const ts = timestamp(entry.timestamp) || result.sessionStart;
13342
+ if (!ts) continue;
13343
+ const cached = Math.min(delta.input, delta.cached);
13344
+ const written = Math.min(delta.input - cached, delta.cacheWrite);
13345
+ result.events.push({
13346
+ timestamp: ts,
13347
+ date: ts.slice(0, 10),
13348
+ model: normalizeModel(model),
13349
+ workingDir: result.workingDir,
13350
+ runId: result.runId,
13351
+ costUSD: codexSessionCost(model, delta, {
13352
+ inputTokens: last?.input ?? delta.input,
13353
+ serviceTier: typeof info.service_tier === "string" ? info.service_tier : serviceTier
13354
+ }),
13355
+ inputTokens: delta.input - cached - written,
13356
+ outputTokens: delta.output,
13357
+ cacheReadTokens: cached,
13358
+ cacheWriteTokens: written
13359
+ });
13360
+ }
13361
+ result.legacyModels = [...legacyModels.size ? legacyModels : ["gpt-5"]];
13362
+ return result;
13363
+ }
13364
+ function codexUsageInWindow(usage2, start, end) {
13365
+ return usage2.events.filter(
13366
+ (e) => (!start || Date.parse(e.timestamp) >= start.getTime()) && (!end || Date.parse(e.timestamp) <= end.getTime())
13367
+ );
13368
+ }
13369
+ function parseCodexSession(lines) {
13370
+ const parsed = parseCodexUsage(lines);
13371
+ if (!parsed.events.length) return [];
13372
+ const rows = /* @__PURE__ */ new Map();
13373
+ if (parsed.sessionStart) {
13374
+ for (const model of parsed.legacyModels) {
13375
+ rows.set(`${parsed.sessionStart.slice(0, 10)}::${model}`, {
13376
+ date: parsed.sessionStart.slice(0, 10),
13377
+ model,
13378
+ workingDir: parsed.workingDir,
13379
+ runId: parsed.runId,
13380
+ costUSD: 0,
13381
+ inputTokens: 0,
13382
+ outputTokens: 0,
13383
+ cacheReadTokens: 0,
13384
+ cacheWriteTokens: 0
13385
+ });
13386
+ }
13387
+ }
13388
+ for (const event of parsed.events) {
13389
+ const e = {
13390
+ date: event.date,
13391
+ model: event.model,
13392
+ workingDir: event.workingDir,
13393
+ runId: event.runId,
13394
+ costUSD: event.costUSD,
13395
+ inputTokens: event.inputTokens,
13396
+ outputTokens: event.outputTokens,
13397
+ cacheReadTokens: event.cacheReadTokens,
13398
+ cacheWriteTokens: event.cacheWriteTokens
13399
+ };
13400
+ const key = `${e.date}::${e.model}`;
13401
+ const prev = rows.get(key);
13402
+ if (!prev) rows.set(key, { ...e });
13403
+ else {
13404
+ prev.costUSD += e.costUSD;
13405
+ prev.inputTokens += e.inputTokens;
13406
+ prev.outputTokens += e.outputTokens;
13407
+ prev.cacheReadTokens += e.cacheReadTokens;
13408
+ prev.cacheWriteTokens += e.cacheWriteTokens;
12929
13409
  }
12930
13410
  }
12931
- if (!sessionStart || !sawUsage) return null;
12932
- const nonCached = Math.max(0, input - cached);
12933
- if (nonCached === 0 && output === 0 && cached === 0) return null;
12934
- const norm = normalizeModel(model || "gpt-5");
12935
- const costUSD = codexSessionCost(model, { input, cached, output });
12936
- return {
12937
- date: sessionStart.slice(0, 10),
12938
- model: norm,
12939
- workingDir: cwd,
12940
- runId,
12941
- costUSD,
12942
- inputTokens: nonCached,
12943
- outputTokens: output,
12944
- cacheReadTokens: cached,
12945
- cacheWriteTokens: 0
12946
- };
13411
+ return [...rows.values()];
12947
13412
  }
12948
13413
  var import_fs20, import_os19, import_path22, CODEX_FALLBACK, codexSource;
12949
13414
  var init_cost_codex = __esm({
@@ -12956,43 +13421,17 @@ var init_cost_codex = __esm({
12956
13421
  CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
12957
13422
  codexSource = {
12958
13423
  id: "codex",
12959
- available() {
12960
- try {
12961
- return import_fs20.default.existsSync(codexSessionsDir());
12962
- } catch {
12963
- return false;
12964
- }
12965
- },
13424
+ available: () => import_fs20.default.existsSync(codexSessionsDir()) || import_fs20.default.existsSync(import_path22.default.join(import_path22.default.dirname(codexSessionsDir()), "archived_sessions")),
12966
13425
  collect(sinceMs) {
12967
- const base = codexSessionsDir();
12968
- const combined = /* @__PURE__ */ new Map();
12969
- for (const file of listCodexSessionFiles(base)) {
13426
+ const entries = [];
13427
+ for (const file of listCodexSessionFiles()) {
12970
13428
  try {
12971
13429
  if (sinceMs !== void 0 && import_fs20.default.statSync(file).mtimeMs < sinceMs) continue;
13430
+ entries.push(...parseCodexSession(import_fs20.default.readFileSync(file, "utf8").split("\n")));
12972
13431
  } catch {
12973
- continue;
12974
- }
12975
- let content;
12976
- try {
12977
- content = import_fs20.default.readFileSync(file, "utf8");
12978
- } catch {
12979
- continue;
12980
- }
12981
- const e = parseCodexSession(content.split("\n"));
12982
- if (!e) continue;
12983
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
12984
- const prev = combined.get(key);
12985
- if (prev) {
12986
- prev.costUSD += e.costUSD;
12987
- prev.inputTokens += e.inputTokens;
12988
- prev.outputTokens += e.outputTokens;
12989
- prev.cacheReadTokens += e.cacheReadTokens;
12990
- prev.cacheWriteTokens += e.cacheWriteTokens;
12991
- } else {
12992
- combined.set(key, { ...e });
12993
13432
  }
12994
13433
  }
12995
- return [...combined.values()];
13434
+ return entries;
12996
13435
  }
12997
13436
  };
12998
13437
  }
@@ -13002,7 +13441,7 @@ var init_cost_codex = __esm({
13002
13441
  function copilotSessionsDir() {
13003
13442
  return import_path23.default.join(import_os20.default.homedir(), ".copilot", "session-state");
13004
13443
  }
13005
- function safeReaddir3(dir) {
13444
+ function safeReaddir2(dir) {
13006
13445
  try {
13007
13446
  return import_fs21.default.readdirSync(dir);
13008
13447
  } catch {
@@ -13091,7 +13530,7 @@ var init_cost_copilot = __esm({
13091
13530
  collect(sinceMs) {
13092
13531
  const base = copilotSessionsDir();
13093
13532
  const combined = /* @__PURE__ */ new Map();
13094
- for (const sid of safeReaddir3(base)) {
13533
+ for (const sid of safeReaddir2(base)) {
13095
13534
  const file = import_path23.default.join(base, sid, "events.jsonl");
13096
13535
  try {
13097
13536
  if (sinceMs !== void 0 && import_fs21.default.statSync(file).mtimeMs < sinceMs) continue;
@@ -13705,8 +14144,8 @@ function originForRule(ruleName, sections, enabled) {
13705
14144
  }
13706
14145
  return "";
13707
14146
  }
13708
- function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
13709
- const t = new Date(timestamp).getTime();
14147
+ function relativeDate(timestamp2, now = /* @__PURE__ */ new Date()) {
14148
+ const t = new Date(timestamp2).getTime();
13710
14149
  if (Number.isNaN(t)) return "?";
13711
14150
  const days = Math.floor((now.getTime() - t) / 864e5);
13712
14151
  if (days < 1) return "today";
@@ -13817,7 +14256,7 @@ function readPreviousScan(opts = {}) {
13817
14256
  return null;
13818
14257
  }
13819
14258
  }
13820
- function appendScanHistory(record, opts = {}) {
14259
+ function appendScanHistory(record2, opts = {}) {
13821
14260
  const filePath = opts.path ?? defaultHistoryPath();
13822
14261
  const cap = opts.cap ?? SCAN_HISTORY_CAP;
13823
14262
  try {
@@ -13832,7 +14271,7 @@ function appendScanHistory(record, opts = {}) {
13832
14271
  } catch {
13833
14272
  }
13834
14273
  }
13835
- history.push(record);
14274
+ history.push(record2);
13836
14275
  if (history.length > cap) {
13837
14276
  history = history.slice(history.length - cap);
13838
14277
  }
@@ -13893,17 +14332,17 @@ function parseJSONLFile(filePath, fallbackWorkingDir) {
13893
14332
  if (row["type"] !== "assistant") continue;
13894
14333
  const msg = row["message"];
13895
14334
  if (!msg?.["usage"] || typeof msg["model"] !== "string") continue;
13896
- const usage = msg["usage"];
14335
+ const usage2 = msg["usage"];
13897
14336
  const model = msg["model"];
13898
- const timestamp = row["timestamp"];
13899
- if (typeof timestamp !== "string" || timestamp.length < 10) continue;
13900
- const date = timestamp.slice(0, 10);
14337
+ const timestamp2 = row["timestamp"];
14338
+ if (typeof timestamp2 !== "string" || timestamp2.length < 10) continue;
14339
+ const date = timestamp2.slice(0, 10);
13901
14340
  const p = pricingFor(model);
13902
14341
  if (!p) continue;
13903
- const inp = Number(usage["input_tokens"] ?? 0);
13904
- const out = Number(usage["output_tokens"] ?? 0);
13905
- const cw = Number(usage["cache_creation_input_tokens"] ?? 0);
13906
- const cr = Number(usage["cache_read_input_tokens"] ?? 0);
14342
+ const inp = Number(usage2["input_tokens"] ?? 0);
14343
+ const out = Number(usage2["output_tokens"] ?? 0);
14344
+ const cw = Number(usage2["cache_creation_input_tokens"] ?? 0);
14345
+ const cr = Number(usage2["cache_read_input_tokens"] ?? 0);
13907
14346
  const cost = inp * p[0] + out * p[1] + cw * p[2] + cr * p[3];
13908
14347
  const rowCwd = typeof row["cwd"] === "string" ? row["cwd"] : null;
13909
14348
  const workingDir = rowCwd && rowCwd.startsWith("/") ? rowCwd : fallbackWorkingDir;
@@ -14931,7 +15370,7 @@ function safeCanaryScanValues() {
14931
15370
  return [];
14932
15371
  }
14933
15372
  }
14934
- function recordCanaries(scanned, toolName, timestamp, projLabel, sessionId, agent, result, dedup, values) {
15373
+ function recordCanaries(scanned, toolName, timestamp2, projLabel, sessionId, agent, result, dedup, values) {
14935
15374
  if (values.length === 0) return [];
14936
15375
  let pool = [...values];
14937
15376
  const matched = [];
@@ -14945,8 +15384,8 @@ function recordCanaries(scanned, toolName, timestamp, projLabel, sessionId, agen
14945
15384
  const existing = dedup.canaryIndex.get(key);
14946
15385
  if (existing) {
14947
15386
  existing.count++;
14948
- if (timestamp && (!existing.timestamp || timestamp < existing.timestamp)) {
14949
- existing.timestamp = timestamp;
15387
+ if (timestamp2 && (!existing.timestamp || timestamp2 < existing.timestamp)) {
15388
+ existing.timestamp = timestamp2;
14950
15389
  }
14951
15390
  continue;
14952
15391
  }
@@ -14959,7 +15398,7 @@ function recordCanaries(scanned, toolName, timestamp, projLabel, sessionId, agen
14959
15398
  view: hit.view,
14960
15399
  retired: hit.retired,
14961
15400
  toolName,
14962
- timestamp,
15401
+ timestamp: timestamp2,
14963
15402
  project: projLabel,
14964
15403
  sessionId,
14965
15404
  agent,
@@ -14991,7 +15430,7 @@ function scrubDecoys(subject, values) {
14991
15430
  };
14992
15431
  return walk(subject, 0);
14993
15432
  }
14994
- function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
15433
+ function pushFsOpAstFinding(command, toolName, input, timestamp2, projLabel, sessionId, agent, result, dedup) {
14995
15434
  const fsVerdict = analyzeFsOperation(command);
14996
15435
  if (!fsVerdict) return false;
14997
15436
  const synthRule = {
@@ -15021,7 +15460,7 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
15021
15460
  source: synthSource,
15022
15461
  toolName,
15023
15462
  input,
15024
- timestamp,
15463
+ timestamp: timestamp2,
15025
15464
  project: projLabel,
15026
15465
  sessionId,
15027
15466
  agent
@@ -15029,9 +15468,9 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
15029
15468
  }
15030
15469
  return true;
15031
15470
  }
15032
- function isStaleFinding(timestamp, now = Date.now()) {
15033
- if (!timestamp) return false;
15034
- const t = Date.parse(timestamp);
15471
+ function isStaleFinding(timestamp2, now = Date.now()) {
15472
+ if (!timestamp2) return false;
15473
+ const t = Date.parse(timestamp2);
15035
15474
  if (Number.isNaN(t)) return false;
15036
15475
  const ageDays = (now - t) / 864e5;
15037
15476
  return ageDays > STALE_AGE_DAYS;
@@ -15179,37 +15618,7 @@ function countScanFiles() {
15179
15618
  } catch {
15180
15619
  }
15181
15620
  }
15182
- const codexDir = import_path29.default.join(import_os26.default.homedir(), ".codex", "sessions");
15183
- if (import_fs27.default.existsSync(codexDir)) {
15184
- try {
15185
- for (const year of import_fs27.default.readdirSync(codexDir)) {
15186
- const yp = import_path29.default.join(codexDir, year);
15187
- try {
15188
- if (!import_fs27.default.statSync(yp).isDirectory()) continue;
15189
- for (const month of import_fs27.default.readdirSync(yp)) {
15190
- const mp = import_path29.default.join(yp, month);
15191
- try {
15192
- if (!import_fs27.default.statSync(mp).isDirectory()) continue;
15193
- for (const day of import_fs27.default.readdirSync(mp)) {
15194
- const dp = import_path29.default.join(mp, day);
15195
- try {
15196
- if (!import_fs27.default.statSync(dp).isDirectory()) continue;
15197
- total += listSessionFiles(dp).length;
15198
- } catch {
15199
- continue;
15200
- }
15201
- }
15202
- } catch {
15203
- continue;
15204
- }
15205
- }
15206
- } catch {
15207
- continue;
15208
- }
15209
- }
15210
- } catch {
15211
- }
15212
- }
15621
+ total += listCodexSessionFiles().length;
15213
15622
  return total;
15214
15623
  }
15215
15624
  function renderProgressBar(done, total, lines) {
@@ -15332,12 +15741,12 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
15332
15741
  }
15333
15742
  continue;
15334
15743
  }
15335
- const usage = entry.message?.usage;
15744
+ const usage2 = entry.message?.usage;
15336
15745
  const model = entry.message?.model;
15337
- if (usage && model) {
15746
+ if (usage2 && model) {
15338
15747
  const p = claudeModelPrice(model);
15339
15748
  if (p) {
15340
- const rowCost = (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
15749
+ const rowCost = (usage2.input_tokens ?? 0) * p.i + (usage2.output_tokens ?? 0) * p.o + (usage2.cache_creation_input_tokens ?? 0) * p.cw + (usage2.cache_read_input_tokens ?? 0) * p.cr;
15341
15750
  result.totalCostUSD += rowCost;
15342
15751
  session.costUSD += rowCost;
15343
15752
  }
@@ -15871,15 +16280,15 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15871
16280
  } catch {
15872
16281
  continue;
15873
16282
  }
15874
- const timestamp = step.created_at ?? "";
15875
- if (startDate && timestamp && new Date(timestamp) < startDate) continue;
16283
+ const timestamp2 = step.created_at ?? "";
16284
+ if (startDate && timestamp2 && new Date(timestamp2) < startDate) continue;
15876
16285
  if (step.type === "USER_INPUT") {
15877
16286
  const text = typeof step.content === "string" ? step.content : "";
15878
16287
  if (text) {
15879
16288
  const decoysHere5 = recordCanaries(
15880
16289
  { text },
15881
16290
  "user-prompt",
15882
- timestamp,
16291
+ timestamp2,
15883
16292
  projLabel,
15884
16293
  sessionId,
15885
16294
  "antigravity",
@@ -15896,7 +16305,7 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15896
16305
  patternName: dlpMatch.patternName,
15897
16306
  redactedSample: dlpMatch.redactedSample,
15898
16307
  toolName: "user-prompt",
15899
- timestamp,
16308
+ timestamp: timestamp2,
15900
16309
  project: projLabel,
15901
16310
  sessionId,
15902
16311
  agent: "antigravity"
@@ -15907,16 +16316,16 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15907
16316
  continue;
15908
16317
  }
15909
16318
  if (!Array.isArray(step.tool_calls) || step.tool_calls.length === 0) continue;
15910
- if (timestamp) {
15911
- if (!result.firstDate || timestamp < result.firstDate) result.firstDate = timestamp;
15912
- if (!result.lastDate || timestamp > result.lastDate) result.lastDate = timestamp;
16319
+ if (timestamp2) {
16320
+ if (!result.firstDate || timestamp2 < result.firstDate) result.firstDate = timestamp2;
16321
+ if (!result.lastDate || timestamp2 > result.lastDate) result.lastDate = timestamp2;
15913
16322
  }
15914
16323
  for (const tc of step.tool_calls) {
15915
16324
  result.totalToolCalls++;
15916
16325
  const toolName = tc.name ?? "";
15917
16326
  const toolNameLower = toolName.toLowerCase();
15918
16327
  const input = canonicalToolInput(toolName, tc.args ?? {});
15919
- sessionCalls.push({ toolName, input, timestamp });
16328
+ sessionCalls.push({ toolName, input, timestamp: timestamp2 });
15920
16329
  const isShellTool = toolNameLower === "run_command";
15921
16330
  if (isShellTool) {
15922
16331
  result.bashCalls++;
@@ -15931,7 +16340,7 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15931
16340
  const decoysHere6 = recordCanaries(
15932
16341
  input,
15933
16342
  toolName,
15934
- timestamp,
16343
+ timestamp2,
15935
16344
  projLabel,
15936
16345
  sessionId,
15937
16346
  "antigravity",
@@ -15948,7 +16357,7 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15948
16357
  patternName: dlpMatch.patternName,
15949
16358
  redactedSample: dlpMatch.redactedSample,
15950
16359
  toolName,
15951
- timestamp,
16360
+ timestamp: timestamp2,
15952
16361
  project: projLabel,
15953
16362
  sessionId,
15954
16363
  agent: "antigravity"
@@ -15961,7 +16370,7 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15961
16370
  String(input.command ?? ""),
15962
16371
  toolName,
15963
16372
  input,
15964
- timestamp,
16373
+ timestamp2,
15965
16374
  projLabel,
15966
16375
  sessionId,
15967
16376
  "antigravity",
@@ -15985,7 +16394,7 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
15985
16394
  source,
15986
16395
  toolName,
15987
16396
  input,
15988
- timestamp,
16397
+ timestamp: timestamp2,
15989
16398
  project: projLabel,
15990
16399
  sessionId,
15991
16400
  agent: "antigravity"
@@ -16017,7 +16426,7 @@ function scanAntigravityHistory(startDate, onProgress, onLine) {
16017
16426
  },
16018
16427
  toolName,
16019
16428
  input,
16020
- timestamp,
16429
+ timestamp: timestamp2,
16021
16430
  project: projLabel,
16022
16431
  sessionId,
16023
16432
  agent: "antigravity"
@@ -16087,7 +16496,7 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16087
16496
  } catch {
16088
16497
  continue;
16089
16498
  }
16090
- const timestamp = ev.timestamp ?? "";
16499
+ const timestamp2 = ev.timestamp ?? "";
16091
16500
  if (ev.type === "session.start") {
16092
16501
  const cwd = ev.data?.context?.cwd;
16093
16502
  if (typeof cwd === "string" && cwd) {
@@ -16095,14 +16504,14 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16095
16504
  }
16096
16505
  continue;
16097
16506
  }
16098
- if (startDate && timestamp && new Date(timestamp) < startDate) continue;
16507
+ if (startDate && timestamp2 && new Date(timestamp2) < startDate) continue;
16099
16508
  if (ev.type === "user.message") {
16100
16509
  const text = ev.data?.content ?? ev.data?.text ?? "";
16101
16510
  if (typeof text === "string" && text) {
16102
16511
  const decoysHere7 = recordCanaries(
16103
16512
  { text },
16104
16513
  "user-prompt",
16105
- timestamp,
16514
+ timestamp2,
16106
16515
  projLabel,
16107
16516
  sessionId,
16108
16517
  "copilot",
@@ -16119,7 +16528,7 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16119
16528
  patternName: dlpMatch2.patternName,
16120
16529
  redactedSample: dlpMatch2.redactedSample,
16121
16530
  toolName: "user-prompt",
16122
- timestamp,
16531
+ timestamp: timestamp2,
16123
16532
  project: projLabel,
16124
16533
  sessionId,
16125
16534
  agent: "copilot"
@@ -16134,19 +16543,19 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16134
16543
  const toolNameLower = toolName.toLowerCase();
16135
16544
  const input = ev.data?.arguments ?? {};
16136
16545
  result.totalToolCalls++;
16137
- sessionCalls.push({ toolName, input, timestamp });
16546
+ sessionCalls.push({ toolName, input, timestamp: timestamp2 });
16138
16547
  const isShellTool = isShellShapedTool(toolNameLower, toolInspectionMap);
16139
16548
  if (isShellTool) result.bashCalls++;
16140
- if (timestamp) {
16141
- if (!result.firstDate || timestamp < result.firstDate) result.firstDate = timestamp;
16142
- if (!result.lastDate || timestamp > result.lastDate) result.lastDate = timestamp;
16549
+ if (timestamp2) {
16550
+ if (!result.firstDate || timestamp2 < result.firstDate) result.firstDate = timestamp2;
16551
+ if (!result.lastDate || timestamp2 > result.lastDate) result.lastDate = timestamp2;
16143
16552
  }
16144
16553
  const rawCmd = String(input.command ?? "").trimStart();
16145
16554
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
16146
16555
  const decoysHere8 = recordCanaries(
16147
16556
  input,
16148
16557
  toolName,
16149
- timestamp,
16558
+ timestamp2,
16150
16559
  projLabel,
16151
16560
  sessionId,
16152
16561
  "copilot",
@@ -16163,7 +16572,7 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16163
16572
  patternName: dlpMatch.patternName,
16164
16573
  redactedSample: dlpMatch.redactedSample,
16165
16574
  toolName,
16166
- timestamp,
16575
+ timestamp: timestamp2,
16167
16576
  project: projLabel,
16168
16577
  sessionId,
16169
16578
  agent: "copilot"
@@ -16176,7 +16585,7 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16176
16585
  String(input.command ?? ""),
16177
16586
  toolName,
16178
16587
  input,
16179
- timestamp,
16588
+ timestamp2,
16180
16589
  projLabel,
16181
16590
  sessionId,
16182
16591
  "copilot",
@@ -16199,7 +16608,7 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16199
16608
  source,
16200
16609
  toolName,
16201
16610
  input,
16202
- timestamp,
16611
+ timestamp: timestamp2,
16203
16612
  project: projLabel,
16204
16613
  sessionId,
16205
16614
  agent: "copilot"
@@ -16231,7 +16640,7 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16231
16640
  },
16232
16641
  toolName,
16233
16642
  input,
16234
- timestamp,
16643
+ timestamp: timestamp2,
16235
16644
  project: projLabel,
16236
16645
  sessionId,
16237
16646
  agent: "copilot"
@@ -16246,7 +16655,6 @@ function scanCopilotHistory(startDate, onProgress, onLine) {
16246
16655
  }
16247
16656
  function scanCodexHistory(startDate, onProgress, onLine) {
16248
16657
  const canaryVals = safeCanaryScanValues();
16249
- const sessionsBase = import_path29.default.join(import_os26.default.homedir(), ".codex", "sessions");
16250
16658
  const result = {
16251
16659
  filesScanned: 0,
16252
16660
  sessions: 0,
@@ -16263,39 +16671,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
16263
16671
  perSession: []
16264
16672
  };
16265
16673
  const dedup = emptyScanDedup();
16266
- if (!import_fs27.default.existsSync(sessionsBase)) return result;
16267
- const jsonlFiles = [];
16268
- try {
16269
- for (const year of import_fs27.default.readdirSync(sessionsBase)) {
16270
- const yearPath = import_path29.default.join(sessionsBase, year);
16271
- try {
16272
- if (!import_fs27.default.statSync(yearPath).isDirectory()) continue;
16273
- } catch {
16274
- continue;
16275
- }
16276
- for (const month of import_fs27.default.readdirSync(yearPath)) {
16277
- const monthPath = import_path29.default.join(yearPath, month);
16278
- try {
16279
- if (!import_fs27.default.statSync(monthPath).isDirectory()) continue;
16280
- } catch {
16281
- continue;
16282
- }
16283
- for (const day of import_fs27.default.readdirSync(monthPath)) {
16284
- const dayPath = import_path29.default.join(monthPath, day);
16285
- try {
16286
- if (!import_fs27.default.statSync(dayPath).isDirectory()) continue;
16287
- } catch {
16288
- continue;
16289
- }
16290
- for (const file of import_fs27.default.readdirSync(dayPath)) {
16291
- if (file.endsWith(".jsonl")) jsonlFiles.push(import_path29.default.join(dayPath, file));
16292
- }
16293
- }
16294
- }
16295
- }
16296
- } catch {
16297
- return result;
16298
- }
16674
+ const jsonlFiles = listCodexSessionFiles();
16299
16675
  const ruleSources = buildRuleSources();
16300
16676
  for (const filePath of jsonlFiles) {
16301
16677
  result.filesScanned++;
@@ -16311,10 +16687,6 @@ function scanCodexHistory(startDate, onProgress, onLine) {
16311
16687
  let projLabel = "";
16312
16688
  result.sessions++;
16313
16689
  const sessionCalls = [];
16314
- let lastTotalInput = 0;
16315
- let lastTotalCached = 0;
16316
- let lastTotalOutput = 0;
16317
- let model = "";
16318
16690
  for (const line of lines) {
16319
16691
  if (!line.trim()) continue;
16320
16692
  onLine?.();
@@ -16332,18 +16704,6 @@ function scanCodexHistory(startDate, onProgress, onLine) {
16332
16704
  projLabel = stripTerminalEscapes(cwd.replace(import_os26.default.homedir(), "~")).slice(0, 40);
16333
16705
  continue;
16334
16706
  }
16335
- if (entry.type === "turn_context" && typeof payload["model"] === "string") {
16336
- model = payload["model"];
16337
- continue;
16338
- }
16339
- if (entry.type === "event_msg" && payload["type"] === "token_count") {
16340
- const info = payload["info"];
16341
- const usage = info?.["total_token_usage"] ?? {};
16342
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16343
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16344
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16345
- continue;
16346
- }
16347
16707
  if (entry.type === "event_msg" && payload["type"] === "user_message") {
16348
16708
  const text = String(payload["message"] ?? "");
16349
16709
  if (text) {
@@ -16500,13 +16860,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
16500
16860
  }
16501
16861
  }
16502
16862
  }
16503
- const withinWindow = !startDate || startTime !== "" && new Date(startTime) >= startDate;
16504
- if (withinWindow) {
16505
- result.totalCostUSD += codexSessionCost(model, {
16506
- input: lastTotalInput,
16507
- cached: lastTotalCached,
16508
- output: lastTotalOutput
16509
- });
16863
+ for (const event of codexUsageInWindow(parseCodexUsage(lines), startDate)) {
16864
+ result.totalCostUSD += event.costUSD;
16510
16865
  }
16511
16866
  result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "codex"));
16512
16867
  }
@@ -17943,13 +18298,13 @@ var init_taint_store = __esm({
17943
18298
  */
17944
18299
  check(filePath) {
17945
18300
  const resolved = this._resolve(filePath);
17946
- const record = this.records.get(resolved);
17947
- if (!record) return null;
17948
- if (Date.now() > record.expiresAt) {
18301
+ const record2 = this.records.get(resolved);
18302
+ if (!record2) return null;
18303
+ if (Date.now() > record2.expiresAt) {
17949
18304
  this.records.delete(resolved);
17950
18305
  return null;
17951
18306
  }
17952
- return record;
18307
+ return record2;
17953
18308
  }
17954
18309
  /**
17955
18310
  * Propagate taint from sourcePath to destPath (e.g. cp, mv).
@@ -17970,8 +18325,8 @@ var init_taint_store = __esm({
17970
18325
  /** Remove all expired records. Called periodically by the daemon. */
17971
18326
  prune() {
17972
18327
  const now = Date.now();
17973
- for (const [key, record] of this.records) {
17974
- if (now > record.expiresAt) this.records.delete(key);
18328
+ for (const [key, record2] of this.records) {
18329
+ if (now > record2.expiresAt) this.records.delete(key);
17975
18330
  }
17976
18331
  }
17977
18332
  /** Return all non-expired taint records (for audit/debug). */
@@ -18010,13 +18365,13 @@ var init_taint_store = __esm({
18010
18365
  * Expired records are pruned on access. */
18011
18366
  check(sessionId) {
18012
18367
  if (!sessionId) return null;
18013
- const record = this.records.get(sessionId);
18014
- if (!record) return null;
18015
- if (Date.now() > record.expiresAt) {
18368
+ const record2 = this.records.get(sessionId);
18369
+ if (!record2) return null;
18370
+ if (Date.now() > record2.expiresAt) {
18016
18371
  this.records.delete(sessionId);
18017
18372
  return null;
18018
18373
  }
18019
- return record;
18374
+ return record2;
18020
18375
  }
18021
18376
  /** Clear a session's taint (e.g. the user resolved it). Returns true if a
18022
18377
  * record was actually removed (false if the session wasn't tainted). */
@@ -18031,8 +18386,8 @@ var init_taint_store = __esm({
18031
18386
  /** Remove all expired records. Called periodically by the daemon. */
18032
18387
  prune() {
18033
18388
  const now = Date.now();
18034
- for (const [key, record] of this.records) {
18035
- if (now > record.expiresAt) this.records.delete(key);
18389
+ for (const [key, record2] of this.records) {
18390
+ if (now > record2.expiresAt) this.records.delete(key);
18036
18391
  }
18037
18392
  }
18038
18393
  /** Remove all records. Used by tests to reset state between runs. */
@@ -22765,10 +23120,10 @@ data: ${JSON.stringify(item.data)}
22765
23120
  return res.end(JSON.stringify({ error: "all paths must be strings" }));
22766
23121
  }
22767
23122
  for (const p of body.paths) {
22768
- const record = taintStore.check(p);
22769
- if (record) {
23123
+ const record2 = taintStore.check(p);
23124
+ if (record2) {
22770
23125
  res.writeHead(200, { "Content-Type": "application/json" });
22771
- return res.end(JSON.stringify({ tainted: true, record }));
23126
+ return res.end(JSON.stringify({ tainted: true, record: record2 }));
22772
23127
  }
22773
23128
  }
22774
23129
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -22817,9 +23172,9 @@ data: ${JSON.stringify(item.data)}
22817
23172
  res.writeHead(400, { "Content-Type": "application/json" });
22818
23173
  return res.end(JSON.stringify({ error: "sessionId must be a string" }));
22819
23174
  }
22820
- const record = sessionTaintStore.check(body.sessionId);
23175
+ const record2 = sessionTaintStore.check(body.sessionId);
22821
23176
  res.writeHead(200, { "Content-Type": "application/json" });
22822
- return res.end(JSON.stringify(record ? { tainted: true, record } : { tainted: false }));
23177
+ return res.end(JSON.stringify(record2 ? { tainted: true, record: record2 } : { tainted: false }));
22823
23178
  } catch {
22824
23179
  res.writeHead(400).end();
22825
23180
  return;
@@ -28773,8 +29128,8 @@ var require_util2 = __commonJS({
28773
29128
  request2.headersList.append("origin", serializedOrigin, true);
28774
29129
  }
28775
29130
  }
28776
- function coarsenTime(timestamp, crossOriginIsolatedCapability) {
28777
- return timestamp;
29131
+ function coarsenTime(timestamp2, crossOriginIsolatedCapability) {
29132
+ return timestamp2;
28778
29133
  }
28779
29134
  function clampAndCoarsenConnectionTimingInfo(connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
28780
29135
  if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
@@ -38855,20 +39210,20 @@ var require_dns = __commonJS({
38855
39210
  return ip;
38856
39211
  }
38857
39212
  setRecords(origin, addresses) {
38858
- const timestamp = Date.now();
39213
+ const timestamp2 = Date.now();
38859
39214
  const records = { records: { 4: null, 6: null } };
38860
39215
  let minTTL = this.#maxTTL;
38861
- for (const record of addresses) {
38862
- record.timestamp = timestamp;
38863
- if (typeof record.ttl === "number") {
38864
- record.ttl = Math.min(record.ttl, this.#maxTTL);
38865
- minTTL = Math.min(minTTL, record.ttl);
39216
+ for (const record2 of addresses) {
39217
+ record2.timestamp = timestamp2;
39218
+ if (typeof record2.ttl === "number") {
39219
+ record2.ttl = Math.min(record2.ttl, this.#maxTTL);
39220
+ minTTL = Math.min(minTTL, record2.ttl);
38866
39221
  } else {
38867
- record.ttl = this.#maxTTL;
39222
+ record2.ttl = this.#maxTTL;
38868
39223
  }
38869
- const familyRecords = records.records[record.family] ?? { ips: [] };
38870
- familyRecords.ips.push(record);
38871
- records.records[record.family] = familyRecords;
39224
+ const familyRecords = records.records[record2.family] ?? { ips: [] };
39225
+ familyRecords.ips.push(record2);
39226
+ records.records[record2.family] = familyRecords;
38872
39227
  }
38873
39228
  this.storage.set(origin.hostname, records, { ttl: minTTL });
38874
39229
  }
@@ -53532,15 +53887,15 @@ var import_fs55 = __toESM(require("fs"));
53532
53887
  var import_path53 = __toESM(require("path"));
53533
53888
  init_decision();
53534
53889
  var import_os49 = __toESM(require("os"));
53535
- function formatRelativeTime(timestamp) {
53536
- const diff = Date.now() - new Date(timestamp).getTime();
53890
+ function formatRelativeTime(timestamp2) {
53891
+ const diff = Date.now() - new Date(timestamp2).getTime();
53537
53892
  const sec = Math.floor(diff / 1e3);
53538
53893
  if (sec < 60) return `${sec}s ago`;
53539
53894
  const min = Math.floor(sec / 60);
53540
53895
  if (min < 60) return `${min}m ago`;
53541
53896
  const hrs = Math.floor(min / 60);
53542
53897
  if (hrs < 24) return `${hrs}h ago`;
53543
- return new Date(timestamp).toLocaleDateString();
53898
+ return new Date(timestamp2).toLocaleDateString();
53544
53899
  }
53545
53900
  function registerAuditCommand(program2) {
53546
53901
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
@@ -53786,15 +54141,15 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
53786
54141
  if (!entry.timestamp) continue;
53787
54142
  const ts = new Date(entry.timestamp);
53788
54143
  if (ts < start || ts > end) continue;
53789
- const usage = entry.message?.usage;
54144
+ const usage2 = entry.message?.usage;
53790
54145
  const model = entry.message?.model;
53791
- if (!usage || !model) continue;
54146
+ if (!usage2 || !model) continue;
53792
54147
  const p = claudeModelPrice2(model);
53793
54148
  if (!p) continue;
53794
- const inp = usage.input_tokens ?? 0;
53795
- const out = usage.output_tokens ?? 0;
53796
- const cw = usage.cache_creation_input_tokens ?? 0;
53797
- const cr = usage.cache_read_input_tokens ?? 0;
54149
+ const inp = usage2.input_tokens ?? 0;
54150
+ const out = usage2.output_tokens ?? 0;
54151
+ const cw = usage2.cache_creation_input_tokens ?? 0;
54152
+ const cr = usage2.cache_read_input_tokens ?? 0;
53798
54153
  const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
53799
54154
  acc.total += cost;
53800
54155
  acc.inputTokens += inp;
@@ -53842,90 +54197,21 @@ function processCodexCostFile(filePath, start, end, acc) {
53842
54197
  } catch {
53843
54198
  return;
53844
54199
  }
53845
- let sessionStart = "";
53846
- let model = "";
53847
- let lastTotalInput = 0;
53848
- let lastTotalCached = 0;
53849
- let lastTotalOutput = 0;
53850
- let sessionToolCalls = 0;
54200
+ const parsed = parseCodexUsage(lines);
54201
+ for (const event of codexUsageInWindow(parsed, start, end)) {
54202
+ acc.total += event.costUSD;
54203
+ acc.byDay.set(event.date, (acc.byDay.get(event.date) ?? 0) + event.costUSD);
54204
+ acc.byModel.set(event.model, (acc.byModel.get(event.model) ?? 0) + event.costUSD);
54205
+ }
53851
54206
  for (const line of lines) {
53852
- if (!line.trim()) continue;
53853
- let entry;
53854
54207
  try {
53855
- entry = JSON.parse(line);
54208
+ const entry = JSON.parse(line);
54209
+ if (entry?.type !== "response_item" || entry.payload?.type !== "function_call") continue;
54210
+ const ts = new Date(entry.timestamp ?? parsed.sessionStart);
54211
+ if (ts >= start && ts <= end) acc.toolCalls++;
53856
54212
  } catch {
53857
- continue;
53858
- }
53859
- const p = entry.payload ?? {};
53860
- if (entry.type === "session_meta") {
53861
- sessionStart = String(p["timestamp"] ?? "");
53862
- continue;
53863
- }
53864
- if (entry.type === "turn_context" && typeof p["model"] === "string") {
53865
- model = p["model"];
53866
- continue;
53867
- }
53868
- if (entry.type === "event_msg" && p["type"] === "token_count") {
53869
- const info = p["info"] ?? {};
53870
- const usage = info["total_token_usage"] ?? {};
53871
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
53872
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
53873
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
53874
- }
53875
- if (entry.type === "response_item" && p["type"] === "function_call") {
53876
- sessionToolCalls++;
53877
- }
53878
- }
53879
- if (!sessionStart) return;
53880
- const ts = new Date(sessionStart);
53881
- if (ts < start || ts > end) return;
53882
- const cost = codexSessionCost(model, {
53883
- input: lastTotalInput,
53884
- cached: lastTotalCached,
53885
- output: lastTotalOutput
53886
- });
53887
- acc.total += cost;
53888
- acc.toolCalls += sessionToolCalls;
53889
- const dateKey = sessionStart.slice(0, 10);
53890
- acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
53891
- const normModel = normalizeModel(model || "gpt-5");
53892
- acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
53893
- }
53894
- function listCodexSessionFiles2(sessionsBase) {
53895
- const jsonlFiles = [];
53896
- if (!import_fs56.default.existsSync(sessionsBase)) return jsonlFiles;
53897
- try {
53898
- for (const year of import_fs56.default.readdirSync(sessionsBase)) {
53899
- const yearPath = import_path54.default.join(sessionsBase, year);
53900
- try {
53901
- if (!import_fs56.default.statSync(yearPath).isDirectory()) continue;
53902
- } catch {
53903
- continue;
53904
- }
53905
- for (const month of import_fs56.default.readdirSync(yearPath)) {
53906
- const monthPath = import_path54.default.join(yearPath, month);
53907
- try {
53908
- if (!import_fs56.default.statSync(monthPath).isDirectory()) continue;
53909
- } catch {
53910
- continue;
53911
- }
53912
- for (const day of import_fs56.default.readdirSync(monthPath)) {
53913
- const dayPath = import_path54.default.join(monthPath, day);
53914
- try {
53915
- if (!import_fs56.default.statSync(dayPath).isDirectory()) continue;
53916
- } catch {
53917
- continue;
53918
- }
53919
- for (const file of import_fs56.default.readdirSync(dayPath)) {
53920
- if (file.endsWith(".jsonl")) jsonlFiles.push(import_path54.default.join(dayPath, file));
53921
- }
53922
- }
53923
- }
53924
54213
  }
53925
- } catch {
53926
- return [];
53927
54214
  }
53928
- return jsonlFiles;
53929
54215
  }
53930
54216
  function mergeByModel(...maps) {
53931
54217
  const out = /* @__PURE__ */ new Map();
@@ -53941,7 +54227,7 @@ function loadCodexCost(start, end, sessionsBase) {
53941
54227
  byDay: /* @__PURE__ */ new Map(),
53942
54228
  byModel: /* @__PURE__ */ new Map()
53943
54229
  };
53944
- const files = listCodexSessionFiles2(sessionsBase);
54230
+ const files = listCodexSessionFiles(sessionsBase);
53945
54231
  for (const filePath of files) {
53946
54232
  processCodexCostFile(filePath, start, end, acc);
53947
54233
  }
@@ -54081,7 +54367,7 @@ function aggregateReportFromAudit(period, opts = {}) {
54081
54367
  const now = opts.now ?? /* @__PURE__ */ new Date();
54082
54368
  const auditLogPath = opts.auditLogPath ?? import_path54.default.join(import_os50.default.homedir(), ".node9", "audit.log");
54083
54369
  const claudeProjectsDir = opts.claudeProjectsDir ?? import_path54.default.join(import_os50.default.homedir(), ".claude", "projects");
54084
- const codexSessionsDir2 = opts.codexSessionsDir ?? import_path54.default.join(import_os50.default.homedir(), ".codex", "sessions");
54370
+ const codexSessionsDir2 = opts.codexSessionsDir ?? codexSessionsDir();
54085
54371
  const geminiTmpDir2 = opts.geminiTmpDir ?? import_path54.default.join(import_os50.default.homedir(), ".gemini", "tmp");
54086
54372
  const hasAuditFile = import_fs56.default.existsSync(auditLogPath);
54087
54373
  const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
@@ -59406,7 +59692,11 @@ var BUILTIN_JAIL = [
59406
59692
  "~/.ssh \u2014 SSH private keys",
59407
59693
  "~/.aws \u2014 AWS credentials",
59408
59694
  ".env files",
59409
- "credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud"
59695
+ "credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud",
59696
+ // Stage 4 (2026-09-11): reads are blocked; a COPY out of the jail (cp, tar,
59697
+ // scp, rsync, aws s3 cp, ...) is reviewed, because a backup and a theft are
59698
+ // the same command shape.
59699
+ "copies out of the jail (cp, tar, scp, rsync, cloud upload) \u2014 review"
59410
59700
  ];
59411
59701
  function registerJailCommand(program2) {
59412
59702
  const jail = program2.command("jail").description("Grow the credential jail \u2014 block/review reads of your sensitive paths");
@@ -60280,12 +60570,12 @@ function parseSessionLines(lines) {
60280
60570
  continue;
60281
60571
  }
60282
60572
  if (entry.type !== "assistant") continue;
60283
- const usage = entry.message?.usage;
60573
+ const usage2 = entry.message?.usage;
60284
60574
  const model = entry.message?.model;
60285
- if (usage && model) {
60575
+ if (usage2 && model) {
60286
60576
  const p = modelPrice(model);
60287
60577
  if (p) {
60288
- costUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
60578
+ costUSD += (usage2.input_tokens ?? 0) * p.i + (usage2.output_tokens ?? 0) * p.o + (usage2.cache_creation_input_tokens ?? 0) * p.cw + (usage2.cache_read_input_tokens ?? 0) * p.cr;
60289
60579
  }
60290
60580
  }
60291
60581
  const content = entry.message?.content;
@@ -60465,46 +60755,13 @@ function buildGeminiSessions(days, allAuditEntries) {
60465
60755
  return summaries;
60466
60756
  }
60467
60757
  function buildCodexSessions(days, allAuditEntries) {
60468
- const sessionsBase = import_path65.default.join(import_os57.default.homedir(), ".codex", "sessions");
60469
- if (!import_fs70.default.existsSync(sessionsBase)) return [];
60470
60758
  const cutoff = days !== null ? (() => {
60471
60759
  const d = /* @__PURE__ */ new Date();
60472
60760
  d.setDate(d.getDate() - days);
60473
60761
  d.setHours(0, 0, 0, 0);
60474
60762
  return d;
60475
60763
  })() : null;
60476
- const jsonlFiles = [];
60477
- try {
60478
- for (const year of import_fs70.default.readdirSync(sessionsBase)) {
60479
- const yearPath = import_path65.default.join(sessionsBase, year);
60480
- try {
60481
- if (!import_fs70.default.statSync(yearPath).isDirectory()) continue;
60482
- } catch {
60483
- continue;
60484
- }
60485
- for (const month of import_fs70.default.readdirSync(yearPath)) {
60486
- const monthPath = import_path65.default.join(yearPath, month);
60487
- try {
60488
- if (!import_fs70.default.statSync(monthPath).isDirectory()) continue;
60489
- } catch {
60490
- continue;
60491
- }
60492
- for (const day of import_fs70.default.readdirSync(monthPath)) {
60493
- const dayPath = import_path65.default.join(monthPath, day);
60494
- try {
60495
- if (!import_fs70.default.statSync(dayPath).isDirectory()) continue;
60496
- } catch {
60497
- continue;
60498
- }
60499
- for (const file of import_fs70.default.readdirSync(dayPath)) {
60500
- if (file.endsWith(".jsonl")) jsonlFiles.push(import_path65.default.join(dayPath, file));
60501
- }
60502
- }
60503
- }
60504
- }
60505
- } catch {
60506
- return [];
60507
- }
60764
+ const jsonlFiles = listCodexSessionFiles();
60508
60765
  const summaries = [];
60509
60766
  for (const filePath of jsonlFiles) {
60510
60767
  let lines;
@@ -60519,10 +60776,6 @@ function buildCodexSessions(days, allAuditEntries) {
60519
60776
  let firstPrompt = "";
60520
60777
  const toolCalls = [];
60521
60778
  let lastToolTs = "";
60522
- let lastTotalInput = 0;
60523
- let lastTotalCached = 0;
60524
- let lastTotalOutput = 0;
60525
- let model = "";
60526
60779
  for (const line of lines) {
60527
60780
  if (!line.trim()) continue;
60528
60781
  let entry;
@@ -60538,22 +60791,10 @@ function buildCodexSessions(days, allAuditEntries) {
60538
60791
  cwd = String(p["cwd"] ?? "");
60539
60792
  continue;
60540
60793
  }
60541
- if (entry.type === "turn_context" && typeof p["model"] === "string") {
60542
- model = p["model"];
60543
- continue;
60544
- }
60545
60794
  if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
60546
60795
  firstPrompt = String(p["message"] ?? "");
60547
60796
  continue;
60548
60797
  }
60549
- if (entry.type === "event_msg" && p["type"] === "token_count") {
60550
- const info = p["info"] ?? {};
60551
- const usage = info["total_token_usage"] ?? {};
60552
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
60553
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
60554
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
60555
- continue;
60556
- }
60557
60798
  if (entry.type === "response_item" && p["type"] === "function_call") {
60558
60799
  const tool = String(p["name"] ?? "");
60559
60800
  let input = {};
@@ -60567,12 +60808,13 @@ function buildCodexSessions(days, allAuditEntries) {
60567
60808
  }
60568
60809
  }
60569
60810
  if (!sessionId || !startTime) continue;
60570
- if (cutoff && new Date(startTime) < cutoff) continue;
60571
- const costUSD = codexSessionCost(model, {
60572
- input: lastTotalInput,
60573
- cached: lastTotalCached,
60574
- output: lastTotalOutput
60575
- });
60811
+ const parsedUsage = parseCodexUsage(lines);
60812
+ const usageEvents = codexUsageInWindow(parsedUsage, cutoff);
60813
+ if (cutoff && new Date(startTime) < cutoff && usageEvents.length === 0 && !toolCalls.some((call) => new Date(call.timestamp) >= cutoff))
60814
+ continue;
60815
+ const costUSD = usageEvents.reduce((sum, event) => sum + event.costUSD, 0);
60816
+ const lastUsageTs = parsedUsage.events.at(-1)?.timestamp ?? "";
60817
+ if (lastUsageTs > lastToolTs) lastToolTs = lastUsageTs;
60576
60818
  const windowEnd = new Date(
60577
60819
  Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
60578
60820
  ).toISOString();