@mrciphersmith/keryx 0.3.77 → 0.3.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1445 -128
  2. package/dist/core.js +1015 -59
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -1757,13 +1757,367 @@ function isValidIp(value) {
1757
1757
  }
1758
1758
  return false;
1759
1759
  }
1760
- function countDigits(value) {
1761
- let n = 0;
1762
- for (const ch of value) {
1763
- if (ch >= "0" && ch <= "9")
1764
- n += 1;
1760
+ function isWordBoundary(flavour, content, index) {
1761
+ flavour.boundary.lastIndex = index;
1762
+ return flavour.boundary.test(content);
1763
+ }
1764
+ function wordCharBefore(content, index) {
1765
+ if (index <= 0) {
1766
+ return "";
1765
1767
  }
1766
- return n;
1768
+ const low = content.charCodeAt(index - 1);
1769
+ if (low >= 56320 && low <= 57343 && index >= 2) {
1770
+ const high = content.charCodeAt(index - 2);
1771
+ if (high >= 55296 && high <= 56319) {
1772
+ return content.slice(index - 2, index);
1773
+ }
1774
+ }
1775
+ return content[index - 1];
1776
+ }
1777
+ function wordCharAt(content, index) {
1778
+ const code = content.codePointAt(index);
1779
+ return code === undefined ? "" : String.fromCodePoint(code);
1780
+ }
1781
+ function execEmail(flavour, content, from) {
1782
+ let at = content.indexOf("@", from);
1783
+ while (at !== -1) {
1784
+ let start = at;
1785
+ while (start > from && flavour.localChar.test(wordCharBefore(content, start))) {
1786
+ start -= wordCharBefore(content, start).length;
1787
+ }
1788
+ while (start < at && !isWordBoundary(flavour, content, start)) {
1789
+ start += wordCharAt(content, start).length;
1790
+ }
1791
+ if (start < at) {
1792
+ flavour.from.lastIndex = start;
1793
+ const m = flavour.from.exec(content);
1794
+ if (m !== null) {
1795
+ return m;
1796
+ }
1797
+ }
1798
+ at = content.indexOf("@", at + 1);
1799
+ }
1800
+ return null;
1801
+ }
1802
+ function emailScannerFor(flavour) {
1803
+ return {
1804
+ lastIndex: 0,
1805
+ exec(content) {
1806
+ const m = execEmail(flavour, content, this.lastIndex);
1807
+ this.lastIndex = m === null ? 0 : m.index + m[0].length;
1808
+ return m;
1809
+ }
1810
+ };
1811
+ }
1812
+ function execPhone(content, from) {
1813
+ let pos = from;
1814
+ while (pos < content.length) {
1815
+ const digitAt = content[pos] === "+" ? pos + 1 : pos;
1816
+ if (!DIGIT.test(content[digitAt] ?? "") || pos > 0 && PHONE_START_BLOCKER.test(content[pos - 1])) {
1817
+ pos += 1;
1818
+ continue;
1819
+ }
1820
+ PHONE_FROM.lastIndex = pos;
1821
+ const m = PHONE_FROM.exec(content);
1822
+ if (m !== null) {
1823
+ return m;
1824
+ }
1825
+ pos = digitAt + 1;
1826
+ while (pos < content.length && PHONE_RUN_CHAR.test(content[pos])) {
1827
+ pos += 1;
1828
+ }
1829
+ }
1830
+ return null;
1831
+ }
1832
+ function phoneVerdict(content, start, end) {
1833
+ let digits = 0;
1834
+ for (let i = start;i < end && digits <= PHONE_MAX_DIGITS; i += 1) {
1835
+ if (DIGIT.test(content[i])) {
1836
+ digits += 1;
1837
+ }
1838
+ }
1839
+ if (digits < PHONE_MIN_DIGITS || digits > PHONE_MAX_DIGITS) {
1840
+ return false;
1841
+ }
1842
+ const value = content.slice(start, end);
1843
+ return hasPhoneSeparatorShape(value) && !containsCalendarDate(value) && !isIdentifierFragment(content, start, end);
1844
+ }
1845
+ function hasPhoneEvidence(value, singleToken) {
1846
+ return PHONE_SEPARATOR.test(value) || singleToken && value.includes(".") || PHONE_NANP_SPACED.test(value);
1847
+ }
1848
+ function phoneTokens(content, from, to) {
1849
+ const tokens = [];
1850
+ let i = from;
1851
+ while (i < to) {
1852
+ const gapStart = i;
1853
+ while (i < to && WHITESPACE.test(content[i])) {
1854
+ i += 1;
1855
+ }
1856
+ if (i >= to) {
1857
+ break;
1858
+ }
1859
+ const breakBefore = tokens.length > 0 && (i - gapStart >= 2 || LINE_BREAK.test(content.slice(gapStart, i)));
1860
+ const start = i;
1861
+ let digits = 0;
1862
+ let lastDigit = -1;
1863
+ let coreStart = -1;
1864
+ let leadDigits = 0;
1865
+ while (i < to && !WHITESPACE.test(content[i])) {
1866
+ const isDigit = DIGIT.test(content[i]);
1867
+ if (coreStart < 0) {
1868
+ const startsHere = (isDigit || content[i] === "+" && i + 1 < to && DIGIT.test(content[i + 1])) && (i === 0 || !PHONE_START_BLOCKER.test(content[i - 1]));
1869
+ if (startsHere) {
1870
+ coreStart = i;
1871
+ } else if (isDigit) {
1872
+ leadDigits += 1;
1873
+ }
1874
+ }
1875
+ if (isDigit) {
1876
+ digits += 1;
1877
+ lastDigit = i;
1878
+ }
1879
+ i += 1;
1880
+ }
1881
+ const text = content.slice(start, i);
1882
+ const groupable = (PHONE_DIAL_GROUP.test(text) || PHONE_FIRST_GROUP.test(text)) && !containsCalendarDate(text);
1883
+ tokens.push({ start, end: i, coreStart, coreEnd: lastDigit + 1, leadDigits, digits, breakBefore, groupable });
1884
+ }
1885
+ return tokens;
1886
+ }
1887
+ function rescanPhoneRun(content, from, to) {
1888
+ const tokens = phoneTokens(content, from, to);
1889
+ const found = [];
1890
+ const chainAfter = new Array(tokens.length).fill(0);
1891
+ for (let i = tokens.length - 2;i >= 0; i -= 1) {
1892
+ const next = tokens[i + 1];
1893
+ chainAfter[i] = next.groupable && !next.breakBefore ? chainAfter[i + 1] + next.digits : 0;
1894
+ }
1895
+ let first = 0;
1896
+ let resume = -1;
1897
+ while (first < tokens.length) {
1898
+ const head = tokens[first];
1899
+ if (head.coreStart < 0 || head.coreEnd <= head.coreStart) {
1900
+ first += 1;
1901
+ continue;
1902
+ }
1903
+ const startsRun = first === 0 || first === resume || head.breakBefore || !tokens[first - 1].groupable;
1904
+ let longest = -1;
1905
+ let clean = -1;
1906
+ const consider = (last) => {
1907
+ const end = tokens[last].coreEnd;
1908
+ if (hasPhoneEvidence(content.slice(head.coreStart, end), last === first) && phoneVerdict(content, head.coreStart, end)) {
1909
+ longest = last;
1910
+ const rest = chainAfter[last];
1911
+ if (rest === 0 || rest >= PHONE_MIN_DIGITS) {
1912
+ clean = last;
1913
+ }
1914
+ }
1915
+ };
1916
+ let digits = head.digits - head.leadDigits;
1917
+ if (digits >= PHONE_MIN_DIGITS) {
1918
+ consider(first);
1919
+ }
1920
+ const headGroup = content.slice(head.coreStart, head.end);
1921
+ let extendable = startsRun && (PHONE_FIRST_GROUP.test(headGroup) || PHONE_DIAL_GROUP.test(headGroup));
1922
+ for (let last = first + 1;last < tokens.length && extendable; last += 1) {
1923
+ const tail = tokens[last];
1924
+ digits += tail.digits;
1925
+ if (digits > PHONE_MAX_DIGITS || tail.digits === 0 || tail.breakBefore) {
1926
+ break;
1927
+ }
1928
+ if (digits >= PHONE_MIN_DIGITS && PHONE_DIAL_GROUP.test(content.slice(tail.start, tail.coreEnd))) {
1929
+ consider(last);
1930
+ }
1931
+ extendable = PHONE_DIAL_GROUP.test(content.slice(tail.start, tail.end));
1932
+ }
1933
+ const best = clean >= 0 ? clean : longest;
1934
+ if (best >= 0) {
1935
+ const value = content.slice(head.coreStart, tokens[best].coreEnd);
1936
+ found.push({ index: head.coreStart, 0: value, 1: value });
1937
+ first = best + 1;
1938
+ resume = first;
1939
+ } else {
1940
+ first += 1;
1941
+ }
1942
+ }
1943
+ return found;
1944
+ }
1945
+ function joinedPhones(content, from, to) {
1946
+ const found = [];
1947
+ let i = from;
1948
+ while (i < to) {
1949
+ while (i < to && WHITESPACE.test(content[i])) {
1950
+ i += 1;
1951
+ }
1952
+ let zoneEnd = i;
1953
+ let total = 0;
1954
+ while (zoneEnd < to && !WHITESPACE.test(content[zoneEnd])) {
1955
+ if (DIGIT.test(content[zoneEnd])) {
1956
+ total += 1;
1957
+ }
1958
+ zoneEnd += 1;
1959
+ }
1960
+ if (total > PHONE_MAX_DIGITS && !containsCalendarDate(content.slice(i, zoneEnd))) {
1961
+ joinedZone(content, i, zoneEnd, total, found);
1962
+ }
1963
+ i = zoneEnd;
1964
+ }
1965
+ return found;
1966
+ }
1967
+ function isJoinerAt(content, k, to) {
1968
+ const ch = content[k];
1969
+ return (ch === "-" || ch === ".") && k > 0 && DIGIT.test(content[k - 1]) && k + 1 < to && DIGIT.test(content[k + 1]);
1970
+ }
1971
+ function joinedZone(content, zoneStart, zoneEnd, total, found) {
1972
+ let s = zoneStart;
1973
+ let before = 0;
1974
+ for (;; ) {
1975
+ while (s < zoneEnd && !DIGIT.test(content[s]) && !(content[s] === "+" && s + 1 < zoneEnd && DIGIT.test(content[s + 1]))) {
1976
+ s += 1;
1977
+ }
1978
+ if (s >= zoneEnd) {
1979
+ return;
1980
+ }
1981
+ let digits = 0;
1982
+ let lastDigit = s;
1983
+ let open = true;
1984
+ const cuts = [];
1985
+ for (let k = s;k < zoneEnd && open; k += 1) {
1986
+ if (DIGIT.test(content[k])) {
1987
+ digits += 1;
1988
+ lastDigit = k;
1989
+ open = digits <= PHONE_MAX_DIGITS;
1990
+ } else if (digits >= PHONE_MIN_DIGITS && isJoinerAt(content, k, zoneEnd)) {
1991
+ cuts.push({ end: k, digits });
1992
+ }
1993
+ }
1994
+ if (open && digits >= PHONE_MIN_DIGITS) {
1995
+ cuts.push({ end: lastDigit + 1, digits });
1996
+ }
1997
+ let pick = -1;
1998
+ for (let c = cuts.length - 1;c >= 0; c -= 1) {
1999
+ const cut = cuts[c];
2000
+ const piece = content.slice(s, cut.end);
2001
+ if (!PHONE_JOINED_PIECE.test(piece) || PHONE_CARD_SHAPE.test(piece) || !phoneVerdict(content, s, cut.end)) {
2002
+ continue;
2003
+ }
2004
+ if (pick < 0) {
2005
+ pick = c;
2006
+ }
2007
+ const rest = total - before - cut.digits;
2008
+ if (rest === 0 || rest >= PHONE_MIN_DIGITS) {
2009
+ pick = c;
2010
+ break;
2011
+ }
2012
+ }
2013
+ if (pick < 0) {
2014
+ return;
2015
+ }
2016
+ const cut = cuts[pick];
2017
+ const value = content.slice(s, cut.end);
2018
+ found.push({ index: s, 0: value, 1: value });
2019
+ before += cut.digits;
2020
+ s = cut.end + 1;
2021
+ }
2022
+ }
2023
+ function withJoined(joined, rescans) {
2024
+ if (joined.length === 0) {
2025
+ return rescans;
2026
+ }
2027
+ const merged = [];
2028
+ let cursor = 0;
2029
+ let pending = 0;
2030
+ for (const rescan of rescans) {
2031
+ const rescanEnd = rescan.index + (rescan[0] ?? "").length;
2032
+ while (cursor < joined.length && joined[cursor].index + (joined[cursor][0] ?? "").length <= rescan.index) {
2033
+ cursor += 1;
2034
+ }
2035
+ if (cursor < joined.length && joined[cursor].index < rescanEnd) {
2036
+ continue;
2037
+ }
2038
+ while (pending < joined.length && joined[pending].index < rescan.index) {
2039
+ merged.push(joined[pending]);
2040
+ pending += 1;
2041
+ }
2042
+ merged.push(rescan);
2043
+ }
2044
+ while (pending < joined.length) {
2045
+ merged.push(joined[pending]);
2046
+ pending += 1;
2047
+ }
2048
+ return merged;
2049
+ }
2050
+ function phoneBaselineEnd(content, runEnd) {
2051
+ for (let e = runEnd;e > 0; e -= 1) {
2052
+ if (DIGIT.test(content[e - 1]) && (e >= content.length || !PHONE_START_BLOCKER.test(content[e]))) {
2053
+ return e;
2054
+ }
2055
+ }
2056
+ return -1;
2057
+ }
2058
+ function isIbanChar(code) {
2059
+ return code >= 65 && code <= 90 || code >= 48 && code <= 57;
2060
+ }
2061
+ function isAsciiWordChar(code) {
2062
+ return isIbanChar(code) || code >= 97 && code <= 122 || code === 95;
2063
+ }
2064
+ function ibanEnd(content, base, pos, groups) {
2065
+ const key = (pos - base) * (IBAN_MAX_GROUPS + 1) + groups;
2066
+ if (ibanFailed[key] === ibanStamp) {
2067
+ return -1;
2068
+ }
2069
+ if (groups < IBAN_MAX_GROUPS) {
2070
+ const from = content.charCodeAt(pos) === 32 ? pos + 1 : pos;
2071
+ let run = 0;
2072
+ while (run < 4 && isIbanChar(content.charCodeAt(from + run))) {
2073
+ run += 1;
2074
+ }
2075
+ for (let length = run;length >= 1; length -= 1) {
2076
+ const end = ibanEnd(content, base, from + length, groups + 1);
2077
+ if (end >= 0) {
2078
+ return end;
2079
+ }
2080
+ }
2081
+ }
2082
+ if (groups >= 2 && !isAsciiWordChar(content.charCodeAt(pos))) {
2083
+ return pos;
2084
+ }
2085
+ ibanFailed[key] = ibanStamp;
2086
+ return -1;
2087
+ }
2088
+ function isCardCandidate(value) {
2089
+ const separators = value.match(CARD_GROUP_SEPARATOR) ?? [];
2090
+ const first = separators[0];
2091
+ if (first === undefined) {
2092
+ return true;
2093
+ }
2094
+ if (!separators.every((separator) => separator === first)) {
2095
+ return false;
2096
+ }
2097
+ const lengths = value.split(first).map((group) => group.length);
2098
+ const last = lengths[lengths.length - 1];
2099
+ if (lengths.length === 3 && lengths[0] === 4 && lengths[1] === 6 && (last === 5 || last === 4)) {
2100
+ return true;
2101
+ }
2102
+ return lengths.slice(0, -1).every((length) => length === 4) && last >= 1 && last <= 7;
2103
+ }
2104
+ function hasNonHexGlue(content, start, end) {
2105
+ const before = GLUE_LETTER_BEFORE.exec(content.slice(Math.max(0, start - GLUE_FLANK), start))?.[0] ?? "";
2106
+ const after = GLUE_LETTER_AFTER.exec(content.slice(end, end + GLUE_FLANK))?.[0] ?? "";
2107
+ return before === "" && after === "" || NON_HEX_LETTER.test(before) || NON_HEX_LETTER.test(after);
2108
+ }
2109
+ function outsideUuid(content, start, end) {
2110
+ const from = Math.max(0, start - UUID_REACH);
2111
+ const window = content.slice(from, Math.min(content.length, end + UUID_REACH));
2112
+ UUID_ANYWHERE.lastIndex = 0;
2113
+ let m;
2114
+ while ((m = UUID_ANYWHERE.exec(window)) !== null) {
2115
+ if (from + m.index < end && from + m.index + m[0].length > start) {
2116
+ return false;
2117
+ }
2118
+ UUID_ANYWHERE.lastIndex = m.index + 1;
2119
+ }
2120
+ return true;
1767
2121
  }
1768
2122
  function containsCalendarDate(value) {
1769
2123
  return CALENDAR_DATE.test(value);
@@ -1816,71 +2170,644 @@ function hasPhoneSeparatorShape(value) {
1816
2170
  return /^\(?\d{2,4}\)?(?:[-.]\d{2,4})*$/.test(group);
1817
2171
  });
1818
2172
  }
1819
- function detectPii(content) {
2173
+ function scanWith(content, rule, scanner, validate, accept) {
1820
2174
  const matches = [];
1821
- for (const rule of RULES) {
1822
- rule.regex.lastIndex = 0;
1823
- let m;
1824
- while ((m = rule.regex.exec(content)) !== null) {
1825
- const group = rule.valueGroup ?? 0;
1826
- const value = m[group];
1827
- if (value === undefined || value.length === 0) {
2175
+ scanner.reset?.();
2176
+ scanner.lastIndex = 0;
2177
+ let m;
2178
+ while ((m = scanner.exec(content)) !== null) {
2179
+ const group = rule.valueGroup ?? 0;
2180
+ const value = m[group];
2181
+ if (value === undefined || value.length === 0) {
2182
+ continue;
2183
+ }
2184
+ if (validate && !validate(value)) {
2185
+ continue;
2186
+ }
2187
+ const start = group === 0 ? m.index : m.index + m[0].indexOf(value);
2188
+ const end = start + value.length;
2189
+ if (rule.accept && !rule.accept(content, start, end) || accept && !accept(content, start, end)) {
2190
+ if (m.index === scanner.lastIndex) {
2191
+ scanner.lastIndex += 1;
2192
+ }
2193
+ continue;
2194
+ }
2195
+ const match = {
2196
+ category: "pii",
2197
+ policyId: rule.policyId,
2198
+ severity: rule.severity,
2199
+ confidence: rule.confidence,
2200
+ start,
2201
+ end,
2202
+ value,
2203
+ mask: rule.mask,
2204
+ remediation: "Redact personal data before persisting or publishing."
2205
+ };
2206
+ if (rescanned.has(m)) {
2207
+ rescanTag.add(match);
2208
+ }
2209
+ matches.push(match);
2210
+ if (m.index === scanner.lastIndex) {
2211
+ scanner.lastIndex += 1;
2212
+ }
2213
+ }
2214
+ return matches;
2215
+ }
2216
+ function scanRule(content, rule) {
2217
+ const matches = scanWith(content, rule, rule.regex, rule.validate, undefined);
2218
+ if (rule.also === undefined) {
2219
+ return matches;
2220
+ }
2221
+ let result = matches;
2222
+ for (const extra of rule.also) {
2223
+ const found = scanWith(content, rule, extra.regex, extra.validate ?? rule.validate, extra.accept);
2224
+ const added = found.filter((match) => !containedIn(result, match.start, match.end));
2225
+ if (added.length > 0) {
2226
+ result = coalesce(content, [...result, ...added].sort((x, y) => x.start - y.start || y.end - x.end));
2227
+ }
2228
+ }
2229
+ return result;
2230
+ }
2231
+ function scanRules(content) {
2232
+ return RULES.map((rule) => scanRule(content, rule));
2233
+ }
2234
+ function decimalDigitValue(codePoint) {
2235
+ if (codePoint < 1632) {
2236
+ return -1;
2237
+ }
2238
+ const cached = digitValues.get(codePoint);
2239
+ if (cached !== undefined) {
2240
+ return cached;
2241
+ }
2242
+ let value = -1;
2243
+ if (DECIMAL_DIGIT.test(String.fromCodePoint(codePoint))) {
2244
+ let runStart = codePoint;
2245
+ while (runStart > 0 && DECIMAL_DIGIT.test(String.fromCodePoint(runStart - 1))) {
2246
+ runStart -= 1;
2247
+ }
2248
+ value = (codePoint - runStart) % 10;
2249
+ }
2250
+ digitValues.set(codePoint, value);
2251
+ return value;
2252
+ }
2253
+ function isHyphenLookalike(code) {
2254
+ return code >= 8208 && code <= 8213 || code === 8722 || code === 8315 || code === 8331 || code === 65112 || code === 65123 || code === 65293 || code === 1418 || code === 1470 || code === 6150 || code === 8259 || code === 12540 || code === 12448 || code === 11834 || code === 11835;
2255
+ }
2256
+ function isInvisible(code) {
2257
+ return code === 173 || code >= 8203 && code <= 8205 || code >= 8288 && code <= 8292 || code === 6158 || code === 847 || code === 8206 || code === 8207;
2258
+ }
2259
+ function foldNumberChar(code, options) {
2260
+ if (isInvisible(code)) {
2261
+ return code === 173 && options.shyHyphen ? "-" : "";
2262
+ }
2263
+ if (code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279) {
2264
+ return " ";
2265
+ }
2266
+ if (isHyphenLookalike(code)) {
2267
+ return "-";
2268
+ }
2269
+ if (code === 8228 || code === 65106 || code === 65294 || code === 12290 || code === 65377) {
2270
+ return ".";
2271
+ }
2272
+ if (code === 65312 || code === 65131) {
2273
+ return "@";
2274
+ }
2275
+ if (code === 65291 || code === 8314 || code === 8330 || code === 65122) {
2276
+ return "+";
2277
+ }
2278
+ if (code === 65288 || code === 8317 || code === 8333 || code === 65113) {
2279
+ return "(";
2280
+ }
2281
+ if (code === 65289 || code === 8318 || code === 8334 || code === 65114) {
2282
+ return ")";
2283
+ }
2284
+ if (options.letters && code >= 65313 && code <= 65338) {
2285
+ return String.fromCharCode(code - 65313 + 65);
2286
+ }
2287
+ const digit = decimalDigitValue(code);
2288
+ return digit < 0 ? null : String(digit);
2289
+ }
2290
+ function isNumberBreak(content, from, to) {
2291
+ const before = content[from - 1];
2292
+ const after = content[to];
2293
+ return from > 1 && (before === "-" || before === ".") && DIGIT.test(content[from - 2]) && DIGIT.test(after ?? "") || from > 0 && DIGIT.test(before ?? "") && (after === "-" || after === ".") && DIGIT.test(content[to + 1] ?? "");
2294
+ }
2295
+ function normalizeWindow(content, from, to, options) {
2296
+ const origin = new Int32Array(to - from + 1);
2297
+ const parts = [];
2298
+ let length = 0;
2299
+ let flushed = from;
2300
+ let changed = false;
2301
+ for (let i = from;i < to; ) {
2302
+ const code = content.charCodeAt(i);
2303
+ if (code < 128) {
2304
+ if (options.breaks && (code === 9 || code === 10 || code === 13)) {
2305
+ let j = i + 1;
2306
+ while (j < to && BREAK_CHAR.test(content[j])) {
2307
+ j += 1;
2308
+ }
2309
+ if (isNumberBreak(content, i, j)) {
2310
+ changed = true;
2311
+ parts.push(content.slice(flushed, i));
2312
+ flushed = j;
2313
+ } else {
2314
+ for (let k = i;k < j; k += 1) {
2315
+ origin[length++] = k;
2316
+ }
2317
+ }
2318
+ i = j;
1828
2319
  continue;
1829
2320
  }
1830
- if (rule.policyId === "pii.phone") {
1831
- const digits = countDigits(value);
1832
- if (digits < 9 || digits > 15) {
2321
+ origin[length++] = i;
2322
+ i += 1;
2323
+ continue;
2324
+ }
2325
+ const codePoint = content.codePointAt(i);
2326
+ const width = codePoint > 65535 ? 2 : 1;
2327
+ const folded = foldNumberChar(codePoint, options);
2328
+ if (folded === null) {
2329
+ for (let k = 0;k < width; k += 1) {
2330
+ origin[length++] = i;
2331
+ }
2332
+ } else {
2333
+ changed = true;
2334
+ parts.push(content.slice(flushed, i));
2335
+ flushed = i + width;
2336
+ if (folded !== "") {
2337
+ parts.push(folded);
2338
+ origin[length++] = i;
2339
+ }
2340
+ }
2341
+ i += width;
2342
+ }
2343
+ if (!changed) {
2344
+ return null;
2345
+ }
2346
+ parts.push(content.slice(flushed, to));
2347
+ origin[length] = to;
2348
+ return { text: parts.join(""), origin: origin.subarray(0, length + 1) };
2349
+ }
2350
+ function toOriginal(window, content, start, end) {
2351
+ const last = window.origin[end - 1];
2352
+ const width = content.codePointAt(last) > 65535 ? 2 : 1;
2353
+ return [window.origin[start], last + width];
2354
+ }
2355
+ function containedIn(spans, start, end) {
2356
+ let low = 0;
2357
+ let high = spans.length - 1;
2358
+ while (low <= high) {
2359
+ const mid = low + high >> 1;
2360
+ if (spans[mid].start <= start) {
2361
+ low = mid + 1;
2362
+ } else {
2363
+ high = mid - 1;
2364
+ }
2365
+ }
2366
+ const candidate = spans[high];
2367
+ return candidate !== undefined && candidate.start <= start && candidate.end >= end;
2368
+ }
2369
+ function coalesce(content, spans) {
2370
+ const merged = [];
2371
+ for (const span of spans) {
2372
+ const last = merged[merged.length - 1];
2373
+ if (last === undefined || span.start >= last.end) {
2374
+ merged.push(span);
2375
+ continue;
2376
+ }
2377
+ const end = Math.max(last.end, span.end);
2378
+ const union = { ...last, end, value: content.slice(last.start, end) };
2379
+ if (rescanTag.has(last) && rescanTag.has(span)) {
2380
+ rescanTag.add(union);
2381
+ }
2382
+ merged[merged.length - 1] = union;
2383
+ }
2384
+ return merged;
2385
+ }
2386
+ function isUniformCard(card) {
2387
+ const separators = new Set(card.value.replace(/\d/g, ""));
2388
+ return separators.size <= 1;
2389
+ }
2390
+ function dropRescansInsideCards(spans, cards) {
2391
+ if (cards.length === 0 || !spans.some((span) => rescanTag.has(span))) {
2392
+ return spans;
2393
+ }
2394
+ const sorted = cards.filter(isUniformCard).sort((a, b) => a.start - b.start);
2395
+ const reach = [];
2396
+ let farthest = 0;
2397
+ for (const card of sorted) {
2398
+ farthest = Math.max(farthest, card.end);
2399
+ reach.push(farthest);
2400
+ }
2401
+ return spans.filter((span) => {
2402
+ if (!rescanTag.has(span)) {
2403
+ return true;
2404
+ }
2405
+ let low = 0;
2406
+ let high = sorted.length - 1;
2407
+ while (low <= high) {
2408
+ const mid = low + high >> 1;
2409
+ if (sorted[mid].start < span.end) {
2410
+ low = mid + 1;
2411
+ } else {
2412
+ high = mid - 1;
2413
+ }
2414
+ }
2415
+ return high < 0 || reach[high] <= span.start;
2416
+ });
2417
+ }
2418
+ function viewsOf(content, nonAscii) {
2419
+ const views = [];
2420
+ if (nonAscii) {
2421
+ views.push({});
2422
+ if (FULLWIDTH_CAPITAL.test(content)) {
2423
+ views.push({ letters: true });
2424
+ }
2425
+ if (SOFT_HYPHEN.test(content)) {
2426
+ views.push({ shyHyphen: true });
2427
+ }
2428
+ }
2429
+ if (NUMBER_BREAK.test(content)) {
2430
+ views.push({ breaks: true });
2431
+ }
2432
+ return views;
2433
+ }
2434
+ function detectPii(content) {
2435
+ const perRule = scanRules(content);
2436
+ const nonAscii = NON_ASCII.test(content);
2437
+ if (nonAscii) {
2438
+ const emailRule = RULES[0];
2439
+ const ascii = scanRule(content, { ...emailRule, regex: asciiEmailScanner });
2440
+ const unicode = perRule[0];
2441
+ const extraEmails = ascii.filter((match) => !containedIn(unicode, match.start, match.end));
2442
+ if (extraEmails.length > 0) {
2443
+ perRule[0] = [...unicode, ...extraEmails].sort((x, y) => x.start - y.start || y.end - x.end);
2444
+ }
2445
+ }
2446
+ const views = viewsOf(content, nonAscii);
2447
+ for (const options of views) {
2448
+ for (let from = 0;from < content.length; ) {
2449
+ let start = from;
2450
+ let to = Math.min(content.length, from + NORMALIZE_CHUNK);
2451
+ if (start > 0 && (content.charCodeAt(start) & 64512) === 56320) {
2452
+ start -= 1;
2453
+ }
2454
+ if (to < content.length && (content.charCodeAt(to - 1) & 64512) === 55296) {
2455
+ to += 1;
2456
+ }
2457
+ const window = normalizeWindow(content, start, to, options);
2458
+ if (window !== null) {
2459
+ const extra = scanRules(window.text);
2460
+ extra.forEach((found, index) => {
2461
+ if (options.breaks && !NUMBER_RULES.has(index)) {
2462
+ return;
2463
+ }
2464
+ const numbers = NUMBER_RULES.has(index);
2465
+ const base = perRule[index];
2466
+ const added = [];
2467
+ for (const match of found) {
2468
+ const [a, b] = toOriginal(window, content, match.start, match.end);
2469
+ if (!numbers && b - a !== match.end - match.start) {
2470
+ continue;
2471
+ }
2472
+ if (start > 0 && a < start + NORMALIZE_SKIP) {
2473
+ continue;
2474
+ }
2475
+ if (!containedIn(base, a, b) && !containedIn(added, a, b)) {
2476
+ const copy = { ...match, start: a, end: b, value: content.slice(a, b) };
2477
+ if (rescanTag.has(match)) {
2478
+ rescanTag.add(copy);
2479
+ }
2480
+ added.push(copy);
2481
+ }
2482
+ }
2483
+ if (added.length > 0) {
2484
+ perRule[index] = coalesce(content, [...base, ...added].sort((x, y) => x.start - y.start || y.end - x.end));
2485
+ }
2486
+ });
2487
+ }
2488
+ if (to >= content.length) {
2489
+ break;
2490
+ }
2491
+ from = to - NORMALIZE_OVERLAP;
2492
+ }
2493
+ }
2494
+ perRule[PHONE_RULE] = dropRescansInsideCards(perRule[PHONE_RULE], perRule[CARD_RULE]);
2495
+ return perRule.flat();
2496
+ }
2497
+ var UNICODE_EMAIL, ASCII_EMAIL, emailScanner, asciiEmailScanner, PHONE_FROM, PHONE_RUN_CHAR, PHONE_START_BLOCKER, DIGIT, PHONE_DIAL_GROUP, PHONE_FIRST_GROUP, PHONE_SEPARATOR, PHONE_NANP_SPACED, WHITESPACE, LINE_BREAK, PHONE_MAX_DIGITS = 15, PHONE_MIN_DIGITS = 9, rescanned, PHONE_JOINED_PIECE, PHONE_CARD_SHAPE, PHONE_GLUE, PHONE_GLUE_LETTER, PHONE_GLUE_EVIDENCE, phoneGlueScanner, phoneScanner, IBAN_START, IBAN_MAX_GROUPS = 8, IBAN_STATES, ibanFailed, ibanStamp = 0, ibanScanner, IBAN_LENGTHS, IBAN_GLUE_START, ibanGluedScanner, CARD_SEPARATOR = "[ \\t\\r\\n._/-]", CARD_EXTRA, CARD_GROUP_SEPARATOR, GLUE_LETTER_BEFORE, GLUE_LETTER_AFTER, NON_HEX_LETTER, GLUE_FLANK = 8, UUID_ANYWHERE, UUID_REACH = 36, SSN_BREAK_EXTRA, IPV4_EXTRA, RULES, CALENDAR_DATE, IDENTIFIER_CHAR, TOKEN_SCAN_LIMIT = 64, UUID_TOKEN, rescanTag, NORMALIZE_CHUNK, NORMALIZE_OVERLAP = 512, NORMALIZE_SKIP = 256, NON_ASCII, DECIMAL_DIGIT, digitValues, BREAK_CHAR, PHONE_RULE, CARD_RULE, NUMBER_RULES, FULLWIDTH_CAPITAL, SOFT_HYPHEN, NUMBER_BREAK;
2498
+ var init_pii = __esm(() => {
2499
+ UNICODE_EMAIL = {
2500
+ from: /(?:(?<=[\p{L}\p{N}\p{M}_])(?![\p{L}\p{N}\p{M}_])|(?<![\p{L}\p{N}\p{M}_])(?=[\p{L}\p{N}\p{M}_]))[\p{L}\p{N}\p{M}._%+-]+@[\p{L}\p{N}\p{M}.-]+\.[\p{L}\p{M}]{2,}(?![\p{L}\p{N}\p{M}_])/uy,
2501
+ localChar: /[\p{L}\p{N}\p{M}._%+-]/u,
2502
+ boundary: /(?:(?<=[\p{L}\p{N}\p{M}_])(?![\p{L}\p{N}\p{M}_])|(?<![\p{L}\p{N}\p{M}_])(?=[\p{L}\p{N}\p{M}_]))/uy
2503
+ };
2504
+ ASCII_EMAIL = {
2505
+ from: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/y,
2506
+ localChar: /[A-Za-z0-9._%+-]/,
2507
+ boundary: /\b/y
2508
+ };
2509
+ emailScanner = emailScannerFor(UNICODE_EMAIL);
2510
+ asciiEmailScanner = emailScannerFor(ASCII_EMAIL);
2511
+ PHONE_FROM = /(?<![\w.])(\+?\d[\d\s().-]{7,}\d)(?!\w|\.\w)/y;
2512
+ PHONE_RUN_CHAR = /[\d\s().-]/;
2513
+ PHONE_START_BLOCKER = /[\w.]/;
2514
+ DIGIT = /\d/;
2515
+ PHONE_DIAL_GROUP = /^\(?\d{2,4}\)?(?:[-.]\d{2,4})*$/;
2516
+ PHONE_FIRST_GROUP = /^\+?\d{1,4}$/;
2517
+ PHONE_SEPARATOR = /[+()-]/;
2518
+ PHONE_NANP_SPACED = /^(?:\d{1,3} )?\d{3} \d{3} \d{4}$/;
2519
+ WHITESPACE = /\s/;
2520
+ LINE_BREAK = /[\n\r\u2028\u2029]/;
2521
+ rescanned = new WeakSet;
2522
+ PHONE_JOINED_PIECE = /^\+?\d{1,4}(?:[-.]\d{1,4})+$/;
2523
+ PHONE_CARD_SHAPE = /^\d{4}(?:[-.]\d{4})+$/;
2524
+ PHONE_GLUE = /(?<![\d.])\+?\d[\d().-]{7,}\d/g;
2525
+ PHONE_GLUE_LETTER = /[A-Za-z_]/;
2526
+ PHONE_GLUE_EVIDENCE = /[+()-]|\./;
2527
+ phoneGlueScanner = {
2528
+ lastIndex: 0,
2529
+ exec(content) {
2530
+ PHONE_GLUE.lastIndex = this.lastIndex;
2531
+ let m;
2532
+ while ((m = PHONE_GLUE.exec(content)) !== null) {
2533
+ const start = m.index;
2534
+ const end = start + m[0].length;
2535
+ const glued = start > 0 && PHONE_GLUE_LETTER.test(content[start - 1]) || end < content.length && PHONE_GLUE_LETTER.test(content[end]);
2536
+ if (glued && PHONE_GLUE_EVIDENCE.test(m[0]) && phoneVerdict(content, start, end)) {
2537
+ this.lastIndex = end;
2538
+ return { index: start, 0: m[0], 1: m[0] };
2539
+ }
2540
+ }
2541
+ this.lastIndex = 0;
2542
+ return null;
2543
+ }
2544
+ };
2545
+ phoneScanner = {
2546
+ lastIndex: 0,
2547
+ pending: [],
2548
+ cursor: 0,
2549
+ runEnd: -1,
2550
+ runBaseline: -1,
2551
+ tailEnd: 0,
2552
+ reset() {
2553
+ this.lastIndex = 0;
2554
+ this.pending = [];
2555
+ this.cursor = 0;
2556
+ this.runEnd = -1;
2557
+ this.runBaseline = -1;
2558
+ this.tailEnd = 0;
2559
+ },
2560
+ tail(content) {
2561
+ for (let pos = this.lastIndex;pos < this.tailEnd; pos += 1) {
2562
+ if (!DIGIT.test(content[pos]) || PHONE_START_BLOCKER.test(content[pos - 1])) {
1833
2563
  continue;
1834
2564
  }
1835
- if (!hasPhoneSeparatorShape(value)) {
1836
- continue;
2565
+ if (this.tailEnd - pos < 9) {
2566
+ break;
1837
2567
  }
1838
- if (containsCalendarDate(value)) {
1839
- continue;
2568
+ const value = content.slice(pos, this.tailEnd);
2569
+ return { index: pos, 0: value, 1: value };
2570
+ }
2571
+ this.lastIndex = this.tailEnd;
2572
+ this.tailEnd = 0;
2573
+ return null;
2574
+ },
2575
+ prefix(content, start, end) {
2576
+ if (this.runEnd !== end) {
2577
+ this.runEnd = end;
2578
+ this.runBaseline = phoneBaselineEnd(content, end);
2579
+ }
2580
+ let digits = 0;
2581
+ let limit = end;
2582
+ const ends = [];
2583
+ for (let i = start;i < end; i += 1) {
2584
+ const ch = content[i];
2585
+ if (DIGIT.test(ch)) {
2586
+ digits += 1;
2587
+ if (digits > PHONE_MAX_DIGITS) {
2588
+ limit = i;
2589
+ break;
2590
+ }
2591
+ } else if (WHITESPACE.test(ch) && DIGIT.test(content[i - 1]) && digits >= PHONE_MIN_DIGITS) {
2592
+ ends.push(i);
1840
2593
  }
1841
- if (isIdentifierFragment(content, m.index, m.index + m[0].length)) {
1842
- continue;
2594
+ }
2595
+ const baseline = this.runBaseline;
2596
+ if (baseline < end && baseline >= start + 9 && baseline <= limit && phoneVerdict(content, start, baseline)) {
2597
+ return { end: baseline, baseline: true };
2598
+ }
2599
+ for (let k = ends.length - 1;k >= 0; k -= 1) {
2600
+ const e = ends[k];
2601
+ const value = content.slice(start, e);
2602
+ if (e !== baseline && hasPhoneEvidence(value, !WHITESPACE.test(value)) && phoneVerdict(content, start, e)) {
2603
+ return { end: e, baseline: false };
1843
2604
  }
1844
2605
  }
1845
- if (rule.validate && !rule.validate(value)) {
1846
- continue;
2606
+ return null;
2607
+ },
2608
+ exec(content) {
2609
+ for (;; ) {
2610
+ if (this.cursor < this.pending.length) {
2611
+ return this.pending[this.cursor++];
2612
+ }
2613
+ this.pending = [];
2614
+ this.cursor = 0;
2615
+ let m;
2616
+ if (this.tailEnd > this.lastIndex) {
2617
+ m = this.tail(content);
2618
+ if (m === null) {
2619
+ continue;
2620
+ }
2621
+ } else {
2622
+ m = execPhone(content, this.lastIndex);
2623
+ if (m === null) {
2624
+ this.lastIndex = 0;
2625
+ return null;
2626
+ }
2627
+ }
2628
+ const end = m.index + (m[0] ?? "").length;
2629
+ this.lastIndex = end;
2630
+ this.tailEnd = 0;
2631
+ if (phoneVerdict(content, m.index, end)) {
2632
+ return m;
2633
+ }
2634
+ const prefix = this.prefix(content, m.index, end);
2635
+ if (prefix !== null) {
2636
+ const value = content.slice(m.index, prefix.end);
2637
+ const found = { index: m.index, 0: value, 1: value };
2638
+ if (!prefix.baseline) {
2639
+ rescanned.add(found);
2640
+ }
2641
+ this.lastIndex = prefix.end;
2642
+ this.tailEnd = end;
2643
+ return found;
2644
+ }
2645
+ this.pending = withJoined(joinedPhones(content, m.index, end), rescanPhoneRun(content, m.index, end));
2646
+ for (const found of this.pending) {
2647
+ rescanned.add(found);
2648
+ }
1847
2649
  }
1848
- const start = group === 0 ? m.index : m.index + m[0].indexOf(value);
1849
- matches.push({
1850
- category: "pii",
1851
- policyId: rule.policyId,
1852
- severity: rule.severity,
1853
- confidence: rule.confidence,
1854
- start,
1855
- end: start + value.length,
1856
- value,
1857
- mask: rule.mask,
1858
- remediation: "Redact personal data before persisting or publishing."
1859
- });
1860
- if (m.index === rule.regex.lastIndex) {
1861
- rule.regex.lastIndex += 1;
2650
+ }
2651
+ };
2652
+ IBAN_START = /(?<!\w)[A-Z]{2}\d{2}/g;
2653
+ IBAN_STATES = (4 + IBAN_MAX_GROUPS * 5 + 1) * (IBAN_MAX_GROUPS + 1);
2654
+ ibanFailed = new Uint32Array(IBAN_STATES);
2655
+ ibanScanner = {
2656
+ lastIndex: 0,
2657
+ exec(content) {
2658
+ IBAN_START.lastIndex = this.lastIndex;
2659
+ let m;
2660
+ while ((m = IBAN_START.exec(content)) !== null) {
2661
+ ibanStamp += 1;
2662
+ if (ibanStamp === 4294967295) {
2663
+ ibanFailed.fill(0);
2664
+ ibanStamp = 1;
2665
+ }
2666
+ const end = ibanEnd(content, m.index, m.index + 4, 0);
2667
+ if (end >= 0) {
2668
+ this.lastIndex = end;
2669
+ return { index: m.index, 0: content.slice(m.index, end) };
2670
+ }
2671
+ IBAN_START.lastIndex = m.index + 1;
1862
2672
  }
2673
+ this.lastIndex = 0;
2674
+ return null;
1863
2675
  }
1864
- }
1865
- return matches;
1866
- }
1867
- var RULES, CALENDAR_DATE, IDENTIFIER_CHAR, TOKEN_SCAN_LIMIT = 64, UUID_TOKEN;
1868
- var init_pii = __esm(() => {
2676
+ };
2677
+ IBAN_LENGTHS = {
2678
+ AD: 24,
2679
+ AE: 23,
2680
+ AL: 28,
2681
+ AT: 20,
2682
+ AZ: 28,
2683
+ BA: 20,
2684
+ BE: 16,
2685
+ BG: 22,
2686
+ BH: 22,
2687
+ BR: 29,
2688
+ BY: 28,
2689
+ CH: 21,
2690
+ CR: 22,
2691
+ CY: 28,
2692
+ CZ: 24,
2693
+ DE: 22,
2694
+ DK: 18,
2695
+ DO: 28,
2696
+ EE: 20,
2697
+ EG: 29,
2698
+ ES: 24,
2699
+ FI: 18,
2700
+ FO: 18,
2701
+ FR: 27,
2702
+ GB: 22,
2703
+ GE: 22,
2704
+ GI: 23,
2705
+ GL: 18,
2706
+ GR: 27,
2707
+ GT: 28,
2708
+ HR: 21,
2709
+ HU: 28,
2710
+ IE: 22,
2711
+ IL: 23,
2712
+ IQ: 23,
2713
+ IS: 26,
2714
+ IT: 27,
2715
+ JO: 30,
2716
+ KW: 30,
2717
+ KZ: 20,
2718
+ LB: 28,
2719
+ LC: 32,
2720
+ LI: 21,
2721
+ LT: 20,
2722
+ LU: 20,
2723
+ LV: 21,
2724
+ MC: 27,
2725
+ MD: 24,
2726
+ ME: 22,
2727
+ MK: 19,
2728
+ MR: 27,
2729
+ MT: 31,
2730
+ MU: 30,
2731
+ NL: 18,
2732
+ NO: 15,
2733
+ PK: 24,
2734
+ PL: 28,
2735
+ PS: 29,
2736
+ PT: 25,
2737
+ QA: 29,
2738
+ RO: 24,
2739
+ RS: 22,
2740
+ SA: 24,
2741
+ SC: 31,
2742
+ SE: 24,
2743
+ SI: 19,
2744
+ SK: 24,
2745
+ SM: 27,
2746
+ ST: 25,
2747
+ SV: 28,
2748
+ TL: 23,
2749
+ TN: 24,
2750
+ TR: 26,
2751
+ UA: 29,
2752
+ VA: 22,
2753
+ VG: 24,
2754
+ XK: 20
2755
+ };
2756
+ IBAN_GLUE_START = /[A-Z]{2}\d{2}/g;
2757
+ ibanGluedScanner = {
2758
+ lastIndex: 0,
2759
+ exec(content) {
2760
+ IBAN_GLUE_START.lastIndex = this.lastIndex;
2761
+ let m;
2762
+ while ((m = IBAN_GLUE_START.exec(content)) !== null) {
2763
+ const length = IBAN_LENGTHS[m[0].slice(0, 2)];
2764
+ if (length !== undefined) {
2765
+ let count = 0;
2766
+ let i = m.index;
2767
+ while (i < content.length && count < length) {
2768
+ const code = content.charCodeAt(i);
2769
+ if (isIbanChar(code)) {
2770
+ count += 1;
2771
+ } else if (!(code === 32 && count >= 4 && isIbanChar(content.charCodeAt(i + 1)))) {
2772
+ break;
2773
+ }
2774
+ i += 1;
2775
+ }
2776
+ if (count === length) {
2777
+ this.lastIndex = i;
2778
+ return { index: m.index, 0: content.slice(m.index, i) };
2779
+ }
2780
+ }
2781
+ IBAN_GLUE_START.lastIndex = m.index + 1;
2782
+ }
2783
+ this.lastIndex = 0;
2784
+ return null;
2785
+ }
2786
+ };
2787
+ CARD_EXTRA = new RegExp(`(?<!\\d${CARD_SEPARATOR}{0,2})[2-6](?:${CARD_SEPARATOR}{0,2}\\d){12,18}(?!\\d)(?!${CARD_SEPARATOR}{1,2}\\d)`, "g");
2788
+ CARD_GROUP_SEPARATOR = /[^\d]+/g;
2789
+ GLUE_LETTER_BEFORE = /[A-Za-z_]+$/;
2790
+ GLUE_LETTER_AFTER = /^[A-Za-z_]+/;
2791
+ NON_HEX_LETTER = /[G-Zg-z_]/;
2792
+ UUID_ANYWHERE = /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/g;
2793
+ SSN_BREAK_EXTRA = /(?<![0-9])\d{3}(?:\r\n|[\t\r\n])\d{2}(?:\r\n|[\t\r\n])\d{4}(?![0-9])/g;
2794
+ IPV4_EXTRA = /(?<![0-9.])(?<!\b[vV])\d{1,3}(?:[\t\r\n]{0,2}\.[\t\r\n]{0,2}\d{1,3}){3}(?![0-9])/g;
1869
2795
  RULES = [
1870
2796
  {
1871
2797
  policyId: "pii.email",
1872
2798
  mask: "email",
1873
- regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
2799
+ regex: emailScanner,
1874
2800
  severity: "medium",
1875
2801
  confidence: 0.85
1876
2802
  },
1877
2803
  {
1878
2804
  policyId: "pii.phone",
1879
2805
  mask: "phone",
1880
- regex: /(?<![\w.])(\+?\d[\d\s().-]{7,}\d)(?![\w.])/g,
2806
+ regex: phoneScanner,
1881
2807
  severity: "medium",
1882
2808
  confidence: 0.7,
1883
- valueGroup: 1
2809
+ valueGroup: 1,
2810
+ also: [{ regex: phoneGlueScanner }]
1884
2811
  },
1885
2812
  {
1886
2813
  policyId: "pii.address",
@@ -1892,7 +2819,7 @@ var init_pii = __esm(() => {
1892
2819
  {
1893
2820
  policyId: "pii.person-name",
1894
2821
  mask: "name",
1895
- regex: /\b(?:name\s+is|customer|client|user|patient|employee)\s*:?\s+([A-Z][a-z]+\s+[A-Z][a-z]+)\b/g,
2822
+ regex: /\b(?:name\s+is|customer|client|user|patient|employee)(?:\s*:)?\s+([A-Z][a-z]+\s+[A-Z][a-z]+)\b/g,
1896
2823
  severity: "low",
1897
2824
  confidence: 0.45,
1898
2825
  valueGroup: 1
@@ -1900,10 +2827,11 @@ var init_pii = __esm(() => {
1900
2827
  {
1901
2828
  policyId: "pii.iban",
1902
2829
  mask: "iban",
1903
- regex: /\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]{1,4}){2,8}\b/g,
2830
+ regex: ibanScanner,
1904
2831
  severity: "high",
1905
2832
  confidence: 0.9,
1906
- validate: isValidIban
2833
+ validate: isValidIban,
2834
+ also: [{ regex: ibanGluedScanner }]
1907
2835
  },
1908
2836
  {
1909
2837
  policyId: "pii.credit-card",
@@ -1911,15 +2839,24 @@ var init_pii = __esm(() => {
1911
2839
  regex: /\b\d(?:[ -]?\d){12,18}\b/g,
1912
2840
  severity: "high",
1913
2841
  confidence: 0.9,
1914
- validate: isValidCreditCard
2842
+ validate: isValidCreditCard,
2843
+ accept: outsideUuid,
2844
+ also: [
2845
+ {
2846
+ regex: CARD_EXTRA,
2847
+ validate: (value) => isCardCandidate(value) && isValidCreditCard(value.replace(/\D/g, "")),
2848
+ accept: hasNonHexGlue
2849
+ }
2850
+ ]
1915
2851
  },
1916
2852
  {
1917
2853
  policyId: "pii.ssn",
1918
2854
  mask: "ssn",
1919
- regex: /\b\d{3}-\d{2}-\d{4}\b/g,
2855
+ regex: /(?<![0-9])\d{3}-\d{2}-\d{4}(?![0-9])/g,
1920
2856
  severity: "high",
1921
2857
  confidence: 0.85,
1922
- validate: isValidSsn
2858
+ validate: isValidSsn,
2859
+ also: [{ regex: SSN_BREAK_EXTRA, validate: (value) => isValidSsn(value.replace(/\r\n|[\t\r\n]/g, "-")) }]
1923
2860
  },
1924
2861
  {
1925
2862
  policyId: "pii.ip",
@@ -1927,12 +2864,25 @@ var init_pii = __esm(() => {
1927
2864
  regex: /\b(?:(?:\d{1,3}\.){3}\d{1,3}|(?:[0-9A-Fa-f]{1,4}:){2,7}[0-9A-Fa-f]{1,4}|::1)\b/g,
1928
2865
  severity: "low",
1929
2866
  confidence: 0.6,
1930
- validate: isValidIp
2867
+ validate: isValidIp,
2868
+ also: [{ regex: IPV4_EXTRA, validate: (value) => isValidIp(value.replace(/[\t\r\n]/g, "")) }]
1931
2869
  }
1932
2870
  ];
1933
2871
  CALENDAR_DATE = /(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])/;
1934
2872
  IDENTIFIER_CHAR = /[0-9A-Za-z_-]/;
1935
2873
  UUID_TOKEN = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
2874
+ rescanTag = new WeakSet;
2875
+ NORMALIZE_CHUNK = 1 << 21;
2876
+ NON_ASCII = /[\u0080-\uffff]/;
2877
+ DECIMAL_DIGIT = /\p{Nd}/u;
2878
+ digitValues = new Map;
2879
+ BREAK_CHAR = /[\t\r\n]/;
2880
+ PHONE_RULE = RULES.findIndex((rule) => rule.policyId === "pii.phone");
2881
+ CARD_RULE = RULES.findIndex((rule) => rule.policyId === "pii.credit-card");
2882
+ NUMBER_RULES = new Set(RULES.flatMap((rule, index) => ["pii.phone", "pii.iban", "pii.credit-card", "pii.ssn", "pii.ip"].includes(rule.policyId) ? [index] : []));
2883
+ FULLWIDTH_CAPITAL = /[\uff21-\uff3a]/;
2884
+ SOFT_HYPHEN = /\u00ad/;
2885
+ NUMBER_BREAK = /[-.][\t\r\n]|[\t\r\n][-.]/;
1936
2886
  });
1937
2887
 
1938
2888
  // src/security/detect/secrets.ts
@@ -4348,17 +5298,20 @@ function locationFor(content, match) {
4348
5298
  };
4349
5299
  }
4350
5300
  function applyRedaction(content, matches) {
4351
- const redactable = matches.filter((m) => m.mask !== undefined).sort((a, b) => a.start - b.start);
5301
+ const redactable = matches.filter((m) => m.mask !== undefined).sort((a, b) => a.start - b.start || b.end - a.end);
4352
5302
  let out = "";
4353
5303
  let cursor = 0;
4354
5304
  for (const match of redactable) {
4355
- const start = Math.max(match.start, cursor);
4356
- if (start >= match.end) {
5305
+ if (match.end <= cursor) {
5306
+ continue;
5307
+ }
5308
+ if (match.start < cursor) {
5309
+ cursor = match.end;
4357
5310
  continue;
4358
5311
  }
4359
- out += content.slice(cursor, start);
5312
+ out += content.slice(cursor, match.start);
4360
5313
  out += maskFor(match);
4361
- cursor = Math.max(cursor, match.end);
5314
+ cursor = match.end;
4362
5315
  }
4363
5316
  out += content.slice(cursor);
4364
5317
  return out;
@@ -20145,7 +21098,7 @@ var package_default;
20145
21098
  var init_package = __esm(() => {
20146
21099
  package_default = {
20147
21100
  name: "@mrciphersmith/keryx",
20148
- version: "0.3.77",
21101
+ version: "0.3.79",
20149
21102
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20150
21103
  private: false,
20151
21104
  publishConfig: {
@@ -20223,10 +21176,10 @@ var init_package = __esm(() => {
20223
21176
  qs: "^6.16.0",
20224
21177
  "@hono/node-server": "^1.19.15",
20225
21178
  protobufjs: "7.6.6",
20226
- sharp: "0.35.4"
21179
+ sharp: "0.35.5"
20227
21180
  },
20228
21181
  optionalDependencies: {
20229
- "@modelcontextprotocol/sdk": "^1.30.0",
21182
+ "@modelcontextprotocol/sdk": "^1.32.1",
20230
21183
  "@opentui/core": "^0.4.5",
20231
21184
  "web-tree-sitter": "^0.22.6"
20232
21185
  },
@@ -24868,6 +25821,7 @@ var init_service2 = __esm(() => {
24868
25821
  init_path_scan();
24869
25822
  init_read_source();
24870
25823
  init_redact();
25824
+ init_pii();
24871
25825
  init_secrets();
24872
25826
  init_entropy();
24873
25827
  init_incidents();
@@ -109172,6 +110126,22 @@ var init_theme_text = __esm(() => {
109172
110126
  init_theme();
109173
110127
  });
109174
110128
 
110129
+ // src/tui/transcript-history.ts
110130
+ function trackTranscriptNode(node, policy) {
110131
+ policies.set(node, policy);
110132
+ }
110133
+ function transcriptNodeProtected(node) {
110134
+ return policies.get(node)?.protected() === true;
110135
+ }
110136
+ function forgetTranscriptNode(node) {
110137
+ policies.get(node)?.evict?.();
110138
+ policies.delete(node);
110139
+ }
110140
+ var TRANSCRIPT_HISTORY_LIMIT = 256, policies;
110141
+ var init_transcript_history = __esm(() => {
110142
+ policies = new WeakMap;
110143
+ });
110144
+
109175
110145
  // src/tui/transcript-blocks.ts
109176
110146
  function createBlockRegistry(options = {}) {
109177
110147
  const maxBlocks = options.maxBlocks ?? DEFAULT_MAX_BLOCKS;
@@ -109232,6 +110202,15 @@ function createBlockRegistry(options = {}) {
109232
110202
  }
109233
110203
  return id;
109234
110204
  },
110205
+ remove: (id) => {
110206
+ const index = blocks.findIndex((block) => block.id === id);
110207
+ if (index < 0)
110208
+ return;
110209
+ blocks.splice(index, 1);
110210
+ if (focusIndex > index)
110211
+ focusIndex -= 1;
110212
+ focusIndex = Math.min(focusIndex, blocks.length - 1);
110213
+ },
109235
110214
  get: (id) => {
109236
110215
  const block = find(id);
109237
110216
  return block === undefined ? undefined : snapshot(block);
@@ -109629,8 +110608,13 @@ function createAssistantMessageStream(otui, renderer, parent) {
109629
110608
  flexDirection: "column",
109630
110609
  flexShrink: 0
109631
110610
  });
110611
+ let live = true;
110612
+ trackTranscriptNode(container, { protected: () => live });
110613
+ const complete = () => {
110614
+ live = false;
110615
+ };
109632
110616
  parent.add(container);
109633
- return { container, segmenter: createStreamSegmenter(), views: [], frozen: 0 };
110617
+ return { complete, container, segmenter: createStreamSegmenter(), views: [], frozen: 0 };
109634
110618
  };
109635
110619
  const paint = (m, segments, frozen) => {
109636
110620
  for (let i = m.frozen;i < segments.length; i++) {
@@ -109673,9 +110657,10 @@ function createAssistantMessageStream(otui, renderer, parent) {
109673
110657
  m.frozen = 0;
109674
110658
  const segments = segmentMarkdown(text);
109675
110659
  paint(m, segments, segments.length);
110660
+ m.complete();
109676
110661
  if (segments.length === 0) {
109677
110662
  try {
109678
- parent.remove(m.container);
110663
+ m.container.destroyRecursively();
109679
110664
  } catch {}
109680
110665
  }
109681
110666
  message = undefined;
@@ -109685,11 +110670,12 @@ function createAssistantMessageStream(otui, renderer, parent) {
109685
110670
  if (message === undefined) {
109686
110671
  return;
109687
110672
  }
110673
+ message.complete();
109688
110674
  for (const view of message.views.splice(0)) {
109689
110675
  view.destroy();
109690
110676
  }
109691
110677
  try {
109692
- parent.remove(message.container);
110678
+ message.container.destroyRecursively();
109693
110679
  } catch {}
109694
110680
  message = undefined;
109695
110681
  },
@@ -109707,6 +110693,8 @@ function createBlockView(otui, renderer, parent, block, options = {}) {
109707
110693
  });
109708
110694
  const header = new otui.TextRenderable(renderer, { id: `${id}-h`, content: "" });
109709
110695
  box.add(header);
110696
+ let protectedView = false;
110697
+ trackTranscriptNode(box, { protected: () => protectedView, evict: () => options.onHistoryEvict?.() });
109710
110698
  parent.add(box);
109711
110699
  let body;
109712
110700
  let bodyText;
@@ -109779,6 +110767,9 @@ function createBlockView(otui, renderer, parent, block, options = {}) {
109779
110767
  return {
109780
110768
  id: block.id,
109781
110769
  render: (state, opts = {}) => {
110770
+ if (box.isDestroyed)
110771
+ return;
110772
+ protectedView = opts.focused === true;
109782
110773
  paintHeader(state, opts.focused === true);
109783
110774
  if (state.collapsed) {
109784
110775
  if (options.collapsedPreview !== undefined) {
@@ -109835,7 +110826,14 @@ function createBlockMount(otui, renderer, parent, registry) {
109835
110826
  }
109836
110827
  const state = registry.get(id);
109837
110828
  if (state !== undefined) {
109838
- views.set(id, createBlockView(otui, renderer, parent, state, options));
110829
+ views.set(id, createBlockView(otui, renderer, parent, state, {
110830
+ ...options,
110831
+ onHistoryEvict: () => {
110832
+ views.delete(id);
110833
+ registry.remove(id);
110834
+ options.onHistoryEvict?.();
110835
+ }
110836
+ }));
109839
110837
  }
109840
110838
  return id;
109841
110839
  },
@@ -110061,6 +111059,7 @@ var init_transcript_blocks = __esm(() => {
110061
111059
  init_md_blocks();
110062
111060
  init_theme();
110063
111061
  init_theme_text();
111062
+ init_transcript_history();
110064
111063
  });
110065
111064
 
110066
111065
  // src/tui/modal-host.ts
@@ -110109,6 +111108,20 @@ function resolveModalInnerWidth(availableWidth) {
110109
111108
  function formatModalFooter(actions) {
110110
111109
  return actions.map((action) => `${action.key} ${action.label}`).join(" \xB7 ");
110111
111110
  }
111111
+ function modalFocusFallback(node) {
111112
+ return modalHandBack.get(node) ?? null;
111113
+ }
111114
+ function focusIsLive(node, root) {
111115
+ let current = node;
111116
+ while (current !== null) {
111117
+ if (current.isDestroyed || !current.visible)
111118
+ return false;
111119
+ if (current === root)
111120
+ return true;
111121
+ current = current.parent;
111122
+ }
111123
+ return false;
111124
+ }
110112
111125
  function destroyModalHost(renderer) {
110113
111126
  hosts.get(renderer)?.unsubTheme();
110114
111127
  }
@@ -110123,6 +111136,10 @@ function containsNode(root, node) {
110123
111136
  }
110124
111137
  return false;
110125
111138
  }
111139
+ function modalHides(renderer, node) {
111140
+ const state = hosts.get(renderer);
111141
+ return state !== undefined && state.open && !containsNode(state.backdrop, node);
111142
+ }
110126
111143
  function resolveInitialTab(tabs, initialTab) {
110127
111144
  const first = tabs[0];
110128
111145
  if (first === undefined) {
@@ -110207,7 +111224,15 @@ function closeHost(state, opts) {
110207
111224
  if (state.savedScrollTop !== undefined && scroll !== undefined) {
110208
111225
  scroll.scrollTop = state.savedScrollTop;
110209
111226
  }
110210
- state.chrome.focusComposer();
111227
+ const r = state.chrome.renderer;
111228
+ const current = r.currentFocusedRenderable;
111229
+ if (current === null || containsNode(state.backdrop, current)) {
111230
+ const previous = state.savedFocus;
111231
+ if (previous !== null && focusIsLive(previous, r.root))
111232
+ previous.focus();
111233
+ else
111234
+ state.chrome.focusComposer();
111235
+ }
110211
111236
  }
110212
111237
  if (opts.runOnClose && onClose !== undefined) {
110213
111238
  onClose();
@@ -110337,6 +111362,7 @@ function ensureHost(otui, chrome) {
110337
111362
  releaseOverlay: undefined,
110338
111363
  unsubKeys: undefined,
110339
111364
  savedScrollTop: undefined,
111365
+ savedFocus: null,
110340
111366
  unsubTheme: () => {}
110341
111367
  };
110342
111368
  state.releaseOverlay = chrome.addOverlaySource(() => state.open, { kind: "modal" });
@@ -110461,6 +111487,8 @@ function openModal(otui, chrome, input) {
110461
111487
  state.onClose = undefined;
110462
111488
  previousClose?.();
110463
111489
  } else {
111490
+ state.savedFocus = chrome.renderer.currentFocusedRenderable;
111491
+ modalHandBack.set(state.tabStrip, state.savedFocus);
110464
111492
  state.savedScrollTop = chrome.scroll.scrollTop;
110465
111493
  chrome.hideMenu();
110466
111494
  chrome.blurComposer();
@@ -110487,7 +111515,7 @@ function openModal(otui, chrome, input) {
110487
111515
  }, 0);
110488
111516
  return makeHandle(state, generation);
110489
111517
  }
110490
- var BACKDROP_ID = "modal-backdrop", PANEL_ID = "modal-panel", BACKDROP_ALPHA = 1, MODAL_PANEL_MARGIN = 1, MODAL_PANEL_CHROME_X = 4, MODAL_PANEL_MIN_WIDTH = 72, MODAL_PANEL_MIN_HEIGHT = 18, MODAL_PANEL_SIZE_RATIO = 0.95, MODAL_PANEL_MAX_HEIGHT_RATIO = 0.85, MODAL_CHROME_ROWS = 5, MODAL_PANEL_WIDTH, MODAL_PANEL_HEIGHT, MODAL_PANEL_INNER_WIDTH, CLOSE_HINT = "[x] esc", DEFAULT_FOOTER, hosts, MODAL_BODY_SCROLLBAR_COLS = 1;
111518
+ var BACKDROP_ID = "modal-backdrop", PANEL_ID = "modal-panel", BACKDROP_ALPHA = 1, MODAL_PANEL_MARGIN = 1, MODAL_PANEL_CHROME_X = 4, MODAL_PANEL_MIN_WIDTH = 72, MODAL_PANEL_MIN_HEIGHT = 18, MODAL_PANEL_SIZE_RATIO = 0.95, MODAL_PANEL_MAX_HEIGHT_RATIO = 0.85, MODAL_CHROME_ROWS = 5, MODAL_PANEL_WIDTH, MODAL_PANEL_HEIGHT, MODAL_PANEL_INNER_WIDTH, CLOSE_HINT = "[x] esc", DEFAULT_FOOTER, modalHandBack, hosts, MODAL_BODY_SCROLLBAR_COLS = 1;
110491
111519
  var init_modal_host = __esm(() => {
110492
111520
  init_theme();
110493
111521
  init_theme_text();
@@ -110499,6 +111527,7 @@ var init_modal_host = __esm(() => {
110499
111527
  { key: "\u2190/\u2192", label: "tabs" },
110500
111528
  { key: "esc", label: "close" }
110501
111529
  ];
111530
+ modalHandBack = new WeakMap;
110502
111531
  hosts = new WeakMap;
110503
111532
  });
110504
111533
 
@@ -111336,6 +112365,22 @@ async function attemptJevRequest(fetchFn, apiKey, body, timeoutMs, external) {
111336
112365
  }
111337
112366
  }
111338
112367
  }
112368
+ async function cancellableJevSleep(sleep, ms, signal, timeoutMs) {
112369
+ if (signal === undefined)
112370
+ return sleep(ms);
112371
+ if (signal.aborted)
112372
+ throw new JevTimeoutError(timeoutMs);
112373
+ let onAbort = () => {};
112374
+ const aborted = new Promise((_, reject) => {
112375
+ onAbort = () => reject(new JevTimeoutError(timeoutMs));
112376
+ signal.addEventListener("abort", onAbort, { once: true });
112377
+ });
112378
+ try {
112379
+ await Promise.race([sleep(ms), aborted]);
112380
+ } finally {
112381
+ signal.removeEventListener("abort", onAbort);
112382
+ }
112383
+ }
111339
112384
  async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
111340
112385
  const externalSetting = await resolveExternalSetting({ cwd: opts?.cwd ?? process.cwd(), ...opts?.dir !== undefined ? { dir: opts.dir } : {} });
111341
112386
  if (externalSetting.value === "off") {
@@ -111361,6 +112406,8 @@ async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
111361
112406
  let text;
111362
112407
  let attempt = 0;
111363
112408
  for (;; ) {
112409
+ if (external?.aborted)
112410
+ throw new JevTimeoutError(timeoutMs);
111364
112411
  attempt += 1;
111365
112412
  const outcome = await attemptJevRequest(fetchFn, apiKey, body, timeoutMs, external);
111366
112413
  if (outcome.kind === "timeout") {
@@ -111370,7 +112417,7 @@ async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
111370
112417
  if (attempt >= JEV_MAX_ATTEMPTS) {
111371
112418
  throw new JevNetworkError(attempt, outcome.cause);
111372
112419
  }
111373
- await sleepFn(jevBackoffMs(attempt));
112420
+ await cancellableJevSleep(sleepFn, jevBackoffMs(attempt), external, timeoutMs);
111374
112421
  continue;
111375
112422
  }
111376
112423
  if (outcome.ok) {
@@ -111381,7 +112428,7 @@ async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
111381
112428
  text = outcome.text;
111382
112429
  if (JEV_RETRYABLE_STATUSES.has(status) && attempt < JEV_MAX_ATTEMPTS) {
111383
112430
  const retryAfterMs = retryAfterMsFromHeader(outcome.retryAfterHeader);
111384
- await sleepFn(retryAfterMs ?? jevBackoffMs(attempt));
112431
+ await cancellableJevSleep(sleepFn, retryAfterMs ?? jevBackoffMs(attempt), external, timeoutMs);
111385
112432
  continue;
111386
112433
  }
111387
112434
  if (status === 401) {
@@ -133873,14 +134920,60 @@ async function createShellChrome(otui, renderer, opts) {
133873
134920
  let liveStatus;
133874
134921
  const originalTranscriptAdd = transcript.add.bind(transcript);
133875
134922
  const originalTranscriptRemove = transcript.remove.bind(transcript);
134923
+ let historyNotice;
134924
+ let historyAnchor;
134925
+ const restoreHistoryAnchor = () => {
134926
+ const anchor = historyAnchor;
134927
+ historyAnchor = undefined;
134928
+ if (anchor === undefined || anchor.node.isDestroyed || scroll.stickyScroll || scroll.scrollTop !== anchor.scrollTop)
134929
+ return;
134930
+ scroll.scrollTop += anchor.node.y - anchor.y;
134931
+ };
134932
+ r.on(otui.CliRenderEvents.FRAME, restoreHistoryAnchor);
134933
+ const trimHistory = () => {
134934
+ const completed = transcript.getChildren().filter((node) => node !== liveStatus && node !== historyNotice && !transcriptNodeProtected(node));
134935
+ const excess = completed.length - TRANSCRIPT_HISTORY_LIMIT;
134936
+ if (excess <= 0)
134937
+ return;
134938
+ if (!scroll.stickyScroll && historyAnchor === undefined) {
134939
+ const evicted = new Set(completed.slice(0, excess));
134940
+ const node = transcript.getChildren().find((child) => child !== liveStatus && child !== historyNotice && !evicted.has(child) && child.y + child.height > scroll.y && child.y < scroll.y + scroll.height);
134941
+ if (node !== undefined)
134942
+ historyAnchor = { node, y: node.y, scrollTop: scroll.scrollTop };
134943
+ }
134944
+ for (const node of completed.slice(0, excess)) {
134945
+ forgetTranscriptNode(node);
134946
+ node.destroyRecursively();
134947
+ }
134948
+ for (let i = userPrompts.length - 1;i >= 0; i--) {
134949
+ if (userPrompts[i]?.box.isDestroyed)
134950
+ userPrompts.splice(i, 1);
134951
+ }
134952
+ if (pinnedPrompt?.box.isDestroyed) {
134953
+ pinnedPrompt = undefined;
134954
+ promptPin.visible = false;
134955
+ }
134956
+ if (historyNotice === undefined || historyNotice.isDestroyed) {
134957
+ historyNotice = new otui.TextRenderable(r, {
134958
+ id: "transcript-history-notice",
134959
+ content: "Older output is no longer mounted in this view.",
134960
+ fg: getTheme().muted,
134961
+ flexShrink: 0
134962
+ });
134963
+ originalTranscriptAdd(historyNotice, 0);
134964
+ }
134965
+ };
133876
134966
  transcript.add = (child, index) => {
133877
134967
  if (liveStatus !== undefined && child !== liveStatus) {
133878
134968
  originalTranscriptRemove(liveStatus);
133879
134969
  const added = originalTranscriptAdd(child, index);
133880
134970
  originalTranscriptAdd(liveStatus);
134971
+ trimHistory();
133881
134972
  return added;
133882
134973
  }
133883
- return originalTranscriptAdd(child, index);
134974
+ const added = originalTranscriptAdd(child, index);
134975
+ trimHistory();
134976
+ return added;
133884
134977
  };
133885
134978
  const queueDock = new otui.BoxRenderable(r, {
133886
134979
  id: "queue-dock",
@@ -134421,6 +135514,7 @@ async function createShellChrome(otui, renderer, opts) {
134421
135514
  } catch {}
134422
135515
  try {
134423
135516
  r.off(otui.CliRenderEvents.FRAME, syncPromptPin);
135517
+ r.off(otui.CliRenderEvents.FRAME, restoreHistoryAnchor);
134424
135518
  } catch {}
134425
135519
  }
134426
135520
  };
@@ -134433,6 +135527,7 @@ var init_shell_chrome = __esm(() => {
134433
135527
  init_modal_host();
134434
135528
  init_debug_log();
134435
135529
  init_renderer_debug();
135530
+ init_transcript_history();
134436
135531
  SIDEBAR_TEXT_WIDTH = SIDEBAR_WIDTH - SIDEBAR_BORDER_LEFT - SIDEBAR_PADDING_LEFT - SIDEBAR_PADDING_RIGHT;
134437
135532
  SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
134438
135533
  });
@@ -135770,7 +136865,7 @@ function openRouting(otui, chrome, options) {
135770
136865
  cwd: options.cwd,
135771
136866
  ...options.userConfigDir !== undefined ? { userConfigDir: options.userConfigDir } : {}
135772
136867
  };
135773
- const loadProviders = options.providers ?? defaultProviders;
136868
+ const loadProviders = options.providers ?? loadRoutingProviders;
135774
136869
  const loadProfiles = options.loadProfiles ?? ((dir) => loadModelProfiles(dir));
135775
136870
  const rendererHint = options.renderer ?? chrome?.renderer;
135776
136871
  const panelRows = typeof rendererHint?.width === "number" && typeof rendererHint.height === "number" ? modalBodyRows(resolveModalPanelSize(rendererHint.width, rendererHint.height).height) : 13;
@@ -135985,7 +137080,7 @@ function describePickerRowProfile(opt, profiles) {
135985
137080
  parts.push("non-chat/free \u2014 never auto-derived");
135986
137081
  return parts.join(" \xB7 ");
135987
137082
  }
135988
- async function defaultProviders() {
137083
+ async function loadRoutingProviders() {
135989
137084
  await Promise.resolve().then(() => init_provider_catalog());
135990
137085
  await Promise.resolve().then(() => init_shell_config());
135991
137086
  const catalog = await loadOrRefreshProviderCatalog({ fetch, env: envWithSavedApiKeys() });
@@ -144312,52 +145407,77 @@ class NullClassifier {
144312
145407
  }
144313
145408
 
144314
145409
  // src/harness/decision/classify-turn.ts
144315
- async function withTimeout4(source, run, timeoutMs) {
145410
+ async function withTimeout4(source, run, timeoutMs, external) {
145411
+ const cancelled = () => ({ ok: false, reason: "classification cancelled" });
145412
+ if (external?.aborted)
145413
+ return cancelled();
145414
+ if (timeoutMs <= 0)
145415
+ return { ok: false, reason: "classification deadline exceeded" };
144316
145416
  const controller = new AbortController;
145417
+ const onExternalAbort = () => controller.abort();
145418
+ external?.addEventListener("abort", onExternalAbort, { once: true });
144317
145419
  const timer = setTimeout(() => controller.abort(), timeoutMs);
145420
+ let onAbort = () => {};
144318
145421
  try {
144319
- return await Promise.race([
144320
- run(controller.signal),
144321
- new Promise((resolve) => {
144322
- controller.signal.addEventListener("abort", () => resolve({ ok: false, reason: `${source} classifier timed out after ${timeoutMs}ms` }));
144323
- })
144324
- ]);
145422
+ const aborted = new Promise((resolve) => {
145423
+ onAbort = () => resolve(external?.aborted ? cancelled() : { ok: false, reason: `${source} classifier timed out after ${timeoutMs}ms` });
145424
+ controller.signal.addEventListener("abort", onAbort, { once: true });
145425
+ });
145426
+ return await Promise.race([run(controller.signal), aborted]);
144325
145427
  } catch (error) {
144326
145428
  return { ok: false, reason: `${source} classifier errored: ${error instanceof Error ? error.message : String(error)}` };
144327
145429
  } finally {
144328
145430
  clearTimeout(timer);
145431
+ external?.removeEventListener("abort", onExternalAbort);
145432
+ controller.signal.removeEventListener("abort", onAbort);
145433
+ controller.abort();
144329
145434
  }
144330
145435
  }
144331
145436
  async function classifyTurn(line, categories, opts = {}) {
144332
145437
  if (isSlashCommandLine(line) || categories.length === 0)
144333
145438
  return;
145439
+ if (opts.signal?.aborted)
145440
+ return { result: { ok: false, reason: "classification cancelled" }, trace: [] };
144334
145441
  const deterministic = classifyDeterministic(line);
144335
145442
  if (deterministic !== undefined && categories.includes(deterministic)) {
144336
145443
  const result = { ok: true, category: deterministic, confidence: 1, source: "deterministic" };
144337
145444
  return { result, trace: [{ source: "deterministic", result }] };
144338
145445
  }
144339
145446
  const timeoutMs = opts.timeoutMs ?? CLASSIFY_TURN_TIMEOUT_MS;
145447
+ const deadline = performance.now() + Math.max(0, timeoutMs);
145448
+ const remaining = () => Math.max(0, deadline - performance.now());
145449
+ const fallback = opts.fallbackModel !== undefined ? opts.fallbackModel : opts.sessionProvider !== undefined && opts.sessionModel !== undefined ? { providerId: opts.sessionProvider, modelId: opts.sessionModel } : undefined;
144340
145450
  const trace = [];
145451
+ const stopped = () => opts.signal?.aborted ? { result: { ok: false, reason: "classification cancelled" }, trace } : remaining() <= 0 ? { result: { ok: false, reason: "classification deadline exceeded" }, trace } : undefined;
145452
+ const initialStop = stopped();
145453
+ if (initialStop)
145454
+ return initialStop;
144341
145455
  if (opts.jevEnabled === true) {
144342
145456
  const credential = resolveJevClassifierCredential(opts.env, opts.dir);
144343
145457
  if (credential.key !== undefined) {
144344
145458
  const jev = new JevTaskClassifier({ ...opts.fetch !== undefined ? { fetch: opts.fetch } : {}, ...opts.env !== undefined ? { env: opts.env } : {}, ...opts.dir !== undefined ? { dir: opts.dir } : {}, timeoutMs });
144345
- const jevResult = await withTimeout4("jev", (signal) => jev.classify(line, categories, { signal }), timeoutMs);
145459
+ const jevResult = await withTimeout4("jev", (signal) => jev.classify(line, categories, { signal }), Math.min(remaining(), fallback != null ? timeoutMs / 2 : timeoutMs), opts.signal);
144346
145460
  trace.push({ source: "jev", result: jevResult });
145461
+ const jevStop = stopped();
145462
+ if (jevStop)
145463
+ return jevStop;
144347
145464
  if (jevResult.ok)
144348
145465
  return { result: jevResult, trace };
144349
145466
  }
144350
145467
  }
144351
- if (opts.sessionProvider !== undefined && opts.sessionModel !== undefined) {
145468
+ if (fallback != null) {
144352
145469
  const mainModel = new MainModelTaskClassifier({
144353
- provider: opts.sessionProvider,
144354
- model: opts.sessionModel,
145470
+ provider: fallback.providerId,
145471
+ model: fallback.modelId,
144355
145472
  ...opts.env !== undefined ? { env: opts.env } : {},
144356
145473
  ...opts.fetch !== undefined ? { fetch: opts.fetch } : {},
144357
145474
  ...opts.providerFactory !== undefined ? { providerFactory: opts.providerFactory } : {}
144358
145475
  });
144359
- const mainModelResult = await withTimeout4("main-model", (signal) => mainModel.classify(line, categories, { signal }), timeoutMs);
145476
+ const mainModelResult = await withTimeout4("main-model", (signal) => mainModel.classify(line, categories, { signal }), remaining(), opts.signal);
144360
145477
  trace.push({ source: "main-model", result: mainModelResult });
145478
+ const mainStop = stopped();
145479
+ if (mainStop)
145480
+ return mainStop;
144361
145481
  if (mainModelResult.ok)
144362
145482
  return { result: mainModelResult, trace };
144363
145483
  }
@@ -144371,42 +145491,151 @@ var init_classify_turn = __esm(() => {
144371
145491
  init_main_model_classifier();
144372
145492
  });
144373
145493
 
145494
+ // src/harness/routing/classifier-model.ts
145495
+ function selectClassifierModel(detected, profiles, allowed) {
145496
+ const candidates = [];
145497
+ for (const provider of detected) {
145498
+ for (const modelId of provider.models ?? []) {
145499
+ const profile = profiles[profileKey(provider.name, modelId)];
145500
+ if (!profile?.available || !isModelDerivable(profile))
145501
+ continue;
145502
+ if (profile.strengthTier.value === "light" || profile.strengthTier.source === "unknown" || profile.strengthTier.source === "guessed")
145503
+ continue;
145504
+ if (allowed(provider.name, modelId))
145505
+ candidates.push(profile);
145506
+ }
145507
+ }
145508
+ candidates.sort((a, b) => Number(a.strengthTier.value === "deep") - Number(b.strengthTier.value === "deep") || b.priority.value - a.priority.value || profileKey(a.providerId, a.modelId).localeCompare(profileKey(b.providerId, b.modelId)));
145509
+ const selected = candidates[0];
145510
+ return selected === undefined ? undefined : { providerId: selected.providerId, modelId: selected.modelId };
145511
+ }
145512
+ var init_classifier_model = __esm(() => {
145513
+ init_model_profile();
145514
+ });
145515
+
144374
145516
  // src/tui/routing-classifier-source.ts
145517
+ async function loadTurnProviders(load, signal, timeoutMs) {
145518
+ if (signal?.aborted || timeoutMs <= 0)
145519
+ return;
145520
+ let stop;
145521
+ const stopped = new Promise((resolve) => {
145522
+ stop = () => resolve(undefined);
145523
+ });
145524
+ signal?.addEventListener("abort", stop, { once: true });
145525
+ const timer = setTimeout(stop, timeoutMs);
145526
+ try {
145527
+ return await Promise.race([Promise.resolve().then(load), stopped]);
145528
+ } finally {
145529
+ clearTimeout(timer);
145530
+ signal?.removeEventListener("abort", stop);
145531
+ }
145532
+ }
144375
145533
  async function runRoutingClassifierForTurn(line, opts) {
145534
+ if (opts.signal?.aborted)
145535
+ return;
144376
145536
  if (!opts.enabled)
144377
145537
  return;
145538
+ const deadline = performance.now() + Math.max(0, opts.timeoutMs ?? CLASSIFY_TURN_TIMEOUT_MS);
145539
+ const profiles = loadModelProfiles(opts.userConfigDir);
145540
+ const providers = opts.providers === undefined ? opts.detected : await loadTurnProviders(opts.providers, opts.signal, Math.max(0, deadline - performance.now()));
145541
+ if (providers === undefined)
145542
+ return;
145543
+ if (opts.signal?.aborted)
145544
+ return;
145545
+ const env = envWithSavedApiKeys(opts.env ?? process.env);
145546
+ const external = await resolveExternalSetting({ cwd: opts.cwd, ...opts.userConfigDir !== undefined ? { dir: opts.userConfigDir } : {} });
145547
+ if (opts.signal?.aborted)
145548
+ return;
145549
+ const externalAllowed = externalAllowedConnectedPredicate(connectedPredicateFrom(providers), external.value === "on", loadExternalProvidersConfig(opts.userConfigDir).config);
145550
+ const fallbackModel = selectClassifierModel(providers, profiles, (providerId, modelId) => externalAllowed(providerId, modelId) && (opts.classifierAllowed?.(providerId, modelId) ?? true) && (opts.providerFactory !== undefined || hasCredential(providerId, env)));
144378
145551
  const classification = await classifyTurn(line, TURN_CLASSIFIER_CATEGORIES, {
144379
- jevEnabled: opts.jevEnabled,
144380
- sessionProvider: opts.sessionProvider,
144381
- sessionModel: opts.sessionModel,
145552
+ jevEnabled: opts.jevEnabled && opts.jevAllowed !== false && external.value === "on",
145553
+ ...opts.signal !== undefined ? { signal: opts.signal } : {},
145554
+ fallbackModel: fallbackModel ?? null,
145555
+ ...opts.providerFactory !== undefined ? { providerFactory: opts.providerFactory } : {},
144382
145556
  ...opts.env !== undefined ? { env: opts.env } : {},
144383
145557
  ...opts.fetch !== undefined ? { fetch: opts.fetch } : {},
144384
- timeoutMs: opts.timeoutMs ?? CLASSIFY_TURN_TIMEOUT_MS
145558
+ timeoutMs: Math.max(0, deadline - performance.now())
144385
145559
  });
144386
- if (classification === undefined || !classification.result.ok)
145560
+ if (opts.signal?.aborted)
145561
+ return;
145562
+ if (classification === undefined)
144387
145563
  return;
145564
+ const baseline = { providerId: opts.sessionProvider, modelId: opts.sessionModel };
145565
+ const skippedJev = opts.jevEnabled && opts.jevAllowed === false ? "jev: caller policy denied" : opts.jevEnabled && external.value !== "on" ? "jev: external policy denied" : opts.jevEnabled && resolveJevClassifierCredential(opts.env).key === undefined ? "jev: credentials unavailable" : undefined;
145566
+ if (!classification.result.ok) {
145567
+ const failures = classification.trace.flatMap((stage) => stage.result.ok ? [] : [`${stage.source}: ${stage.result.reason}`]);
145568
+ return {
145569
+ classification,
145570
+ baseline,
145571
+ assignment: { kind: "session-default" },
145572
+ fallbackReason: redactSensitiveText([
145573
+ skippedJev,
145574
+ ...failures,
145575
+ fallbackModel === undefined ? "no available authorized sufficient classifier" : undefined,
145576
+ classification.result.reason
145577
+ ].filter(Boolean).join("; "))
145578
+ };
145579
+ }
144388
145580
  const category = classification.result.category;
144389
145581
  const location = { cwd: opts.cwd, ...opts.userConfigDir !== undefined ? { userConfigDir: opts.userConfigDir } : {} };
144390
145582
  const [project, user] = await Promise.all([loadRoutingConfig("project", location), loadRoutingConfig("user", location)]);
144391
- const connected = connectedPredicateFrom(opts.detected);
144392
- const profiles = loadModelProfiles(opts.userConfigDir);
145583
+ if (opts.signal?.aborted)
145584
+ return;
145585
+ const connected = connectedPredicateFrom(providers);
144393
145586
  const available = availablePredicateFromProfiles(profiles);
144394
- const sessionProvider = opts.detected.find((p) => p.name === opts.sessionProvider);
145587
+ let fallbackReason;
145588
+ const connectedForTurn = (providerId, modelId) => {
145589
+ if (modelId !== undefined) {
145590
+ const reason = !connected(providerId, modelId) ? "model not connected" : !externalAllowed(providerId, modelId) ? "external policy denied" : !available(providerId, modelId) ? "model unavailable" : !(opts.executorAllowed?.(providerId, modelId) ?? true) ? "executor policy denied" : opts.providerFactory === undefined && !hasCredential(providerId, env) ? "credentials unavailable" : undefined;
145591
+ if (reason !== undefined) {
145592
+ fallbackReason ??= `${providerId}/${modelId}: ${reason}`;
145593
+ return false;
145594
+ }
145595
+ return true;
145596
+ }
145597
+ const defaultId = resolveProviderDefaultModelId(providerId);
145598
+ const reason = !connected(providerId) ? "provider not connected" : defaultId === undefined ? "no documented provider default" : !connected(providerId, defaultId) ? "default model not connected" : !externalAllowed(providerId, defaultId) ? "external policy denied" : !available(providerId, defaultId) ? "default model unavailable" : !(opts.executorAllowed?.(providerId, defaultId) ?? true) ? "executor policy denied" : opts.providerFactory === undefined && !hasCredential(providerId, env) ? "credentials unavailable" : undefined;
145599
+ if (reason !== undefined) {
145600
+ fallbackReason ??= `${providerId} (provider default): ${reason}`;
145601
+ return false;
145602
+ }
145603
+ return true;
145604
+ };
145605
+ const sessionProvider = providers.find((p) => p.name === opts.sessionProvider);
144395
145606
  const models = sessionProvider?.models ?? [opts.sessionModel];
144396
145607
  const derived = deriveDefaultTable(opts.sessionProvider, models, profiles, opts.sessionModel);
144397
- const resolved = resolveCategoryDetailed(category, { project: project.table, user: user.table, derived }, connected, available);
144398
- if (resolved.assignment.kind === "session-default") {
144399
- return { classification, category, assignment: resolved.assignment };
144400
- }
145608
+ const resolved = resolveCategoryDetailed(category, { project: project.table, user: user.table, derived }, connectedForTurn, available);
145609
+ fallbackReason ??= resolved.rejected === undefined ? skippedJev : describeRejectionNotice(resolved.rejected, resolved.assignment);
145610
+ const stageFailures = classification.trace.flatMap((stage) => stage.result.ok ? [] : [`${stage.source}: ${stage.result.reason}`]);
145611
+ const reasons = [...new Set([skippedJev, ...stageFailures, fallbackReason].filter((reason) => reason !== undefined))];
145612
+ fallbackReason = reasons.length === 0 ? undefined : redactSensitiveText(reasons.join("; "));
145613
+ const result = {
145614
+ classification,
145615
+ category,
145616
+ baseline,
145617
+ assignment: resolved.assignment,
145618
+ ...fallbackReason !== undefined ? { fallbackReason: redactSensitiveText(fallbackReason) } : {}
145619
+ };
145620
+ if (resolved.assignment.kind === "session-default")
145621
+ return result;
144401
145622
  if (resolved.assignment.kind === "model") {
144402
- return { classification, category, assignment: resolved.assignment, routed: { providerId: resolved.assignment.providerId, modelId: resolved.assignment.modelId } };
145623
+ return { ...result, routed: { providerId: resolved.assignment.providerId, modelId: resolved.assignment.modelId } };
144403
145624
  }
144404
- return { classification, category, assignment: resolved.assignment };
145625
+ const modelId = resolveProviderDefaultModelId(resolved.assignment.providerId);
145626
+ return modelId === undefined ? result : { ...result, routed: { providerId: resolved.assignment.providerId, modelId } };
145627
+ }
145628
+ function classificationSource(result) {
145629
+ if (!result.classification.result.ok)
145630
+ return "none";
145631
+ const source = result.classification.result.source;
145632
+ return source === "jev" ? "JEV" : source === "main-model" ? "fallback" : source;
144405
145633
  }
144406
145634
  function renderRoutingFallbackLine(result) {
144407
145635
  if (result === undefined)
144408
145636
  return "[route fallback: no classifier category; using session model]";
144409
- return `[route fallback: ${result.category} -> ${describeAssignment(result.assignment)}; using session model]`;
145637
+ const target = result.baseline === undefined ? "session model" : `${result.baseline.providerId}/${result.baseline.modelId}`;
145638
+ return redactSensitiveText(`[route fallback: ${result.category ?? "no classifier category"} -> ${describeAssignment(result.assignment)}; classifier: ${classificationSource(result)}${result.fallbackReason !== undefined ? `; ${result.fallbackReason}` : ""}; using ${target}]`);
144410
145639
  }
144411
145640
  function pct6(n) {
144412
145641
  return `${Math.round(n * 100)}%`;
@@ -144414,9 +145643,9 @@ function pct6(n) {
144414
145643
  function renderRoutingTagLine(result) {
144415
145644
  if (result.routed === undefined)
144416
145645
  return;
144417
- const source = result.classification.result.ok ? result.classification.result.source : "none";
145646
+ const source = classificationSource(result);
144418
145647
  const confidence = result.classification.result.ok ? ` ${pct6(result.classification.result.confidence)}` : "";
144419
- return `[${result.category} -> ${result.routed.providerId}/${result.routed.modelId}] (${source}${confidence})`;
145648
+ return `[${result.category} -> ${result.routed.providerId}/${result.routed.modelId}] (${source}${confidence}${result.fallbackReason !== undefined ? `; fallback: ${result.fallbackReason}` : ""})`;
144420
145649
  }
144421
145650
  function renderRoutingSidebarValue(enabled, routedCount, lastCategory) {
144422
145651
  if (!enabled)
@@ -144429,6 +145658,14 @@ var init_routing_classifier_source = __esm(() => {
144429
145658
  init_table();
144430
145659
  init_config2();
144431
145660
  init_derive_default_table();
145661
+ init_provider_default();
145662
+ init_classifier_model();
145663
+ init_single_turn();
145664
+ init_shell_config();
145665
+ init_service2();
145666
+ init_jev_classifier();
145667
+ init_external_switch();
145668
+ init_external_providers();
144432
145669
  init_model_profile();
144433
145670
  TURN_CLASSIFIER_CATEGORIES = ROUTING_CATEGORIES.filter((c) => c !== "default");
144434
145671
  });
@@ -150462,6 +151699,53 @@ function onKeypress3(r, handler) {
150462
151699
  r._internalKeyInput.onInternal("keypress", handler);
150463
151700
  return () => r._internalKeyInput.offInternal("keypress", handler);
150464
151701
  }
151702
+ function focusedNode(r) {
151703
+ return r.currentFocusedRenderable;
151704
+ }
151705
+ function canTakeKeyboard(node, root) {
151706
+ let current = node;
151707
+ while (current !== null && current !== undefined) {
151708
+ if (current.isDestroyed === true || current.visible === false) {
151709
+ return false;
151710
+ }
151711
+ if (current === root) {
151712
+ return true;
151713
+ }
151714
+ current = current.parent;
151715
+ }
151716
+ return false;
151717
+ }
151718
+ function resolveHandBack(start, root) {
151719
+ const seen = new Set;
151720
+ let candidate = start;
151721
+ while (candidate !== null && !seen.has(candidate)) {
151722
+ if (canTakeKeyboard(candidate, root)) {
151723
+ return candidate;
151724
+ }
151725
+ seen.add(candidate);
151726
+ candidate = handBackFrom.get(candidate) ?? modalFocusFallback(candidate);
151727
+ }
151728
+ return;
151729
+ }
151730
+ function handBackFocus(r, previous, mounted, title) {
151731
+ if (r.isDestroyed === true) {
151732
+ return;
151733
+ }
151734
+ const current = focusedNode(r);
151735
+ if (current !== null && !mounted.some((node) => containsNode(node, current))) {
151736
+ debugEvent("choice.focus-kept", { title, focused: current.id ?? null });
151737
+ return;
151738
+ }
151739
+ const target = resolveHandBack(previous, r.root);
151740
+ if (target === undefined || modalHides(r, target)) {
151741
+ debugEvent("choice.focus-not-restored", { title, previous: previous?.id ?? null, modal: target !== undefined });
151742
+ return;
151743
+ }
151744
+ try {
151745
+ target.focus();
151746
+ debugEvent("choice.focus-restored", { title, focused: target.id ?? null });
151747
+ } catch {}
151748
+ }
150465
151749
  function dockBusy(dock) {
150466
151750
  return (dockDepth.get(dock) ?? 0) > 0;
150467
151751
  }
@@ -150518,6 +151802,7 @@ function presentComposerChoice(otui, r, dock, request) {
150518
151802
  return Promise.resolve({ kind: "id", id: request.cancelId });
150519
151803
  }
150520
151804
  return new Promise((resolve) => {
151805
+ const previousFocus = focusedNode(r);
150521
151806
  const ownEnabled = request.ownAnswer !== undefined;
150522
151807
  const options = request.options.map((o) => ({
150523
151808
  ...o,
@@ -150568,6 +151853,7 @@ function presentComposerChoice(otui, r, dock, request) {
150568
151853
  scrollY: true,
150569
151854
  contentOptions: { flexDirection: "column" }
150570
151855
  });
151856
+ handBackFrom.set(subtitleScroll, previousFocus);
150571
151857
  for (const [i, line] of subtitleLines.entries()) {
150572
151858
  subtitleScroll.content.add(new otui.TextRenderable(r, {
150573
151859
  id: `ch-sub-line-${i}-${Date.now()}`,
@@ -150591,6 +151877,7 @@ function presentComposerChoice(otui, r, dock, request) {
150591
151877
  contentOptions: { flexDirection: "column" }
150592
151878
  });
150593
151879
  dock.add(optionsScroll);
151880
+ handBackFrom.set(optionsScroll, previousFocus);
150594
151881
  const optionsBox = optionsScroll.content;
150595
151882
  const rows = [];
150596
151883
  const paintOptions = () => {
@@ -150651,6 +151938,7 @@ function presentComposerChoice(otui, r, dock, request) {
150651
151938
  content: otui.t`${dimChunk(otui, picked === undefined ? "Your own answer \u2014 Enter sends, Esc goes back to the options" : `Reason for "${picked.label}" \u2014 Enter sends, Esc goes back to the options`)}`
150652
151939
  });
150653
151940
  ownInput = new otui.InputRenderable(r, { id: `ch-own-input-${Date.now()}`, value: "", marginTop: 0 });
151941
+ handBackFrom.set(ownInput, previousFocus);
150654
151942
  dock.add(ownNote);
150655
151943
  dock.add(ownInput);
150656
151944
  paintTitle();
@@ -150707,7 +151995,8 @@ function presentComposerChoice(otui, r, dock, request) {
150707
151995
  id: rowId,
150708
151996
  width: "100%",
150709
151997
  flexDirection: "column",
150710
- onMouseDown: () => {
151998
+ onMouseDown: (event) => {
151999
+ event.preventDefault();
150711
152000
  finish(o.id, "click");
150712
152001
  }
150713
152002
  });
@@ -150765,6 +152054,9 @@ function presentComposerChoice(otui, r, dock, request) {
150765
152054
  }
150766
152055
  } catch {}
150767
152056
  dock.visible = false;
152057
+ queueMicrotask(() => {
152058
+ handBackFocus(r, previousFocus, mounted, request.title);
152059
+ });
150768
152060
  setTimeout(() => {
150769
152061
  for (const node of mounted) {
150770
152062
  try {
@@ -150874,12 +152166,13 @@ function presentComposerChoice(otui, r, dock, request) {
150874
152166
  });
150875
152167
  });
150876
152168
  }
150877
- var OWN_ROW_ID = "\x00own-answer", DEFAULT_ACCEPT_DELAY_MS = 400, MAX_SUBTITLE_CHARS = 8000, MAX_SUBTITLE_ROWS = 10, SUBTITLE_INLINE_MAX_CHARS = 76, OPTION_ROWS = 2, MAX_OPTIONS_ROWS = 16, dockChain, dockDepth;
152169
+ var OWN_ROW_ID = "\x00own-answer", DEFAULT_ACCEPT_DELAY_MS = 400, MAX_SUBTITLE_CHARS = 8000, MAX_SUBTITLE_ROWS = 10, SUBTITLE_INLINE_MAX_CHARS = 76, OPTION_ROWS = 2, MAX_OPTIONS_ROWS = 16, handBackFrom, dockChain, dockDepth;
150878
152170
  var init_composer_choice = __esm(() => {
150879
152171
  init_theme();
150880
152172
  init_theme_text();
150881
152173
  init_modal_host();
150882
152174
  init_debug_log();
152175
+ handBackFrom = new WeakMap;
150883
152176
  dockChain = new WeakMap;
150884
152177
  dockDepth = new WeakMap;
150885
152178
  });
@@ -155518,6 +156811,15 @@ var init_digest_summary = __esm(() => {
155518
156811
  import { randomUUID as randomUUID41 } from "crypto";
155519
156812
  import { mkdir as mkdir68, mkdtemp as mkdtemp10, rm as rm26, writeFile as writeFile44 } from "fs/promises";
155520
156813
  import path303 from "path";
156814
+ function scrubDigestReportLocator(locator, env) {
156815
+ const scrubbed = scrubGrantedOutput(locator, env);
156816
+ const basename = path303.basename(locator);
156817
+ if (!/^dig-\d{8}T\d{6}-[a-f0-9]{8}\.md$/.test(basename))
156818
+ return scrubbed;
156819
+ const from = locator.length - basename.length;
156820
+ const phones = detectPii(locator).filter((m) => m.mask === "phone" && m.start >= from);
156821
+ return scrubbed === applyRedaction(locator, phones) ? locator : scrubbed;
156822
+ }
155521
156823
  function capForDelivery(text, reportPath) {
155522
156824
  if (text.length <= DELIVERY_TEXT_MAX)
155523
156825
  return text;
@@ -155691,8 +156993,8 @@ async function runLocked2(projectRoot, entry, action, deps, runId, now) {
155691
156993
  const statusReason = stopReason ?? (crashed !== undefined ? `the run threw: ${crashed}` : everySourceFailed ? "no source could be read" : undefined);
155692
156994
  const statusDetail = everySourceFailed && stopReason === undefined && crashed === undefined ? partial.slice(0, 6).map((f) => `
155693
156995
  - ${f.source}: ${f.detail}`).join("") : "";
155694
- const message = statusReason !== undefined ? scrubGrantedOutput(`Digest ${entry.name} stopped \u2014 ${statusReason}${statusDetail}${reportPath !== undefined ? `
155695
- Report: ${reportPath}` : ""}`, env) : capForDelivery(scrubGrantedOutput(summaryText !== undefined ? `Summary (written by the model)
156996
+ const message = statusReason !== undefined ? `${scrubGrantedOutput(`Digest ${entry.name} stopped \u2014 ${statusReason}${statusDetail}`, env)}${reportPath !== undefined ? `
156997
+ Report: ${scrubDigestReportLocator(reportPath, env)}` : ""}` : capForDelivery(scrubGrantedOutput(summaryText !== undefined ? `Summary (written by the model)
155696
156998
  ${summaryText}
155697
156999
 
155698
157000
  ${body}` : body, env), reportPath);
@@ -155769,6 +157071,7 @@ var init_digest_run = __esm(() => {
155769
157071
  init_trigger_dispatch();
155770
157072
  init_fs();
155771
157073
  init_maintenance_lock();
157074
+ init_service2();
155772
157075
  init_digest_config();
155773
157076
  init_granted_tools();
155774
157077
  init_schedule_verify();
@@ -160059,7 +161362,6 @@ async function launchTuiAgentShell(opts) {
160059
161362
  let routingEnabled = loadShellConfig().routingClassifier?.enabled === true;
160060
161363
  let routingRoutedCount = 0;
160061
161364
  let routingLastCategory;
160062
- let sessionModelExplicit = false;
160063
161365
  const refreshRoutingSidebar = () => {
160064
161366
  clearTranscriptChildren(sbRoute);
160065
161367
  if (!routingEnabled)
@@ -161950,7 +163252,6 @@ ${formatThemeList(getThemeId())}`);
161950
163252
  };
161951
163253
  const switchTo = async (ns) => {
161952
163254
  currentSel = ns;
161953
- sessionModelExplicit = true;
161954
163255
  deps = {
161955
163256
  ...await opts.makeAgentDeps(ns, liveSlateSession, busClientRef),
161956
163257
  onContextCompaction,
@@ -163438,23 +164739,22 @@ ${formatThemeList(getThemeId())}`);
163438
164739
  const sessionDeps = deps;
163439
164740
  (async () => {
163440
164741
  let deps = sessionDeps;
164742
+ const turnSignal = foregroundOperation.signal;
163441
164743
  let routingOutcome;
163442
- if (routingEnabled && sessionModelExplicit && origin === "operator") {
163443
- io.onSystem?.(`[route skipped: manual /model selection pins this session]
163444
- `);
163445
- }
163446
- if (routingEnabled && !sessionModelExplicit && origin === "operator" && line.trim().length > 0) {
164744
+ if (routingEnabled && origin === "operator" && line.trim().length > 0) {
163447
164745
  try {
163448
164746
  routingOutcome = await runRoutingClassifierForTurn(line, {
163449
164747
  enabled: routingEnabled,
164748
+ signal: turnSignal,
163450
164749
  jevEnabled: routingEnabled,
163451
164750
  cwd: opts.session?.cwd ?? process.cwd(),
163452
164751
  detected: opts.detected ?? [],
164752
+ providers: loadRoutingProviders,
163453
164753
  sessionProvider: currentSel.provider,
163454
164754
  sessionModel: currentSel.model
163455
164755
  });
163456
164756
  } catch {}
163457
- if (routingOutcome?.routed !== undefined) {
164757
+ if (!turnSignal.aborted && routingOutcome?.routed !== undefined) {
163458
164758
  try {
163459
164759
  deps = {
163460
164760
  ...await opts.makeAgentDeps({ provider: routingOutcome.routed.providerId, model: routingOutcome.routed.modelId }, liveSlateSession, busClientRef),
@@ -163463,19 +164763,22 @@ ${formatThemeList(getThemeId())}`);
163463
164763
  ...sessionDeps.busAck !== undefined ? { busAck: sessionDeps.busAck } : {},
163464
164764
  ...sessionDeps.busLeases !== undefined ? { busLeases: sessionDeps.busLeases } : {}
163465
164765
  };
163466
- routingRoutedCount += 1;
163467
- routingLastCategory = routingOutcome.category;
163468
- refreshRoutingSidebar();
163469
- const tag = renderRoutingTagLine(routingOutcome);
163470
- if (tag !== undefined)
163471
- io.onSystem?.(`${tag}
164766
+ if (!turnSignal.aborted) {
164767
+ routingRoutedCount += 1;
164768
+ routingLastCategory = routingOutcome.category;
164769
+ refreshRoutingSidebar();
164770
+ const tag = renderRoutingTagLine(routingOutcome);
164771
+ if (tag !== undefined)
164772
+ io.onSystem?.(`${tag}
163472
164773
  `);
164774
+ }
163473
164775
  } catch {
163474
164776
  deps = sessionDeps;
163475
- io.onSystem?.(`[route fallback: selected model could not be prepared; check availability and external policy; using session model]
164777
+ if (!turnSignal.aborted)
164778
+ io.onSystem?.(`[route fallback: selected model could not be prepared; check availability and external policy; using session model]
163476
164779
  `);
163477
164780
  }
163478
- } else {
164781
+ } else if (!turnSignal.aborted) {
163479
164782
  io.onSystem?.(`${renderRoutingFallbackLine(routingOutcome)}
163480
164783
  `);
163481
164784
  }
@@ -163485,12 +164788,11 @@ ${formatThemeList(getThemeId())}`);
163485
164788
  syncArchive();
163486
164789
  rewindRecorder.beginTurn({ archiveIndex: archive.length, prompt: line });
163487
164790
  const foregroundIo = createForegroundAgentIoFacade(foregroundOperation, operation, io, telegramTurn ? telegramTurnIo : {});
163488
- const turnSignal = foregroundOperation.signal;
163489
- runAgentTurn(foregroundIo, deps, history, line, {
164791
+ (turnSignal.aborted ? Promise.resolve() : runAgentTurn(foregroundIo, deps, history, line, {
163490
164792
  signal: turnSignal,
163491
164793
  ...origin === "operator" ? {} : { origin },
163492
164794
  ...slateSession !== undefined ? { slateSession, pruneArchive: true } : {}
163493
- }).finally(() => {
164795
+ })).finally(() => {
163494
164796
  foregroundOperation.settle(operation);
163495
164797
  remoteQueue.turnSettled({ failed: turnFailed, aborted: turnSignal.aborted });
163496
164798
  if (foregroundOperation.isDisposed)
@@ -201678,7 +202980,22 @@ async function startServeListener(input) {
201678
202980
  } catch (error) {
201679
202981
  console.error(`keryx serve: remote surface did not close cleanly: ${error instanceof Error ? error.message : String(error)}`);
201680
202982
  }
201681
- await server.stop(true);
202983
+ if (input.remote === undefined) {
202984
+ await server.stop(true);
202985
+ } else {
202986
+ let timer;
202987
+ try {
202988
+ await Promise.race([
202989
+ server.stop(false),
202990
+ new Promise((resolve) => {
202991
+ timer = setTimeout(resolve, 250);
202992
+ })
202993
+ ]);
202994
+ await server.stop(true);
202995
+ } finally {
202996
+ clearTimeout(timer);
202997
+ }
202998
+ }
201682
202999
  state = "stopped";
201683
203000
  }
201684
203001
  }
@@ -213769,7 +215086,7 @@ function parseCategory(raw) {
213769
215086
  return;
213770
215087
  return raw;
213771
215088
  }
213772
- async function defaultProviders2() {
215089
+ async function defaultProviders() {
213773
215090
  await Promise.resolve().then(() => init_provider_catalog());
213774
215091
  const catalog = await loadOrRefreshProviderCatalog({ fetch, env: envWithSavedApiKeys() });
213775
215092
  return catalogToFlatPickerProviders(catalog);
@@ -213782,7 +215099,7 @@ function defaultSession(userConfigDir) {
213782
215099
  return { providerId: cfg.provider, modelId: cfg.model };
213783
215100
  }
213784
215101
  async function runList5(args, location, deps) {
213785
- const loadProviders = deps.providers ?? defaultProviders2;
215102
+ const loadProviders = deps.providers ?? defaultProviders;
213786
215103
  const loadSession = deps.session ?? (() => defaultSession(location.userConfigDir));
213787
215104
  const [project, user, providers] = await Promise.all([
213788
215105
  loadRoutingConfig("project", location),