pi-smart-compact 9.2.1 → 9.3.1

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.
@@ -156,7 +156,7 @@ function formatProviderEvaluation(report) {
156
156
  import fs2 from "fs";
157
157
 
158
158
  // src/constants.ts
159
- var VERSION = "9.2.1";
159
+ var VERSION = "9.3.1";
160
160
  var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
161
161
  var COMPACT_SYSTEM_PREFIX = "You are an expert conversation summarizer for a coding agent. " + "Produce structured markdown summaries. " + "Follow output format exactly. " + "Use EXACT names \u2014 never paraphrase code identifiers. " + "Trust deterministic extraction data over intuition.";
162
162
  var PROFILES = {
@@ -400,6 +400,188 @@ function lruSet(m, key, value, max) {
400
400
  }
401
401
 
402
402
  // src/utils/tokens.ts
403
+ var PROVIDER_MAP = {
404
+ "zai-anthropic": {
405
+ maxOutputTokens: 8192,
406
+ supportsTools: "probe",
407
+ jsonReliability: "high",
408
+ instructionFollowing: "high",
409
+ tokenRatioEstimate: 3.5,
410
+ concurrencyLimit: 3,
411
+ cacheStrategy: "anthropic",
412
+ timeoutMultiplier: 1.2,
413
+ singlePassTokenMultiplier: 1,
414
+ multimodal: "metadata-only"
415
+ },
416
+ "kimi-coding": {
417
+ maxOutputTokens: 8192,
418
+ supportsTools: "probe",
419
+ jsonReliability: "high",
420
+ instructionFollowing: "high",
421
+ tokenRatioEstimate: 3.5,
422
+ concurrencyLimit: 2,
423
+ cacheStrategy: "anthropic",
424
+ timeoutMultiplier: 1.5,
425
+ singlePassTokenMultiplier: 0.95,
426
+ multimodal: "metadata-only"
427
+ },
428
+ anthropic: {
429
+ maxOutputTokens: 8192,
430
+ supportsTools: true,
431
+ jsonReliability: "high",
432
+ instructionFollowing: "high",
433
+ tokenRatioEstimate: 3.5,
434
+ concurrencyLimit: 3,
435
+ cacheStrategy: "anthropic",
436
+ timeoutMultiplier: 1.2,
437
+ singlePassTokenMultiplier: 1,
438
+ multimodal: "native"
439
+ },
440
+ openai: {
441
+ maxOutputTokens: 16384,
442
+ supportsTools: true,
443
+ jsonReliability: "high",
444
+ instructionFollowing: "high",
445
+ tokenRatioEstimate: 4,
446
+ concurrencyLimit: 5,
447
+ cacheStrategy: "openai",
448
+ timeoutMultiplier: 1,
449
+ singlePassTokenMultiplier: 1.15,
450
+ multimodal: "native"
451
+ },
452
+ google: {
453
+ maxOutputTokens: 8192,
454
+ supportsTools: true,
455
+ jsonReliability: "high",
456
+ instructionFollowing: "high",
457
+ tokenRatioEstimate: 3.8,
458
+ concurrencyLimit: 3,
459
+ cacheStrategy: "openai",
460
+ timeoutMultiplier: 1.15,
461
+ singlePassTokenMultiplier: 1.1,
462
+ multimodal: "native"
463
+ },
464
+ deepseek: {
465
+ maxOutputTokens: 8192,
466
+ supportsTools: true,
467
+ jsonReliability: "medium",
468
+ instructionFollowing: "medium",
469
+ tokenRatioEstimate: 3.6,
470
+ concurrencyLimit: 2,
471
+ cacheStrategy: "none",
472
+ timeoutMultiplier: 1.5,
473
+ singlePassTokenMultiplier: 0.85,
474
+ multimodal: "metadata-only"
475
+ },
476
+ minimax: {
477
+ maxOutputTokens: 4096,
478
+ supportsTools: "probe",
479
+ jsonReliability: "medium",
480
+ instructionFollowing: "medium",
481
+ tokenRatioEstimate: 3.8,
482
+ concurrencyLimit: 2,
483
+ cacheStrategy: "anthropic",
484
+ timeoutMultiplier: 1.6,
485
+ singlePassTokenMultiplier: 0.8,
486
+ multimodal: "metadata-only"
487
+ },
488
+ "xiaomi-token-plan": {
489
+ maxOutputTokens: 8192,
490
+ supportsTools: "probe",
491
+ jsonReliability: "medium",
492
+ instructionFollowing: "medium",
493
+ tokenRatioEstimate: 3.3,
494
+ concurrencyLimit: 2,
495
+ cacheStrategy: "openai",
496
+ timeoutMultiplier: 1.35,
497
+ singlePassTokenMultiplier: 0.9,
498
+ multimodal: "metadata-only"
499
+ },
500
+ "xiaomi-mimo": {
501
+ maxOutputTokens: 8192,
502
+ supportsTools: "probe",
503
+ jsonReliability: "medium",
504
+ instructionFollowing: "medium",
505
+ tokenRatioEstimate: 3.3,
506
+ concurrencyLimit: 2,
507
+ cacheStrategy: "anthropic",
508
+ timeoutMultiplier: 1.35,
509
+ singlePassTokenMultiplier: 0.9,
510
+ multimodal: "metadata-only"
511
+ },
512
+ crofai: {
513
+ maxOutputTokens: 8192,
514
+ supportsTools: "probe",
515
+ jsonReliability: "medium",
516
+ instructionFollowing: "medium",
517
+ tokenRatioEstimate: 3.8,
518
+ concurrencyLimit: 3,
519
+ cacheStrategy: "none",
520
+ timeoutMultiplier: 1.2,
521
+ singlePassTokenMultiplier: 0.95,
522
+ multimodal: "metadata-only"
523
+ },
524
+ mistral: {
525
+ maxOutputTokens: 8192,
526
+ supportsTools: true,
527
+ jsonReliability: "high",
528
+ instructionFollowing: "high",
529
+ tokenRatioEstimate: 3.5,
530
+ concurrencyLimit: 3,
531
+ cacheStrategy: "openai",
532
+ timeoutMultiplier: 1.2,
533
+ singlePassTokenMultiplier: 1,
534
+ multimodal: "metadata-only"
535
+ },
536
+ xai: {
537
+ maxOutputTokens: 8192,
538
+ supportsTools: true,
539
+ jsonReliability: "medium",
540
+ instructionFollowing: "high",
541
+ tokenRatioEstimate: 3.8,
542
+ concurrencyLimit: 3,
543
+ cacheStrategy: "openai",
544
+ timeoutMultiplier: 1.2,
545
+ singlePassTokenMultiplier: 1,
546
+ multimodal: "native"
547
+ }
548
+ };
549
+ var PROVIDER_ALIASES = [
550
+ { pattern: /anthropic/i, provider: "anthropic" },
551
+ { pattern: /kimi/i, provider: "kimi-coding" },
552
+ { pattern: /zai/i, provider: "zai-anthropic" },
553
+ { pattern: /openai/i, provider: "openai" },
554
+ { pattern: /gpt/i, provider: "openai" },
555
+ { pattern: /google|gemini/i, provider: "google" },
556
+ { pattern: /deepseek/i, provider: "deepseek" },
557
+ { pattern: /minimax/i, provider: "minimax" },
558
+ { pattern: /xiaomi-mimo/i, provider: "xiaomi-mimo" },
559
+ { pattern: /xiaomi/i, provider: "xiaomi-token-plan" },
560
+ { pattern: /crofai/i, provider: "crofai" },
561
+ { pattern: /mistral/i, provider: "mistral" },
562
+ { pattern: /xai|grok/i, provider: "xai" }
563
+ ];
564
+ var DEFAULT_CAPS = {
565
+ maxOutputTokens: 8192,
566
+ supportsTools: "probe",
567
+ jsonReliability: "medium",
568
+ instructionFollowing: "medium",
569
+ tokenRatioEstimate: 3.8,
570
+ concurrencyLimit: 2,
571
+ cacheStrategy: "none",
572
+ timeoutMultiplier: 1.35,
573
+ singlePassTokenMultiplier: 0.9,
574
+ multimodal: "metadata-only"
575
+ };
576
+ function getProviderCaps(provider) {
577
+ if (PROVIDER_MAP[provider])
578
+ return PROVIDER_MAP[provider];
579
+ for (const { pattern, provider: key } of PROVIDER_ALIASES) {
580
+ if (pattern.test(provider))
581
+ return PROVIDER_MAP[key] ?? DEFAULT_CAPS;
582
+ }
583
+ return DEFAULT_CAPS;
584
+ }
403
585
  class TokenCalibrationStore {
404
586
  maxEntries;
405
587
  factors = new Map;
@@ -497,13 +679,54 @@ function buildUniquePathNeedles(filePath, allPaths) {
497
679
  });
498
680
  }
499
681
  function isKnownPathReference(ref, knownPaths) {
500
- const normalizedRef = normalizePath(ref);
682
+ const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
683
+ if (!normalizedRef)
684
+ return false;
685
+ const pathShaped = normalizedRef.includes("/");
501
686
  return knownPaths.some((path) => {
502
- const normalizedPath = normalizePath(path);
503
- return normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef);
687
+ const normalizedPath = normalizePath(path).replace(/^\/+/, "");
688
+ if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
689
+ return true;
690
+ if (normalizedPath.endsWith(normalizedRef)) {
691
+ const boundary = normalizedPath[normalizedPath.length - normalizedRef.length - 1];
692
+ if (boundary && !/[\w./-]/.test(boundary))
693
+ return true;
694
+ }
695
+ if (!pathShaped)
696
+ return false;
697
+ return normalizedPath.split("/").some((_, index, parts) => parts.slice(index).join("/").startsWith(normalizedRef + "/"));
504
698
  });
505
699
  }
506
700
 
701
+ // src/utils/logger.ts
702
+ var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
703
+ function warn(msg, err) {
704
+ const detail = err instanceof Error ? err.message : err ?? "";
705
+ console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
706
+ }
707
+
708
+ // src/infra/paths.ts
709
+ import path from "path";
710
+ import os from "os";
711
+ function home() {
712
+ return process.env.HOME ?? os.homedir();
713
+ }
714
+ function piAgentDir() {
715
+ return path.join(home(), ".pi", "agent");
716
+ }
717
+ function cacheDir() {
718
+ return path.join(piAgentDir(), ".cache");
719
+ }
720
+ function smartCompactCacheDir() {
721
+ return path.join(cacheDir(), "smart-compact");
722
+ }
723
+ function metricsLogFile() {
724
+ return path.join(cacheDir(), "compact-metrics.jsonl");
725
+ }
726
+ function damageReportsFile() {
727
+ return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
728
+ }
729
+
507
730
  // src/domain/tool-semantics.ts
508
731
  var PATH_KEYS = [
509
732
  "path",
@@ -586,10 +809,20 @@ function isLikelyFileRef(candidate) {
586
809
  return CODE_EXT_RE.test(candidate);
587
810
  }
588
811
  function extractFileRefs(summary) {
589
- const candidates = summary.match(FILE_REF_CANDIDATE_RE) ?? [];
590
- return candidates.filter(isLikelyFileRef);
812
+ const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
813
+ const refs = [];
814
+ for (const match of summary.matchAll(matcher)) {
815
+ if (/[\\/]/.test(summary[(match.index ?? 0) + match[0].length] ?? ""))
816
+ continue;
817
+ if (isLikelyFileRef(match[0]))
818
+ refs.push(match[0]);
819
+ }
820
+ return refs;
591
821
  }
592
822
 
823
+ // src/domain/summary-parse.ts
824
+ import { createHash } from "crypto";
825
+
593
826
  // src/domain/summary-schema.ts
594
827
  function classifyHeading(raw) {
595
828
  const text = raw.replace(/^#+\s*/, "").replace(/[:\s]+$/, "").trim().toLowerCase();
@@ -629,6 +862,58 @@ var HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
629
862
  function summaryEvidenceLine(value, maxLength) {
630
863
  return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:(?:#{1,6}|[-*+]|>)\s+)+/, "").slice(0, maxLength).trim();
631
864
  }
865
+ function summaryPathLine(value) {
866
+ return JSON.stringify(value);
867
+ }
868
+ function compactPathLine(value, maxLength, digest) {
869
+ const minimal = JSON.stringify("#" + digest);
870
+ if (minimal.length >= maxLength)
871
+ return minimal;
872
+ const chars = Array.from(value.replace(/\\/g, "/"));
873
+ let low = 0;
874
+ let high = chars.length;
875
+ let best = minimal;
876
+ while (low <= high) {
877
+ const length = Math.floor((low + high) / 2);
878
+ const candidate = JSON.stringify("\u2026/" + chars.slice(-length).join("") + "#" + digest);
879
+ if (candidate.length <= maxLength) {
880
+ best = candidate;
881
+ low = length + 1;
882
+ } else {
883
+ high = length - 1;
884
+ }
885
+ }
886
+ return best;
887
+ }
888
+ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summaryBudgetTokens) {
889
+ const unique = Array.from(new Set(paths.filter(Boolean)));
890
+ if (!unique.length)
891
+ return new Map;
892
+ const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
893
+ const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
894
+ const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
895
+ if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
896
+ return new Map(full);
897
+ }
898
+ const digests = new Map;
899
+ const owners = new Map;
900
+ for (const path2 of unique) {
901
+ const fullDigest = createHash("sha256").update(path2).digest("base64url");
902
+ let digest = fullDigest.slice(0, 12);
903
+ const owner = owners.get(digest);
904
+ if (owner && owner !== path2) {
905
+ digest = fullDigest;
906
+ digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
907
+ }
908
+ owners.set(digest, path2);
909
+ digests.set(path2, digest);
910
+ }
911
+ const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
912
+ return new Map(unique.map((path2) => [
913
+ path2,
914
+ compactPathLine(path2, perPath, digests.get(path2) ?? "")
915
+ ]));
916
+ }
632
917
  function mergeBodies(first, second) {
633
918
  const seen = new Set;
634
919
  return [first, second].filter(Boolean).flatMap((body) => body.split(`
@@ -653,7 +938,11 @@ function parseSummary(markdown) {
653
938
  if (existing)
654
939
  existing.body = mergeBodies(existing.body, body);
655
940
  else
656
- sections.push({ kind: currentKind, heading: currentHeading.trim(), body });
941
+ sections.push({
942
+ kind: currentKind,
943
+ heading: currentHeading.trim(),
944
+ body
945
+ });
657
946
  };
658
947
  for (const line of lines) {
659
948
  const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
@@ -744,7 +1033,11 @@ function buildToolCallIndex(msgs) {
744
1033
  for (let t = 0;t < nested.length; t++) {
745
1034
  const tool = nested[t];
746
1035
  const id = nestedToolCallId(b.id, i, t, tool.id);
747
- idx.set(id, { name: tool.name, arguments: tool.arguments, msgIndex: i });
1036
+ idx.set(id, {
1037
+ name: tool.name,
1038
+ arguments: tool.arguments,
1039
+ msgIndex: i
1040
+ });
748
1041
  }
749
1042
  }
750
1043
  }
@@ -752,46 +1045,42 @@ function buildToolCallIndex(msgs) {
752
1045
  return idx;
753
1046
  }
754
1047
  var CONSTRAINT_PATTERNS = [
755
- { re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i, cat: "requirement", conf: TUNING.CONFIDENCE_HIGH },
756
- { re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
757
- { re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i, cat: "preference", conf: TUNING.CONFIDENCE_LOW },
758
- { re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu, cat: "prohibition", conf: TUNING.CONFIDENCE_MEDIUM },
759
- { re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu, cat: "requirement", conf: TUNING.CONFIDENCE_MEDIUM },
760
- { re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu, cat: "preference", conf: TUNING.CONFIDENCE_LOW }
1048
+ {
1049
+ re: /\b(?:must|need|require|has to|important)\b.*\b(?:be|use|have|include|support)\b/i,
1050
+ cat: "requirement",
1051
+ conf: TUNING.CONFIDENCE_HIGH
1052
+ },
1053
+ {
1054
+ re: /\b(?:don't|never|avoid|shouldn't|must not|do not|no\s+(?:need|want))\b/i,
1055
+ cat: "prohibition",
1056
+ conf: TUNING.CONFIDENCE_MEDIUM
1057
+ },
1058
+ {
1059
+ re: /\b(?:prefer|like|want|would rather|should)\b.*\b(?:use|be|have|with)\b/i,
1060
+ cat: "preference",
1061
+ conf: TUNING.CONFIDENCE_LOW
1062
+ },
1063
+ {
1064
+ re: /(?<![A-Za-z0-9_])(?:yapma|kullanma|sak\u0131n|sak\u0131nha|asla(?:\s+(?:kullanma|yapma|getirme))?|bunu yapma)(?![A-Za-z0-9_])/iu,
1065
+ cat: "prohibition",
1066
+ conf: TUNING.CONFIDENCE_MEDIUM
1067
+ },
1068
+ {
1069
+ re: /(?<![A-Za-z0-9_])(?:kritik|kritikal|\u00F6nemli|onemli|\u015Fart|sart|zorunlu|\u015Fart ko\u015Ful|\u00F6nemli \u015Fart|kesinlikle|kesinlikle \u015Fart|b\u00F6yle olsun|b\u00F6yle yap\u0131n|\u015F\u00F6yle olsun|\u015F\u00F6yle yap\u0131n)(?![A-Za-z0-9_])/iu,
1070
+ cat: "requirement",
1071
+ conf: TUNING.CONFIDENCE_MEDIUM
1072
+ },
1073
+ {
1074
+ re: /(?<![A-Za-z0-9_])(?:tercih|isterim|olsun|kullanal\u0131m|yapal\u0131m|istiyorum)(?![A-Za-z0-9_])/iu,
1075
+ cat: "preference",
1076
+ conf: TUNING.CONFIDENCE_LOW
1077
+ }
761
1078
  ];
762
1079
  function isDiagnosticConstraintText(text) {
763
1080
  const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
764
1081
  return /^(?:\[[^\]]+\]\s*)?(?:npm\s+(?:error|warn|notice|audit|verbose|info)\b|(?:rg|grep):|command exited\b)/i.test(candidate);
765
1082
  }
766
1083
 
767
- // src/utils/logger.ts
768
- var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
769
- function warn(msg, err) {
770
- const detail = err instanceof Error ? err.message : err ?? "";
771
- console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
772
- }
773
-
774
- // src/infra/paths.ts
775
- import path from "path";
776
- function home() {
777
- return process.env.HOME ?? "/tmp";
778
- }
779
- function piAgentDir() {
780
- return path.join(home(), ".pi", "agent");
781
- }
782
- function cacheDir() {
783
- return path.join(piAgentDir(), ".cache");
784
- }
785
- function smartCompactCacheDir() {
786
- return path.join(cacheDir(), "smart-compact");
787
- }
788
- function metricsLogFile() {
789
- return path.join(cacheDir(), "compact-metrics.jsonl");
790
- }
791
- function damageReportsFile() {
792
- return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
793
- }
794
-
795
1084
  // src/infra/fs.ts
796
1085
  import fs from "fs";
797
1086
  function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
@@ -886,7 +1175,10 @@ function withCodexWireLimit(model, opts) {
886
1175
  onPayload: async (payload, requestModel) => {
887
1176
  const transformed = await previous?.(payload, requestModel);
888
1177
  const body = transformed ?? payload;
889
- return body && typeof body === "object" ? { ...body, max_output_tokens: opts.maxTokens } : body;
1178
+ return body && typeof body === "object" ? {
1179
+ ...body,
1180
+ max_output_tokens: opts.maxTokens
1181
+ } : body;
890
1182
  }
891
1183
  };
892
1184
  }
@@ -907,11 +1199,12 @@ function assertSuccessful(message) {
907
1199
  }
908
1200
  return message;
909
1201
  }
910
- async function withProviderDeadline(opts, invoke) {
1202
+ async function withProviderDeadline(opts, invoke, modelId) {
911
1203
  if (opts.signal?.aborted)
912
1204
  throw new Error("LLM request aborted before dispatch");
913
1205
  const controller = new AbortController;
914
- const watchdogMs = resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs);
1206
+ const multiplier = modelId && !((opts.codexWatchdogMs ?? 0) > 0) ? getProviderCaps(modelId).timeoutMultiplier : 1;
1207
+ const watchdogMs = Math.round(resolveCodexWatchdogMs(opts.maxTokens, opts.codexWatchdogMs) * multiplier);
915
1208
  const abort = Promise.withResolvers();
916
1209
  const abortFromCaller = () => {
917
1210
  controller.abort(opts.signal?.reason);
@@ -984,7 +1277,7 @@ var rawLlmClient = {
984
1277
  return completeChatGptCodex(model, body, bounded);
985
1278
  const response = bounded.reasoning === undefined ? await (await resolveComplete())(model, body, bounded) : await (await resolveCompleteSimple())(model, body, bounded);
986
1279
  return assertSuccessful(response);
987
- });
1280
+ }, model.id);
988
1281
  }
989
1282
  };
990
1283
  var defaultLlmClient = rawLlmClient;
@@ -998,7 +1291,10 @@ import crypto from "crypto";
998
1291
 
999
1292
  // src/domain/scrub.ts
1000
1293
  var SECRET_PATTERNS = [
1001
- { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
1294
+ {
1295
+ kind: "private-key",
1296
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
1297
+ },
1002
1298
  { kind: "aws-access-key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
1003
1299
  { kind: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
1004
1300
  { kind: "stripe-key", regex: /\b[rs]k_(?:live|test)_[0-9A-Za-z]{16,}\b/g },
@@ -1007,8 +1303,15 @@ var SECRET_PATTERNS = [
1007
1303
  { kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
1008
1304
  { kind: "api-key", regex: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/g },
1009
1305
  { kind: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
1010
- { kind: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
1011
- { kind: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}=*/gi, replacement: () => "Bearer [REDACTED:bearer-token]" },
1306
+ {
1307
+ kind: "jwt",
1308
+ regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g
1309
+ },
1310
+ {
1311
+ kind: "bearer-token",
1312
+ regex: /\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
1313
+ replacement: () => "Bearer [REDACTED:bearer-token]"
1314
+ },
1012
1315
  {
1013
1316
  kind: "connection-password",
1014
1317
  regex: /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s/]+(@)/gi,
@@ -1017,12 +1320,35 @@ var SECRET_PATTERNS = [
1017
1320
  {
1018
1321
  kind: "credential",
1019
1322
  regex: /\b((?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|auth[_-]?token|token|password|passwd|secret(?:[_-]?(?:access)?[_-]?key)?|client[_-]?secret)(?:[_-][A-Za-z0-9]+)*)\s*([:=])\s*["']?([^\s"']{16,})["']?/gi,
1020
- replacement: (name, separator) => name + separator + "[REDACTED:credential]"
1323
+ replacement: (name, separator, value, match) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) ? match : name + separator + "[REDACTED:credential]"
1021
1324
  }
1022
1325
  ];
1326
+ function passesLuhn(candidate) {
1327
+ const digits = candidate.replace(/\D/g, "");
1328
+ if (digits.length < 13 || digits.length > 19)
1329
+ return false;
1330
+ if (/^(\d)\1+$/.test(digits))
1331
+ return false;
1332
+ let sum = 0, double = false;
1333
+ for (let i = digits.length - 1;i >= 0; i--) {
1334
+ let d = digits.charCodeAt(i) - 48;
1335
+ if (double) {
1336
+ d *= 2;
1337
+ if (d > 9)
1338
+ d -= 9;
1339
+ }
1340
+ sum += d;
1341
+ double = !double;
1342
+ }
1343
+ return sum % 10 === 0;
1344
+ }
1023
1345
  var PII_PATTERNS = [
1024
1346
  { kind: "email", regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi },
1025
- { kind: "payment-card", regex: /\b(?:\d[ -]*?){13,19}\b/g },
1347
+ {
1348
+ kind: "payment-card",
1349
+ regex: /\b(?:\d[ -]*?){13,19}\b/g,
1350
+ replacement: (candidate) => passesLuhn(candidate) ? "[REDACTED:payment-card]" : candidate
1351
+ },
1026
1352
  { kind: "phone", regex: /(?<![\w.])(?:\+?\d[\d ()-]{8,}\d)(?![\w.])/g }
1027
1353
  ];
1028
1354
  function redact(text, patterns) {
@@ -1030,15 +1356,22 @@ function redact(text, patterns) {
1030
1356
  let value = text;
1031
1357
  for (const pattern of patterns) {
1032
1358
  value = value.replace(pattern.regex, (...args) => {
1033
- counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
1359
+ const match = String(args[0]);
1360
+ let replacement = "[REDACTED:" + pattern.kind + "]";
1034
1361
  if (pattern.replacement) {
1035
1362
  const groups = args.slice(1, -2).map(String);
1036
- return pattern.replacement(...groups);
1363
+ replacement = pattern.replacement(...groups, match);
1037
1364
  }
1038
- return "[REDACTED:" + pattern.kind + "]";
1365
+ if (replacement === match)
1366
+ return match;
1367
+ counts.set(pattern.kind, (counts.get(pattern.kind) ?? 0) + 1);
1368
+ return replacement;
1039
1369
  });
1040
1370
  }
1041
- return { value, findings: [...counts].map(([kind, count]) => ({ kind, count })) };
1371
+ return {
1372
+ value,
1373
+ findings: [...counts].map(([kind, count]) => ({ kind, count }))
1374
+ };
1042
1375
  }
1043
1376
  function mergeFindings(target, findings) {
1044
1377
  for (const finding of findings)
@@ -1068,7 +1401,6 @@ var SECRET_KEY_NAMES = {
1068
1401
  set_cookie: true,
1069
1402
  otp: true,
1070
1403
  one_time_password: true,
1071
- pin: true,
1072
1404
  passcode: true
1073
1405
  };
1074
1406
  function normalizeObjectKey(key) {
@@ -1134,7 +1466,7 @@ class SecretScrubber {
1134
1466
  const output = {};
1135
1467
  seen.set(value2, output);
1136
1468
  for (const [key, item] of Object.entries(value2)) {
1137
- const carriesSecret = typeof item === "string" ? item.length > 0 : item != null;
1469
+ const carriesSecret = typeof item === "string" && item.length >= 8;
1138
1470
  if (this.secretsEnabled && isSecretBearingKey(key) && carriesSecret) {
1139
1471
  output[key] = "[REDACTED:credential]";
1140
1472
  recordCredential();
@@ -1145,7 +1477,10 @@ class SecretScrubber {
1145
1477
  return output;
1146
1478
  };
1147
1479
  const value = visit(input);
1148
- return { value, findings: [...findings].map(([kind, count]) => ({ kind, count })) };
1480
+ return {
1481
+ value,
1482
+ findings: [...findings].map(([kind, count]) => ({ kind, count }))
1483
+ };
1149
1484
  }
1150
1485
  count() {
1151
1486
  return this.total;