@pretense/cli 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PRETENSE_VERSION
4
- } from "./chunk-2WW3XY6A.js";
4
+ } from "./chunk-MH6CXHTM.js";
5
5
  import {
6
6
  __commonJS,
7
7
  __toESM,
@@ -221,8 +221,8 @@ var require_brace_expansion = __commonJS({
221
221
 
222
222
  // src/index.ts
223
223
  init_esm_shims();
224
- import { Command as Command22 } from "commander";
225
- import chalk21 from "chalk";
224
+ import { Command as Command23 } from "commander";
225
+ import chalk23 from "chalk";
226
226
 
227
227
  // src/commands/init.ts
228
228
  init_esm_shims();
@@ -7572,16 +7572,29 @@ var DETECTOR_FRAMEWORKS = {
7572
7572
  "vault-token": ["APRA_CPS234", "DORA", "FISMA", "FedRAMP", "HITRUST", "ISO_27001", "ISO_27701", "NIS2", "NIST_800_171", "NIST_800_53", "NYDFS_500", "SOC2", "SOX"],
7573
7573
  "vehicle-vin": ["APPI", "AU_PRIVACY", "CCPA_CPRA", "COPPA", "DPDP", "FADP", "FERPA", "GDPR", "HIPAA", "HITECH", "HITRUST", "ISO_27701", "LGPD", "PDPA_SG", "PDPA_TH", "PIPA_KR", "PIPEDA", "PIPL", "POPIA", "UK_GDPR"]
7574
7574
  };
7575
+ var FRAMEWORK_ALIASES = {
7576
+ SOC_1: "SOX"
7577
+ };
7578
+ function canonicalFrameworkKey(input) {
7579
+ return input.trim().toUpperCase().replace(/[\s._-]+/g, "");
7580
+ }
7581
+ var ALIAS_BASE_BY_CANON = new Map(Object.entries(FRAMEWORK_ALIASES).map(([name, base]) => [canonicalFrameworkKey(name), base]));
7582
+ var POLICY_FRAMEWORKS = [
7583
+ ...COMPLIANCE_FRAMEWORKS,
7584
+ ...Object.keys(FRAMEWORK_ALIASES)
7585
+ ];
7575
7586
  function frameworksForDetector(detector) {
7576
7587
  return Object.prototype.hasOwnProperty.call(DETECTOR_FRAMEWORKS, detector) ? DETECTOR_FRAMEWORKS[detector] : [];
7577
7588
  }
7578
7589
  function detectorsMatchFramework(detectors, framework) {
7579
- const want = framework.toUpperCase();
7580
- return detectors.some((d) => frameworksForDetector(d).some((f) => f.toUpperCase() === want));
7590
+ const canon = canonicalFrameworkKey(framework);
7591
+ const base = ALIAS_BASE_BY_CANON.get(canon);
7592
+ const target = base ? canonicalFrameworkKey(base) : canon;
7593
+ return detectors.some((d) => frameworksForDetector(d).some((f) => canonicalFrameworkKey(f) === target));
7581
7594
  }
7582
7595
  function resolveFrameworkName(input) {
7583
- const want = input.toUpperCase();
7584
- return COMPLIANCE_FRAMEWORKS.find((f) => f.toUpperCase() === want) ?? null;
7596
+ const canon = canonicalFrameworkKey(input);
7597
+ return POLICY_FRAMEWORKS.find((f) => canonicalFrameworkKey(f) === canon) ?? null;
7585
7598
  }
7586
7599
 
7587
7600
  // src/compliance-preset.ts
@@ -7611,21 +7624,21 @@ function detectorsForFramework(framework) {
7611
7624
  return ALL_TAGGED_DETECTORS.filter((d) => detectorsMatchFramework([d], framework));
7612
7625
  }
7613
7626
  function policyChoices() {
7614
- return [...COMPLIANCE_FRAMEWORKS, ...COMPLIANCE_PRESET_IDS];
7627
+ return [...POLICY_FRAMEWORKS, ...COMPLIANCE_PRESET_IDS];
7615
7628
  }
7616
7629
  function resolvePolicyName(input) {
7617
- const key = normalizePolicyKey(input);
7618
- const shortId = key.toLowerCase();
7630
+ const shortId = input.trim().toLowerCase();
7619
7631
  const viaShortId = isCompliancePresetId(shortId) ? SHORT_PRESET_ALIASES[shortId] : void 0;
7620
- const framework = viaShortId ?? COMPLIANCE_FRAMEWORKS.find((f) => normalizePolicyKey(f) === key);
7632
+ const canon = canonicalFrameworkKey(input);
7633
+ const framework = viaShortId ?? POLICY_FRAMEWORKS.find((f) => canonicalFrameworkKey(f) === canon);
7621
7634
  if (!framework) {
7622
7635
  throw new Error(
7623
- `Unknown compliance preset: "${input}". Valid presets: ${COMPLIANCE_PRESET_IDS.join(", ")}. Any of the ${COMPLIANCE_FRAMEWORKS.length} framework names from \`pretense audit --list-frameworks\` is also accepted (case- and separator-insensitive, e.g. pci-dss, PCI_DSS, pci_dss).`
7636
+ `Unknown compliance preset: "${input}". Valid presets: ${COMPLIANCE_PRESET_IDS.join(", ")}. Any of the ${POLICY_FRAMEWORKS.length} framework names from \`pretense audit --list-frameworks\` is also accepted (case- and separator-insensitive, e.g. pci-dss, PCI_DSS, pci_dss).`
7624
7637
  );
7625
7638
  }
7626
7639
  const detectors = detectorsForFramework(framework);
7627
7640
  const fingerprint = detectors.join("|");
7628
- const sharedWith = COMPLIANCE_FRAMEWORKS.filter(
7641
+ const sharedWith = POLICY_FRAMEWORKS.filter(
7629
7642
  (f) => f !== framework && detectorsForFramework(f).join("|") === fingerprint
7630
7643
  );
7631
7644
  const scannerPreset = SCANNER_PRESET_BY_FRAMEWORK[framework];
@@ -7818,7 +7831,7 @@ function startSpinner(text) {
7818
7831
 
7819
7832
  // src/commands/start.ts
7820
7833
  init_esm_shims();
7821
- import { Command as Command2 } from "commander";
7834
+ import { Command as Command2, InvalidArgumentError } from "commander";
7822
7835
  import chalk2 from "chalk";
7823
7836
  import { execSync } from "child_process";
7824
7837
 
@@ -12002,6 +12015,70 @@ var EXTENDED_PATTERNS = [
12002
12015
  validate: looksLikeSecretValue
12003
12016
  }
12004
12017
  ];
12018
+ var THRESHOLD_HIGH_ENTROPY = 4.5;
12019
+ function shannonEntropy(str) {
12020
+ if (str.length === 0) return 0;
12021
+ const freq = {};
12022
+ for (const c of str) {
12023
+ freq[c] = (freq[c] ?? 0) + 1;
12024
+ }
12025
+ let entropy = 0;
12026
+ for (const count of Object.values(freq)) {
12027
+ const p = count / str.length;
12028
+ entropy -= p * Math.log2(p);
12029
+ }
12030
+ return entropy;
12031
+ }
12032
+ var MIN_ENTROPY_LENGTH = 20;
12033
+ var SECRET_CHARSET_RE = /^[A-Za-z0-9+/=_\-]+$/;
12034
+ var UUID_RE = /^[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}$/;
12035
+ var HEX_BLOB_RE = /^[a-f0-9]{32,}$/;
12036
+ var BASE64_BLOB_RE = /^[A-Za-z0-9+/]+={0,2}$/;
12037
+ var SECRET_KEYWORD_RE = /\b(?:password|passwd|pwd|secret|key|token|credential|creds?|api[_-]?key|apikey|auth|authorization|bearer|private[_-]?key|access[_-]?key|client[_-]?secret)\b/i;
12038
+ var SECRET_KEY_NAME_RE = /(?:password|passwd|pwd|secret|token|credential|cred|apikey|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key|authorization|bearer|\bauth\b|\bkey\b)/i;
12039
+ function isAllowlistedShape(value) {
12040
+ if (UUID_RE.test(value)) return true;
12041
+ if (HEX_BLOB_RE.test(value)) return true;
12042
+ if (BASE64_BLOB_RE.test(value) && value.length % 4 === 0 && !/[_-]/.test(value)) {
12043
+ return true;
12044
+ }
12045
+ return false;
12046
+ }
12047
+ function hasSecretContext(text, start, end) {
12048
+ const WINDOW = 80;
12049
+ const before = text.slice(Math.max(0, start - WINDOW), start);
12050
+ const after = text.slice(end, Math.min(text.length, end + WINDOW));
12051
+ if (SECRET_KEYWORD_RE.test(before) || SECRET_KEYWORD_RE.test(after)) {
12052
+ return true;
12053
+ }
12054
+ const assignMatch = before.match(/([A-Za-z0-9_$'"-]+)\s*[:=]\s*['"`]?\s*$/);
12055
+ if (assignMatch && SECRET_KEY_NAME_RE.test(assignMatch[1])) {
12056
+ return true;
12057
+ }
12058
+ return false;
12059
+ }
12060
+ function findHighEntropyStrings(text) {
12061
+ const matches = [];
12062
+ const tokenRe = /(?:['"`])([A-Za-z0-9+/=_\-]{20,})(?:['"`])/g;
12063
+ let m;
12064
+ while ((m = tokenRe.exec(text)) !== null) {
12065
+ const value = m[1];
12066
+ if (value.length < MIN_ENTROPY_LENGTH) continue;
12067
+ if (!SECRET_CHARSET_RE.test(value)) continue;
12068
+ const entropy = shannonEntropy(value);
12069
+ if (entropy < THRESHOLD_HIGH_ENTROPY) continue;
12070
+ const start = m.index + 1;
12071
+ const end = start + value.length;
12072
+ if (isAllowlistedShape(value) && !hasSecretContext(text, start, end)) {
12073
+ continue;
12074
+ }
12075
+ matches.push({ value, entropy, start, end });
12076
+ }
12077
+ return matches;
12078
+ }
12079
+ function awsSecretKeyIsHighEntropy(token) {
12080
+ return shannonEntropy(token) >= 3.9;
12081
+ }
12005
12082
  function envAssignmentValueIsReal(match3) {
12006
12083
  const sep2 = match3.search(/[=:]/);
12007
12084
  if (sep2 === -1) return true;
@@ -12114,6 +12191,24 @@ var SECRET_PATTERNS = [
12114
12191
  defaultAction: "block",
12115
12192
  pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/g
12116
12193
  },
12194
+ {
12195
+ // BLOCKER: the assignment-anchored `aws-secret-key` above misses the SECRET
12196
+ // access key when it appears in PROSE — "deploy with wJalrXUtnFEMI/K7MDENG/
12197
+ // bPxRfiCYEXAMPLEKEY" or next to its AKIA id — and the raw 40-char key then
12198
+ // reached the LLM verbatim (scan reported clean). This detector catches the
12199
+ // bare token, but ONLY within 80 chars of AWS context (an AKIA id, "aws
12200
+ // secret", or "secret access key"), so an unrelated 40-char base64 string is
12201
+ // not swept up. `valueGroup: 1` scopes the block/tokenization to the token,
12202
+ // never the surrounding words; the `d` flag is REQUIRED for that span, and
12203
+ // the entropy `validate` is the secondary FP guard (rejects a git SHA).
12204
+ name: "aws-secret-access-key-contextual",
12205
+ category: "secret",
12206
+ severity: "critical",
12207
+ defaultAction: "block",
12208
+ pattern: /(?:AKIA[0-9A-Z]{16}|aws[_\s-]{0,3}secret(?:[_\s-]{0,3}access)?[_\s-]{0,3}key|secret[_\s-]{0,3}access[_\s-]{0,3}key)[\s\S]{0,80}?(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+=])/gid,
12209
+ valueGroup: 1,
12210
+ validate: awsSecretKeyIsHighEntropy
12211
+ },
12117
12212
  {
12118
12213
  name: "gcp-service-account",
12119
12214
  category: "secret",
@@ -12787,67 +12882,6 @@ var ALL_PATTERNS = [...SECRET_PATTERNS, ...PII_PATTERNS, ...ENV_PATTERNS, ...EXT
12787
12882
  function getPatternCount() {
12788
12883
  return ALL_PATTERNS.length;
12789
12884
  }
12790
- var THRESHOLD_HIGH_ENTROPY = 4.5;
12791
- function shannonEntropy(str) {
12792
- if (str.length === 0) return 0;
12793
- const freq = {};
12794
- for (const c of str) {
12795
- freq[c] = (freq[c] ?? 0) + 1;
12796
- }
12797
- let entropy = 0;
12798
- for (const count of Object.values(freq)) {
12799
- const p = count / str.length;
12800
- entropy -= p * Math.log2(p);
12801
- }
12802
- return entropy;
12803
- }
12804
- var MIN_ENTROPY_LENGTH = 20;
12805
- var SECRET_CHARSET_RE = /^[A-Za-z0-9+/=_\-]+$/;
12806
- var UUID_RE = /^[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}$/;
12807
- var HEX_BLOB_RE = /^[a-f0-9]{32,}$/;
12808
- var BASE64_BLOB_RE = /^[A-Za-z0-9+/]+={0,2}$/;
12809
- var SECRET_KEYWORD_RE = /\b(?:password|passwd|pwd|secret|key|token|credential|creds?|api[_-]?key|apikey|auth|authorization|bearer|private[_-]?key|access[_-]?key|client[_-]?secret)\b/i;
12810
- var SECRET_KEY_NAME_RE = /(?:password|passwd|pwd|secret|token|credential|cred|apikey|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key|authorization|bearer|\bauth\b|\bkey\b)/i;
12811
- function isAllowlistedShape(value) {
12812
- if (UUID_RE.test(value)) return true;
12813
- if (HEX_BLOB_RE.test(value)) return true;
12814
- if (BASE64_BLOB_RE.test(value) && value.length % 4 === 0 && !/[_-]/.test(value)) {
12815
- return true;
12816
- }
12817
- return false;
12818
- }
12819
- function hasSecretContext(text, start, end) {
12820
- const WINDOW = 80;
12821
- const before = text.slice(Math.max(0, start - WINDOW), start);
12822
- const after = text.slice(end, Math.min(text.length, end + WINDOW));
12823
- if (SECRET_KEYWORD_RE.test(before) || SECRET_KEYWORD_RE.test(after)) {
12824
- return true;
12825
- }
12826
- const assignMatch = before.match(/([A-Za-z0-9_$'"-]+)\s*[:=]\s*['"`]?\s*$/);
12827
- if (assignMatch && SECRET_KEY_NAME_RE.test(assignMatch[1])) {
12828
- return true;
12829
- }
12830
- return false;
12831
- }
12832
- function findHighEntropyStrings(text) {
12833
- const matches = [];
12834
- const tokenRe = /(?:['"`])([A-Za-z0-9+/=_\-]{20,})(?:['"`])/g;
12835
- let m;
12836
- while ((m = tokenRe.exec(text)) !== null) {
12837
- const value = m[1];
12838
- if (value.length < MIN_ENTROPY_LENGTH) continue;
12839
- if (!SECRET_CHARSET_RE.test(value)) continue;
12840
- const entropy = shannonEntropy(value);
12841
- if (entropy < THRESHOLD_HIGH_ENTROPY) continue;
12842
- const start = m.index + 1;
12843
- const end = start + value.length;
12844
- if (isAllowlistedShape(value) && !hasSecretContext(text, start, end)) {
12845
- continue;
12846
- }
12847
- matches.push({ value, entropy, start, end });
12848
- }
12849
- return matches;
12850
- }
12851
12885
  function icd10Check(match3) {
12852
12886
  const code = match3.trim().toUpperCase();
12853
12887
  return /^[A-Z]\d{2}(?:\.[A-Z0-9]{1,4})?$/.test(code);
@@ -16118,6 +16152,7 @@ var AuditStore = class {
16118
16152
  blocked: 0,
16119
16153
  mutated: 0,
16120
16154
  redacted: 0,
16155
+ reversed: 0,
16121
16156
  error: 0
16122
16157
  };
16123
16158
  for (const row of actionRows) {
@@ -16445,11 +16480,11 @@ function versionFromCliPackageJson() {
16445
16480
  return void 0;
16446
16481
  }
16447
16482
  function productVersion() {
16448
- return override ?? (true ? "0.6.14" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
16483
+ return override ?? (true ? "0.6.16" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
16449
16484
  }
16450
16485
  var UNKNOWN_COMMIT = "unknown";
16451
16486
  function bakedCommitSha() {
16452
- return "13f5fb3bd88295258e3733458b63315e9382678e".length > 0 ? "13f5fb3bd88295258e3733458b63315e9382678e" : UNKNOWN_COMMIT;
16487
+ return "65c9faca2983b357369c6f91aa0cad0cf3a70b82".length > 0 ? "65c9faca2983b357369c6f91aa0cad0cf3a70b82" : UNKNOWN_COMMIT;
16453
16488
  }
16454
16489
  var FEATURE_TIERS = {
16455
16490
  compliance_export: "pro",
@@ -16571,7 +16606,10 @@ var PlanEnforcer = class {
16571
16606
  upgradeMessage,
16572
16607
  upgradeTier,
16573
16608
  upgradeUrl,
16574
- resetDate
16609
+ resetDate,
16610
+ limitMetric: worstMetric,
16611
+ limitCurrent: worstCurrent,
16612
+ limitValue: worstLimit
16575
16613
  };
16576
16614
  }
16577
16615
  if (overallPercent >= 90) {
@@ -17514,20 +17552,26 @@ function createProxy(opts = {}) {
17514
17552
  if (_enforcement.upgradeUrl) c.header("X-Pretense-Upgrade-Url", _enforcement.upgradeUrl);
17515
17553
  }
17516
17554
  if (!_enforcement.allowed) {
17517
- const _planConfig = PLAN_CONFIGS[_effectiveTier];
17518
- const _mutLimit = _planConfig.mutationsPerPeriod ?? 0;
17519
- const _mutCurrent = _periodStats.totalMutationsApplied;
17520
17555
  const _upgradeTier = _enforcement.upgradeTier ?? "pro";
17556
+ const _metric = _enforcement.limitMetric ?? "mutations";
17557
+ const _mutCurrent = _enforcement.limitCurrent ?? _periodStats.totalMutationsApplied;
17558
+ const _mutLimit = _enforcement.limitValue ?? (PLAN_CONFIGS[_effectiveTier].mutationsPerPeriod ?? 0);
17559
+ const _metricLabel = {
17560
+ mutations: "Mutation",
17561
+ scans: "Request",
17562
+ bytes: "Data",
17563
+ seats: "Seat"
17564
+ };
17565
+ const _label = _metricLabel[_metric] ?? "Usage";
17521
17566
  return c.json({
17522
17567
  error: {
17523
17568
  type: "plan_limit_exceeded",
17524
- code: "MUTATION_LIMIT_EXCEEDED",
17525
- // Human-readable error matching the spec format
17569
+ code: `${_metric.toUpperCase()}_LIMIT_EXCEEDED`,
17526
17570
  // 100,000 = PLAN_CONFIGS.pro.mutationsPerPeriod; $29/seat =
17527
17571
  // PLAN_CONFIGS.pro.pricePerSeatCents (monthly rate). Enterprise
17528
17572
  // prints no price and no self-serve `pretense upgrade` path: it
17529
17573
  // is contact-sales only. DO NOT restore "$99/seat".
17530
- message: `\u2717 Mutation limit reached for this 7-day period (${_mutCurrent.toLocaleString()}/${_mutLimit.toLocaleString()})
17574
+ message: `\u2717 ${_label} limit reached for this 7-day period (${_mutCurrent.toLocaleString()}/${_mutLimit.toLocaleString()})
17531
17575
 
17532
17576
  ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014 contact sales for a quote.\n\u2192 visit pretense.ai/pricing" : "Upgrade to Pro for 100,000 mutations / 7 days ($29/seat)\n\u2192 pretense upgrade OR visit pretense.ai/pricing"}`,
17533
17577
  upgrade: {
@@ -17540,7 +17584,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17540
17584
  current: _mutCurrent,
17541
17585
  limit: _mutLimit,
17542
17586
  percent: _enforcement.usagePercent,
17543
- metric: "mutations",
17587
+ metric: _metric,
17544
17588
  resetDate: _enforcement.resetDate
17545
17589
  }
17546
17590
  }
@@ -17681,6 +17725,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17681
17725
  return replaceCodeBlocks(text, blocks, mutatedBlocks);
17682
17726
  });
17683
17727
  const mutDurationMs = Math.round((performance.now() - mutT0) * 100) / 100;
17728
+ const mutationsApplied = tokensMutated + secretsTokenized + piiTokenized;
17684
17729
  const totalChars = fullText.length;
17685
17730
  const identifierExposureRate = totalChars > 0 ? Math.min(1, tokensMutated / Math.max(1, totalChars / 10)) : 0;
17686
17731
  const riskCtx = {
@@ -17709,7 +17754,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17709
17754
  processedSize: 0,
17710
17755
  secretsBlocked: secretsTokenized,
17711
17756
  piiRedacted: piiTokenized,
17712
- mutationsApplied: tokensMutated,
17757
+ mutationsApplied,
17713
17758
  scanDurationMs,
17714
17759
  mutationDurationMs: mutDurationMs,
17715
17760
  action: "blocked",
@@ -17791,7 +17836,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17791
17836
  processedSize,
17792
17837
  secretsBlocked: secretsTokenized,
17793
17838
  piiRedacted: piiTokenized,
17794
- mutationsApplied: tokensMutated,
17839
+ mutationsApplied,
17795
17840
  scanDurationMs,
17796
17841
  mutationDurationMs: mutDurationMs,
17797
17842
  action: "error",
@@ -17828,7 +17873,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17828
17873
  processedSize,
17829
17874
  secretsBlocked: secretsTokenized,
17830
17875
  piiRedacted: piiTokenized,
17831
- mutationsApplied: tokensMutated,
17876
+ mutationsApplied,
17832
17877
  scanDurationMs,
17833
17878
  mutationDurationMs: mutDurationMs,
17834
17879
  action,
@@ -17904,10 +17949,16 @@ function renderClientEnvLines(port, protocol) {
17904
17949
  " Easiest \u2014 let Pretense wire the env var and run the tool for you:",
17905
17950
  ' pretense run claude "build me X" # or: cursor, gemini, any tool',
17906
17951
  "",
17907
- " Or point your AI client at Pretense yourself:",
17908
- ` ANTHROPIC_BASE_URL=${base}`,
17909
- ` OPENAI_BASE_URL=${base}/v1`,
17910
- ` GOOGLE_GEMINI_BASE_URL=${base}`
17952
+ // The manual lines are SAME-SHELL (env var and tool on one command) on
17953
+ // purpose: a user who runs `export …BASE_URL=…` here and then `claude` in
17954
+ // a NEW terminal is unprotected — env vars are per-shell. The warning
17955
+ // below states that, and points back at `pretense run` as the fix.
17956
+ " Or point your AI client at Pretense yourself (SAME terminal as the tool):",
17957
+ ` ANTHROPIC_BASE_URL=${base} claude`,
17958
+ ` OPENAI_BASE_URL=${base}/v1 cursor`,
17959
+ ` GOOGLE_GEMINI_BASE_URL=${base} gemini`,
17960
+ " \u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT",
17961
+ " protected. Simplest: use `pretense run` and skip the export entirely."
17911
17962
  ].join("\n") + "\n";
17912
17963
  }
17913
17964
  function printBanner(port, protocol, tls) {
@@ -18051,6 +18102,16 @@ async function stopProxy(path2 = pidFilePath(), graceMs = 2e3, pollMs = 50) {
18051
18102
  }
18052
18103
 
18053
18104
  // src/commands/start.ts
18105
+ function parsePort(raw2) {
18106
+ if (!/^\d+$/.test(raw2.trim())) {
18107
+ throw new InvalidArgumentError(`"${raw2}" is not a number. Use a port between 1 and 65535.`);
18108
+ }
18109
+ const n = Number(raw2);
18110
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
18111
+ throw new InvalidArgumentError(`${raw2} is out of range. Use a port between 1 and 65535.`);
18112
+ }
18113
+ return String(n);
18114
+ }
18054
18115
  function pidListeningOn(port) {
18055
18116
  try {
18056
18117
  const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
@@ -18084,7 +18145,7 @@ function explainAddrInUse(port) {
18084
18145
  );
18085
18146
  }
18086
18147
  function startCommand() {
18087
- return new Command2("start").description("Start the Pretense proxy server").option("-p, --port <port>", "Port to listen on", "9339").option("--verbose", "Enable verbose logging", false).option("-c, --config <path>", "Path to pretense.yaml").option("--strict-port", "Fail if the requested port is busy instead of auto-advancing", false).action(async (opts) => {
18148
+ return new Command2("start").description("Start the Pretense proxy server").option("-p, --port <port>", "Port to listen on (1\u201365535)", parsePort, "9339").option("--verbose", "Enable verbose logging", false).option("-c, --config <path>", "Path to pretense.yaml").option("--strict-port", "Fail if the requested port is busy instead of auto-advancing", false).action(async (opts) => {
18088
18149
  const requested = parseInt(opts.port, 10);
18089
18150
  const existing = readProxyRecord();
18090
18151
  if (existing) {
@@ -18092,7 +18153,10 @@ function startCommand() {
18092
18153
  if (existing.port === requested) {
18093
18154
  console.error(
18094
18155
  chalk2.red(`
18095
- x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) + chalk2.dim("\n Stop it with ") + chalk2.cyan("pretense stop") + chalk2.dim(", or start this one on another port with ") + chalk2.cyan(`--port ${requested + 1}`) + "\n"
18156
+ x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) + // The proxy is UP the next thing the user wants is to route a
18157
+ // tool through it, and `pretense run` reuses this exact proxy
18158
+ // (no second export, no second terminal). Lead with that.
18159
+ chalk2.dim("\n Route a tool through it with ") + chalk2.cyan('pretense run claude "\u2026"') + chalk2.dim(" (or cursor, gemini, any tool).") + chalk2.dim("\n Stop it with ") + chalk2.cyan("pretense stop") + chalk2.dim(", or start this one on another port with ") + chalk2.cyan(`--port ${requested + 1}`) + "\n"
18096
18160
  );
18097
18161
  process.exit(1);
18098
18162
  }
@@ -18155,114 +18219,490 @@ function startCommand() {
18155
18219
 
18156
18220
  // src/commands/stop.ts
18157
18221
  init_esm_shims();
18158
- import { Command as Command3 } from "commander";
18159
- import chalk3 from "chalk";
18160
- function stopCommand() {
18161
- return new Command3("stop").description("Stop the running Pretense proxy").option("--json", "Machine-readable outcome", false).action(async (opts) => {
18162
- const path2 = pidFilePath();
18163
- const outcome = await stopProxy(path2);
18164
- if (opts.json) {
18165
- console.log(JSON.stringify(outcome, null, 2));
18166
- process.exit(outcome.kind === "refused" || outcome.kind === "not-permitted" ? 1 : 0);
18167
- }
18168
- switch (outcome.kind) {
18169
- case "not-running":
18170
- console.log(
18171
- chalk3.dim(`
18172
- \xB7 No running Pretense proxy recorded (${path2} does not exist).`) + chalk3.dim(`
18173
- Start one with `) + chalk3.cyan("pretense start") + "\n"
18174
- );
18175
- process.exit(0);
18176
- break;
18177
- case "stale":
18178
- console.log(
18179
- chalk3.yellow(
18180
- `
18181
- \xB7 Stale PID file cleaned up${outcome.pid ? ` (pid ${outcome.pid} is not running)` : ""}.`
18182
- ) + chalk3.dim(`
18183
- No proxy was running.
18184
- `)
18185
- );
18186
- process.exit(0);
18187
- break;
18188
- case "stopped":
18189
- console.log(
18190
- chalk3.green(
18191
- `
18192
- \u2713 Pretense proxy stopped (pid ${outcome.pid}${outcome.port ? `, port ${outcome.port}` : ""})` + (outcome.forced ? " \u2014 did not exit on SIGTERM, sent SIGKILL" : "") + "\n"
18193
- )
18194
- );
18195
- process.exit(0);
18196
- break;
18197
- case "not-permitted":
18198
- console.error(
18199
- chalk3.red(`
18200
- x Cannot stop pid ${outcome.pid}: it belongs to another user.`) + chalk3.dim(`
18201
- The proxy is still running. Stop it as its owner, or: sudo kill ${outcome.pid}
18202
- `)
18203
- );
18204
- process.exit(1);
18205
- break;
18206
- case "refused":
18207
- console.error(
18208
- chalk3.red(`
18209
- x Pid ${outcome.pid} is still alive after SIGTERM and SIGKILL.`) + chalk3.dim(`
18210
- ${path2} was left in place so you can investigate: ps -p ${outcome.pid}
18211
- `)
18212
- );
18213
- process.exit(1);
18214
- break;
18215
- }
18216
- });
18217
- }
18218
-
18219
- // src/commands/scan.ts
18220
- init_esm_shims();
18221
- import { Command as Command4, Option, InvalidArgumentError } from "commander";
18222
+ import { Command as Command4 } from "commander";
18222
18223
  import chalk4 from "chalk";
18223
- import { readFileSync as readFileSync7, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
18224
18224
  import { execSync as execSync2 } from "child_process";
18225
- import { resolve as resolve2, join as join7, extname as extname2, basename } from "path";
18226
18225
 
18227
- // src/safe-output.ts
18226
+ // src/commands/doctor.ts
18228
18227
  init_esm_shims();
18229
- var UNSAFE_VALUES_FLAG = "--unsafe-show-values";
18230
- function lineColFromOffset2(text, offset) {
18231
- const clamped = Math.max(0, Math.min(offset, text.length));
18232
- let line = 1;
18233
- let lineStart = 0;
18234
- for (let i = 0; i < clamped; i++) {
18235
- if (text.charCodeAt(i) === 10) {
18236
- line++;
18237
- lineStart = i + 1;
18238
- }
18239
- }
18240
- return { line, column: clamped - lineStart + 1 };
18241
- }
18242
- function withLocations(result, content) {
18243
- const locate = (m) => lineColFromOffset2(content, m.start);
18244
- return {
18245
- ...result,
18246
- matches: result.matches.map((m) => ({ ...m, ...locate(m) }))
18247
- };
18228
+ import { Command as Command3 } from "commander";
18229
+ import chalk3 from "chalk";
18230
+
18231
+ // src/plan-usage.ts
18232
+ init_esm_shims();
18233
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
18234
+ import { join as join7 } from "path";
18235
+ import { homedir as homedir4 } from "os";
18236
+ function credentialsPath() {
18237
+ return join7(homedir4(), ".pretense", "credentials.json");
18248
18238
  }
18249
- function safeLocator(m) {
18250
- const len = Math.max(0, m.end - m.start);
18251
- if (m.deobfuscated && len === 0) return "len=0 unmapped";
18252
- const pos = m.line !== void 0 && m.column !== void 0 ? `L${m.line}:${m.column} ` : "";
18253
- return `${pos}len=${len}${m.deobfuscated ? " enc" : ""}`;
18239
+ function readApiKey() {
18240
+ const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
18241
+ if (fromEnv) return fromEnv;
18242
+ const path2 = credentialsPath();
18243
+ if (!existsSync8(path2)) return null;
18244
+ try {
18245
+ const creds = JSON.parse(readFileSync7(path2, "utf-8"));
18246
+ const key = creds["apiKey"] ?? creds["api_key"];
18247
+ if (typeof key === "string" && key.trim().length > 0) return key.trim();
18248
+ } catch {
18249
+ }
18250
+ return null;
18254
18251
  }
18255
- var VALUE_FIELDS = ["value", "masked", "deobfuscatedValue"];
18256
- function stripRow(row) {
18257
- const out = {};
18258
- for (const [k, v] of Object.entries(row)) {
18259
- if (VALUE_FIELDS.includes(k)) continue;
18260
- out[k] = v;
18252
+ var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
18253
+ var DEFAULT_TIER_TIMEOUT_MS = 5e3;
18254
+ function getConfiguredApiRoot() {
18255
+ const raw2 = (process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
18256
+ if (!raw2) return DEFAULT_PRETENSE_API_URL;
18257
+ const normalized = /^https?:\/\//i.test(raw2) ? raw2 : `https://${raw2}`;
18258
+ try {
18259
+ const u = new URL(normalized);
18260
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL : u.origin;
18261
+ const path2 = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
18262
+ return `${origin}${path2}`;
18263
+ } catch {
18264
+ return DEFAULT_PRETENSE_API_URL;
18261
18265
  }
18262
- out.length = typeof row.end === "number" && typeof row.start === "number" ? row.end - row.start : 0;
18263
- return out;
18264
18266
  }
18265
- function stripValues(result) {
18267
+ function tierRequestTimeoutMs() {
18268
+ const raw2 = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
18269
+ if (!raw2 || !/^\d+$/.test(raw2)) return DEFAULT_TIER_TIMEOUT_MS;
18270
+ return Math.min(Math.max(parseInt(raw2, 10), 1e3), 12e4);
18271
+ }
18272
+ function planToTier(plan) {
18273
+ switch (String(plan).toUpperCase()) {
18274
+ case "FREE":
18275
+ return "free";
18276
+ case "PRO":
18277
+ return "pro";
18278
+ case "ENTERPRISE":
18279
+ return "enterprise";
18280
+ default:
18281
+ return null;
18282
+ }
18283
+ }
18284
+ async function tierFromBackend(apiKey) {
18285
+ const url = `${getConfiguredApiRoot()}/api/cli/auth/validate`;
18286
+ const controller = new AbortController();
18287
+ const timer = setTimeout(() => controller.abort(), tierRequestTimeoutMs());
18288
+ try {
18289
+ const resp = await fetch(url, {
18290
+ method: "POST",
18291
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
18292
+ body: "{}",
18293
+ signal: controller.signal
18294
+ });
18295
+ if (resp.status === 401 || resp.status === 403) {
18296
+ return {
18297
+ tier: null,
18298
+ source: "unverified",
18299
+ detail: "the server rejected this API key \u2014 run `pretense auth login` again"
18300
+ };
18301
+ }
18302
+ if (!resp.ok) {
18303
+ return {
18304
+ tier: null,
18305
+ source: "unverified",
18306
+ detail: `the plan service returned HTTP ${resp.status}`
18307
+ };
18308
+ }
18309
+ const body = await resp.json();
18310
+ const tier = planToTier(body["plan"]);
18311
+ if (body["valid"] === false || !tier) {
18312
+ return {
18313
+ tier: null,
18314
+ source: "unverified",
18315
+ detail: "the plan service did not report a recognizable plan"
18316
+ };
18317
+ }
18318
+ return { tier, source: "backend" };
18319
+ } catch (err) {
18320
+ const aborted = err?.name === "AbortError";
18321
+ return {
18322
+ tier: null,
18323
+ source: "unverified",
18324
+ detail: aborted ? `could not reach ${url} within ${tierRequestTimeoutMs()}ms` : `could not reach ${url}`
18325
+ };
18326
+ } finally {
18327
+ clearTimeout(timer);
18328
+ }
18329
+ }
18330
+ async function resolvePlanTier(opts = {}) {
18331
+ const apiKey = readApiKey();
18332
+ if (!apiKey) return { tier: "free", source: "no-credentials" };
18333
+ const fromPrefix = tierFromApiKey(apiKey);
18334
+ if (fromPrefix) return { tier: fromPrefix, source: "key-prefix" };
18335
+ if (opts.offline) {
18336
+ return {
18337
+ tier: null,
18338
+ source: "unverified",
18339
+ detail: "--offline was set, and a dashboard key carries no plan of its own"
18340
+ };
18341
+ }
18342
+ return tierFromBackend(apiKey);
18343
+ }
18344
+ var QUOTA_PERIOD_DAYS2 = 7;
18345
+ function collectUsage(dbPath) {
18346
+ const now = /* @__PURE__ */ new Date();
18347
+ const periodMs = QUOTA_PERIOD_DAYS2 * 24 * 60 * 60 * 1e3;
18348
+ const periodStartTs = Date.now() - periodMs;
18349
+ const sinceMs = periodMs;
18350
+ let store = null;
18351
+ try {
18352
+ store = new AuditStore(dbPath);
18353
+ const stats = store.getStats(sinceMs);
18354
+ return {
18355
+ periodStartTs,
18356
+ metrics: {
18357
+ tenantId: "local",
18358
+ // The trailing window these numbers actually cover — previously the
18359
+ // calendar month, which did not match the aggregation OR the quota.
18360
+ periodStart: new Date(periodStartTs).toISOString().split("T")[0],
18361
+ periodEnd: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
18362
+ mutationsApplied: stats.totalMutationsApplied,
18363
+ secretsBlocked: stats.totalSecretsBlocked,
18364
+ piiRedacted: stats.totalPiiRedacted,
18365
+ totalScans: stats.totalRequests,
18366
+ bytesProtected: 0,
18367
+ activeSeats: 1,
18368
+ providers: Object.fromEntries(stats.topProviders.map((p) => [p.provider, p.count]))
18369
+ }
18370
+ };
18371
+ } catch {
18372
+ return null;
18373
+ } finally {
18374
+ store?.close();
18375
+ }
18376
+ }
18377
+ function resetCadenceLabel() {
18378
+ return `every ${QUOTA_PERIOD_DAYS2} days`;
18379
+ }
18380
+ function priceLabel(tier) {
18381
+ const cents = PLAN_CONFIGS[tier].pricePerSeatCents;
18382
+ if (cents === null) return "custom \u2014 contact sales";
18383
+ return cents === 0 ? "free" : `$${cents / 100}/seat/month`;
18384
+ }
18385
+ function tierName(tier) {
18386
+ return { free: "Free", pro: "Pro", enterprise: "Enterprise" }[tier];
18387
+ }
18388
+ function tierLabel(tier) {
18389
+ if (tier === null) return "Unknown (unverified)";
18390
+ if (tier === "free") return "Free";
18391
+ return `${tierName(tier)} (${priceLabel(tier)})`;
18392
+ }
18393
+ function planMismatchHint(resolution) {
18394
+ if (resolution.tier !== "free" || resolution.source !== "backend") return [];
18395
+ return [
18396
+ "Not the plan you expected? An org flagged PRO without an active",
18397
+ "subscription/comp resolves to Free \u2014 see pretense.ai/dashboard/settings or contact support."
18398
+ ];
18399
+ }
18400
+ function unverifiedNote(detail) {
18401
+ return [
18402
+ `Could not confirm your plan: ${detail ?? "unknown reason"}.`,
18403
+ "Limits are not shown because guessing could under- or over-state what you pay for."
18404
+ ];
18405
+ }
18406
+
18407
+ // src/commands/doctor.ts
18408
+ var BASE_URL_ENVS = [
18409
+ { name: "ANTHROPIC_BASE_URL", provider: "Claude / Claude Code" },
18410
+ { name: "OPENAI_BASE_URL", provider: "OpenAI / Cursor / any OpenAI-compatible" },
18411
+ { name: "GEMINI_BASE_URL", provider: "Google Gemini" },
18412
+ { name: "GOOGLE_GEMINI_BASE_URL", provider: "Google Gemini (legacy var)" }
18413
+ ];
18414
+ var COMMON_PORTS = Array.from({ length: 7 }, (_, i) => 9339 + i);
18415
+ async function probePort(port, timeoutMs = 700) {
18416
+ const ctrl = new AbortController();
18417
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
18418
+ try {
18419
+ const res = await fetch(`http://localhost:${port}/health`, { signal: ctrl.signal });
18420
+ if (!res.ok) return { alive: true, isPretense: false };
18421
+ const hasHeader = res.headers.has("x-pretense-version");
18422
+ let body = {};
18423
+ try {
18424
+ body = await res.json();
18425
+ } catch {
18426
+ return { alive: true, isPretense: false };
18427
+ }
18428
+ const looksPretense = hasHeader && "auditAvailable" in body && "version" in body;
18429
+ return {
18430
+ alive: true,
18431
+ isPretense: looksPretense,
18432
+ status: typeof body["status"] === "string" ? body["status"] : void 0,
18433
+ auditAvailable: typeof body["auditAvailable"] === "boolean" ? body["auditAvailable"] : void 0
18434
+ };
18435
+ } catch {
18436
+ return { alive: false, isPretense: false };
18437
+ } finally {
18438
+ clearTimeout(t);
18439
+ }
18440
+ }
18441
+ function portFromBaseUrl(value) {
18442
+ try {
18443
+ const u = new URL(value);
18444
+ if (u.port) return parseInt(u.port, 10);
18445
+ return u.protocol === "https:" ? 443 : 80;
18446
+ } catch {
18447
+ return null;
18448
+ }
18449
+ }
18450
+ function doctorCommand() {
18451
+ return new Command3("doctor").description("Diagnose why the proxy isn't catching your traffic (read-only; exits non-zero if this shell is unprotected)").option("--offline", "Skip the plan-service lookup", false).action(async (opts) => {
18452
+ let problems = 0;
18453
+ const note = (s) => console.log(s);
18454
+ console.log(chalk3.bold.cyan("\n Pretense Doctor\n"));
18455
+ note(chalk3.bold(" Proxy"));
18456
+ const record = readProxyRecord(pidFilePath());
18457
+ let recordedPort = null;
18458
+ if (record) {
18459
+ if (isProcessAlive(record.pid)) {
18460
+ const probe = await probePort(record.port);
18461
+ if (probe.isPretense) {
18462
+ recordedPort = record.port;
18463
+ const degraded = probe.status === "degraded" || probe.auditAvailable === false;
18464
+ note(chalk3.green(` \u2713 Pretense proxy running on :${record.port} (pid ${record.pid}).`));
18465
+ if (degraded) {
18466
+ note(chalk3.yellow(` \u26A0 Audit store unavailable \u2014 requests are still filtered, but ` + chalk3.cyan("pretense audit") + chalk3.yellow(" will be empty this session.")));
18467
+ }
18468
+ } else if (probe.alive) {
18469
+ problems += 1;
18470
+ note(chalk3.yellow(` \u26A0 Pidfile says a proxy is on :${record.port} (pid ${record.pid}), but that port is answering as something ELSE.`));
18471
+ note(chalk3.dim(` Something took the port. Stop the stray process, then ` + chalk3.cyan("pretense start") + chalk3.dim(".")));
18472
+ } else {
18473
+ problems += 1;
18474
+ note(chalk3.yellow(` \u26A0 Pidfile records pid ${record.pid} on :${record.port}, but it is not answering /health.`));
18475
+ note(chalk3.dim(" It may be starting, wedged, or dead. Try ") + chalk3.cyan("pretense stop") + chalk3.dim(" then ") + chalk3.cyan("pretense start") + chalk3.dim("."));
18476
+ }
18477
+ } else {
18478
+ problems += 1;
18479
+ note(chalk3.yellow(` \u26A0 Stale pidfile: pid ${record.pid} (port ${record.port}) is no longer alive.`));
18480
+ note(chalk3.dim(" Harmless, cleared on the next ") + chalk3.cyan("pretense start") + chalk3.dim("."));
18481
+ }
18482
+ } else {
18483
+ note(chalk3.dim(" \u25CB No Pretense proxy recorded (pidfile absent)."));
18484
+ }
18485
+ const stray = [];
18486
+ for (const p of COMMON_PORTS) {
18487
+ if (p === recordedPort) continue;
18488
+ const probe = await probePort(p, 400);
18489
+ if (probe.isPretense) stray.push(p);
18490
+ }
18491
+ if (stray.length > 0) {
18492
+ note(chalk3.yellow(` \u26A0 Other Pretense proxies answering on: ${stray.map((p) => `:${p}`).join(", ")}.`));
18493
+ note(chalk3.dim(" Multiple proxies can send your tool to the wrong one. ") + chalk3.cyan("pretense stop") + chalk3.dim(" clears the recorded one; stop extras by pid."));
18494
+ }
18495
+ note(chalk3.bold("\n This shell"));
18496
+ const setEnvs = BASE_URL_ENVS.filter((e) => (process.env[e.name] ?? "").trim() !== "");
18497
+ let protectedHere = false;
18498
+ if (setEnvs.length === 0) {
18499
+ problems += 1;
18500
+ note(chalk3.red(" \u2717 No base-URL env var is set in THIS terminal \u2014 your AI tool here is NOT protected."));
18501
+ note(chalk3.dim(" Env vars are per-shell: a proxy running elsewhere does not protect this terminal."));
18502
+ note(chalk3.dim(" Simplest fix (no export, no second terminal):"));
18503
+ note(" " + chalk3.cyan('pretense run claude "\u2026"') + chalk3.dim(" # or cursor, gemini, any tool"));
18504
+ if (recordedPort !== null) {
18505
+ note(chalk3.dim(" Or, IN THIS terminal:"));
18506
+ note(" " + chalk3.cyan(`export ANTHROPIC_BASE_URL=http://localhost:${recordedPort}`));
18507
+ }
18508
+ } else {
18509
+ for (const e of setEnvs) {
18510
+ const val = (process.env[e.name] ?? "").trim();
18511
+ const port = portFromBaseUrl(val);
18512
+ if (port === null) {
18513
+ problems += 1;
18514
+ note(chalk3.red(` \u2717 ${e.name}=${val} is not a valid URL.`));
18515
+ continue;
18516
+ }
18517
+ const probe = await probePort(port);
18518
+ if (probe.isPretense) {
18519
+ protectedHere = true;
18520
+ note(chalk3.green(` \u2713 ${e.name} \u2192 :${port} is a live Pretense proxy (${e.provider}).`));
18521
+ } else if (probe.alive) {
18522
+ problems += 1;
18523
+ note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is answering, but it is NOT a Pretense proxy.`));
18524
+ note(chalk3.dim(" Your traffic is going somewhere, but not through the firewall. Fix the URL or use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
18525
+ } else {
18526
+ problems += 1;
18527
+ note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is set, but nothing is listening there.`));
18528
+ note(chalk3.dim(" Start the proxy (") + chalk3.cyan("pretense start") + chalk3.dim(") or, simplest, use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
18529
+ }
18530
+ }
18531
+ }
18532
+ note(chalk3.bold("\n Plan"));
18533
+ try {
18534
+ const resolution = await resolvePlanTier({ offline: opts.offline });
18535
+ note(chalk3.dim(` Plan: ${tierLabel(resolution.tier)}`));
18536
+ for (const line of planMismatchHint(resolution)) {
18537
+ note(chalk3.dim(` ${line}`));
18538
+ }
18539
+ } catch {
18540
+ note(chalk3.dim(" Plan: could not be resolved (offline or no credentials)."));
18541
+ }
18542
+ if (protectedHere && problems === 0) {
18543
+ note(chalk3.bold.green("\n \u2713 This shell is protected \u2014 traffic routes through Pretense.\n"));
18544
+ process.exitCode = 0;
18545
+ } else if (protectedHere) {
18546
+ note(chalk3.bold.yellow(`
18547
+ \u26A0 This shell is protected, but doctor found ${problems} thing(s) to look at above.
18548
+ `));
18549
+ process.exitCode = 1;
18550
+ } else {
18551
+ note(chalk3.bold.red("\n \u2717 This shell is NOT protected. Use ") + chalk3.cyan("pretense run <tool>") + chalk3.bold.red(" \u2014 it wires everything for you.\n"));
18552
+ process.exitCode = 1;
18553
+ }
18554
+ });
18555
+ }
18556
+
18557
+ // src/commands/stop.ts
18558
+ function pidListeningOn2(port) {
18559
+ try {
18560
+ const out = execSync2(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
18561
+ encoding: "utf-8",
18562
+ stdio: ["pipe", "pipe", "ignore"]
18563
+ }).trim();
18564
+ const pid = parseInt(out.split("\n")[0] ?? "", 10);
18565
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
18566
+ } catch {
18567
+ return null;
18568
+ }
18569
+ }
18570
+ async function isRecordOurs(record) {
18571
+ const port = record.port;
18572
+ if (port > 0) {
18573
+ const probe = await probePort(port, 700);
18574
+ const listener = pidListeningOn2(port);
18575
+ if (probe.isPretense) {
18576
+ return listener === null || listener === record.pid;
18577
+ }
18578
+ return listener !== null && listener === record.pid;
18579
+ }
18580
+ return true;
18581
+ }
18582
+ function stopCommand() {
18583
+ return new Command4("stop").description("Stop the running Pretense proxy").option("--json", "Machine-readable outcome", false).action(async (opts) => {
18584
+ const path2 = pidFilePath();
18585
+ const record = readProxyRecord(path2);
18586
+ if (record && isProcessAlive(record.pid) && !await isRecordOurs(record)) {
18587
+ removeProxyRecord(path2);
18588
+ const outcome2 = { kind: "foreign", pid: record.pid, port: record.port };
18589
+ if (opts.json) {
18590
+ console.log(JSON.stringify(outcome2, null, 2));
18591
+ process.exit(0);
18592
+ }
18593
+ console.log(
18594
+ chalk4.yellow(`
18595
+ \xB7 Not stopping pid ${record.pid}: it is not the Pretense proxy.`) + chalk4.dim(`
18596
+ The recorded proxy (port ${record.port}) is gone and that pid now`) + chalk4.dim(`
18597
+ belongs to an unrelated process \u2014 Pretense will not signal it.`) + chalk4.dim(`
18598
+ Cleaned up the stale record; nothing was killed.
18599
+ `)
18600
+ );
18601
+ process.exit(0);
18602
+ }
18603
+ const outcome = await stopProxy(path2);
18604
+ if (opts.json) {
18605
+ console.log(JSON.stringify(outcome, null, 2));
18606
+ process.exit(outcome.kind === "refused" || outcome.kind === "not-permitted" ? 1 : 0);
18607
+ }
18608
+ switch (outcome.kind) {
18609
+ case "not-running":
18610
+ console.log(
18611
+ chalk4.dim(`
18612
+ \xB7 No running Pretense proxy recorded (${path2} does not exist).`) + chalk4.dim(`
18613
+ Start one with `) + chalk4.cyan("pretense start") + "\n"
18614
+ );
18615
+ process.exit(0);
18616
+ break;
18617
+ case "stale":
18618
+ console.log(
18619
+ chalk4.yellow(
18620
+ `
18621
+ \xB7 Stale PID file cleaned up${outcome.pid ? ` (pid ${outcome.pid} is not running)` : ""}.`
18622
+ ) + chalk4.dim(`
18623
+ No proxy was running.
18624
+ `)
18625
+ );
18626
+ process.exit(0);
18627
+ break;
18628
+ case "stopped":
18629
+ console.log(
18630
+ chalk4.green(
18631
+ `
18632
+ \u2713 Pretense proxy stopped (pid ${outcome.pid}${outcome.port ? `, port ${outcome.port}` : ""})` + (outcome.forced ? " \u2014 did not exit on SIGTERM, sent SIGKILL" : "") + "\n"
18633
+ )
18634
+ );
18635
+ process.exit(0);
18636
+ break;
18637
+ case "not-permitted":
18638
+ console.error(
18639
+ chalk4.red(`
18640
+ x Cannot stop pid ${outcome.pid}: it belongs to another user.`) + chalk4.dim(`
18641
+ The proxy is still running. Stop it as its owner, or: sudo kill ${outcome.pid}
18642
+ `)
18643
+ );
18644
+ process.exit(1);
18645
+ break;
18646
+ case "refused":
18647
+ console.error(
18648
+ chalk4.red(`
18649
+ x Pid ${outcome.pid} is still alive after SIGTERM and SIGKILL.`) + chalk4.dim(`
18650
+ ${path2} was left in place so you can investigate: ps -p ${outcome.pid}
18651
+ `)
18652
+ );
18653
+ process.exit(1);
18654
+ break;
18655
+ }
18656
+ });
18657
+ }
18658
+
18659
+ // src/commands/scan.ts
18660
+ init_esm_shims();
18661
+ import { Command as Command5, Option, InvalidArgumentError as InvalidArgumentError2 } from "commander";
18662
+ import chalk5 from "chalk";
18663
+ import { readFileSync as readFileSync8, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
18664
+ import { execSync as execSync3 } from "child_process";
18665
+ import { resolve as resolve2, join as join8, extname as extname2, basename } from "path";
18666
+
18667
+ // src/safe-output.ts
18668
+ init_esm_shims();
18669
+ var UNSAFE_VALUES_FLAG = "--unsafe-show-values";
18670
+ function lineColFromOffset2(text, offset) {
18671
+ const clamped = Math.max(0, Math.min(offset, text.length));
18672
+ let line = 1;
18673
+ let lineStart = 0;
18674
+ for (let i = 0; i < clamped; i++) {
18675
+ if (text.charCodeAt(i) === 10) {
18676
+ line++;
18677
+ lineStart = i + 1;
18678
+ }
18679
+ }
18680
+ return { line, column: clamped - lineStart + 1 };
18681
+ }
18682
+ function withLocations(result, content) {
18683
+ const locate = (m) => lineColFromOffset2(content, m.start);
18684
+ return {
18685
+ ...result,
18686
+ matches: result.matches.map((m) => ({ ...m, ...locate(m) }))
18687
+ };
18688
+ }
18689
+ function safeLocator(m) {
18690
+ const len = Math.max(0, m.end - m.start);
18691
+ if (m.deobfuscated && len === 0) return "len=0 unmapped";
18692
+ const pos = m.line !== void 0 && m.column !== void 0 ? `L${m.line}:${m.column} ` : "";
18693
+ return `${pos}len=${len}${m.deobfuscated ? " enc" : ""}`;
18694
+ }
18695
+ var VALUE_FIELDS = ["value", "masked", "deobfuscatedValue"];
18696
+ function stripRow(row) {
18697
+ const out = {};
18698
+ for (const [k, v] of Object.entries(row)) {
18699
+ if (VALUE_FIELDS.includes(k)) continue;
18700
+ out[k] = v;
18701
+ }
18702
+ out.length = typeof row.end === "number" && typeof row.start === "number" ? row.end - row.start : 0;
18703
+ return out;
18704
+ }
18705
+ function stripValues(result) {
18266
18706
  const r = result;
18267
18707
  return {
18268
18708
  ...r,
@@ -18404,7 +18844,7 @@ function walkDir(dir) {
18404
18844
  } catch (err) {
18405
18845
  if (isSkippableWalkError(err)) {
18406
18846
  const code = err.code;
18407
- console.error(chalk4.yellow(` Skipping unreadable path (${code}): ${dir}`));
18847
+ console.error(chalk5.yellow(` Skipping unreadable path (${code}): ${dir}`));
18408
18848
  return files;
18409
18849
  }
18410
18850
  throw err;
@@ -18412,16 +18852,16 @@ function walkDir(dir) {
18412
18852
  for (const entry of entries) {
18413
18853
  if (entry.isDirectory()) {
18414
18854
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
18415
- files.push(...walkDir(join7(dir, entry.name)));
18855
+ files.push(...walkDir(join8(dir, entry.name)));
18416
18856
  } else if (entry.isFile()) {
18417
- files.push(join7(dir, entry.name));
18857
+ files.push(join8(dir, entry.name));
18418
18858
  }
18419
18859
  }
18420
18860
  return files;
18421
18861
  }
18422
18862
  function gitExecChecked(cmd) {
18423
18863
  try {
18424
- const out = execSync2(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
18864
+ const out = execSync3(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
18425
18865
  return { ok: true, lines: out ? out.split("\n").filter(Boolean) : [] };
18426
18866
  } catch (err) {
18427
18867
  const e = err;
@@ -18468,7 +18908,7 @@ function warnIfLargeForL2(byteLength, filePath) {
18468
18908
  const mb = (byteLength / (1024 * 1024)).toFixed(1);
18469
18909
  const where = filePath ? ` \u2014 ${basename(filePath)}` : "";
18470
18910
  console.error(
18471
- chalk4.yellow(
18911
+ chalk5.yellow(
18472
18912
  ` Note: L2 AST scanning is slow on very large files (${mb}MB${where}); this may take a while.`
18473
18913
  )
18474
18914
  );
@@ -18531,14 +18971,14 @@ async function scanContentRaw(content, opts, filePath) {
18531
18971
  return result;
18532
18972
  }
18533
18973
  async function scanFile(filePath, opts) {
18534
- const content = readFileSync7(filePath, "utf-8");
18974
+ const content = readFileSync8(filePath, "utf-8");
18535
18975
  return scanContent(content, opts, filePath);
18536
18976
  }
18537
18977
  function severityIcon(severity) {
18538
18978
  return severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
18539
18979
  }
18540
18980
  function levelTag(level) {
18541
- return level === 2 ? chalk4.magenta("L2") : chalk4.blue("L1");
18981
+ return level === 2 ? chalk5.magenta("L2") : chalk5.blue("L1");
18542
18982
  }
18543
18983
  function rowTail(match3, opts) {
18544
18984
  if (opts.unsafeShowValues) return match3.masked;
@@ -18549,12 +18989,12 @@ function emitResult(result, opts) {
18549
18989
  }
18550
18990
  function printFindings(filePath, result, opts) {
18551
18991
  if (result.clean) return;
18552
- console.log(chalk4.underline(`
18992
+ console.log(chalk5.underline(`
18553
18993
  ${filePath}`));
18554
18994
  for (const match3 of result.matches) {
18555
- const actionColor = match3.action === "block" ? chalk4.red : match3.action === "redact" ? chalk4.yellow : chalk4.dim;
18995
+ const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
18556
18996
  console.log(
18557
- ` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk4.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk4.dim(rowTail(match3, opts))}`
18997
+ ` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts))}`
18558
18998
  );
18559
18999
  }
18560
19000
  }
@@ -18640,7 +19080,7 @@ async function scanFiles(files, opts) {
18640
19080
  let blocked = 0;
18641
19081
  for (const f of files) {
18642
19082
  if (isBinaryFile(f)) {
18643
- console.error(chalk4.dim(` Skipping binary file: ${f}`));
19083
+ console.error(chalk5.dim(` Skipping binary file: ${f}`));
18644
19084
  continue;
18645
19085
  }
18646
19086
  try {
@@ -18682,17 +19122,17 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
18682
19122
  }
18683
19123
  if (files.length === 0) {
18684
19124
  console.error(
18685
- chalk4.yellow(`
18686
- No supported files found in ${label}.`) + chalk4.dim(`
19125
+ chalk5.yellow(`
19126
+ No supported files found in ${label}.`) + chalk5.dim(`
18687
19127
  Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
18688
19128
  `)
18689
19129
  );
18690
19130
  } else {
18691
19131
  console.log(
18692
- chalk4.yellow(`
18693
- No supported source files found.`) + chalk4.dim(`
19132
+ chalk5.yellow(`
19133
+ No supported source files found.`) + chalk5.dim(`
18694
19134
  Currently supports: TypeScript, JavaScript, Python, Go, Java, C#, Ruby, Rust
18695
- `) + chalk4.dim(` (${SUPPORTED_EXTENSIONS_DISPLAY})
19135
+ `) + chalk5.dim(` (${SUPPORTED_EXTENSIONS_DISPLAY})
18696
19136
  `)
18697
19137
  );
18698
19138
  }
@@ -18723,18 +19163,18 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
18723
19163
  if (allResults.length === 0) {
18724
19164
  if (notice) console.log(notice);
18725
19165
  if (policyLine) console.log(policyLine);
18726
- console.log(chalk4.green(` \u2713 No secrets or PII detected`));
19166
+ console.log(chalk5.green(` \u2713 No secrets or PII detected`));
18727
19167
  process.exit(0);
18728
19168
  }
18729
- console.log(chalk4.cyan(`
19169
+ console.log(chalk5.cyan(`
18730
19170
  Pretense Scan \u2014 ${label}
18731
19171
  `));
18732
- console.log(chalk4.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)}
19172
+ console.log(chalk5.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)}
18733
19173
  `));
18734
19174
  if (policyLine) console.log(policyLine + "\n");
18735
19175
  if (notice) console.log(notice + "\n");
18736
19176
  for (const { file, result } of allResults) printFindings(file, result, opts);
18737
- console.log(chalk4.dim(`
19177
+ console.log(chalk5.dim(`
18738
19178
  Total: ${total} findings | ${blocked} blocked
18739
19179
  `));
18740
19180
  process.exit(findingsExitCode);
@@ -18764,7 +19204,7 @@ function levelOption() {
18764
19204
  function parseLevel(v) {
18765
19205
  if (v === "1") return 1;
18766
19206
  if (v === "2") return 2;
18767
- throw new InvalidArgumentError("Detection level must be 1 (regex) or 2 (AST).");
19207
+ throw new InvalidArgumentError2("Detection level must be 1 (regex) or 2 (AST).");
18768
19208
  }
18769
19209
  function levelsLabel(opts) {
18770
19210
  if (opts.level === 1) return "L1 only (--level 1)";
@@ -18776,13 +19216,13 @@ function l2Notice(results, opts) {
18776
19216
  if (isAstCoreAvailable()) {
18777
19217
  const errored = results.filter((r) => r.result.l2?.parseErrors).length;
18778
19218
  if (errored > 0) {
18779
- return chalk4.yellow(
19219
+ return chalk5.yellow(
18780
19220
  ` ! L2 parsed ${errored} file(s) with syntax errors \u2014 AST findings may be incomplete. L1 results are unaffected.`
18781
19221
  );
18782
19222
  }
18783
19223
  return null;
18784
19224
  }
18785
- return chalk4.yellow(
19225
+ return chalk5.yellow(
18786
19226
  " ! L2 unavailable \u2014 the native AST addon (@pretense/scanner-rs) is not installed for this platform.\n Scanned with L1 (regex) only. All L1 findings are reported in full; no finding was dropped."
18787
19227
  );
18788
19228
  }
@@ -18794,9 +19234,9 @@ function validateScanFlags(opts) {
18794
19234
  const raw2 = String(opts.format).trim().toLowerCase();
18795
19235
  if (!VALID_FORMATS.includes(raw2)) {
18796
19236
  console.error(
18797
- chalk4.red(`
19237
+ chalk5.red(`
18798
19238
  x Unknown --format: "${opts.format}"
18799
- `) + chalk4.dim(` Valid formats: ${VALID_FORMATS.join(", ")}.
19239
+ `) + chalk5.dim(` Valid formats: ${VALID_FORMATS.join(", ")}.
18800
19240
  `)
18801
19241
  );
18802
19242
  process.exit(USAGE_EXIT);
@@ -18807,9 +19247,9 @@ function validateScanFlags(opts) {
18807
19247
  const raw2 = String(opts.severity).trim().toLowerCase();
18808
19248
  if (!VALID_SEVERITIES.includes(raw2)) {
18809
19249
  console.error(
18810
- chalk4.red(`
19250
+ chalk5.red(`
18811
19251
  x Unknown --severity: "${opts.severity}"
18812
- `) + chalk4.dim(` Valid severities: ${VALID_SEVERITIES.join(", ")}.
19252
+ `) + chalk5.dim(` Valid severities: ${VALID_SEVERITIES.join(", ")}.
18813
19253
  `)
18814
19254
  );
18815
19255
  process.exit(USAGE_EXIT);
@@ -18826,7 +19266,7 @@ function applyPolicy(opts) {
18826
19266
  if (resolved.scannerPreset) opts.presetName = resolved.scannerPreset;
18827
19267
  }
18828
19268
  } catch (err) {
18829
- console.error(chalk4.red(`
19269
+ console.error(chalk5.red(`
18830
19270
  x ${err.message}
18831
19271
  `));
18832
19272
  process.exit(USAGE_EXIT);
@@ -18836,15 +19276,15 @@ function applyPolicy(opts) {
18836
19276
  function policyNotice(opts) {
18837
19277
  if (!opts.policyResolved) return null;
18838
19278
  const p = opts.policyResolved;
18839
- const line = chalk4.dim(` ${describePolicy(p)}`);
18840
- return p.scannerPreset ? line : chalk4.yellow(` ! ${describePolicy(p)}`);
19279
+ const line = chalk5.dim(` ${describePolicy(p)}`);
19280
+ return p.scannerPreset ? line : chalk5.yellow(` ! ${describePolicy(p)}`);
18841
19281
  }
18842
19282
  function inheritedScanOpts(cmd) {
18843
19283
  return cmd.optsWithGlobals();
18844
19284
  }
18845
19285
  function ciSubcommand() {
18846
19286
  return addScanOpts(
18847
- new Command4("ci").description("CI-optimized scan (non-interactive, exit code 0=clean 2=findings)").argument("[path]", "File or directory to scan (default: all git-tracked files)").allowExcessArguments(false)
19287
+ new Command5("ci").description("CI-optimized scan (non-interactive, exit code 0=clean 2=findings)").argument("[path]", "File or directory to scan (default: all git-tracked files)").allowExcessArguments(false)
18848
19288
  ).action(async (target, opts, cmd) => {
18849
19289
  opts = inheritedScanOpts(cmd);
18850
19290
  applyPolicy(opts);
@@ -18852,8 +19292,8 @@ function ciSubcommand() {
18852
19292
  const files2 = collectFiles(target);
18853
19293
  if (files2.length === 0) {
18854
19294
  console.error(
18855
- chalk4.red(`
18856
- x Path not found or empty: ${target}`) + chalk4.dim(`
19295
+ chalk5.red(`
19296
+ x Path not found or empty: ${target}`) + chalk5.dim(`
18857
19297
  Try: pretense scan ci <file-or-directory>, or omit the path to scan git-tracked files.
18858
19298
  `)
18859
19299
  );
@@ -18865,11 +19305,11 @@ function ciSubcommand() {
18865
19305
  const tracked = gitExecChecked("git ls-files");
18866
19306
  if (!tracked.ok) {
18867
19307
  console.error(
18868
- chalk4.red(`
18869
- x scan ci could not enumerate git-tracked files \u2014 NOTHING WAS SCANNED.`) + chalk4.dim(`
18870
- git: ${tracked.error}`) + chalk4.dim(`
18871
- This is a scan FAILURE (exit 1), not a clean result. Reporting 0 here`) + chalk4.dim(`
18872
- would mark a CI gate green over a tree that was never read.`) + chalk4.dim(`
19308
+ chalk5.red(`
19309
+ x scan ci could not enumerate git-tracked files \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
19310
+ git: ${tracked.error}`) + chalk5.dim(`
19311
+ This is a scan FAILURE (exit 1), not a clean result. Reporting 0 here`) + chalk5.dim(`
19312
+ would mark a CI gate green over a tree that was never read.`) + chalk5.dim(`
18873
19313
  Fix the checkout, or scan a path directly: pretense scan ci <path>
18874
19314
  `)
18875
19315
  );
@@ -18885,7 +19325,7 @@ function ciSubcommand() {
18885
19325
  }
18886
19326
  function commitRangeSubcommand() {
18887
19327
  return addScanOpts(
18888
- new Command4("commit-range").description("Scan files changed between two commits").argument("<range>", "Commit range (e.g. base..head)")
19328
+ new Command5("commit-range").description("Scan files changed between two commits").argument("<range>", "Commit range (e.g. base..head)")
18889
19329
  ).action(async (range, opts, cmd) => {
18890
19330
  opts = inheritedScanOpts(cmd);
18891
19331
  applyPolicy(opts);
@@ -18893,7 +19333,7 @@ function commitRangeSubcommand() {
18893
19333
  if (files.length === 0) {
18894
19334
  const fmt2 = resolveFormat(opts);
18895
19335
  if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
18896
- else console.log(chalk4.green(" \u2713 No changed files in range"));
19336
+ else console.log(chalk5.green(" \u2713 No changed files in range"));
18897
19337
  process.exit(0);
18898
19338
  }
18899
19339
  await outputMultiFileResults(`commit-range ${range}`, files, opts);
@@ -18901,7 +19341,7 @@ function commitRangeSubcommand() {
18901
19341
  }
18902
19342
  function preCommitSubcommand() {
18903
19343
  return addScanOpts(
18904
- new Command4("pre-commit").description("Scan git staged files")
19344
+ new Command5("pre-commit").description("Scan git staged files")
18905
19345
  ).action(async (opts, cmd) => {
18906
19346
  opts = inheritedScanOpts(cmd);
18907
19347
  applyPolicy(opts);
@@ -18909,23 +19349,79 @@ function preCommitSubcommand() {
18909
19349
  if (files.length === 0) {
18910
19350
  const fmt2 = resolveFormat(opts);
18911
19351
  if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
18912
- else console.log(chalk4.green(" \u2713 No staged files to scan"));
19352
+ else console.log(chalk5.green(" \u2713 No staged files to scan"));
18913
19353
  process.exit(0);
18914
19354
  }
18915
19355
  await outputMultiFileResults(`pre-commit (${files.length} staged files)`, files, opts);
18916
19356
  });
18917
19357
  }
19358
+ function isZeroOid(oid) {
19359
+ return /^0+$/.test(oid.trim());
19360
+ }
19361
+ function emptyTreeOid() {
19362
+ const r = gitExecChecked("git hash-object -t tree /dev/null");
19363
+ return r.ok && r.lines[0] ? r.lines[0] : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
19364
+ }
19365
+ function changedFilesForPush(localSha, remoteSha) {
19366
+ if (isZeroOid(localSha)) return { ok: true, lines: [] };
19367
+ if (!isZeroOid(remoteSha)) {
19368
+ return gitExecChecked(`git diff --name-only ${remoteSha} ${localSha}`);
19369
+ }
19370
+ const revs = gitExecChecked(`git rev-list ${localSha} --not --remotes`);
19371
+ if (revs.ok && revs.lines.length > 0) {
19372
+ const oldest = revs.lines[revs.lines.length - 1];
19373
+ const parent = gitExecChecked(`git rev-parse --verify --quiet ${oldest}^`);
19374
+ const base = parent.ok && parent.lines[0] ? parent.lines[0] : emptyTreeOid();
19375
+ return gitExecChecked(`git diff --name-only ${base} ${localSha}`);
19376
+ }
19377
+ if (!revs.ok) return revs;
19378
+ return gitExecChecked(`git diff --name-only ${emptyTreeOid()} ${localSha}`);
19379
+ }
19380
+ async function readStdin() {
19381
+ if (process.stdin.isTTY) return "";
19382
+ const chunks = [];
19383
+ try {
19384
+ for await (const c of process.stdin) chunks.push(c);
19385
+ } catch {
19386
+ return "";
19387
+ }
19388
+ return Buffer.concat(chunks).toString("utf-8");
19389
+ }
18918
19390
  function prePushSubcommand() {
18919
19391
  return addScanOpts(
18920
- new Command4("pre-push").description("Scan files changed between local and remote")
19392
+ new Command5("pre-push").description("Scan the commits being pushed (reads git's pre-push ref lines on stdin)")
18921
19393
  ).action(async (opts, cmd) => {
18922
19394
  opts = inheritedScanOpts(cmd);
18923
19395
  applyPolicy(opts);
18924
- const files = filterExisting(gitExec("git diff --name-only @{push}..HEAD"));
19396
+ const stdin = await readStdin();
19397
+ const refLines = stdin.split("\n").map((l) => l.trim()).filter(Boolean);
19398
+ const changed = /* @__PURE__ */ new Set();
19399
+ if (refLines.length > 0) {
19400
+ for (const line of refLines) {
19401
+ const [, localSha, , remoteSha] = line.split(/\s+/);
19402
+ if (!localSha || !remoteSha) continue;
19403
+ const r = changedFilesForPush(localSha, remoteSha);
19404
+ if (!r.ok) {
19405
+ console.error(
19406
+ chalk5.red(`
19407
+ x pre-push: could not determine what is being pushed \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
19408
+ git: ${r.error}`) + chalk5.dim(`
19409
+ Blocking the push (exit 1) rather than letting an unscanned commit through.`) + chalk5.dim(`
19410
+ Override at your own risk with: git push --no-verify
19411
+ `)
19412
+ );
19413
+ process.exit(1);
19414
+ }
19415
+ for (const f of r.lines) changed.add(f);
19416
+ }
19417
+ } else {
19418
+ for (const f of gitExec("git diff --name-only @{push}..HEAD")) changed.add(f);
19419
+ }
19420
+ const files = filterExisting([...changed]);
18925
19421
  if (files.length === 0) {
18926
19422
  const fmt2 = resolveFormat(opts);
18927
19423
  if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
18928
- else console.log(chalk4.green(" \u2713 No changed files to push"));
19424
+ else console.log(chalk5.green(" \u2713 No changed files to push"));
18929
19425
  process.exit(0);
18930
19426
  }
18931
19427
  await outputMultiFileResults(`pre-push (${files.length} changed files)`, files, opts);
@@ -18933,15 +19429,15 @@ function prePushSubcommand() {
18933
19429
  }
18934
19430
  function pathSubcommand() {
18935
19431
  return addScanOpts(
18936
- new Command4("path").description("Scan a file or directory (recursive)").argument("<target>", "File or directory to scan")
19432
+ new Command5("path").description("Scan a file or directory (recursive)").argument("<target>", "File or directory to scan")
18937
19433
  ).action(async (target, opts, cmd) => {
18938
19434
  opts = inheritedScanOpts(cmd);
18939
19435
  applyPolicy(opts);
18940
19436
  const files = collectFiles(target);
18941
19437
  if (files.length === 0) {
18942
19438
  console.error(
18943
- chalk4.red(`
18944
- x Path not found or empty: ${target}`) + chalk4.dim(`
19439
+ chalk5.red(`
19440
+ x Path not found or empty: ${target}`) + chalk5.dim(`
18945
19441
  Try: pretense scan path <file-or-directory>
18946
19442
  `)
18947
19443
  );
@@ -18951,7 +19447,7 @@ function pathSubcommand() {
18951
19447
  });
18952
19448
  }
18953
19449
  function scanCommand() {
18954
- const cmd = new Command4("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").allowExcessArguments(false).option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option(
19450
+ const cmd = new Command5("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").allowExcessArguments(false).option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option(
18955
19451
  "--policy <preset>",
18956
19452
  // A preset RE-WEIGHTS detector severity/action for that framework
18957
19453
  // (see compliance-preset.ts `actionOverrides`). It does NOT narrow the
@@ -18974,8 +19470,8 @@ function scanCommand() {
18974
19470
  const allFiles = collectFiles(file);
18975
19471
  if (allFiles.length === 0) {
18976
19472
  console.error(
18977
- chalk4.yellow(`
18978
- No supported files found in ${file}.`) + chalk4.dim(`
19473
+ chalk5.yellow(`
19474
+ No supported files found in ${file}.`) + chalk5.dim(`
18979
19475
  Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
18980
19476
  `)
18981
19477
  );
@@ -18986,17 +19482,17 @@ function scanCommand() {
18986
19482
  } catch {
18987
19483
  }
18988
19484
  if (isBinaryFile(file)) {
18989
- console.error(chalk4.yellow(`
19485
+ console.error(chalk5.yellow(`
18990
19486
  Skipping binary file: ${file}
18991
19487
  `));
18992
19488
  process.exit(0);
18993
19489
  }
18994
19490
  try {
18995
- content = readFileSync7(file, "utf-8");
19491
+ content = readFileSync8(file, "utf-8");
18996
19492
  } catch {
18997
19493
  console.error(
18998
- chalk4.red(`
18999
- x Cannot read file: ${file}`) + chalk4.dim(`
19494
+ chalk5.red(`
19495
+ x Cannot read file: ${file}`) + chalk5.dim(`
19000
19496
  Try: check the file exists and you have read permission.
19001
19497
  `)
19002
19498
  );
@@ -19005,7 +19501,7 @@ function scanCommand() {
19005
19501
  } else {
19006
19502
  if (process.stdin.isTTY) {
19007
19503
  console.error(
19008
- chalk4.yellow("\n x Nothing to scan.\n") + chalk4.dim(" Scan this project: ") + chalk4.cyan("pretense scan .") + "\n" + chalk4.dim(" Scan a path: ") + chalk4.cyan("pretense scan <file-or-dir>") + "\n" + chalk4.dim(" Or pipe content: ") + chalk4.cyan("cat file | pretense scan") + "\n"
19504
+ chalk5.yellow("\n x Nothing to scan.\n") + chalk5.dim(" Scan this project: ") + chalk5.cyan("pretense scan .") + "\n" + chalk5.dim(" Scan a path: ") + chalk5.cyan("pretense scan <file-or-dir>") + "\n" + chalk5.dim(" Or pipe content: ") + chalk5.cyan("cat file | pretense scan") + "\n"
19009
19505
  );
19010
19506
  process.exit(USAGE_EXIT);
19011
19507
  }
@@ -19014,484 +19510,313 @@ function scanCommand() {
19014
19510
  content = Buffer.concat(chunks).toString("utf-8");
19015
19511
  if (content.trim() === "") {
19016
19512
  console.error(
19017
- chalk4.yellow("\n x Nothing to scan \u2014 no file argument and empty input.\n") + chalk4.dim(" Scan this project: ") + chalk4.cyan("pretense scan .") + "\n" + chalk4.dim(" Scan a path: ") + chalk4.cyan("pretense scan <file-or-dir>") + "\n" + chalk4.dim(" Or pipe content: ") + chalk4.cyan("cat file | pretense scan") + "\n"
19018
- );
19019
- process.exit(USAGE_EXIT);
19020
- }
19021
- }
19022
- const rawResult = await scanContent(content, opts, file);
19023
- const filteredMatches = filterBySeverity(rawResult.matches, opts?.severity);
19024
- const result = { ...rawResult, matches: filteredMatches, clean: filteredMatches.length === 0 };
19025
- const fmt2 = resolveFormat(opts ?? {});
19026
- if (fmt2 === "json") {
19027
- console.log(JSON.stringify(emitResult(result, opts ?? {}), null, 2));
19028
- process.exit(result.clean ? 0 : 1);
19029
- }
19030
- if (fmt2 === "sarif") {
19031
- const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
19032
- console.log(JSON.stringify(toSarif(fileResults, opts ?? {}), null, 2));
19033
- process.exit(result.clean ? 0 : 1);
19034
- }
19035
- if (fmt2 === "csv") {
19036
- const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
19037
- console.log(toCsv(fileResults, opts ?? {}));
19038
- process.exit(result.clean ? 0 : 1);
19039
- }
19040
- console.log(chalk4.cyan(`
19041
- Pretense Scan \u2014 ${file ?? "stdin"}
19042
- `));
19043
- console.log(chalk4.dim(` Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)} | Duration: ${result.durationMs}ms
19044
- `));
19045
- {
19046
- const policyLine = policyNotice(opts);
19047
- if (policyLine) console.log(policyLine + "\n");
19048
- }
19049
- if (opts.level !== 1 && result.l2 && !result.l2.ran && result.l2.reason) {
19050
- console.log(chalk4.yellow(` ! ${result.l2.reason}`));
19051
- console.log(chalk4.dim(" L1 (regex) ran in full; no finding was dropped.\n"));
19052
- }
19053
- if (result.clean) {
19054
- console.log(chalk4.green(" \u2713 No secrets or PII detected\n"));
19055
- process.exit(0);
19056
- }
19057
- for (const match3 of result.matches) {
19058
- const actionColor = match3.action === "block" ? chalk4.red : match3.action === "redact" ? chalk4.yellow : chalk4.dim;
19059
- console.log(
19060
- ` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk4.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk4.dim(rowTail(match3, opts ?? {}))}`
19061
- );
19062
- }
19063
- const blocked = result.matches.filter((m) => m.action === "block").length;
19064
- const redacted = result.matches.filter((m) => m.action === "redact").length;
19065
- console.log(chalk4.dim(`
19066
- Total: ${result.matches.length} findings | ${blocked} blocked, ${redacted} redacted
19067
- `));
19068
- process.exit(1);
19069
- });
19070
- cmd.addCommand(ciSubcommand());
19071
- cmd.addCommand(commitRangeSubcommand());
19072
- cmd.addCommand(preCommitSubcommand());
19073
- cmd.addCommand(prePushSubcommand());
19074
- cmd.addCommand(pathSubcommand());
19075
- cmd.exitOverride((err) => {
19076
- process.exit(err.exitCode === 0 ? 0 : 3);
19077
- });
19078
- return cmd;
19079
- }
19080
-
19081
- // src/commands/status.ts
19082
- init_esm_shims();
19083
- import { Command as Command5 } from "commander";
19084
- import chalk5 from "chalk";
19085
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
19086
- import { join as join8 } from "path";
19087
- function statusCommand() {
19088
- return new Command5("status").description("Show Pretense protection status").action(async () => {
19089
- const dir = process.cwd();
19090
- const fingerprintPath = join8(dir, ".pretense", "fingerprint.json");
19091
- const configPath = join8(dir, "pretense.yaml");
19092
- console.log(chalk5.cyan("\n Pretense Status\n"));
19093
- const record = readProxyRecord(pidFilePath());
19094
- if (record && !isProcessAlive(record.pid)) {
19095
- console.log(
19096
- chalk5.yellow(
19097
- ` \u25CB Proxy not running ${chalk5.dim(`(stale record for pid ${record.pid}; run: pretense start)`)}`
19098
- )
19099
- );
19100
- } else {
19101
- const port = record && record.port > 0 ? record.port : 9339;
19102
- try {
19103
- const resp = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
19104
- if (!resp.ok) throw new Error(`health returned ${resp.status}`);
19105
- const health = await resp.json();
19106
- console.log(chalk5.green(` \u2713 Proxy running on :${port} (uptime: ${health["uptime"]}s)`));
19107
- const stats = health["stats"];
19108
- console.log(chalk5.dim(` Requests: ${stats["requestsProcessed"] ?? 0} | Mutations: ${stats["mutationsApplied"] ?? 0} | Blocked: ${stats["secretsBlocked"] ?? 0}`));
19109
- } catch {
19110
- console.log(chalk5.yellow(` \u25CB Proxy not running ${chalk5.dim("(run: pretense start)")}`));
19111
- }
19112
- }
19113
- if (existsSync8(configPath)) {
19114
- console.log(chalk5.green(` \u2713 Config: pretense.yaml`));
19115
- } else {
19116
- console.log(chalk5.yellow(` \u25CB No config ${chalk5.dim("(run: pretense config)")}`));
19117
- }
19118
- if (existsSync8(fingerprintPath)) {
19119
- try {
19120
- const fp = JSON.parse(readFileSync8(fingerprintPath, "utf-8"));
19121
- const age = Math.round((Date.now() - fp.scannedAt) / (60 * 1e3));
19122
- console.log(chalk5.green(` \u2713 Fingerprint: ${fp.filesScanned} files, ${fp.functions.length + fp.classes.length + fp.constants.length} identifiers`));
19123
- console.log(chalk5.dim(` Last scanned: ${age} minutes ago`));
19124
- } catch {
19125
- console.log(chalk5.yellow(" \u26A0 Fingerprint corrupted (run: pretense init)"));
19126
- }
19127
- } else {
19128
- console.log(chalk5.yellow(` \u25CB No fingerprint ${chalk5.dim("(run: pretense init)")}`));
19129
- }
19130
- console.log(chalk5.dim(`
19131
- Scanner: ${getPatternCount()} patterns active
19132
- `));
19133
- });
19134
- }
19135
-
19136
- // src/commands/config.ts
19137
- init_esm_shims();
19138
- import { Command as Command6 } from "commander";
19139
- import chalk6 from "chalk";
19140
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
19141
- import { join as join9 } from "path";
19142
- import YAML2 from "yaml";
19143
- var DEFAULT_CONFIG = {
19144
- version: 1,
19145
- languages: ["typescript", "python", "java", "go", "ruby", "rust"],
19146
- exclude_paths: ["node_modules", ".git", "dist"],
19147
- mutation_mode: "full",
19148
- output_format: "interactive"
19149
- };
19150
- function findConfigPath() {
19151
- const cwd = process.cwd();
19152
- const candidates = [".pretense.yaml", "pretense.yaml"];
19153
- for (const name of candidates) {
19154
- const full = join9(cwd, name);
19155
- if (existsSync9(full)) return full;
19156
- }
19157
- return null;
19158
- }
19159
- function loadConfig2() {
19160
- const path2 = findConfigPath();
19161
- if (!path2) return { ...DEFAULT_CONFIG };
19162
- try {
19163
- const raw2 = readFileSync9(path2, "utf-8");
19164
- return YAML2.parse(raw2) ?? { ...DEFAULT_CONFIG };
19165
- } catch {
19166
- return { ...DEFAULT_CONFIG };
19167
- }
19168
- }
19169
- function configFilePath() {
19170
- return findConfigPath() ?? join9(process.cwd(), "pretense.yaml");
19171
- }
19172
- function getNestedValue(obj, key) {
19173
- const parts = key.split(".");
19174
- let cur = obj;
19175
- for (const p of parts) {
19176
- if (cur == null || typeof cur !== "object") return void 0;
19177
- cur = cur[p];
19178
- }
19179
- return cur;
19180
- }
19181
- function setNestedValue(obj, key, value) {
19182
- const parts = key.split(".");
19183
- const leaf = parts.pop();
19184
- if (leaf === void 0) return;
19185
- let cur = obj;
19186
- for (const p of parts) {
19187
- const child = cur[p];
19188
- if (child == null || typeof child !== "object" || Array.isArray(child)) {
19189
- cur[p] = {};
19190
- }
19191
- cur = cur[p];
19192
- }
19193
- cur[leaf] = value;
19194
- }
19195
- function parseValue(raw2) {
19196
- if (raw2.startsWith("[") && raw2.endsWith("]")) {
19197
- return raw2.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
19198
- }
19199
- if (raw2 === "true") return true;
19200
- if (raw2 === "false") return false;
19201
- const n = Number(raw2);
19202
- if (!Number.isNaN(n) && raw2.length > 0) return n;
19203
- return raw2;
19204
- }
19205
- function configCommand() {
19206
- const cmd = new Command6("config").description("Manage pretense configuration");
19207
- cmd.command("list").description("Print current configuration").action(() => {
19208
- const config = loadConfig2();
19209
- const path2 = findConfigPath();
19210
- console.log(
19211
- chalk6.cyan(`
19212
- Pretense Config`) + chalk6.dim(` (${path2 ?? "defaults - no config file found"})
19213
- `)
19214
- );
19215
- console.log(YAML2.stringify(config).replace(/^/gm, " "));
19216
- });
19217
- cmd.command("get").description("Print a single config value").argument("<key>", "Config key (supports dotted paths)").action((key) => {
19218
- const config = loadConfig2();
19219
- const val = getNestedValue(config, key);
19220
- if (val === void 0) {
19221
- console.error(chalk6.red(`
19222
- \u2717 Key not found: ${key}
19223
- `));
19224
- process.exit(1);
19513
+ chalk5.yellow("\n x Nothing to scan \u2014 no file argument and empty input.\n") + chalk5.dim(" Scan this project: ") + chalk5.cyan("pretense scan .") + "\n" + chalk5.dim(" Scan a path: ") + chalk5.cyan("pretense scan <file-or-dir>") + "\n" + chalk5.dim(" Or pipe content: ") + chalk5.cyan("cat file | pretense scan") + "\n"
19514
+ );
19515
+ process.exit(USAGE_EXIT);
19516
+ }
19225
19517
  }
19226
- const display = typeof val === "object" ? JSON.stringify(val) : String(val);
19227
- console.log(display);
19228
- });
19229
- cmd.command("set").description("Set a config value").argument("<key>", "Config key (supports dotted paths)").argument("<value>", "Value to set").action((key, rawValue) => {
19230
- const config = loadConfig2();
19231
- const value = parseValue(rawValue);
19232
- setNestedValue(config, key, value);
19233
- const outPath = configFilePath();
19234
- writeFileSync4(outPath, YAML2.stringify(config));
19235
- console.log(chalk6.green(`
19236
- \u2713 Set ${key} = ${JSON.stringify(value)} in ${outPath}
19518
+ const rawResult = await scanContent(content, opts, file);
19519
+ const filteredMatches = filterBySeverity(rawResult.matches, opts?.severity);
19520
+ const result = { ...rawResult, matches: filteredMatches, clean: filteredMatches.length === 0 };
19521
+ const fmt2 = resolveFormat(opts ?? {});
19522
+ if (fmt2 === "json") {
19523
+ console.log(JSON.stringify(emitResult(result, opts ?? {}), null, 2));
19524
+ process.exit(result.clean ? 0 : 1);
19525
+ }
19526
+ if (fmt2 === "sarif") {
19527
+ const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
19528
+ console.log(JSON.stringify(toSarif(fileResults, opts ?? {}), null, 2));
19529
+ process.exit(result.clean ? 0 : 1);
19530
+ }
19531
+ if (fmt2 === "csv") {
19532
+ const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
19533
+ console.log(toCsv(fileResults, opts ?? {}));
19534
+ process.exit(result.clean ? 0 : 1);
19535
+ }
19536
+ console.log(chalk5.cyan(`
19537
+ Pretense Scan \u2014 ${file ?? "stdin"}
19237
19538
  `));
19238
- });
19239
- cmd.option("--force", "Overwrite existing config with defaults", false).action((opts) => {
19240
- if (opts.force) {
19241
- const outPath = join9(process.cwd(), "pretense.yaml");
19242
- writeFileSync4(outPath, YAML2.stringify(DEFAULT_CONFIG));
19243
- console.log(chalk6.green(`
19244
- \u2713 Created pretense.yaml
19539
+ console.log(chalk5.dim(` Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)} | Duration: ${result.durationMs}ms
19245
19540
  `));
19246
- } else {
19247
- cmd.help();
19541
+ {
19542
+ const policyLine = policyNotice(opts);
19543
+ if (policyLine) console.log(policyLine + "\n");
19544
+ }
19545
+ if (opts.level !== 1 && result.l2 && !result.l2.ran && result.l2.reason) {
19546
+ console.log(chalk5.yellow(` ! ${result.l2.reason}`));
19547
+ console.log(chalk5.dim(" L1 (regex) ran in full; no finding was dropped.\n"));
19548
+ }
19549
+ if (result.clean) {
19550
+ console.log(chalk5.green(" \u2713 No secrets or PII detected\n"));
19551
+ process.exit(0);
19552
+ }
19553
+ for (const match3 of result.matches) {
19554
+ const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
19555
+ console.log(
19556
+ ` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts ?? {}))}`
19557
+ );
19248
19558
  }
19559
+ const blocked = result.matches.filter((m) => m.action === "block").length;
19560
+ const redacted = result.matches.filter((m) => m.action === "redact").length;
19561
+ console.log(chalk5.dim(`
19562
+ Total: ${result.matches.length} findings | ${blocked} blocked, ${redacted} redacted
19563
+ `));
19564
+ process.exit(1);
19565
+ });
19566
+ cmd.addCommand(ciSubcommand());
19567
+ cmd.addCommand(commitRangeSubcommand());
19568
+ cmd.addCommand(preCommitSubcommand());
19569
+ cmd.addCommand(prePushSubcommand());
19570
+ cmd.addCommand(pathSubcommand());
19571
+ cmd.exitOverride((err) => {
19572
+ process.exit(err.exitCode === 0 ? 0 : 3);
19249
19573
  });
19250
19574
  return cmd;
19251
19575
  }
19252
19576
 
19253
- // src/commands/logs.ts
19577
+ // src/commands/status.ts
19254
19578
  init_esm_shims();
19255
- import { Command as Command7 } from "commander";
19256
- import chalk7 from "chalk";
19257
- import { homedir as homedir4 } from "os";
19258
- import { join as join10 } from "path";
19259
- function logsCommand() {
19260
- return new Command7("logs").description("Show recent audit log entries").option("-l, --limit <n>", "Number of entries to show", "50").option("--json", "Output as JSON", false).action((opts) => {
19261
- const limit = parseInt(opts.limit, 10);
19262
- let store;
19263
- let entries;
19264
- try {
19265
- store = new AuditStore(join10(homedir4(), ".pretense", "audit.db"));
19266
- entries = store.getRecentEntries(limit);
19267
- store.close();
19268
- } catch {
19269
- if (opts.json) {
19270
- console.log("[]");
19271
- return;
19579
+ import { Command as Command6 } from "commander";
19580
+ import chalk6 from "chalk";
19581
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
19582
+ import { join as join9 } from "path";
19583
+ function statusCommand() {
19584
+ return new Command6("status").description("Show Pretense protection status").action(async () => {
19585
+ const dir = process.cwd();
19586
+ const fingerprintPath = join9(dir, ".pretense", "fingerprint.json");
19587
+ const configPath = join9(dir, "pretense.yaml");
19588
+ console.log(chalk6.cyan("\n Pretense Status\n"));
19589
+ const record = readProxyRecord(pidFilePath());
19590
+ if (record && !isProcessAlive(record.pid)) {
19591
+ console.log(
19592
+ chalk6.yellow(
19593
+ ` \u25CB Proxy not running ${chalk6.dim(`(stale record for pid ${record.pid}; run: pretense start)`)}`
19594
+ )
19595
+ );
19596
+ } else {
19597
+ const port = record && record.port > 0 ? record.port : 9339;
19598
+ try {
19599
+ const resp = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
19600
+ if (!resp.ok) throw new Error(`health returned ${resp.status}`);
19601
+ const health = await resp.json();
19602
+ console.log(chalk6.green(` \u2713 Proxy running on :${port} (uptime: ${health["uptime"]}s)`));
19603
+ const stats = health["stats"];
19604
+ console.log(chalk6.dim(` Requests: ${stats["requestsProcessed"] ?? 0} | Mutations: ${stats["mutationsApplied"] ?? 0} | Blocked: ${stats["secretsBlocked"] ?? 0}`));
19605
+ } catch {
19606
+ console.log(chalk6.yellow(` \u25CB Proxy not running ${chalk6.dim("(run: pretense start)")}`));
19272
19607
  }
19273
- console.error(chalk7.dim("\n No audit data available. The audit database has not been initialized yet."));
19274
- console.error(chalk7.dim(" Run " + chalk7.cyan("pretense start") + " and make some requests first.\n"));
19275
- return;
19276
19608
  }
19277
- if (opts.json) {
19278
- console.log(JSON.stringify(entries, null, 2));
19279
- return;
19609
+ if (existsSync9(configPath)) {
19610
+ console.log(chalk6.green(` \u2713 Config: pretense.yaml`));
19611
+ } else {
19612
+ console.log(chalk6.yellow(` \u25CB No config ${chalk6.dim("(run: pretense config)")}`));
19280
19613
  }
19281
- if (entries.length === 0) {
19282
- console.log(chalk7.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
19283
- return;
19614
+ if (existsSync9(fingerprintPath)) {
19615
+ try {
19616
+ const fp = JSON.parse(readFileSync9(fingerprintPath, "utf-8"));
19617
+ const age = Math.round((Date.now() - fp.scannedAt) / (60 * 1e3));
19618
+ console.log(chalk6.green(` \u2713 Fingerprint: ${fp.filesScanned} files, ${fp.functions.length + fp.classes.length + fp.constants.length} identifiers`));
19619
+ console.log(chalk6.dim(` Last scanned: ${age} minutes ago`));
19620
+ } catch {
19621
+ console.log(chalk6.yellow(" \u26A0 Fingerprint corrupted (run: pretense init)"));
19622
+ }
19623
+ } else {
19624
+ console.log(chalk6.yellow(` \u25CB No fingerprint ${chalk6.dim("(run: pretense init)")}`));
19284
19625
  }
19285
- console.log(chalk7.cyan(`
19286
- Pretense Audit Log \u2014 last ${entries.length} entries
19626
+ console.log(chalk6.dim(`
19627
+ Scanner: ${getPatternCount()} patterns active
19287
19628
  `));
19288
- console.log(chalk7.dim(" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATIONS".padEnd(12) + "BLOCKED".padEnd(10) + "MS"));
19289
- console.log(chalk7.dim(" " + "\u2500".repeat(70)));
19290
- for (const entry of entries) {
19291
- const time = new Date(entry.timestamp).toTimeString().slice(0, 8);
19292
- const actionColor = entry.action === "blocked" ? chalk7.red : entry.action === "mutated" ? chalk7.green : entry.action === "redacted" ? chalk7.yellow : entry.action === "error" ? chalk7.red : chalk7.dim;
19293
- console.log(
19294
- " " + chalk7.dim(time.padEnd(10)) + actionColor(entry.action.padEnd(12)) + chalk7.dim(entry.provider.padEnd(14)) + chalk7.dim(String(entry.mutationsApplied).padEnd(12)) + (entry.secretsBlocked > 0 ? chalk7.red(String(entry.secretsBlocked).padEnd(10)) : chalk7.dim("0".padEnd(10))) + chalk7.dim(String(entry.scanDurationMs).slice(0, 5))
19295
- );
19296
- }
19297
- console.log();
19298
19629
  });
19299
19630
  }
19300
19631
 
19301
- // src/commands/usage.ts
19302
- init_esm_shims();
19303
- import { Command as Command8 } from "commander";
19304
- import chalk8 from "chalk";
19305
- import { join as join12 } from "path";
19306
- import { homedir as homedir6 } from "os";
19307
-
19308
- // src/plan-usage.ts
19632
+ // src/commands/config.ts
19309
19633
  init_esm_shims();
19310
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
19311
- import { join as join11 } from "path";
19312
- import { homedir as homedir5 } from "os";
19313
- function credentialsPath() {
19314
- return join11(homedir5(), ".pretense", "credentials.json");
19315
- }
19316
- function readApiKey() {
19317
- const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
19318
- if (fromEnv) return fromEnv;
19319
- const path2 = credentialsPath();
19320
- if (!existsSync10(path2)) return null;
19321
- try {
19322
- const creds = JSON.parse(readFileSync10(path2, "utf-8"));
19323
- const key = creds["apiKey"] ?? creds["api_key"];
19324
- if (typeof key === "string" && key.trim().length > 0) return key.trim();
19325
- } catch {
19326
- }
19327
- return null;
19328
- }
19329
- var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
19330
- var DEFAULT_TIER_TIMEOUT_MS = 5e3;
19331
- function getConfiguredApiRoot() {
19332
- const raw2 = (process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
19333
- if (!raw2) return DEFAULT_PRETENSE_API_URL;
19334
- const normalized = /^https?:\/\//i.test(raw2) ? raw2 : `https://${raw2}`;
19335
- try {
19336
- const u = new URL(normalized);
19337
- const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL : u.origin;
19338
- const path2 = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
19339
- return `${origin}${path2}`;
19340
- } catch {
19341
- return DEFAULT_PRETENSE_API_URL;
19342
- }
19343
- }
19344
- function tierRequestTimeoutMs() {
19345
- const raw2 = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
19346
- if (!raw2 || !/^\d+$/.test(raw2)) return DEFAULT_TIER_TIMEOUT_MS;
19347
- return Math.min(Math.max(parseInt(raw2, 10), 1e3), 12e4);
19348
- }
19349
- function planToTier(plan) {
19350
- switch (String(plan).toUpperCase()) {
19351
- case "FREE":
19352
- return "free";
19353
- case "PRO":
19354
- return "pro";
19355
- case "ENTERPRISE":
19356
- return "enterprise";
19357
- default:
19358
- return null;
19359
- }
19360
- }
19361
- async function tierFromBackend(apiKey) {
19362
- const url = `${getConfiguredApiRoot()}/api/cli/auth/validate`;
19363
- const controller = new AbortController();
19364
- const timer = setTimeout(() => controller.abort(), tierRequestTimeoutMs());
19365
- try {
19366
- const resp = await fetch(url, {
19367
- method: "POST",
19368
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
19369
- body: "{}",
19370
- signal: controller.signal
19371
- });
19372
- if (resp.status === 401 || resp.status === 403) {
19373
- return {
19374
- tier: null,
19375
- source: "unverified",
19376
- detail: "the server rejected this API key \u2014 run `pretense auth login` again"
19377
- };
19378
- }
19379
- if (!resp.ok) {
19380
- return {
19381
- tier: null,
19382
- source: "unverified",
19383
- detail: `the plan service returned HTTP ${resp.status}`
19384
- };
19385
- }
19386
- const body = await resp.json();
19387
- const tier = planToTier(body["plan"]);
19388
- if (body["valid"] === false || !tier) {
19389
- return {
19390
- tier: null,
19391
- source: "unverified",
19392
- detail: "the plan service did not report a recognizable plan"
19393
- };
19394
- }
19395
- return { tier, source: "backend" };
19396
- } catch (err) {
19397
- const aborted = err?.name === "AbortError";
19398
- return {
19399
- tier: null,
19400
- source: "unverified",
19401
- detail: aborted ? `could not reach ${url} within ${tierRequestTimeoutMs()}ms` : `could not reach ${url}`
19402
- };
19403
- } finally {
19404
- clearTimeout(timer);
19405
- }
19406
- }
19407
- async function resolvePlanTier(opts = {}) {
19408
- const apiKey = readApiKey();
19409
- if (!apiKey) return { tier: "free", source: "no-credentials" };
19410
- const fromPrefix = tierFromApiKey(apiKey);
19411
- if (fromPrefix) return { tier: fromPrefix, source: "key-prefix" };
19412
- if (opts.offline) {
19413
- return {
19414
- tier: null,
19415
- source: "unverified",
19416
- detail: "--offline was set, and a dashboard key carries no plan of its own"
19417
- };
19418
- }
19419
- return tierFromBackend(apiKey);
19420
- }
19421
- var QUOTA_PERIOD_DAYS2 = 7;
19422
- function collectUsage(dbPath) {
19423
- const now = /* @__PURE__ */ new Date();
19424
- const periodMs = QUOTA_PERIOD_DAYS2 * 24 * 60 * 60 * 1e3;
19425
- const periodStartTs = Date.now() - periodMs;
19426
- const sinceMs = periodMs;
19427
- let store = null;
19634
+ import { Command as Command7 } from "commander";
19635
+ import chalk7 from "chalk";
19636
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
19637
+ import { join as join10 } from "path";
19638
+ import YAML2 from "yaml";
19639
+ var DEFAULT_CONFIG = {
19640
+ version: 1,
19641
+ languages: ["typescript", "python", "java", "go", "ruby", "rust"],
19642
+ exclude_paths: ["node_modules", ".git", "dist"],
19643
+ mutation_mode: "full",
19644
+ output_format: "interactive"
19645
+ };
19646
+ function findConfigPath() {
19647
+ const cwd = process.cwd();
19648
+ const candidates = [".pretense.yaml", "pretense.yaml"];
19649
+ for (const name of candidates) {
19650
+ const full = join10(cwd, name);
19651
+ if (existsSync10(full)) return full;
19652
+ }
19653
+ return null;
19654
+ }
19655
+ function loadConfig2() {
19656
+ const path2 = findConfigPath();
19657
+ if (!path2) return { ...DEFAULT_CONFIG };
19428
19658
  try {
19429
- store = new AuditStore(dbPath);
19430
- const stats = store.getStats(sinceMs);
19431
- return {
19432
- periodStartTs,
19433
- metrics: {
19434
- tenantId: "local",
19435
- // The trailing window these numbers actually cover — previously the
19436
- // calendar month, which did not match the aggregation OR the quota.
19437
- periodStart: new Date(periodStartTs).toISOString().split("T")[0],
19438
- periodEnd: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
19439
- mutationsApplied: stats.totalMutationsApplied,
19440
- secretsBlocked: stats.totalSecretsBlocked,
19441
- piiRedacted: stats.totalPiiRedacted,
19442
- totalScans: stats.totalRequests,
19443
- bytesProtected: 0,
19444
- activeSeats: 1,
19445
- providers: Object.fromEntries(stats.topProviders.map((p) => [p.provider, p.count]))
19446
- }
19447
- };
19659
+ const raw2 = readFileSync10(path2, "utf-8");
19660
+ return YAML2.parse(raw2) ?? { ...DEFAULT_CONFIG };
19448
19661
  } catch {
19449
- return null;
19450
- } finally {
19451
- store?.close();
19662
+ return { ...DEFAULT_CONFIG };
19452
19663
  }
19453
19664
  }
19454
- function resetCadenceLabel() {
19455
- return `every ${QUOTA_PERIOD_DAYS2} days`;
19665
+ function configFilePath() {
19666
+ return findConfigPath() ?? join10(process.cwd(), "pretense.yaml");
19456
19667
  }
19457
- function priceLabel(tier) {
19458
- const cents = PLAN_CONFIGS[tier].pricePerSeatCents;
19459
- if (cents === null) return "custom \u2014 contact sales";
19460
- return cents === 0 ? "free" : `$${cents / 100}/seat/month`;
19668
+ function getNestedValue(obj, key) {
19669
+ const parts = key.split(".");
19670
+ let cur = obj;
19671
+ for (const p of parts) {
19672
+ if (cur == null || typeof cur !== "object") return void 0;
19673
+ cur = cur[p];
19674
+ }
19675
+ return cur;
19461
19676
  }
19462
- function tierName(tier) {
19463
- return { free: "Free", pro: "Pro", enterprise: "Enterprise" }[tier];
19677
+ function setNestedValue(obj, key, value) {
19678
+ const parts = key.split(".");
19679
+ const leaf = parts.pop();
19680
+ if (leaf === void 0) return;
19681
+ let cur = obj;
19682
+ for (const p of parts) {
19683
+ const child = cur[p];
19684
+ if (child == null || typeof child !== "object" || Array.isArray(child)) {
19685
+ cur[p] = {};
19686
+ }
19687
+ cur = cur[p];
19688
+ }
19689
+ cur[leaf] = value;
19464
19690
  }
19465
- function tierLabel(tier) {
19466
- if (tier === null) return "Unknown (unverified)";
19467
- if (tier === "free") return "Free";
19468
- return `${tierName(tier)} (${priceLabel(tier)})`;
19691
+ function parseValue(raw2) {
19692
+ if (raw2.startsWith("[") && raw2.endsWith("]")) {
19693
+ return raw2.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
19694
+ }
19695
+ if (raw2 === "true") return true;
19696
+ if (raw2 === "false") return false;
19697
+ const n = Number(raw2);
19698
+ if (!Number.isNaN(n) && raw2.length > 0) return n;
19699
+ return raw2;
19469
19700
  }
19470
- function unverifiedNote(detail) {
19471
- return [
19472
- `Could not confirm your plan: ${detail ?? "unknown reason"}.`,
19473
- "Limits are not shown because guessing could under- or over-state what you pay for."
19474
- ];
19701
+ function configCommand() {
19702
+ const cmd = new Command7("config").description("Manage pretense configuration");
19703
+ cmd.command("list").description("Print current configuration").action(() => {
19704
+ const config = loadConfig2();
19705
+ const path2 = findConfigPath();
19706
+ console.log(
19707
+ chalk7.cyan(`
19708
+ Pretense Config`) + chalk7.dim(` (${path2 ?? "defaults - no config file found"})
19709
+ `)
19710
+ );
19711
+ console.log(YAML2.stringify(config).replace(/^/gm, " "));
19712
+ });
19713
+ cmd.command("get").description("Print a single config value").argument("<key>", "Config key (supports dotted paths)").action((key) => {
19714
+ const config = loadConfig2();
19715
+ const val = getNestedValue(config, key);
19716
+ if (val === void 0) {
19717
+ console.error(chalk7.red(`
19718
+ \u2717 Key not found: ${key}
19719
+ `));
19720
+ process.exit(1);
19721
+ }
19722
+ const display = typeof val === "object" ? JSON.stringify(val) : String(val);
19723
+ console.log(display);
19724
+ });
19725
+ cmd.command("set").description("Set a config value").argument("<key>", "Config key (supports dotted paths)").argument("<value>", "Value to set").action((key, rawValue) => {
19726
+ const config = loadConfig2();
19727
+ const value = parseValue(rawValue);
19728
+ setNestedValue(config, key, value);
19729
+ const outPath = configFilePath();
19730
+ writeFileSync4(outPath, YAML2.stringify(config));
19731
+ console.log(chalk7.green(`
19732
+ \u2713 Set ${key} = ${JSON.stringify(value)} in ${outPath}
19733
+ `));
19734
+ });
19735
+ cmd.option("--force", "Overwrite existing config with defaults", false).action((opts) => {
19736
+ if (opts.force) {
19737
+ const outPath = join10(process.cwd(), "pretense.yaml");
19738
+ writeFileSync4(outPath, YAML2.stringify(DEFAULT_CONFIG));
19739
+ console.log(chalk7.green(`
19740
+ \u2713 Created pretense.yaml
19741
+ `));
19742
+ } else {
19743
+ cmd.help();
19744
+ }
19745
+ });
19746
+ return cmd;
19747
+ }
19748
+
19749
+ // src/commands/logs.ts
19750
+ init_esm_shims();
19751
+ import { Command as Command8 } from "commander";
19752
+ import chalk8 from "chalk";
19753
+ import { homedir as homedir5 } from "os";
19754
+ import { join as join11 } from "path";
19755
+ function logsCommand() {
19756
+ return new Command8("logs").description("Show recent audit log entries").option("-l, --limit <n>", "Number of entries to show", "50").option("--json", "Output as JSON", false).action((opts) => {
19757
+ const limit = parseInt(opts.limit, 10);
19758
+ let store;
19759
+ let entries;
19760
+ try {
19761
+ store = new AuditStore(join11(homedir5(), ".pretense", "audit.db"));
19762
+ entries = store.getRecentEntries(limit);
19763
+ store.close();
19764
+ } catch {
19765
+ if (opts.json) {
19766
+ console.log("[]");
19767
+ return;
19768
+ }
19769
+ console.error(chalk8.dim("\n No audit data available. The audit database has not been initialized yet."));
19770
+ console.error(chalk8.dim(" Run " + chalk8.cyan("pretense start") + " and make some requests first.\n"));
19771
+ return;
19772
+ }
19773
+ if (opts.json) {
19774
+ console.log(JSON.stringify(entries, null, 2));
19775
+ return;
19776
+ }
19777
+ if (entries.length === 0) {
19778
+ console.log(chalk8.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
19779
+ return;
19780
+ }
19781
+ console.log(chalk8.cyan(`
19782
+ Pretense Audit Log \u2014 last ${entries.length} entries
19783
+ `));
19784
+ console.log(chalk8.dim(" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATIONS".padEnd(12) + "BLOCKED".padEnd(10) + "MS"));
19785
+ console.log(chalk8.dim(" " + "\u2500".repeat(70)));
19786
+ for (const entry of entries) {
19787
+ const time = new Date(entry.timestamp).toTimeString().slice(0, 8);
19788
+ const actionColor = entry.action === "blocked" ? chalk8.red : entry.action === "mutated" ? chalk8.green : entry.action === "redacted" ? chalk8.yellow : entry.action === "error" ? chalk8.red : chalk8.dim;
19789
+ console.log(
19790
+ " " + chalk8.dim(time.padEnd(10)) + actionColor(entry.action.padEnd(12)) + chalk8.dim(entry.provider.padEnd(14)) + chalk8.dim(String(entry.mutationsApplied).padEnd(12)) + (entry.secretsBlocked > 0 ? chalk8.red(String(entry.secretsBlocked).padEnd(10)) : chalk8.dim("0".padEnd(10))) + chalk8.dim(String(entry.scanDurationMs).slice(0, 5))
19791
+ );
19792
+ }
19793
+ console.log();
19794
+ });
19475
19795
  }
19476
19796
 
19477
19797
  // src/commands/usage.ts
19798
+ init_esm_shims();
19799
+ import { Command as Command9 } from "commander";
19800
+ import chalk9 from "chalk";
19801
+ import { join as join12 } from "path";
19802
+ import { homedir as homedir6 } from "os";
19478
19803
  function renderBar(percent, width = 30) {
19479
19804
  const filled = Math.min(Math.round(percent / 100 * width), width);
19480
19805
  const empty = width - filled;
19481
19806
  const bar2 = "\u2588".repeat(filled) + "\u2591".repeat(empty);
19482
- if (percent >= 100) return chalk8.red(bar2);
19483
- if (percent >= 90) return chalk8.yellow(bar2);
19484
- if (percent >= 80) return chalk8.yellow(bar2);
19485
- return chalk8.green(bar2);
19807
+ if (percent >= 100) return chalk9.red(bar2);
19808
+ if (percent >= 90) return chalk9.yellow(bar2);
19809
+ if (percent >= 80) return chalk9.yellow(bar2);
19810
+ return chalk9.green(bar2);
19486
19811
  }
19487
19812
  function formatMetric(value, limit) {
19488
19813
  if (limit === null) return `${value.toLocaleString("en-US")} / unlimited`;
19489
19814
  return `${value.toLocaleString("en-US")} / ${limit.toLocaleString("en-US")}`;
19490
19815
  }
19491
19816
  function percentLabel(percent) {
19492
- if (percent >= 100) return chalk8.red("OVER LIMIT");
19493
- if (percent >= 80) return chalk8.yellow(`${percent}%`);
19494
- return chalk8.green(`${percent}%`);
19817
+ if (percent >= 100) return chalk9.red("OVER LIMIT");
19818
+ if (percent >= 80) return chalk9.yellow(`${percent}%`);
19819
+ return chalk9.green(`${percent}%`);
19495
19820
  }
19496
19821
  function emptyUsagePayload(resolution) {
19497
19822
  const tier = resolution.tier;
@@ -19513,7 +19838,7 @@ function emptyUsagePayload(resolution) {
19513
19838
  };
19514
19839
  }
19515
19840
  function usageCommand() {
19516
- return new Command8("usage").description("Show plan usage vs limits for the current billing period").option("--db <path>", "Path to audit database", join12(homedir6(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
19841
+ return new Command9("usage").description("Show plan usage vs limits for the current billing period").option("--db <path>", "Path to audit database", join12(homedir6(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
19517
19842
  const resolution = await resolvePlanTier({ offline: opts.offline });
19518
19843
  const tier = resolution.tier;
19519
19844
  const collected = collectUsage(opts.db);
@@ -19524,7 +19849,7 @@ function usageCommand() {
19524
19849
  return;
19525
19850
  }
19526
19851
  console.log(
19527
- chalk8.yellow("\n No audit data found. Run ") + chalk8.cyan("pretense start") + chalk8.yellow(" and make some requests first.\n")
19852
+ chalk9.yellow("\n No audit data found. Run ") + chalk9.cyan("pretense start") + chalk9.yellow(" and make some requests first.\n")
19528
19853
  );
19529
19854
  return;
19530
19855
  }
@@ -19567,7 +19892,7 @@ function usageCommand() {
19567
19892
  }
19568
19893
  const resetDate = resetCadenceLabel();
19569
19894
  console.log(
19570
- chalk8.cyan(
19895
+ chalk9.cyan(
19571
19896
  "\n Pretense Usage \u2014 " + // Header shows today's date, NOT a billing month: quotas roll on a
19572
19897
  // 7-day window anchored to signup, so a month name would imply a
19573
19898
  // reset boundary that does not exist.
@@ -19578,11 +19903,14 @@ function usageCommand() {
19578
19903
  })
19579
19904
  )
19580
19905
  );
19581
- console.log(chalk8.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetDate}
19906
+ console.log(chalk9.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetDate}
19582
19907
  `));
19908
+ for (const line of planMismatchHint(resolution)) {
19909
+ console.log(chalk9.dim(` ${line}`));
19910
+ }
19583
19911
  if (!tier) {
19584
19912
  for (const line of unverifiedNote(resolution.detail)) {
19585
- console.log(chalk8.yellow(` ${line}`));
19913
+ console.log(chalk9.yellow(` ${line}`));
19586
19914
  }
19587
19915
  console.log();
19588
19916
  console.log(" " + "Mutations".padEnd(20) + usageMetrics.mutationsApplied.toLocaleString("en-US"));
@@ -19595,42 +19923,42 @@ function usageCommand() {
19595
19923
  const overall = maxUsagePercent(usageMetrics, tier);
19596
19924
  const nextTier = recommendedTier(usageMetrics, tier);
19597
19925
  console.log(
19598
- " " + chalk8.bold("Metric".padEnd(20)) + chalk8.bold("Used / Limit".padEnd(30)) + chalk8.bold("Progress")
19926
+ " " + chalk9.bold("Metric".padEnd(20)) + chalk9.bold("Used / Limit".padEnd(30)) + chalk9.bold("Progress")
19599
19927
  );
19600
- console.log(" " + chalk8.dim("\u2500".repeat(72)));
19928
+ console.log(" " + chalk9.dim("\u2500".repeat(72)));
19601
19929
  const rows = [
19602
19930
  { label: "Mutations", value: usageMetrics.mutationsApplied, limit: planConfig.mutationsPerPeriod },
19603
19931
  { label: "API Requests", value: usageMetrics.totalScans, limit: planConfig.scansPerPeriod }
19604
19932
  ];
19605
19933
  for (const row of rows) {
19606
19934
  const percent = row.limit === null ? 0 : Math.round(row.value / row.limit * 100);
19607
- const bar2 = row.limit === null ? chalk8.dim(" unlimited ".padEnd(32)) : renderBar(percent);
19935
+ const bar2 = row.limit === null ? chalk9.dim(" unlimited ".padEnd(32)) : renderBar(percent);
19608
19936
  console.log(
19609
19937
  " " + row.label.padEnd(20) + formatMetric(row.value, row.limit).padEnd(30) + bar2 + " " + (row.limit === null ? "" : percentLabel(percent))
19610
19938
  );
19611
19939
  }
19612
19940
  console.log(
19613
- " " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) + chalk8.dim("(no limit)")
19941
+ " " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) + chalk9.dim("(no limit)")
19614
19942
  );
19615
- console.log(" " + chalk8.dim("\u2500".repeat(72)));
19943
+ console.log(" " + chalk9.dim("\u2500".repeat(72)));
19616
19944
  if (tier === "enterprise") {
19617
- console.log(chalk8.green("\n Enterprise plan \u2014 all limits are unlimited.\n"));
19945
+ console.log(chalk9.green("\n Enterprise plan \u2014 all limits are unlimited.\n"));
19618
19946
  } else if (overall >= 100) {
19619
- console.log(chalk8.red(`
19947
+ console.log(chalk9.red(`
19620
19948
  LIMIT REACHED (${overall}% of ${tierLabel(tier)} plan used).`));
19621
19949
  } else if (overall >= 90) {
19622
19950
  console.log(
19623
- chalk8.yellow(
19951
+ chalk9.yellow(
19624
19952
  `
19625
19953
  WARNING: ${overall}% of your ${tierLabel(tier)} plan used \u2014 approaching limit.`
19626
19954
  )
19627
19955
  );
19628
19956
  } else if (overall >= 80) {
19629
- console.log(chalk8.yellow(`
19957
+ console.log(chalk9.yellow(`
19630
19958
  NOTICE: ${overall}% of your ${tierLabel(tier)} plan used.`));
19631
19959
  } else {
19632
19960
  console.log(
19633
- chalk8.green(`
19961
+ chalk9.green(`
19634
19962
  ${overall}% of your ${tierLabel(tier)} plan used \u2014 you're in good shape.`)
19635
19963
  );
19636
19964
  }
@@ -19642,12 +19970,12 @@ function usageCommand() {
19642
19970
  enterprise: "Unlimited mutations + SSO + on-prem + SIEM"
19643
19971
  };
19644
19972
  console.log(
19645
- chalk8.cyan(`
19973
+ chalk9.cyan(`
19646
19974
  Upgrade to ${tierName(nextTier)} (${priceLabel(nextTier)}):`)
19647
19975
  );
19648
- console.log(chalk8.dim(` ${upgradeBenefit[nextTier]}`));
19976
+ console.log(chalk9.dim(` ${upgradeBenefit[nextTier]}`));
19649
19977
  console.log(
19650
- chalk8.bold(`
19978
+ chalk9.bold(`
19651
19979
  \u2192 https://pretense.ai/pricing?ref=cli_usage&from=${tier}
19652
19980
  `)
19653
19981
  );
@@ -19659,12 +19987,12 @@ function usageCommand() {
19659
19987
 
19660
19988
  // src/commands/auth.ts
19661
19989
  init_esm_shims();
19662
- import { Command as Command9 } from "commander";
19663
- import chalk9 from "chalk";
19990
+ import { Command as Command10 } from "commander";
19991
+ import chalk10 from "chalk";
19664
19992
  import { existsSync as existsSync11, readFileSync as readFileSync11, unlinkSync as unlinkSync2 } from "fs";
19665
19993
  import { join as join13 } from "path";
19666
19994
  import { homedir as homedir7 } from "os";
19667
- import { execSync as execSync3 } from "child_process";
19995
+ import { execSync as execSync4 } from "child_process";
19668
19996
  var CREDENTIALS_DIR = join13(homedir7(), ".pretense");
19669
19997
  var CREDENTIALS_PATH = join13(CREDENTIALS_DIR, "credentials.json");
19670
19998
  async function verifyApiKey(apiKey) {
@@ -19708,7 +20036,7 @@ async function verifyApiKey(apiKey) {
19708
20036
  }
19709
20037
  }
19710
20038
  function storageNotice() {
19711
- return chalk9.dim(` Stored: ${CREDENTIALS_PATH}`) + chalk9.dim(`
20039
+ return chalk10.dim(` Stored: ${CREDENTIALS_PATH}`) + chalk10.dim(`
19712
20040
  Mode: 0600 (file), 0700 (~/.pretense) \u2014 plaintext, not encrypted
19713
20041
  `);
19714
20042
  }
@@ -19731,15 +20059,15 @@ function maskApiKey(key) {
19731
20059
  function openUrl(url) {
19732
20060
  try {
19733
20061
  if (process.platform === "darwin") {
19734
- execSync3(`open ${url}`, { stdio: "ignore" });
20062
+ execSync4(`open ${url}`, { stdio: "ignore" });
19735
20063
  } else {
19736
- execSync3(`xdg-open ${url}`, { stdio: "ignore" });
20064
+ execSync4(`xdg-open ${url}`, { stdio: "ignore" });
19737
20065
  }
19738
20066
  } catch {
19739
20067
  }
19740
20068
  }
19741
20069
  function authCommand() {
19742
- const auth = new Command9("auth").description("Manage CLI authentication");
20070
+ const auth = new Command10("auth").description("Manage CLI authentication");
19743
20071
  auth.command("login").description("Verify and store a Pretense API key (or open the browser flow)").option("--api-key <key>", "API key to store (e.g. pt-****)").option("--key <key>", "API key to store (alias for --api-key)").option(
19744
20072
  "--offline",
19745
20073
  "Skip server verification and store the key unverified (says so in the output)",
@@ -19758,9 +20086,9 @@ function authCommand() {
19758
20086
  const isValidFormat = validPrefixes.some((prefix) => opts.apiKey.startsWith(prefix));
19759
20087
  if (!isValidFormat) {
19760
20088
  console.error(
19761
- chalk9.red(`
19762
- x Invalid API key format.`) + chalk9.dim(`
19763
- Key must start with prtns_live_, prtns_test_, or a legacy pt-free-/pt-pro-/pt-ent- prefix`) + chalk9.dim(`
20089
+ chalk10.red(`
20090
+ x Invalid API key format.`) + chalk10.dim(`
20091
+ Key must start with prtns_live_, prtns_test_, or a legacy pt-free-/pt-pro-/pt-ent- prefix`) + chalk10.dim(`
19764
20092
  Try: pretense auth login (opens browser to get a valid key)
19765
20093
  `)
19766
20094
  );
@@ -19775,10 +20103,10 @@ function authCommand() {
19775
20103
  const existing = readCredentials();
19776
20104
  if (existing?.verified === true) {
19777
20105
  console.error(
19778
- chalk9.red("\n x The API key was rejected \u2014 your existing key was kept.") + chalk9.dim(`
19779
- Rejected: ${maskApiKey(opts.apiKey)}`) + chalk9.dim(`
19780
- Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) + chalk9.dim(`
19781
- Server: ${verification.url}`) + chalk9.dim(`
20106
+ chalk10.red("\n x The API key was rejected \u2014 your existing key was kept.") + chalk10.dim(`
20107
+ Rejected: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20108
+ Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) + chalk10.dim(`
20109
+ Server: ${verification.url}`) + chalk10.dim(`
19782
20110
  Reason: ${verification.detail}
19783
20111
  `)
19784
20112
  );
@@ -19793,10 +20121,10 @@ function authCommand() {
19793
20121
  rejected_by: verification.url
19794
20122
  });
19795
20123
  console.error(
19796
- chalk9.red("\n x The Pretense API REJECTED this key. It will not work.") + chalk9.dim(`
19797
- Key: ${maskApiKey(opts.apiKey)}`) + chalk9.dim(`
19798
- Server: ${verification.url}`) + chalk9.dim(`
19799
- Reason: ${verification.detail}`) + chalk9.dim(`
20124
+ chalk10.red("\n x The Pretense API REJECTED this key. It will not work.") + chalk10.dim(`
20125
+ Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20126
+ Server: ${verification.url}`) + chalk10.dim(`
20127
+ Reason: ${verification.detail}`) + chalk10.dim(`
19800
20128
  Get a current key at https://pretense.ai/auth
19801
20129
  `) + storageNotice()
19802
20130
  );
@@ -19814,19 +20142,22 @@ function authCommand() {
19814
20142
  };
19815
20143
  writeCredentials(creds);
19816
20144
  if (verified) {
20145
+ const planIsFree = String(verification.plan ?? "").trim().toLowerCase() === "free";
19817
20146
  console.log(
19818
- chalk9.green("\n Authenticated. The key was verified by the Pretense API.") + chalk9.dim(`
19819
- Key: ${maskApiKey(opts.apiKey)}`) + chalk9.dim(`
19820
- Server: ${verification.url}`) + (verification.plan ? chalk9.dim(`
19821
- Plan: ${verification.plan}`) : "") + "\n" + storageNotice()
20147
+ chalk10.green("\n Authenticated. The key was verified by the Pretense API.") + chalk10.dim(`
20148
+ Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20149
+ Server: ${verification.url}`) + (verification.plan ? chalk10.dim(`
20150
+ Plan: ${verification.plan}`) : "") + (planIsFree ? chalk10.dim(
20151
+ "\n Not the plan you expected? A PRO-flagged org without an active\n subscription/comp resolves to Free \u2014 see pretense.ai/dashboard/settings."
20152
+ ) : "") + "\n" + storageNotice()
19822
20153
  );
19823
20154
  return;
19824
20155
  }
19825
20156
  console.log(
19826
- chalk9.yellow("\n Key saved \u2014 NOT verified.") + chalk9.dim(`
19827
- Key: ${maskApiKey(opts.apiKey)}`) + chalk9.dim(`
19828
- Server: ${verification.url}`) + chalk9.dim(`
19829
- Reason: ${verification.detail}`) + chalk9.dim(
20157
+ chalk10.yellow("\n Key saved \u2014 NOT verified.") + chalk10.dim(`
20158
+ Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20159
+ Server: ${verification.url}`) + chalk10.dim(`
20160
+ Reason: ${verification.detail}`) + chalk10.dim(
19830
20161
  `
19831
20162
  The key's format is valid; nothing has confirmed the key itself.
19832
20163
  Re-run \`pretense auth login --key <key>\` when online to verify it.
@@ -19836,58 +20167,58 @@ function authCommand() {
19836
20167
  return;
19837
20168
  }
19838
20169
  const authUrl = "https://pretense.ai/auth";
19839
- console.log(chalk9.cyan("\n Opening browser to https://pretense.ai/auth"));
19840
- console.log(chalk9.white(" 1. Sign in or create your account"));
19841
- console.log(chalk9.white(" 2. Copy your API key from the page"));
19842
- console.log(chalk9.white(" 3. Return here and run:"));
19843
- console.log(chalk9.green(" pretense auth login --api-key <your-api-key>\n"));
20170
+ console.log(chalk10.cyan("\n Opening browser to https://pretense.ai/auth"));
20171
+ console.log(chalk10.white(" 1. Sign in or create your account"));
20172
+ console.log(chalk10.white(" 2. Copy your API key from the page"));
20173
+ console.log(chalk10.white(" 3. Return here and run:"));
20174
+ console.log(chalk10.green(" pretense auth login --api-key <your-api-key>\n"));
19844
20175
  openUrl(authUrl);
19845
20176
  });
19846
20177
  auth.command("logout").description("Remove stored credentials").action(() => {
19847
20178
  if (!existsSync11(CREDENTIALS_PATH)) {
19848
- console.log(chalk9.yellow("\n No credentials found. Already logged out.\n"));
20179
+ console.log(chalk10.yellow("\n No credentials found. Already logged out.\n"));
19849
20180
  return;
19850
20181
  }
19851
20182
  unlinkSync2(CREDENTIALS_PATH);
19852
- console.log(chalk9.green("\n Logged out. Credentials removed.\n"));
20183
+ console.log(chalk10.green("\n Logged out. Credentials removed.\n"));
19853
20184
  });
19854
20185
  auth.command("status").description("Show current authentication state").action(() => {
19855
20186
  const creds = readCredentials();
19856
20187
  if (!creds) {
19857
- console.log(chalk9.yellow("\n Not authenticated."));
20188
+ console.log(chalk10.yellow("\n Not authenticated."));
19858
20189
  console.log(
19859
- chalk9.dim(" Run ") + chalk9.cyan("pretense auth login --api-key <key>") + chalk9.dim(" to authenticate.\n")
20190
+ chalk10.dim(" Run ") + chalk10.cyan("pretense auth login --api-key <key>") + chalk10.dim(" to authenticate.\n")
19860
20191
  );
19861
20192
  return;
19862
20193
  }
19863
20194
  if (creds.verified === true) {
19864
- console.log(chalk9.green("\n Authenticated (key verified by the Pretense API)"));
20195
+ console.log(chalk10.green("\n Authenticated (key verified by the Pretense API)"));
19865
20196
  } else if (creds.rejected_at) {
19866
- console.log(chalk9.red("\n Key stored \u2014 REJECTED by the Pretense API"));
20197
+ console.log(chalk10.red("\n Key stored \u2014 REJECTED by the Pretense API"));
19867
20198
  console.log(
19868
- chalk9.dim(` Rejected at ${creds.rejected_at} by ${creds.rejected_by ?? "?"}. `) + chalk9.dim("This key will not work; get a current one at https://pretense.ai/auth")
20199
+ chalk10.dim(` Rejected at ${creds.rejected_at} by ${creds.rejected_by ?? "?"}. `) + chalk10.dim("This key will not work; get a current one at https://pretense.ai/auth")
19869
20200
  );
19870
20201
  } else {
19871
- console.log(chalk9.yellow("\n Key stored \u2014 NOT verified"));
20202
+ console.log(chalk10.yellow("\n Key stored \u2014 NOT verified"));
19872
20203
  console.log(
19873
- chalk9.dim(" No server has confirmed this key. Run ") + chalk9.cyan("pretense auth login --key <key>") + chalk9.dim(" while online to verify it.")
20204
+ chalk10.dim(" No server has confirmed this key. Run ") + chalk10.cyan("pretense auth login --key <key>") + chalk10.dim(" while online to verify it.")
19874
20205
  );
19875
20206
  }
19876
- console.log(chalk9.dim(` API Key: ${maskApiKey(creds.api_key)}`));
20207
+ console.log(chalk10.dim(` API Key: ${maskApiKey(creds.api_key)}`));
19877
20208
  if (creds.authenticated_at) {
19878
- console.log(chalk9.dim(` Saved: ${creds.authenticated_at}`));
20209
+ console.log(chalk10.dim(` Saved: ${creds.authenticated_at}`));
19879
20210
  }
19880
20211
  if (creds.verified_at) {
19881
- console.log(chalk9.dim(` Verified: ${creds.verified_at} by ${creds.verified_by ?? "?"}`));
20212
+ console.log(chalk10.dim(` Verified: ${creds.verified_at} by ${creds.verified_by ?? "?"}`));
19882
20213
  }
19883
20214
  if (creds.plan) {
19884
- console.log(chalk9.dim(` Plan: ${creds.plan}`));
20215
+ console.log(chalk10.dim(` Plan: ${creds.plan}`));
19885
20216
  }
19886
- console.log(chalk9.dim(` Stored: ${CREDENTIALS_PATH} (mode 0600, plaintext)`));
20217
+ console.log(chalk10.dim(` Stored: ${CREDENTIALS_PATH} (mode 0600, plaintext)`));
19887
20218
  if (creds.expires_at) {
19888
20219
  const expired = new Date(creds.expires_at) < /* @__PURE__ */ new Date();
19889
- const label = expired ? chalk9.red("Expired") : chalk9.green("Valid");
19890
- console.log(chalk9.dim(` Expires: ${creds.expires_at} (${label})`));
20220
+ const label = expired ? chalk10.red("Expired") : chalk10.green("Valid");
20221
+ console.log(chalk10.dim(` Expires: ${creds.expires_at} (${label})`));
19891
20222
  }
19892
20223
  console.log();
19893
20224
  });
@@ -19896,8 +20227,8 @@ function authCommand() {
19896
20227
 
19897
20228
  // src/commands/review.ts
19898
20229
  init_esm_shims();
19899
- import { Command as Command10 } from "commander";
19900
- import chalk10 from "chalk";
20230
+ import { Command as Command11 } from "commander";
20231
+ import chalk11 from "chalk";
19901
20232
  import { readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
19902
20233
  import { join as join14, extname as extname3, relative, isAbsolute } from "path";
19903
20234
  var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -20072,37 +20403,37 @@ function formatInteractive(results) {
20072
20403
  const filesWithFindings = results.filter((r) => r.mutations.length > 0 || r.secrets.length > 0);
20073
20404
  const totalMutations = results.reduce((sum, r) => sum + r.mutations.length, 0);
20074
20405
  const totalSecrets = results.reduce((sum, r) => sum + r.secrets.length, 0);
20075
- console.log(chalk10.cyan("\n Pretense Review\n"));
20076
- console.log(chalk10.dim(` Files scanned: ${results.length} | With findings: ${filesWithFindings.length}
20406
+ console.log(chalk11.cyan("\n Pretense Review\n"));
20407
+ console.log(chalk11.dim(` Files scanned: ${results.length} | With findings: ${filesWithFindings.length}
20077
20408
  `));
20078
20409
  if (totalMutations === 0 && totalSecrets === 0) {
20079
- console.log(chalk10.green(" No mutations or secrets detected.\n"));
20410
+ console.log(chalk11.green(" No mutations or secrets detected.\n"));
20080
20411
  return;
20081
20412
  }
20082
- console.log(chalk10.bold(` ${totalMutations} mutation(s) | ${totalSecrets} secret(s)
20413
+ console.log(chalk11.bold(` ${totalMutations} mutation(s) | ${totalSecrets} secret(s)
20083
20414
  `));
20084
20415
  for (const r of filesWithFindings) {
20085
- console.log(chalk10.bold(` ${r.file}`));
20416
+ console.log(chalk11.bold(` ${r.file}`));
20086
20417
  for (const m of r.mutations) {
20087
- const catColor = m.category === "function" ? chalk10.blue : m.category === "class" ? chalk10.magenta : m.category === "constant" ? chalk10.yellow : chalk10.white;
20088
- console.log(` ${catColor(m.category.padEnd(10))} ${chalk10.red(m.original)} ${chalk10.dim("->")} ${chalk10.green(m.mutated)} ${chalk10.dim(`L${m.line}`)}`);
20418
+ const catColor = m.category === "function" ? chalk11.blue : m.category === "class" ? chalk11.magenta : m.category === "constant" ? chalk11.yellow : chalk11.white;
20419
+ console.log(` ${catColor(m.category.padEnd(10))} ${chalk11.red(m.original)} ${chalk11.dim("->")} ${chalk11.green(m.mutated)} ${chalk11.dim(`L${m.line}`)}`);
20089
20420
  }
20090
20421
  for (const s of r.secrets) {
20091
- const icon = s.severity === "critical" ? chalk10.red("CRIT") : s.severity === "high" ? chalk10.yellow("HIGH") : chalk10.dim(s.severity.toUpperCase());
20092
- console.log(` ${icon.padEnd(10)} ${chalk10.bold(s.type)} ${chalk10.dim(s.locator)}`);
20422
+ const icon = s.severity === "critical" ? chalk11.red("CRIT") : s.severity === "high" ? chalk11.yellow("HIGH") : chalk11.dim(s.severity.toUpperCase());
20423
+ console.log(` ${icon.padEnd(10)} ${chalk11.bold(s.type)} ${chalk11.dim(s.locator)}`);
20093
20424
  }
20094
20425
  console.log();
20095
20426
  }
20096
20427
  }
20097
20428
  function reviewCommand() {
20098
- return new Command10("review").description("Preview mutations and secrets that would be applied").argument("[path]", "Directory to review (defaults to current directory)").option("--plain", "Plain text output", false).option("--json", "JSON array output with metadata", false).option("--agent", "Structured JSON optimized for AI agent consumption", false).option("--prompt-only", "Minimal output suitable for piping", false).option("--interactive", "Interactive formatted output (default)", false).action(async (path2, opts) => {
20429
+ return new Command11("review").description("Preview mutations and secrets that would be applied").argument("[path]", "Directory to review (defaults to current directory)").option("--plain", "Plain text output", false).option("--json", "JSON array output with metadata", false).option("--agent", "Structured JSON optimized for AI agent consumption", false).option("--prompt-only", "Minimal output suitable for piping", false).option("--interactive", "Interactive formatted output (default)", false).action(async (path2, opts) => {
20099
20430
  const targetDir = path2 ? isAbsolute(path2) ? path2 : join14(process.cwd(), path2) : process.cwd();
20100
20431
  const files = discoverFiles(targetDir);
20101
20432
  if (files.length === 0) {
20102
20433
  if (opts?.json || opts?.agent) {
20103
20434
  console.log("[]");
20104
20435
  } else {
20105
- console.error(chalk10.yellow(" No supported source files found.\n"));
20436
+ console.error(chalk11.yellow(" No supported source files found.\n"));
20106
20437
  }
20107
20438
  return;
20108
20439
  }
@@ -20129,8 +20460,8 @@ function reviewCommand() {
20129
20460
 
20130
20461
  // src/commands/install.ts
20131
20462
  init_esm_shims();
20132
- import { Command as Command11 } from "commander";
20133
- import chalk11 from "chalk";
20463
+ import { Command as Command12 } from "commander";
20464
+ import chalk12 from "chalk";
20134
20465
  import {
20135
20466
  existsSync as existsSync12,
20136
20467
  writeFileSync as writeFileSync5,
@@ -20176,7 +20507,7 @@ function installHook(hooksDir, mode, force) {
20176
20507
  return { installed: true, path: hookPath };
20177
20508
  }
20178
20509
  function installCommand() {
20179
- return new Command11("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option(
20510
+ return new Command12("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option(
20180
20511
  "-m, --mode <mode>",
20181
20512
  "Hook mode: pre-commit, pre-push (omit to install both)"
20182
20513
  ).option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
@@ -20185,7 +20516,7 @@ function installCommand() {
20185
20516
  const modeArg = opts.mode;
20186
20517
  if (modeArg && !VALID_MODES.includes(modeArg)) {
20187
20518
  console.error(
20188
- chalk11.red(
20519
+ chalk12.red(
20189
20520
  `
20190
20521
  Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
20191
20522
  `
@@ -20197,7 +20528,7 @@ function installCommand() {
20197
20528
  const gitDir = findGitDir(dir);
20198
20529
  if (!gitDir) {
20199
20530
  console.error(
20200
- chalk11.red(
20531
+ chalk12.red(
20201
20532
  `
20202
20533
  Error: No .git directory found in ${dir}
20203
20534
  Run this command from a git repository root.
@@ -20208,23 +20539,23 @@ function installCommand() {
20208
20539
  }
20209
20540
  const hooksDir = join15(gitDir, "hooks");
20210
20541
  mkdirSync3(hooksDir, { recursive: true });
20211
- console.log(chalk11.cyan("\n Pretense hook installer\n"));
20542
+ console.log(chalk12.cyan("\n Pretense hook installer\n"));
20212
20543
  let hasError = false;
20213
20544
  for (const mode of modes) {
20214
20545
  const result = installHook(hooksDir, mode, force);
20215
20546
  if (result.installed) {
20216
20547
  console.log(
20217
- chalk11.green(` Installed ${mode} hook at ${result.path}`)
20548
+ chalk12.green(` Installed ${mode} hook at ${result.path}`)
20218
20549
  );
20219
20550
  } else if (result.skipped) {
20220
20551
  console.log(
20221
- chalk11.dim(
20552
+ chalk12.dim(
20222
20553
  ` Pretense ${mode} hook already installed at ${result.path}`
20223
20554
  )
20224
20555
  );
20225
20556
  } else {
20226
20557
  console.error(
20227
- chalk11.yellow(
20558
+ chalk12.yellow(
20228
20559
  ` Hook already exists at ${result.path}. Use --force to overwrite.`
20229
20560
  )
20230
20561
  );
@@ -20235,14 +20566,14 @@ function installCommand() {
20235
20566
  console.log("");
20236
20567
  process.exit(1);
20237
20568
  }
20238
- console.log(chalk11.bold.green("\n Git hooks installed successfully.\n"));
20569
+ console.log(chalk12.bold.green("\n Git hooks installed successfully.\n"));
20239
20570
  });
20240
20571
  }
20241
20572
 
20242
20573
  // src/commands/ignore.ts
20243
20574
  init_esm_shims();
20244
- import { Command as Command12 } from "commander";
20245
- import chalk12 from "chalk";
20575
+ import { Command as Command13 } from "commander";
20576
+ import chalk13 from "chalk";
20246
20577
  import { appendFileSync as appendFileSync3, readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
20247
20578
  import { join as join16 } from "path";
20248
20579
  import { homedir as homedir8 } from "os";
@@ -20261,7 +20592,7 @@ function appendPattern(pattern) {
20261
20592
  const content = existsSync13(p) ? readFileSync14(p, "utf-8") : "";
20262
20593
  const lines = content.split("\n").filter((l) => l.length > 0);
20263
20594
  if (lines.includes(pattern)) {
20264
- console.log(chalk12.yellow(`
20595
+ console.log(chalk13.yellow(`
20265
20596
  Pattern already exists: ${pattern}
20266
20597
  `));
20267
20598
  return;
@@ -20269,26 +20600,26 @@ function appendPattern(pattern) {
20269
20600
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
20270
20601
  appendFileSync3(p, `${prefix}${pattern}
20271
20602
  `);
20272
- console.log(chalk12.green(`
20603
+ console.log(chalk13.green(`
20273
20604
  \u2713 Added to ${IGNORE_FILE}: ${pattern}
20274
20605
  `));
20275
20606
  }
20276
20607
  function ignoreCommand() {
20277
- return new Command12("ignore").description("Add patterns to .pretenseignore").argument("[pattern]", "Glob pattern to ignore").option("--last-found", "Ignore all identifiers from last scan results").option("--list", "List current ignore patterns").action((pattern, opts) => {
20608
+ return new Command13("ignore").description("Add patterns to .pretenseignore").argument("[pattern]", "Glob pattern to ignore").option("--last-found", "Ignore all identifiers from last scan results").option("--list", "List current ignore patterns").action((pattern, opts) => {
20278
20609
  if (opts.list) {
20279
20610
  const patterns = readIgnoreFile();
20280
20611
  if (patterns.length === 0) {
20281
- console.log(chalk12.dim(`
20612
+ console.log(chalk13.dim(`
20282
20613
  No ${IGNORE_FILE} found or file is empty.
20283
20614
  `));
20284
20615
  return;
20285
20616
  }
20286
- console.log(chalk12.cyan(`
20617
+ console.log(chalk13.cyan(`
20287
20618
  ${IGNORE_FILE}:
20288
20619
  `));
20289
20620
  for (const line of patterns) {
20290
20621
  if (line.startsWith("#")) {
20291
- console.log(chalk12.dim(` ${line}`));
20622
+ console.log(chalk13.dim(` ${line}`));
20292
20623
  } else {
20293
20624
  console.log(` ${line}`);
20294
20625
  }
@@ -20298,10 +20629,10 @@ function ignoreCommand() {
20298
20629
  }
20299
20630
  if (opts.lastFound) {
20300
20631
  if (!existsSync13(LAST_SCAN_PATH)) {
20301
- console.error(chalk12.red(`
20632
+ console.error(chalk13.red(`
20302
20633
  \u2717 No last scan results found at ${LAST_SCAN_PATH}
20303
20634
  `));
20304
- console.error(chalk12.dim(" Run 'pretense scan <file>' first.\n"));
20635
+ console.error(chalk13.dim(" Run 'pretense scan <file>' first.\n"));
20305
20636
  process.exit(1);
20306
20637
  }
20307
20638
  try {
@@ -20309,7 +20640,7 @@ function ignoreCommand() {
20309
20640
  const data = JSON.parse(raw2);
20310
20641
  const matches = data.matches ?? [];
20311
20642
  if (matches.length === 0) {
20312
- console.log(chalk12.green("\n \u2713 Last scan was clean - nothing to ignore.\n"));
20643
+ console.log(chalk13.green("\n \u2713 Last scan was clean - nothing to ignore.\n"));
20313
20644
  return;
20314
20645
  }
20315
20646
  let added = 0;
@@ -20320,11 +20651,11 @@ function ignoreCommand() {
20320
20651
  added++;
20321
20652
  }
20322
20653
  }
20323
- console.log(chalk12.green(`
20654
+ console.log(chalk13.green(`
20324
20655
  \u2713 Added ${added} pattern(s) from last scan.
20325
20656
  `));
20326
20657
  } catch {
20327
- console.error(chalk12.red(`
20658
+ console.error(chalk13.red(`
20328
20659
  \u2717 Failed to parse ${LAST_SCAN_PATH}
20329
20660
  `));
20330
20661
  process.exit(1);
@@ -20332,7 +20663,7 @@ function ignoreCommand() {
20332
20663
  return;
20333
20664
  }
20334
20665
  if (!pattern) {
20335
- console.error(chalk12.red("\n \u2717 Please provide a pattern or use --last-found\n"));
20666
+ console.error(chalk13.red("\n \u2717 Please provide a pattern or use --last-found\n"));
20336
20667
  process.exit(1);
20337
20668
  }
20338
20669
  appendPattern(pattern);
@@ -20341,7 +20672,7 @@ function ignoreCommand() {
20341
20672
 
20342
20673
  // src/commands/completion.ts
20343
20674
  init_esm_shims();
20344
- import { Command as Command13 } from "commander";
20675
+ import { Command as Command14 } from "commander";
20345
20676
  var ZSH_COMPLETION = `
20346
20677
  # Pretense zsh completion
20347
20678
  _pretense() {
@@ -20533,7 +20864,7 @@ complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a bash -d "
20533
20864
  complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a fish -d "fish completion script"
20534
20865
  `.trim();
20535
20866
  function completionCommand() {
20536
- return new Command13("completion").description("Print shell tab-completion script").argument("<shell>", "Shell to generate completion for: zsh, bash, fish").action((shell) => {
20867
+ return new Command14("completion").description("Print shell tab-completion script").argument("<shell>", "Shell to generate completion for: zsh, bash, fish").action((shell) => {
20537
20868
  switch (shell) {
20538
20869
  case "zsh":
20539
20870
  console.log(ZSH_COMPLETION);
@@ -20553,8 +20884,8 @@ function completionCommand() {
20553
20884
 
20554
20885
  // src/commands/quickstart.ts
20555
20886
  init_esm_shims();
20556
- import { Command as Command14 } from "commander";
20557
- import chalk13 from "chalk";
20887
+ import { Command as Command15 } from "commander";
20888
+ import chalk14 from "chalk";
20558
20889
  import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "fs";
20559
20890
  import { join as join17, resolve as resolve4 } from "path";
20560
20891
  var DEFAULT_CONFIG_YAML = `scan:
@@ -20582,60 +20913,62 @@ customPatterns: []
20582
20913
  verbose: false
20583
20914
  `;
20584
20915
  function quickstartCommand() {
20585
- return new Command14("quickstart").description("One-command setup: init + install hooks + show next steps").option("-d, --dir <path>", "Directory to scan", ".").option("--dry-run", "Print what would be written without writing", false).action(async (opts) => {
20916
+ return new Command15("quickstart").description("One-command setup: init + install hooks + show next steps").option("-d, --dir <path>", "Directory to scan", ".").option("--dry-run", "Print what would be written without writing", false).action(async (opts) => {
20586
20917
  const dir = resolve4(opts.dir);
20587
- console.log(chalk13.bold.cyan("\n Pretense Quickstart\n"));
20588
- console.log(chalk13.dim(" Setting up AI firewall for this project...\n"));
20589
- console.log(chalk13.dim(" [1/4] Scanning codebase..."));
20918
+ console.log(chalk14.bold.cyan("\n Pretense Quickstart\n"));
20919
+ console.log(chalk14.dim(" Setting up AI firewall for this project...\n"));
20920
+ console.log(chalk14.dim(" [1/4] Scanning codebase..."));
20590
20921
  let fp;
20591
20922
  try {
20592
20923
  fp = await learn({ dir, minOccurrences: 1 });
20593
- console.log(chalk13.green(` ${fp.filesScanned} files | ${fp.functions.length} functions | ${fp.classes.length} classes`));
20924
+ console.log(chalk14.green(` ${fp.filesScanned} files | ${fp.functions.length} functions | ${fp.classes.length} classes`));
20594
20925
  } catch (err) {
20595
- console.error(chalk13.red(` \u2717 Scan failed: ${err.message}`));
20926
+ console.error(chalk14.red(` \u2717 Scan failed: ${err.message}`));
20596
20927
  process.exit(1);
20597
20928
  }
20598
- console.log(chalk13.dim(" [2/4] Writing config..."));
20929
+ console.log(chalk14.dim(" [2/4] Writing config..."));
20599
20930
  const configPath = join17(dir, "pretense.yaml");
20600
20931
  if (!opts.dryRun) {
20601
20932
  if (!existsSync14(configPath)) {
20602
20933
  writeFileSync6(configPath, DEFAULT_CONFIG_YAML);
20603
- console.log(chalk13.green(" Created pretense.yaml"));
20934
+ console.log(chalk14.green(" Created pretense.yaml"));
20604
20935
  } else {
20605
- console.log(chalk13.dim(" pretense.yaml already exists, skipping"));
20936
+ console.log(chalk14.dim(" pretense.yaml already exists, skipping"));
20606
20937
  }
20607
20938
  }
20608
- console.log(chalk13.dim(" [3/4] Updating .gitignore..."));
20939
+ console.log(chalk14.dim(" [3/4] Updating .gitignore..."));
20609
20940
  if (!opts.dryRun) {
20610
20941
  const gitignorePath = join17(dir, ".gitignore");
20611
20942
  let content = existsSync14(gitignorePath) ? (await import("fs")).default.readFileSync(gitignorePath, "utf-8") : "";
20612
20943
  if (!content.includes(".pretense/")) {
20613
20944
  content = content + (content.endsWith("\n") ? "" : "\n") + ".pretense/\n";
20614
20945
  writeFileSync6(gitignorePath, content);
20615
- console.log(chalk13.green(" Added .pretense/ to .gitignore"));
20946
+ console.log(chalk14.green(" Added .pretense/ to .gitignore"));
20616
20947
  } else {
20617
- console.log(chalk13.dim(" .gitignore already includes .pretense/"));
20618
- }
20619
- }
20620
- console.log(chalk13.dim(" [4/4] Done.\n"));
20621
- console.log(chalk13.bold(" Next steps:\n"));
20622
- console.log(` ${chalk13.green("1.")} ${chalk13.cyan('pretense run claude "build me X"')} Run any AI tool through the proxy \u2014 zero setup`);
20623
- console.log(` ${chalk13.dim("(auto-starts the proxy on a free port and sets the base-URL env var for you)")}`);
20624
- console.log(` ${chalk13.green("2.")} ${chalk13.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
20625
- console.log(` ${chalk13.green("3.")} ${chalk13.dim("Prefer to wire it by hand? Start the proxy and set the base URL yourself:")}`);
20626
- console.log(` ${chalk13.dim("pretense start")}`);
20627
- console.log(` ${chalk13.dim("ANTHROPIC_BASE_URL=http://localhost:9339 claude")}`);
20628
- console.log(` ${chalk13.dim("OPENAI_BASE_URL=http://localhost:9339/v1 cursor ...")}`);
20629
- console.log(` ${chalk13.dim("GOOGLE_GEMINI_BASE_URL=http://localhost:9339 gemini ...")}
20948
+ console.log(chalk14.dim(" .gitignore already includes .pretense/"));
20949
+ }
20950
+ }
20951
+ console.log(chalk14.dim(" [4/4] Done.\n"));
20952
+ console.log(chalk14.bold(" Next steps:\n"));
20953
+ console.log(` ${chalk14.green("1.")} ${chalk14.cyan('pretense run claude "build me X"')} Run any AI tool through the proxy \u2014 zero setup`);
20954
+ console.log(` ${chalk14.dim("(auto-starts the proxy on a free port and sets the base-URL env var for you)")}`);
20955
+ console.log(` ${chalk14.dim("Works the same for any tool: ")}${chalk14.cyan("pretense run cursor")}${chalk14.dim(", ")}${chalk14.cyan("pretense run gemini")}`);
20956
+ console.log(` ${chalk14.green("2.")} ${chalk14.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
20957
+ console.log(` ${chalk14.green("3.")} ${chalk14.dim("Prefer to wire it by hand? Set the base URL in the SAME command as the tool:")}`);
20958
+ console.log(` ${chalk14.dim("ANTHROPIC_BASE_URL=http://localhost:9339 claude")}`);
20959
+ console.log(` ${chalk14.dim("OPENAI_BASE_URL=http://localhost:9339/v1 cursor ...")}`);
20960
+ console.log(` ${chalk14.dim("GOOGLE_GEMINI_BASE_URL=http://localhost:9339 gemini ...")}`);
20961
+ console.log(` ${chalk14.bold.yellow("\u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT protected.")}`);
20962
+ console.log(` ${chalk14.bold.yellow(" Simplest: use ")}${chalk14.cyan("pretense run")}${chalk14.bold.yellow(" and skip the export entirely.")}
20630
20963
  `);
20631
- console.log(chalk13.bold.green(" Protected. Happy coding.\n"));
20964
+ console.log(chalk14.bold.green(" Protected. Happy coding.\n"));
20632
20965
  });
20633
20966
  }
20634
20967
 
20635
20968
  // src/commands/mutate.ts
20636
20969
  init_esm_shims();
20637
- import { Command as Command15, Option as Option2 } from "commander";
20638
- import chalk14 from "chalk";
20970
+ import { Command as Command16, Option as Option2 } from "commander";
20971
+ import chalk16 from "chalk";
20639
20972
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, chmodSync as chmodSync4, existsSync as existsSync15 } from "fs";
20640
20973
  import { createHash as createHash3 } from "crypto";
20641
20974
  import { resolve as resolve5, extname as extname4 } from "path";
@@ -20761,6 +21094,48 @@ function ensureDefaultMapPath(absFilePath, override2) {
20761
21094
  return defaultMapPath(absFilePath, override2);
20762
21095
  }
20763
21096
 
21097
+ // src/local-audit.ts
21098
+ init_esm_shims();
21099
+ import { homedir as homedir10 } from "os";
21100
+ import { join as join21 } from "path";
21101
+ import { randomUUID } from "crypto";
21102
+ import chalk15 from "chalk";
21103
+ function localAuditDbPath() {
21104
+ return join21(homedir10(), ".pretense", "audit.db");
21105
+ }
21106
+ function recordLocalAudit(rec, dbPath = localAuditDbPath()) {
21107
+ let store = null;
21108
+ try {
21109
+ store = new AuditStore(dbPath);
21110
+ store.logEntry({
21111
+ timestamp: Date.now(),
21112
+ requestId: randomUUID(),
21113
+ provider: "cli",
21114
+ model: "-",
21115
+ direction: "outbound",
21116
+ originalSize: rec.originalSize,
21117
+ processedSize: rec.processedSize,
21118
+ secretsBlocked: rec.secretsBlocked,
21119
+ piiRedacted: rec.piiRedacted,
21120
+ mutationsApplied: rec.mutationsApplied,
21121
+ scanDurationMs: 0,
21122
+ mutationDurationMs: rec.durationMs,
21123
+ action: rec.action,
21124
+ details: { source: rec.action === "reversed" ? "cli-reverse" : "cli-mutate", file: rec.file },
21125
+ ...rec.detectorKinds && rec.detectorKinds.length > 0 ? { detectorKinds: rec.detectorKinds } : {}
21126
+ });
21127
+ return true;
21128
+ } catch (err) {
21129
+ console.error(
21130
+ chalk15.yellow(` ! Could not record this ${rec.action === "reversed" ? "reversal" : "mutation"} in the audit log`) + chalk15.dim(` (${err.message}).`) + chalk15.dim(`
21131
+ The file was still ${rec.action === "reversed" ? "restored" : "mutated"}; only the audit entry is missing.`)
21132
+ );
21133
+ return false;
21134
+ } finally {
21135
+ store?.close();
21136
+ }
21137
+ }
21138
+
20764
21139
  // src/commands/mutate.ts
20765
21140
  var REVERSAL_MAP_VERSION = 3;
20766
21141
  function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
@@ -20857,8 +21232,10 @@ async function collectMutationFindings(content, file, level, presetName) {
20857
21232
  const cannotMutate = (type) => {
20858
21233
  unmutatable.set(type, (unmutatable.get(type) ?? 0) + 1);
20859
21234
  };
21235
+ const categoryByType = /* @__PURE__ */ new Map();
20860
21236
  for (const m of result.matches) {
20861
21237
  if (level === 2 && m.level !== 2) continue;
21238
+ if (!categoryByType.has(m.type)) categoryByType.set(m.type, m.category);
20862
21239
  const offset = m.start;
20863
21240
  const length = m.end - m.start;
20864
21241
  if (length <= 0) {
@@ -20871,18 +21248,18 @@ async function collectMutationFindings(content, file, level, presetName) {
20871
21248
  }
20872
21249
  findings.push({ value: m.value, offset, length, type: m.type });
20873
21250
  }
20874
- return { findings, unmutatable };
21251
+ return { findings, unmutatable, categoryByType };
20875
21252
  }
20876
21253
  function reportUnmutatable(unmutatable) {
20877
21254
  if (unmutatable.size === 0) return;
20878
21255
  const total = [...unmutatable.values()].reduce((a, b) => a + b, 0);
20879
21256
  const kinds = [...unmutatable.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, n]) => n > 1 ? `${type} (x${n})` : type).join(", ");
20880
21257
  console.error(
20881
- chalk14.yellow(`
20882
- ! ${total} detected value${total === 1 ? "" : "s"} could NOT be mutated: ${kinds}`) + chalk14.dim(`
20883
- These were found only inside an encoded form (base64/hex) or at a span`) + chalk14.dim(`
20884
- that does not map back to the file's bytes, so there is no range to`) + chalk14.dim(`
20885
- rewrite. They are STILL PRESENT in the output. Run \`pretense scan\` on`) + chalk14.dim(`
21258
+ chalk16.yellow(`
21259
+ ! ${total} detected value${total === 1 ? "" : "s"} could NOT be mutated: ${kinds}`) + chalk16.dim(`
21260
+ These were found only inside an encoded form (base64/hex) or at a span`) + chalk16.dim(`
21261
+ that does not map back to the file's bytes, so there is no range to`) + chalk16.dim(`
21262
+ rewrite. They are STILL PRESENT in the output. Run \`pretense scan\` on`) + chalk16.dim(`
20886
21263
  this file to see them, and remove or re-encode them by hand.
20887
21264
  `)
20888
21265
  );
@@ -20892,9 +21269,9 @@ function writeReversalMap(mapPath, mapFile) {
20892
21269
  writeFileSync7(mapPath, JSON.stringify(mapFile, null, 2), { mode: 384 });
20893
21270
  } catch (e) {
20894
21271
  console.error(
20895
- chalk14.red(`
20896
- x Cannot write reversal map: ${mapPath}`) + chalk14.dim(`
20897
- ${e.message}`) + chalk14.dim(`
21272
+ chalk16.red(`
21273
+ x Cannot write reversal map: ${mapPath}`) + chalk16.dim(`
21274
+ ${e.message}`) + chalk16.dim(`
20898
21275
  Nothing was mutated \u2014 without a map the original could not be restored.
20899
21276
  `)
20900
21277
  );
@@ -20905,7 +21282,7 @@ function writeReversalMap(mapPath, mapFile) {
20905
21282
  } catch {
20906
21283
  }
20907
21284
  console.error(
20908
- chalk14.green(` \u2713 Wrote encrypted reversal map \u2192 ${mapPath}`) + chalk14.dim(" (AES-256-GCM, mode 0600)")
21285
+ chalk16.green(` \u2713 Wrote encrypted reversal map \u2192 ${mapPath}`) + chalk16.dim(" (AES-256-GCM, mode 0600)")
20909
21286
  );
20910
21287
  return 0;
20911
21288
  }
@@ -20914,7 +21291,7 @@ async function runMutate(file, opts = {}) {
20914
21291
  try {
20915
21292
  raw2 = readFileSync15(file);
20916
21293
  } catch {
20917
- console.error(chalk14.red(`
21294
+ console.error(chalk16.red(`
20918
21295
  x Cannot read file: ${file}
20919
21296
  `));
20920
21297
  return 1;
@@ -20922,9 +21299,9 @@ async function runMutate(file, opts = {}) {
20922
21299
  const content = raw2.toString("utf-8");
20923
21300
  if (!Buffer.from(content, "utf-8").equals(raw2)) {
20924
21301
  console.error(
20925
- chalk14.red(`
20926
- x Not valid UTF-8: ${file}`) + chalk14.dim(`
20927
- Mutating would replace the undecodable bytes with U+FFFD and the`) + chalk14.dim(`
21302
+ chalk16.red(`
21303
+ x Not valid UTF-8: ${file}`) + chalk16.dim(`
21304
+ Mutating would replace the undecodable bytes with U+FFFD and the`) + chalk16.dim(`
20928
21305
  original content could not be restored. File left untouched.
20929
21306
  `)
20930
21307
  );
@@ -20932,7 +21309,8 @@ async function runMutate(file, opts = {}) {
20932
21309
  }
20933
21310
  const preview = opts.stdout === true;
20934
21311
  const inPlace = !preview && !opts.output;
20935
- const { findings, unmutatable } = await collectMutationFindings(
21312
+ const mutateStartMs = Date.now();
21313
+ const { findings, unmutatable, categoryByType } = await collectMutationFindings(
20936
21314
  content,
20937
21315
  file,
20938
21316
  opts.level,
@@ -20940,7 +21318,7 @@ async function runMutate(file, opts = {}) {
20940
21318
  );
20941
21319
  if (findings.length === 0) {
20942
21320
  if (unmutatable.size === 0) {
20943
- console.error(chalk14.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
21321
+ console.error(chalk16.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
20944
21322
  } else {
20945
21323
  reportUnmutatable(unmutatable);
20946
21324
  }
@@ -20964,9 +21342,9 @@ async function runMutate(file, opts = {}) {
20964
21342
  const pre = preflightReversalMap(effectiveMap, absFile, content, contentSha, opts.forceMap === true);
20965
21343
  if (pre.action === "refuse") {
20966
21344
  console.error(
20967
- chalk14.red(`
20968
- x ${pre.code}: refusing to overwrite an existing reversal map.`) + chalk14.dim(`
20969
- ${pre.message}`) + chalk14.dim(`
21345
+ chalk16.red(`
21346
+ x ${pre.code}: refusing to overwrite an existing reversal map.`) + chalk16.dim(`
21347
+ ${pre.message}`) + chalk16.dim(`
20970
21348
  Nothing was mutated. Both the file and the map are untouched.
20971
21349
  `)
20972
21350
  );
@@ -20976,8 +21354,8 @@ async function runMutate(file, opts = {}) {
20976
21354
  const isNoop = pre.action === "keep" && pre.expectedMutatedSha === mutatedSha;
20977
21355
  if (isNoop) {
20978
21356
  console.error(
20979
- chalk14.dim(` = Reversal map ${effectiveMap} already describes ${file} at this exact content`) + chalk14.dim(`
20980
- hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) + chalk14.dim(`
21357
+ chalk16.dim(` = Reversal map ${effectiveMap} already describes ${file} at this exact content`) + chalk16.dim(`
21358
+ hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) + chalk16.dim(`
20981
21359
  byte-identical).`)
20982
21360
  );
20983
21361
  } else {
@@ -21003,10 +21381,10 @@ async function runMutate(file, opts = {}) {
21003
21381
  if (written !== 0) return written;
21004
21382
  if (pre.action === "write" && pre.generation > 1) {
21005
21383
  console.error(
21006
- chalk14.yellow(`
21007
- ! --force-map: this map is mutation GENERATION ${pre.generation}.`) + chalk14.dim(`
21008
- Its "original" values are synthetics from the previous round, so`) + chalk14.dim(`
21009
- reversing with it returns the EARLIER MUTATED file, not your real`) + chalk14.dim(`
21384
+ chalk16.yellow(`
21385
+ ! --force-map: this map is mutation GENERATION ${pre.generation}.`) + chalk16.dim(`
21386
+ Its "original" values are synthetics from the previous round, so`) + chalk16.dim(`
21387
+ reversing with it returns the EARLIER MUTATED file, not your real`) + chalk16.dim(`
21010
21388
  secrets. \`pretense reverse\` will refuse it unless --allow-partial.
21011
21389
  `)
21012
21390
  );
@@ -21015,8 +21393,8 @@ async function runMutate(file, opts = {}) {
21015
21393
  }
21016
21394
  if (inPlace && !effectiveMap) {
21017
21395
  console.error(
21018
- chalk14.red(`
21019
- x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk14.dim(`
21396
+ chalk16.red(`
21397
+ x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk16.dim(`
21020
21398
  Pass --map <path>, or report this as a bug.
21021
21399
  `)
21022
21400
  );
@@ -21025,29 +21403,48 @@ async function runMutate(file, opts = {}) {
21025
21403
  try {
21026
21404
  if (inPlace) {
21027
21405
  writeFileAtomicSync(file, result.content);
21028
- console.error(chalk14.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
21406
+ console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
21029
21407
  } else if (opts.output) {
21030
21408
  writeFileAtomicSync(opts.output, result.content);
21031
- console.error(chalk14.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
21409
+ console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
21032
21410
  } else {
21033
21411
  process.stdout.write(result.content);
21034
21412
  }
21035
21413
  } catch (e) {
21036
21414
  console.error(
21037
- chalk14.red(`
21038
- x Cannot write mutated content: ${opts.output ?? file}`) + chalk14.dim(`
21415
+ chalk16.red(`
21416
+ x Cannot write mutated content: ${opts.output ?? file}`) + chalk16.dim(`
21039
21417
  ${e.message}
21040
21418
  `)
21041
21419
  );
21042
21420
  return 1;
21043
21421
  }
21422
+ if (!preview && result.mutations.length > 0) {
21423
+ let secretsBlocked = 0;
21424
+ let piiRedacted = 0;
21425
+ for (const m of result.mutations) {
21426
+ if (categoryByType.get(m.type) === "pii") piiRedacted += 1;
21427
+ else secretsBlocked += 1;
21428
+ }
21429
+ recordLocalAudit({
21430
+ action: "mutated",
21431
+ mutationsApplied: result.mutations.length,
21432
+ secretsBlocked,
21433
+ piiRedacted,
21434
+ originalSize: Buffer.byteLength(content, "utf-8"),
21435
+ processedSize: Buffer.byteLength(result.content, "utf-8"),
21436
+ durationMs: Date.now() - mutateStartMs,
21437
+ detectorKinds: [...new Set(result.mutations.map((m) => m.type))],
21438
+ file: absFile
21439
+ });
21440
+ }
21044
21441
  if (opts.showMap) {
21045
21442
  console.error(JSON.stringify(result.mutations, null, 2));
21046
21443
  }
21047
21444
  return 0;
21048
21445
  }
21049
21446
  function mutateCommand() {
21050
- return new Command15("mutate").description(
21447
+ return new Command16("mutate").description(
21051
21448
  "Mutate secrets IN PLACE and auto-write an encrypted reversal map (reverse with `pretense reverse <file>`; --stdout to preview instead)"
21052
21449
  ).argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file (this is the default; kept for compatibility)").option("-o, --output <path>", "Write mutated content to a specific file instead of in place").option("--stdout", "Preview mutated content on stdout and write NOTHING (no file, no map)").option(
21053
21450
  "--map <path>",
@@ -21068,7 +21465,7 @@ function mutateCommand() {
21068
21465
  const resolved = resolvePolicy(opts.policy);
21069
21466
  if (resolved?.scannerPreset) opts.presetName = resolved.scannerPreset;
21070
21467
  } catch (err) {
21071
- console.error(chalk14.red(`
21468
+ console.error(chalk16.red(`
21072
21469
  x ${err.message}
21073
21470
  `));
21074
21471
  process.exit(1);
@@ -21080,8 +21477,8 @@ function mutateCommand() {
21080
21477
 
21081
21478
  // src/commands/reverse.ts
21082
21479
  init_esm_shims();
21083
- import { Command as Command16 } from "commander";
21084
- import chalk15 from "chalk";
21480
+ import { Command as Command17 } from "commander";
21481
+ import chalk17 from "chalk";
21085
21482
  import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync6 } from "fs";
21086
21483
  import { resolve as resolve6 } from "path";
21087
21484
  import { createHash as createHash4 } from "crypto";
@@ -21115,14 +21512,14 @@ function loadReversalMap(mapPath) {
21115
21512
  try {
21116
21513
  parsed = JSON.parse(readFileSync16(mapPath, "utf-8"));
21117
21514
  } catch {
21118
- console.error(chalk15.red(`
21515
+ console.error(chalk17.red(`
21119
21516
  x Cannot read reversal map: ${mapPath}
21120
21517
  `));
21121
21518
  process.exit(1);
21122
21519
  }
21123
21520
  const map = parsed;
21124
21521
  if (!map || !Array.isArray(map.mutations)) {
21125
- console.error(chalk15.red(`
21522
+ console.error(chalk17.red(`
21126
21523
  x Malformed reversal map (missing "mutations"): ${mapPath}
21127
21524
  `));
21128
21525
  process.exit(1);
@@ -21130,9 +21527,9 @@ function loadReversalMap(mapPath) {
21130
21527
  const version = typeof map.version === "number" ? map.version : 1;
21131
21528
  if (version > REVERSAL_MAP_VERSION) {
21132
21529
  console.error(
21133
- chalk15.red(`
21134
- x Reversal map is version ${version}; this build understands up to ${REVERSAL_MAP_VERSION}.`) + chalk15.dim(`
21135
- It was written by a newer Pretense. Upgrade the CLI to reverse it.`) + chalk15.dim(`
21530
+ chalk17.red(`
21531
+ x Reversal map is version ${version}; this build understands up to ${REVERSAL_MAP_VERSION}.`) + chalk17.dim(`
21532
+ It was written by a newer Pretense. Upgrade the CLI to reverse it.`) + chalk17.dim(`
21136
21533
  File left untouched.
21137
21534
  `)
21138
21535
  );
@@ -21141,9 +21538,10 @@ function loadReversalMap(mapPath) {
21141
21538
  return map;
21142
21539
  }
21143
21540
  function runReverse(file, opts = {}) {
21541
+ const reverseStartMs = Date.now();
21144
21542
  const absPath = resolve6(file);
21145
21543
  if (!existsSync16(absPath) || !statSync6(absPath).isFile()) {
21146
- console.error(chalk15.red(`
21544
+ console.error(chalk17.red(`
21147
21545
  x File not found: ${file}
21148
21546
  `));
21149
21547
  return 1;
@@ -21151,11 +21549,11 @@ function runReverse(file, opts = {}) {
21151
21549
  const mapPath = opts.map ?? defaultMapPath(absPath, opts.mapsDir);
21152
21550
  if (!opts.map && !existsSync16(mapPath)) {
21153
21551
  console.error(
21154
- chalk15.red(`
21155
- x No reversal map found for ${absPath}`) + chalk15.dim(`
21156
- Looked in the default location: ${mapPath}`) + chalk15.dim(`
21157
- Mutate the file first with ${chalk15.cyan(`pretense mutate ${file}`)}, or pass`) + chalk15.dim(`
21158
- an explicit ${chalk15.cyan("--map <path>")} if you wrote the map somewhere else.
21552
+ chalk17.red(`
21553
+ x No reversal map found for ${absPath}`) + chalk17.dim(`
21554
+ Looked in the default location: ${mapPath}`) + chalk17.dim(`
21555
+ Mutate the file first with ${chalk17.cyan(`pretense mutate ${file}`)}, or pass`) + chalk17.dim(`
21556
+ an explicit ${chalk17.cyan("--map <path>")} if you wrote the map somewhere else.
21159
21557
  `)
21160
21558
  );
21161
21559
  return 1;
@@ -21167,8 +21565,8 @@ function runReverse(file, opts = {}) {
21167
21565
  const key = readProjectKey(projectRoot, opts.keysDir);
21168
21566
  if (!key) {
21169
21567
  console.error(
21170
- chalk15.red(`
21171
- x Encryption key not found for this project \u2014 cannot decrypt the reversal map.`) + chalk15.dim(`
21568
+ chalk17.red(`
21569
+ x Encryption key not found for this project \u2014 cannot decrypt the reversal map.`) + chalk17.dim(`
21172
21570
  Expected a key derived from ${projectRoot}. Reverse must run where the map was created.
21173
21571
  `)
21174
21572
  );
@@ -21181,7 +21579,7 @@ function runReverse(file, opts = {}) {
21181
21579
  }));
21182
21580
  } catch {
21183
21581
  console.error(
21184
- chalk15.red(`
21582
+ chalk17.red(`
21185
21583
  x Failed to decrypt the reversal map (wrong key or corrupt data). File left untouched.
21186
21584
  `)
21187
21585
  );
@@ -21195,10 +21593,10 @@ function runReverse(file, opts = {}) {
21195
21593
  const absentCount = plainMutations.length - appliedCount;
21196
21594
  if (typeof mapFile.mutatedSha === "string" && mapFile.mutatedSha !== sha256Hex3(mutatedContent)) {
21197
21595
  console.error(
21198
- chalk15.yellow(`
21199
- ! This file is not the exact output this map was written for.`) + chalk15.dim(`
21200
- map expects sha256 ${mapFile.mutatedSha}`) + chalk15.dim(`
21201
- file is sha256 ${sha256Hex3(mutatedContent)}`) + chalk15.dim(`
21596
+ chalk17.yellow(`
21597
+ ! This file is not the exact output this map was written for.`) + chalk17.dim(`
21598
+ map expects sha256 ${mapFile.mutatedSha}`) + chalk17.dim(`
21599
+ file is sha256 ${sha256Hex3(mutatedContent)}`) + chalk17.dim(`
21202
21600
  ${absentCount} of ${plainMutations.length} mapped tokens are not present in it.
21203
21601
  `)
21204
21602
  );
@@ -21208,8 +21606,8 @@ function runReverse(file, opts = {}) {
21208
21606
  restored = applyReverse(mutatedContent, plainMutations);
21209
21607
  } catch (e) {
21210
21608
  console.error(
21211
- chalk15.red(`
21212
- x ${e.message}`) + chalk15.dim(`
21609
+ chalk17.red(`
21610
+ x ${e.message}`) + chalk17.dim(`
21213
21611
  File left untouched.
21214
21612
  `)
21215
21613
  );
@@ -21217,8 +21615,8 @@ function runReverse(file, opts = {}) {
21217
21615
  }
21218
21616
  if (!mapFile.originalSha) {
21219
21617
  console.error(
21220
- chalk15.red(`
21221
- x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) + chalk15.dim(`
21618
+ chalk17.red(`
21619
+ x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) + chalk17.dim(`
21222
21620
  Refusing to write. File left untouched.
21223
21621
  `)
21224
21622
  );
@@ -21227,10 +21625,10 @@ function runReverse(file, opts = {}) {
21227
21625
  const restoredSha = sha256Hex3(restored);
21228
21626
  if (restoredSha !== mapFile.originalSha) {
21229
21627
  console.error(
21230
- chalk15.red(`
21231
- x Reversal integrity check failed for ${absPath}`) + chalk15.dim(`
21232
- expected sha256 ${mapFile.originalSha}`) + chalk15.dim(`
21233
- got sha256 ${restoredSha}`) + chalk15.dim(`
21628
+ chalk17.red(`
21629
+ x Reversal integrity check failed for ${absPath}`) + chalk17.dim(`
21630
+ expected sha256 ${mapFile.originalSha}`) + chalk17.dim(`
21631
+ got sha256 ${restoredSha}`) + chalk17.dim(`
21234
21632
  File left untouched.
21235
21633
  `)
21236
21634
  );
@@ -21241,30 +21639,30 @@ function runReverse(file, opts = {}) {
21241
21639
  const what = lineage.exact ? `This map is mutation GENERATION ${lineage.generation}.` : `This map's "original" values look like Pretense synthetics (${lineage.syntheticCount} of ${plainMutations.length}).`;
21242
21640
  if (!opts.allowPartial) {
21243
21641
  console.error(
21244
- chalk15.red(`
21245
- x NOT A RESTORE: ${what}`) + chalk15.dim(`
21246
- Its originals are themselves synthetics from an earlier mutation of this`) + chalk15.dim(`
21247
- file, so applying it would return the PREVIOUS MUTATED VERSION, not your`) + chalk15.dim(`
21248
- real secrets \u2014 while every integrity check passed, because the map is`) + chalk15.dim(`
21249
- self-consistent with the file. Reverse with the map from the FIRST`) + chalk15.dim(`
21250
- mutation round instead.`) + chalk15.dim(`
21251
- --allow-partial writes it anyway (useful only to walk a chain back one`) + chalk15.dim(`
21252
- generation at a time).`) + chalk15.dim(`
21642
+ chalk17.red(`
21643
+ x NOT A RESTORE: ${what}`) + chalk17.dim(`
21644
+ Its originals are themselves synthetics from an earlier mutation of this`) + chalk17.dim(`
21645
+ file, so applying it would return the PREVIOUS MUTATED VERSION, not your`) + chalk17.dim(`
21646
+ real secrets \u2014 while every integrity check passed, because the map is`) + chalk17.dim(`
21647
+ self-consistent with the file. Reverse with the map from the FIRST`) + chalk17.dim(`
21648
+ mutation round instead.`) + chalk17.dim(`
21649
+ --allow-partial writes it anyway (useful only to walk a chain back one`) + chalk17.dim(`
21650
+ generation at a time).`) + chalk17.dim(`
21253
21651
  File left untouched.
21254
21652
  `)
21255
21653
  );
21256
21654
  return 1;
21257
21655
  }
21258
21656
  console.error(
21259
- chalk15.yellow(`
21260
- ! PARTIAL RESTORE (--allow-partial): ${what}`) + chalk15.dim(`
21261
- The output is the PREVIOUS MUTATED GENERATION of this file, NOT the`) + chalk15.dim(`
21262
- original. ${lineage.syntheticCount || "Some"} restored value(s) are still synthetic. Reverse again with`) + chalk15.dim(`
21657
+ chalk17.yellow(`
21658
+ ! PARTIAL RESTORE (--allow-partial): ${what}`) + chalk17.dim(`
21659
+ The output is the PREVIOUS MUTATED GENERATION of this file, NOT the`) + chalk17.dim(`
21660
+ original. ${lineage.syntheticCount || "Some"} restored value(s) are still synthetic. Reverse again with`) + chalk17.dim(`
21263
21661
  the earlier round's map to reach the real secrets.
21264
21662
  `)
21265
21663
  );
21266
21664
  }
21267
- const mark = lineage.ok ? chalk15.green(" \u2713 Restored") : chalk15.yellow(" ! Partially restored");
21665
+ const mark = lineage.ok ? chalk17.green(" \u2713 Restored") : chalk17.yellow(" ! Partially restored");
21268
21666
  const detail = `${appliedCount} of ${plainMutations.length} mapped tokens` + (lineage.ok ? "" : " \u2014 to an earlier MUTATED generation, not the original");
21269
21667
  const preview = opts.stdout === true;
21270
21668
  const inPlace = !preview && !opts.output;
@@ -21277,6 +21675,19 @@ function runReverse(file, opts = {}) {
21277
21675
  } else {
21278
21676
  process.stdout.write(restored);
21279
21677
  }
21678
+ if (!preview && appliedCount > 0) {
21679
+ recordLocalAudit({
21680
+ action: "reversed",
21681
+ mutationsApplied: appliedCount,
21682
+ secretsBlocked: 0,
21683
+ piiRedacted: 0,
21684
+ originalSize: Buffer.byteLength(mutatedContent, "utf-8"),
21685
+ processedSize: Buffer.byteLength(restored, "utf-8"),
21686
+ durationMs: Date.now() - reverseStartMs,
21687
+ detectorKinds: [...new Set(mapFile.mutations.map((m) => m.type).filter((t) => typeof t === "string"))],
21688
+ file: absPath
21689
+ });
21690
+ }
21280
21691
  if (opts.json) {
21281
21692
  console.error(
21282
21693
  JSON.stringify({
@@ -21294,7 +21705,7 @@ function runReverse(file, opts = {}) {
21294
21705
  return 0;
21295
21706
  }
21296
21707
  function reverseCommand() {
21297
- return new Command16("reverse").description(
21708
+ return new Command17("reverse").description(
21298
21709
  "Restore a mutated file to its EXACT original bytes IN PLACE, auto-finding its reversal map (fails if the file was edited after mutation)"
21299
21710
  ).argument("<file>", "Mutated file to restore (must be byte-identical to the mutate output)").option(
21300
21711
  "--map <path>",
@@ -21309,8 +21720,8 @@ function reverseCommand() {
21309
21720
 
21310
21721
  // src/commands/run.ts
21311
21722
  init_esm_shims();
21312
- import { Command as Command17 } from "commander";
21313
- import chalk16 from "chalk";
21723
+ import { Command as Command18 } from "commander";
21724
+ import chalk18 from "chalk";
21314
21725
  import { spawn } from "child_process";
21315
21726
  import { basename as basename3 } from "path";
21316
21727
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -21384,20 +21795,23 @@ async function waitHealthy(port, deadlineMs, pollMs = 150) {
21384
21795
  }
21385
21796
  function spawnProxy(port) {
21386
21797
  const cliEntry = process.argv[1] ?? fileURLToPath4(import.meta.url);
21387
- return spawn(process.execPath, [cliEntry, "start", "--port", String(port), "--strict-port"], {
21798
+ const startArgs = ["start", "--port", String(port), "--strict-port"];
21799
+ const isCompiledBinary = cliEntry.includes("$bunfs") || cliEntry.includes("~BUN");
21800
+ const spawnArgs = isCompiledBinary ? startArgs : [cliEntry, ...startArgs];
21801
+ return spawn(process.execPath, spawnArgs, {
21388
21802
  stdio: ["ignore", "ignore", "pipe"],
21389
21803
  env: process.env
21390
21804
  });
21391
21805
  }
21392
21806
  function runCommand() {
21393
- return new Command17("run").alias("with").description(
21807
+ return new Command18("run").alias("with").description(
21394
21808
  "Run an AI tool THROUGH the Pretense proxy with zero setup \u2014 auto-starts the proxy, sets the right base-URL env var, and stops the proxy it started when the tool exits"
21395
21809
  ).argument("<tool>", "The AI CLI to run (claude, cursor, gemini, \u2026)").argument("[args...]", "Arguments passed through to <tool>").option("--provider <name>", "Force the provider mapping: anthropic | openai | google").option("-p, --port <port>", "Preferred proxy port when starting one (default 9339)").option("--strict-port", "Fail instead of auto-advancing when starting a proxy on a busy port", false).passThroughOptions().allowUnknownOption().action(async (tool, args, opts) => {
21396
21810
  let provider;
21397
21811
  try {
21398
21812
  provider = resolveProvider(tool, opts.provider);
21399
21813
  } catch (err) {
21400
- console.error(chalk16.red(`
21814
+ console.error(chalk18.red(`
21401
21815
  x ${err.message}
21402
21816
  `));
21403
21817
  process.exit(1);
@@ -21408,13 +21822,13 @@ function runCommand() {
21408
21822
  const rec = readProxyRecord();
21409
21823
  if (rec && isProcessAlive(rec.pid) && await isProxyHealthy(rec.port)) {
21410
21824
  port = rec.port;
21411
- console.error(chalk16.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
21825
+ console.error(chalk18.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
21412
21826
  } else {
21413
21827
  const preferred = opts.port ? parseInt(opts.port, 10) : 9339;
21414
21828
  try {
21415
21829
  port = await findFreePort(preferred);
21416
21830
  } catch (err) {
21417
- console.error(chalk16.red(`
21831
+ console.error(chalk18.red(`
21418
21832
  x ${err.message}
21419
21833
  `));
21420
21834
  process.exit(1);
@@ -21428,14 +21842,14 @@ function runCommand() {
21428
21842
  const ready = await waitHealthy(port, 15e3);
21429
21843
  if (!ready) {
21430
21844
  console.error(
21431
- chalk16.red(`
21432
- x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk16.dim(`
21845
+ chalk18.red(`
21846
+ x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk18.dim(`
21433
21847
  ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21434
21848
  );
21435
21849
  proxyChild.kill("SIGTERM");
21436
21850
  process.exit(1);
21437
21851
  }
21438
- console.error(chalk16.green(` \u2713 Started Pretense proxy on port ${port}.`));
21852
+ console.error(chalk18.green(` \u2713 Started Pretense proxy on port ${port}.`));
21439
21853
  }
21440
21854
  const stopStartedProxy = () => {
21441
21855
  if (startedByUs && proxyChild && !proxyChild.killed) {
@@ -21444,7 +21858,7 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21444
21858
  };
21445
21859
  const inject = injectedEnvFor(provider, port);
21446
21860
  console.error(
21447
- chalk16.dim(
21861
+ chalk18.dim(
21448
21862
  ` \u2192 Routing ${basename3(tool)} through the proxy (${primaryEnvVar(provider)}=http://localhost:${port}).
21449
21863
  `
21450
21864
  )
@@ -21463,15 +21877,15 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21463
21877
  child.on("error", (err) => {
21464
21878
  if (err.code === "ENOENT") {
21465
21879
  console.error(
21466
- chalk16.red(`
21467
- x "${tool}" is not on your PATH.`) + chalk16.dim(`
21468
- Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) + chalk16.dim(`
21469
- You can still point a client at the proxy yourself:`) + chalk16.dim(`
21880
+ chalk18.red(`
21881
+ x "${tool}" is not on your PATH.`) + chalk18.dim(`
21882
+ Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) + chalk18.dim(`
21883
+ You can still point a client at the proxy yourself:`) + chalk18.dim(`
21470
21884
  ${primaryEnvVar(provider)}=http://localhost:${port}
21471
21885
  `)
21472
21886
  );
21473
21887
  } else {
21474
- console.error(chalk16.red(`
21888
+ console.error(chalk18.red(`
21475
21889
  x Could not run "${tool}": ${err.message}
21476
21890
  `));
21477
21891
  }
@@ -21490,8 +21904,8 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21490
21904
 
21491
21905
  // src/commands/policy.ts
21492
21906
  init_esm_shims();
21493
- import { Command as Command18 } from "commander";
21494
- import chalk17 from "chalk";
21907
+ import { Command as Command19 } from "commander";
21908
+ import chalk19 from "chalk";
21495
21909
  function listAction(opts) {
21496
21910
  const ids = listPresetIds();
21497
21911
  if (opts.json) {
@@ -21514,21 +21928,21 @@ function listAction(opts) {
21514
21928
  );
21515
21929
  return;
21516
21930
  }
21517
- console.log(chalk17.cyan("\n Pretense Compliance Frameworks\n"));
21931
+ console.log(chalk19.cyan("\n Pretense Compliance Frameworks\n"));
21518
21932
  console.log(
21519
- chalk17.dim(
21933
+ chalk19.dim(
21520
21934
  " " + "ID".padEnd(8) + "NAME".padEnd(14) + "TIER".padEnd(12) + "AUDIT".padEnd(8) + "DETECTORS"
21521
21935
  )
21522
21936
  );
21523
- console.log(chalk17.dim(" " + "\u2500".repeat(64)));
21937
+ console.log(chalk19.dim(" " + "\u2500".repeat(64)));
21524
21938
  for (const id of ids) {
21525
21939
  const p = COMPLIANCE_PRESETS[id];
21526
21940
  console.log(
21527
- " " + chalk17.bold(p.id.padEnd(8)) + p.name.padEnd(14) + chalk17.dim(p.minTier.padEnd(12)) + (p.auditRequired ? chalk17.green("yes".padEnd(8)) : chalk17.dim("no".padEnd(8))) + chalk17.dim(`${Object.keys(p.actionOverrides).length} overrides`)
21941
+ " " + chalk19.bold(p.id.padEnd(8)) + p.name.padEnd(14) + chalk19.dim(p.minTier.padEnd(12)) + (p.auditRequired ? chalk19.green("yes".padEnd(8)) : chalk19.dim("no".padEnd(8))) + chalk19.dim(`${Object.keys(p.actionOverrides).length} overrides`)
21528
21942
  );
21529
21943
  }
21530
- console.log(chalk17.dim(`
21531
- Run ${chalk17.cyan("pretense policy show <id>")} for detector-level detail.
21944
+ console.log(chalk19.dim(`
21945
+ Run ${chalk19.cyan("pretense policy show <id>")} for detector-level detail.
21532
21946
  `));
21533
21947
  }
21534
21948
  function showAction(id, opts) {
@@ -21537,8 +21951,8 @@ function showAction(id, opts) {
21537
21951
  preset = loadPreset(id);
21538
21952
  } catch (err) {
21539
21953
  console.error(
21540
- chalk17.red(`
21541
- x ${err.message}`) + chalk17.dim(`
21954
+ chalk19.red(`
21955
+ x ${err.message}`) + chalk19.dim(`
21542
21956
  Valid frameworks: ${listPresetIds().join(", ")}
21543
21957
  `)
21544
21958
  );
@@ -21548,24 +21962,24 @@ function showAction(id, opts) {
21548
21962
  console.log(JSON.stringify(preset, null, 2));
21549
21963
  return;
21550
21964
  }
21551
- console.log(chalk17.cyan(`
21552
- ${preset.name} `) + chalk17.dim(`(${preset.id})`));
21553
- console.log(chalk17.dim(` ${preset.description}
21965
+ console.log(chalk19.cyan(`
21966
+ ${preset.name} `) + chalk19.dim(`(${preset.id})`));
21967
+ console.log(chalk19.dim(` ${preset.description}
21554
21968
  `));
21555
- console.log(` Minimum tier: ${chalk17.bold(preset.minTier)}`);
21556
- console.log(` Audit required: ${preset.auditRequired ? chalk17.green("yes") : chalk17.dim("no")}`);
21557
- console.log(` Detector sets: ${chalk17.dim(preset.detectorSets.join(", "))}
21969
+ console.log(` Minimum tier: ${chalk19.bold(preset.minTier)}`);
21970
+ console.log(` Audit required: ${preset.auditRequired ? chalk19.green("yes") : chalk19.dim("no")}`);
21971
+ console.log(` Detector sets: ${chalk19.dim(preset.detectorSets.join(", "))}
21558
21972
  `);
21559
21973
  const overrides = Object.entries(preset.actionOverrides);
21560
- console.log(chalk17.dim(` Detector actions (${overrides.length}):`));
21974
+ console.log(chalk19.dim(` Detector actions (${overrides.length}):`));
21561
21975
  for (const [detector, action] of overrides) {
21562
- const color = action === "block" ? chalk17.red : action === "redact" ? chalk17.yellow : chalk17.dim;
21976
+ const color = action === "block" ? chalk19.red : action === "redact" ? chalk19.yellow : chalk19.dim;
21563
21977
  console.log(` ${detector.padEnd(24)} ${color(action)}`);
21564
21978
  }
21565
21979
  console.log();
21566
21980
  }
21567
21981
  function policyCommand() {
21568
- const cmd = new Command18("policy").description(
21982
+ const cmd = new Command19("policy").description(
21569
21983
  "List and inspect the built-in compliance presets (5: hipaa, gdpr, soc2, nist, pci)"
21570
21984
  );
21571
21985
  cmd.command("list").description("List the 5 built-in compliance presets").option("--json", "Output as JSON").action((opts) => listAction(opts));
@@ -21576,10 +21990,10 @@ function policyCommand() {
21576
21990
 
21577
21991
  // src/commands/audit.ts
21578
21992
  init_esm_shims();
21579
- import { Command as Command19 } from "commander";
21580
- import chalk18 from "chalk";
21581
- import { homedir as homedir10 } from "os";
21582
- import { join as join21 } from "path";
21993
+ import { Command as Command20 } from "commander";
21994
+ import chalk20 from "chalk";
21995
+ import { homedir as homedir11 } from "os";
21996
+ import { join as join24 } from "path";
21583
21997
  var VALID_FORMATS2 = ["table", "csv", "json", "jsonl"];
21584
21998
  function toSafeAuditRow(e) {
21585
21999
  return {
@@ -21650,18 +22064,18 @@ function resolveFormat2(opts) {
21650
22064
  const raw2 = opts.format?.trim().toLowerCase();
21651
22065
  if (raw2 !== void 0 && !VALID_FORMATS2.includes(raw2)) {
21652
22066
  console.error(
21653
- chalk18.red(`
22067
+ chalk20.red(`
21654
22068
  x Unknown --format: "${opts.format}"
21655
- `) + chalk18.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
22069
+ `) + chalk20.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
21656
22070
  );
21657
22071
  return null;
21658
22072
  }
21659
22073
  const fromFormat = raw2 === void 0 ? void 0 : raw2 === "jsonl" ? "json" : raw2;
21660
22074
  if (opts.json && fromFormat !== void 0 && fromFormat !== "json") {
21661
22075
  console.error(
21662
- chalk18.red(`
22076
+ chalk20.red(`
21663
22077
  x --json and --format ${fromFormat} request different outputs.
21664
- `) + chalk18.dim(" Pick one.\n")
22078
+ `) + chalk20.dim(" Pick one.\n")
21665
22079
  );
21666
22080
  return null;
21667
22081
  }
@@ -21669,17 +22083,17 @@ function resolveFormat2(opts) {
21669
22083
  return opts.json ? "json" : "table";
21670
22084
  }
21671
22085
  function auditCommand() {
21672
- return new Command19("audit").description(
22086
+ return new Command20("audit").description(
21673
22087
  "Show recent mutation/tokenization audit entries (leak-proof; JSONL with --json, RFC 4180 CSV with --format csv)"
21674
22088
  ).option("-l, --limit <n>", "Number of entries to show", "20").option("--json", "Output as JSONL (one JSON object per line, jq-pipeable)", false).option("--db <path>", "Path to the audit database (default: ~/.pretense/audit.db)").option(
21675
22089
  "--framework <name>",
21676
- "Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see all 36."
22090
+ "Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see them all."
21677
22091
  ).option("--list-frameworks", "List the compliance frameworks that can be filtered on", false).option(
21678
22092
  "--format <fmt>",
21679
22093
  "Output format: table (default), csv (RFC 4180 compliance export), json (JSONL)"
21680
22094
  ).action((opts) => {
21681
22095
  if (opts.listFrameworks) {
21682
- for (const f of COMPLIANCE_FRAMEWORKS) console.log(f);
22096
+ for (const f of POLICY_FRAMEWORKS) console.log(f);
21683
22097
  return;
21684
22098
  }
21685
22099
  const format = resolveFormat2(opts);
@@ -21689,21 +22103,21 @@ function auditCommand() {
21689
22103
  framework = resolveFrameworkName(opts.framework);
21690
22104
  if (!framework) {
21691
22105
  console.error(
21692
- chalk18.red(`
22106
+ chalk20.red(`
21693
22107
  x Unknown framework: "${opts.framework}"
21694
- `) + chalk18.dim(" Run ") + chalk18.cyan("pretense audit --list-frameworks") + chalk18.dim(" to see all 36.\n")
22108
+ `) + chalk20.dim(" Run ") + chalk20.cyan("pretense audit --list-frameworks") + chalk20.dim(" to see them all.\n")
21695
22109
  );
21696
22110
  process.exit(1);
21697
22111
  }
21698
22112
  }
21699
22113
  const limit = parseInt(opts.limit ?? "20", 10);
21700
22114
  if (!Number.isFinite(limit) || limit <= 0) {
21701
- console.error(chalk18.red(`
22115
+ console.error(chalk20.red(`
21702
22116
  x Invalid --limit: "${opts.limit}". Provide a positive integer.
21703
22117
  `));
21704
22118
  process.exit(1);
21705
22119
  }
21706
- const dbPath = opts.db ?? join21(homedir10(), ".pretense", "audit.db");
22120
+ const dbPath = opts.db ?? join24(homedir11(), ".pretense", "audit.db");
21707
22121
  let rows;
21708
22122
  try {
21709
22123
  const store = new AuditStore(dbPath);
@@ -21723,8 +22137,8 @@ function auditCommand() {
21723
22137
  console.error("pretense: no audit data \u2014 the audit database has not been initialized yet.");
21724
22138
  return;
21725
22139
  }
21726
- console.log(chalk18.dim("\n No audit data available. The audit database has not been initialized yet."));
21727
- console.log(chalk18.dim(" Run " + chalk18.cyan("pretense start") + " and make some requests first.\n"));
22140
+ console.log(chalk20.dim("\n No audit data available. The audit database has not been initialized yet."));
22141
+ console.log(chalk20.dim(" Run " + chalk20.cyan("pretense start") + " and make some requests first.\n"));
21728
22142
  return;
21729
22143
  }
21730
22144
  if (format === "csv") {
@@ -21736,23 +22150,23 @@ function auditCommand() {
21736
22150
  return;
21737
22151
  }
21738
22152
  if (rows.length === 0) {
21739
- console.log(chalk18.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
22153
+ console.log(chalk20.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
21740
22154
  return;
21741
22155
  }
21742
- console.log(chalk18.cyan(`
22156
+ console.log(chalk20.cyan(`
21743
22157
  Pretense Mutation Audit \u2014 last ${rows.length} entries
21744
22158
  `));
21745
22159
  console.log(
21746
- chalk18.dim(
22160
+ chalk20.dim(
21747
22161
  " " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATED".padEnd(10) + "BLOCKED".padEnd(10) + "PII".padEnd(8) + "MS"
21748
22162
  )
21749
22163
  );
21750
- console.log(chalk18.dim(" " + "\u2500".repeat(72)));
22164
+ console.log(chalk20.dim(" " + "\u2500".repeat(72)));
21751
22165
  for (const row of rows) {
21752
22166
  const time = new Date(row.timestamp).toTimeString().slice(0, 8);
21753
- const actionColor = row.action === "blocked" ? chalk18.red : row.action === "mutated" ? chalk18.green : row.action === "redacted" ? chalk18.yellow : row.action === "error" ? chalk18.red : chalk18.dim;
22167
+ const actionColor = row.action === "blocked" ? chalk20.red : row.action === "mutated" ? chalk20.green : row.action === "redacted" ? chalk20.yellow : row.action === "error" ? chalk20.red : chalk20.dim;
21754
22168
  console.log(
21755
- " " + chalk18.dim(time.padEnd(10)) + actionColor(row.action.padEnd(12)) + chalk18.dim(row.provider.padEnd(14)) + chalk18.green(String(row.mutationsApplied).padEnd(10)) + (row.secretsBlocked > 0 ? chalk18.red(String(row.secretsBlocked).padEnd(10)) : chalk18.dim("0".padEnd(10))) + chalk18.dim(String(row.piiRedacted).padEnd(8)) + chalk18.dim(String(row.scanDurationMs).slice(0, 5))
22169
+ " " + chalk20.dim(time.padEnd(10)) + actionColor(row.action.padEnd(12)) + chalk20.dim(row.provider.padEnd(14)) + chalk20.green(String(row.mutationsApplied).padEnd(10)) + (row.secretsBlocked > 0 ? chalk20.red(String(row.secretsBlocked).padEnd(10)) : chalk20.dim("0".padEnd(10))) + chalk20.dim(String(row.piiRedacted).padEnd(8)) + chalk20.dim(String(row.scanDurationMs).slice(0, 5))
21756
22170
  );
21757
22171
  }
21758
22172
  console.log();
@@ -21761,10 +22175,10 @@ function auditCommand() {
21761
22175
 
21762
22176
  // src/commands/credits.ts
21763
22177
  init_esm_shims();
21764
- import { Command as Command20 } from "commander";
21765
- import chalk19 from "chalk";
21766
- import { join as join24 } from "path";
21767
- import { homedir as homedir11 } from "os";
22178
+ import { Command as Command21 } from "commander";
22179
+ import chalk21 from "chalk";
22180
+ import { join as join25 } from "path";
22181
+ import { homedir as homedir12 } from "os";
21768
22182
  function remaining(used, limit) {
21769
22183
  if (limit === null) return null;
21770
22184
  return Math.max(0, limit - used);
@@ -21773,16 +22187,16 @@ function fmt(n) {
21773
22187
  return n === null ? "unlimited" : n.toLocaleString("en-US");
21774
22188
  }
21775
22189
  function bar(used, limit, width = 24) {
21776
- if (limit === null) return chalk19.green("\u2588".repeat(width));
22190
+ if (limit === null) return chalk21.green("\u2588".repeat(width));
21777
22191
  const pct = limit <= 0 ? 1 : Math.min(1, used / limit);
21778
22192
  const filled = Math.min(width, Math.round(pct * width));
21779
22193
  const s = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
21780
- if (pct >= 1) return chalk19.red(s);
21781
- if (pct >= 0.8) return chalk19.yellow(s);
21782
- return chalk19.green(s);
22194
+ if (pct >= 1) return chalk21.red(s);
22195
+ if (pct >= 0.8) return chalk21.yellow(s);
22196
+ return chalk21.green(s);
21783
22197
  }
21784
22198
  function creditsCommand() {
21785
- return new Command20("credits").alias("tokens").description("Show remaining mutation budget for the current period").option("--db <path>", "Path to audit database", join24(homedir11(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
22199
+ return new Command21("credits").alias("tokens").description("Show remaining mutation budget for the current period").option("--db <path>", "Path to audit database", join25(homedir12(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
21786
22200
  const resolution = await resolvePlanTier({ offline: opts.offline });
21787
22201
  const tier = resolution.tier;
21788
22202
  const collected = collectUsage(opts.db);
@@ -21795,7 +22209,7 @@ function creditsCommand() {
21795
22209
  periodEnd: null
21796
22210
  };
21797
22211
  if (!collected && !opts.json) {
21798
- console.log(chalk19.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
22212
+ console.log(chalk21.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
21799
22213
  }
21800
22214
  const mutationLimit = tier ? PLAN_CONFIGS[tier].mutationsPerPeriod : null;
21801
22215
  const scanLimit = tier ? PLAN_CONFIGS[tier].scansPerPeriod : null;
@@ -21827,12 +22241,12 @@ function creditsCommand() {
21827
22241
  );
21828
22242
  return;
21829
22243
  }
21830
- console.log(chalk19.cyan("\n Pretense Credits"));
21831
- console.log(chalk19.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
22244
+ console.log(chalk21.cyan("\n Pretense Credits"));
22245
+ console.log(chalk21.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
21832
22246
  `));
21833
22247
  if (!tier) {
21834
22248
  for (const line of unverifiedNote(resolution.detail)) {
21835
- console.log(chalk19.yellow(` ${line}`));
22249
+ console.log(chalk21.yellow(` ${line}`));
21836
22250
  }
21837
22251
  console.log();
21838
22252
  console.log(" " + "Mutations used".padEnd(20) + m.mutationsApplied.toLocaleString("en-US"));
@@ -21841,18 +22255,18 @@ function creditsCommand() {
21841
22255
  return;
21842
22256
  }
21843
22257
  console.log(
21844
- " " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk19.bold(fmt(mutationsLeft)) + chalk19.dim(` left of ${fmt(mutationLimit)}`)
22258
+ " " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk21.bold(fmt(mutationsLeft)) + chalk21.dim(` left of ${fmt(mutationLimit)}`)
21845
22259
  );
21846
22260
  console.log(
21847
- " " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk19.bold(fmt(scansLeft)) + chalk19.dim(` left of ${fmt(scanLimit)}`)
22261
+ " " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk21.bold(fmt(scansLeft)) + chalk21.dim(` left of ${fmt(scanLimit)}`)
21848
22262
  );
21849
22263
  if (tier !== "enterprise" && mutationLimit !== null && mutationsLeft === 0) {
21850
22264
  console.log(
21851
- chalk19.red("\n Mutation budget exhausted for this period.") + chalk19.dim(" Run ") + chalk19.cyan("pretense upgrade") + chalk19.dim(" to compare plans.")
22265
+ chalk21.red("\n Mutation budget exhausted for this period.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
21852
22266
  );
21853
22267
  } else if (tier !== "enterprise" && mutationLimit !== null && m.mutationsApplied >= mutationLimit * 0.8) {
21854
22268
  console.log(
21855
- chalk19.yellow("\n Running low.") + chalk19.dim(" Run ") + chalk19.cyan("pretense upgrade") + chalk19.dim(" to compare plans.")
22269
+ chalk21.yellow("\n Running low.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
21856
22270
  );
21857
22271
  }
21858
22272
  console.log();
@@ -21861,21 +22275,24 @@ function creditsCommand() {
21861
22275
 
21862
22276
  // src/commands/upgrade.ts
21863
22277
  init_esm_shims();
21864
- import { Command as Command21 } from "commander";
21865
- import chalk20 from "chalk";
22278
+ import { Command as Command22 } from "commander";
22279
+ import chalk22 from "chalk";
21866
22280
  var PRICING_URL = "https://pretense.ai/pricing";
21867
22281
  var TIERS = ["free", "pro", "enterprise"];
21868
22282
  function quota(n) {
21869
22283
  return n === null ? "unlimited" : n.toLocaleString();
21870
22284
  }
21871
22285
  function seats(n) {
21872
- return n === null ? "unlimited" : String(n);
22286
+ return n === null ? "up to 100" : String(n);
22287
+ }
22288
+ function comingSoonOrDash(enabled) {
22289
+ return enabled ? "coming soon" : "\u2014";
21873
22290
  }
21874
22291
  function yesNo(b) {
21875
22292
  return b ? "yes" : "\u2014";
21876
22293
  }
21877
22294
  function upgradeCommand() {
21878
- return new Command21("upgrade").description("Compare Pretense plans and upgrade").option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
22295
+ return new Command22("upgrade").description("Compare Pretense plans and upgrade").option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
21879
22296
  const resolution = await resolvePlanTier({ offline: opts.offline });
21880
22297
  const current = resolution.tier;
21881
22298
  if (opts.json) {
@@ -21903,40 +22320,41 @@ function upgradeCommand() {
21903
22320
  );
21904
22321
  return;
21905
22322
  }
21906
- console.log(chalk20.cyan("\n Pretense Plans\n"));
22323
+ console.log(chalk22.cyan("\n Pretense Plans\n"));
21907
22324
  const col = 16;
21908
22325
  const header = " " + "".padEnd(18) + TIERS.map((t) => tierName(t).padEnd(col)).join("");
21909
- console.log(chalk20.bold(header));
21910
- console.log(" " + chalk20.dim("\u2500".repeat(18 + col * TIERS.length)));
22326
+ console.log(chalk22.bold(header));
22327
+ console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
21911
22328
  const rows = [
21912
22329
  ["Price", (t) => priceLabel(t)],
21913
22330
  ["Mutations/7d", (t) => quota(PLAN_CONFIGS[t].mutationsPerPeriod)],
21914
22331
  ["Requests/7d", (t) => quota(PLAN_CONFIGS[t].scansPerPeriod)],
21915
22332
  ["Seats", (t) => seats(PLAN_CONFIGS[t].maxSeats)],
21916
- ["SSO", (t) => yesNo(PLAN_CONFIGS[t].ssoEnabled)],
22333
+ ["SSO", (t) => comingSoonOrDash(PLAN_CONFIGS[t].ssoEnabled)],
22334
+ // On-prem IS truthful — Docker + Helm ship — so it stays yes/—.
21917
22335
  ["On-prem", (t) => yesNo(PLAN_CONFIGS[t].onPremEnabled)]
21918
22336
  ];
21919
22337
  for (const [label, cell] of rows) {
21920
22338
  console.log(" " + label.padEnd(18) + TIERS.map((t) => cell(t).padEnd(col)).join(""));
21921
22339
  }
21922
- console.log(" " + chalk20.dim("\u2500".repeat(18 + col * TIERS.length)));
22340
+ console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
21923
22341
  if (current) {
21924
- console.log(chalk20.dim(`
22342
+ console.log(chalk22.dim(`
21925
22343
  Current plan: ${tierName(current)}`));
21926
22344
  if (current === "enterprise") {
21927
- console.log(chalk20.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
22345
+ console.log(chalk22.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
21928
22346
  return;
21929
22347
  }
21930
22348
  } else {
21931
22349
  console.log(
21932
- chalk20.yellow(
22350
+ chalk22.yellow(
21933
22351
  `
21934
22352
  Could not confirm your current plan: ${resolution.detail ?? "unknown reason"}.`
21935
22353
  )
21936
22354
  );
21937
22355
  }
21938
22356
  const from = current ?? "unknown";
21939
- console.log(chalk20.bold(`
22357
+ console.log(chalk22.bold(`
21940
22358
  \u2192 ${PRICING_URL}?ref=cli_upgrade&from=${from}
21941
22359
  `));
21942
22360
  });
@@ -21969,35 +22387,36 @@ function findClosestCommand(input, knownCommands) {
21969
22387
  }
21970
22388
  return bestDist <= 3 ? best : null;
21971
22389
  }
21972
- var program = new Command22();
21973
- program.name("pretense").enablePositionalOptions().description(chalk21.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
21974
- ${chalk21.bold("Quick start:")}
21975
- ${chalk21.cyan("pretense run claude")} ${chalk21.dim('"build X"')} Run an AI tool through the proxy (zero setup)
21976
- ${chalk21.cyan("pretense mutate")} ${chalk21.dim("<file>")} Mutate secrets in place (reverse with the same path)
21977
- ${chalk21.cyan("pretense scan")} ${chalk21.dim("<file|dir>")} Scan for secrets and PII
22390
+ var program = new Command23();
22391
+ program.name("pretense").enablePositionalOptions().description(chalk23.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
22392
+ ${chalk23.bold("Quick start:")}
22393
+ ${chalk23.cyan("pretense run claude")} ${chalk23.dim('"build X"')} Run an AI tool through the proxy (zero setup)
22394
+ ${chalk23.cyan("pretense mutate")} ${chalk23.dim("<file>")} Mutate secrets in place (reverse with the same path)
22395
+ ${chalk23.cyan("pretense scan")} ${chalk23.dim("<file|dir>")} Scan for secrets and PII
21978
22396
 
21979
- ${chalk21.dim("All commands:")}
21980
- ${chalk21.cyan("pretense init")} Scan codebase and build protection profile
21981
- ${chalk21.cyan("pretense run <tool>")} Run an AI tool through the proxy, no manual export
21982
- ${chalk21.cyan("pretense start")} Start proxy on localhost:9339
21983
- ${chalk21.cyan("pretense stop")} Stop the running proxy
21984
- ${chalk21.cyan("pretense scan src/api.ts")} Scan file for secrets
21985
- ${chalk21.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
21986
- ${chalk21.cyan("pretense status")} Show protection status
21987
- ${chalk21.cyan("pretense review")} Preview mutations that would be applied
21988
- ${chalk21.cyan("pretense review --json")} Output review as JSON
21989
- ${chalk21.cyan("pretense mutate <file>")} Mutate secrets in place + auto-write reversal map
21990
- ${chalk21.cyan("pretense reverse <file>")} Restore a mutated file (auto-finds its map)
21991
- ${chalk21.cyan("pretense policy list")} List the 5 compliance presets
21992
- ${chalk21.cyan("pretense audit")} Show mutation audit log
21993
- ${chalk21.cyan("pretense usage")} Plan usage vs limits
21994
- ${chalk21.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
21995
- ${chalk21.cyan("pretense upgrade")} Compare plans / upgrade
21996
- ${chalk21.cyan("pretense auth login")} Authenticate with Pretense
21997
- ${chalk21.cyan("pretense install")} Install git hooks
21998
- ${chalk21.cyan("pretense logs --limit 20")} Show recent audit entries
22397
+ ${chalk23.dim("All commands:")}
22398
+ ${chalk23.cyan("pretense init")} Scan codebase and build protection profile
22399
+ ${chalk23.cyan("pretense run <tool>")} Run an AI tool through the proxy, no manual export
22400
+ ${chalk23.cyan("pretense start")} Start proxy on localhost:9339
22401
+ ${chalk23.cyan("pretense stop")} Stop the running proxy
22402
+ ${chalk23.cyan("pretense scan src/api.ts")} Scan file for secrets
22403
+ ${chalk23.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
22404
+ ${chalk23.cyan("pretense status")} Show protection status
22405
+ ${chalk23.cyan("pretense doctor")} Diagnose why the proxy isn't catching traffic
22406
+ ${chalk23.cyan("pretense review")} Preview mutations that would be applied
22407
+ ${chalk23.cyan("pretense review --json")} Output review as JSON
22408
+ ${chalk23.cyan("pretense mutate <file>")} Mutate secrets in place + auto-write reversal map
22409
+ ${chalk23.cyan("pretense reverse <file>")} Restore a mutated file (auto-finds its map)
22410
+ ${chalk23.cyan("pretense policy list")} List the 5 compliance presets
22411
+ ${chalk23.cyan("pretense audit")} Show mutation audit log
22412
+ ${chalk23.cyan("pretense usage")} Plan usage vs limits
22413
+ ${chalk23.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
22414
+ ${chalk23.cyan("pretense upgrade")} Compare plans / upgrade
22415
+ ${chalk23.cyan("pretense auth login")} Authenticate with Pretense
22416
+ ${chalk23.cyan("pretense install")} Install git hooks
22417
+ ${chalk23.cyan("pretense logs --limit 20")} Show recent audit entries
21999
22418
 
22000
- ${chalk21.dim("Run")} ${chalk21.cyan("pretense <command> --help")} ${chalk21.dim("for command-specific help.")}
22419
+ ${chalk23.dim("Run")} ${chalk23.cyan("pretense <command> --help")} ${chalk23.dim("for command-specific help.")}
22001
22420
  `);
22002
22421
  var KNOWN_COMMANDS = [
22003
22422
  "init",
@@ -22025,7 +22444,8 @@ var KNOWN_COMMANDS = [
22025
22444
  "logout",
22026
22445
  "credits",
22027
22446
  "tokens",
22028
- "upgrade"
22447
+ "upgrade",
22448
+ "doctor"
22029
22449
  ];
22030
22450
  program.command("version").description("Print Pretense CLI version").action(() => {
22031
22451
  console.log(`@pretense/cli v${program.version()}`);
@@ -22058,16 +22478,17 @@ program.addCommand(policyCommand());
22058
22478
  program.addCommand(auditCommand());
22059
22479
  program.addCommand(creditsCommand());
22060
22480
  program.addCommand(upgradeCommand());
22481
+ program.addCommand(doctorCommand());
22061
22482
  program.on("command:*", (operands) => {
22062
22483
  const unknown = operands[0] ?? "";
22063
22484
  const suggestion = findClosestCommand(unknown, KNOWN_COMMANDS);
22064
- console.error(chalk21.red(`
22485
+ console.error(chalk23.red(`
22065
22486
  x Unknown command: "${unknown}"`));
22066
22487
  if (suggestion) {
22067
- console.error(chalk21.yellow(` Did you mean: ${chalk21.bold(`pretense ${suggestion}`)}`));
22488
+ console.error(chalk23.yellow(` Did you mean: ${chalk23.bold(`pretense ${suggestion}`)}`));
22068
22489
  }
22069
- console.error(chalk21.dim(`
22070
- Run ${chalk21.cyan("pretense --help")} to see all available commands.
22490
+ console.error(chalk23.dim(`
22491
+ Run ${chalk23.cyan("pretense --help")} to see all available commands.
22071
22492
  `));
22072
22493
  process.exit(1);
22073
22494
  });