@pretense/cli 0.6.15 → 0.6.17

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-AULCU5QN.js";
4
+ } from "./chunk-EFDPBFXJ.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();
@@ -3892,10 +3892,10 @@ var Minipass = class extends EventEmitter {
3892
3892
  * Return a void Promise that resolves once the stream ends.
3893
3893
  */
3894
3894
  async promise() {
3895
- return new Promise((resolve7, reject) => {
3895
+ return new Promise((resolve8, reject) => {
3896
3896
  this.on(DESTROYED, () => reject(new Error("stream destroyed")));
3897
3897
  this.on("error", (er) => reject(er));
3898
- this.on("end", () => resolve7());
3898
+ this.on("end", () => resolve8());
3899
3899
  });
3900
3900
  }
3901
3901
  /**
@@ -3919,7 +3919,7 @@ var Minipass = class extends EventEmitter {
3919
3919
  return Promise.resolve({ done: false, value: res });
3920
3920
  if (this[EOF])
3921
3921
  return stop();
3922
- let resolve7;
3922
+ let resolve8;
3923
3923
  let reject;
3924
3924
  const onerr = (er) => {
3925
3925
  this.off("data", ondata);
@@ -3933,19 +3933,19 @@ var Minipass = class extends EventEmitter {
3933
3933
  this.off("end", onend);
3934
3934
  this.off(DESTROYED, ondestroy);
3935
3935
  this.pause();
3936
- resolve7({ value, done: !!this[EOF] });
3936
+ resolve8({ value, done: !!this[EOF] });
3937
3937
  };
3938
3938
  const onend = () => {
3939
3939
  this.off("error", onerr);
3940
3940
  this.off("data", ondata);
3941
3941
  this.off(DESTROYED, ondestroy);
3942
3942
  stop();
3943
- resolve7({ done: true, value: void 0 });
3943
+ resolve8({ done: true, value: void 0 });
3944
3944
  };
3945
3945
  const ondestroy = () => onerr(new Error("stream destroyed"));
3946
3946
  return new Promise((res2, rej) => {
3947
3947
  reject = rej;
3948
- resolve7 = res2;
3948
+ resolve8 = res2;
3949
3949
  this.once(DESTROYED, ondestroy);
3950
3950
  this.once("error", onerr);
3951
3951
  this.once("end", onend);
@@ -4917,9 +4917,9 @@ var PathBase = class {
4917
4917
  if (this.#asyncReaddirInFlight) {
4918
4918
  await this.#asyncReaddirInFlight;
4919
4919
  } else {
4920
- let resolve7 = () => {
4920
+ let resolve8 = () => {
4921
4921
  };
4922
- this.#asyncReaddirInFlight = new Promise((res) => resolve7 = res);
4922
+ this.#asyncReaddirInFlight = new Promise((res) => resolve8 = res);
4923
4923
  try {
4924
4924
  for (const e of await this.#fs.promises.readdir(fullpath, {
4925
4925
  withFileTypes: true
@@ -4932,7 +4932,7 @@ var PathBase = class {
4932
4932
  children.provisional = 0;
4933
4933
  }
4934
4934
  this.#asyncReaddirInFlight = void 0;
4935
- resolve7();
4935
+ resolve8();
4936
4936
  }
4937
4937
  return children.slice(0, children.provisional);
4938
4938
  }
@@ -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
 
@@ -10400,7 +10413,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
10400
10413
  });
10401
10414
  if (!chunk) {
10402
10415
  if (i === 1) {
10403
- await new Promise((resolve7) => setTimeout(resolve7));
10416
+ await new Promise((resolve8) => setTimeout(resolve8));
10404
10417
  maxReadCount = 3;
10405
10418
  continue;
10406
10419
  }
@@ -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);
@@ -14374,7 +14408,7 @@ import { createHmac, hkdfSync, randomBytes as randomBytes2 } from "crypto";
14374
14408
  import { createHash, randomBytes } from "crypto";
14375
14409
  import { readFileSync as readFileSync4, existsSync as existsSync22 } from "fs";
14376
14410
  import { homedir as homedir2 } from "os";
14377
- import { join as join22, dirname, parse } from "path";
14411
+ import { join as join22, dirname, parse, resolve } from "path";
14378
14412
  import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
14379
14413
  import { homedir } from "os";
14380
14414
  import { join as join4 } from "path";
@@ -14447,6 +14481,7 @@ function tighten(path2, exact, kind) {
14447
14481
  );
14448
14482
  }
14449
14483
  }
14484
+ var PROJECT_ROOT_ENV = "PRETENSE_PROJECT_ROOT";
14450
14485
  var KEYS_DIR_NAME = join22(".pretense", "keys");
14451
14486
  function keysDir() {
14452
14487
  return join22(homedir2(), KEYS_DIR_NAME);
@@ -14469,7 +14504,25 @@ var PROJECT_MARKERS = [
14469
14504
  ".hg",
14470
14505
  ".svn"
14471
14506
  ];
14507
+ function markersAt(dir, home) {
14508
+ return dir === home ? PROJECT_MARKERS.filter((m) => m !== ".pretense") : PROJECT_MARKERS;
14509
+ }
14472
14510
  function findProjectRoot(filePath) {
14511
+ let dir = dirname(filePath);
14512
+ const { root } = parse(dir);
14513
+ const startDir = dir;
14514
+ const home = homedir2();
14515
+ while (true) {
14516
+ if (markersAt(dir, home).some((m) => existsSync22(join22(dir, m)))) {
14517
+ return dir;
14518
+ }
14519
+ if (dir === root) return startDir;
14520
+ const parent = dirname(dir);
14521
+ if (parent === dir) return startDir;
14522
+ dir = parent;
14523
+ }
14524
+ }
14525
+ function legacyFindProjectRoot(filePath) {
14473
14526
  let dir = dirname(filePath);
14474
14527
  const { root } = parse(dir);
14475
14528
  const startDir = dir;
@@ -14483,6 +14536,21 @@ function findProjectRoot(filePath) {
14483
14536
  dir = parent;
14484
14537
  }
14485
14538
  }
14539
+ function resolveProjectRoot(filePath) {
14540
+ const override2 = process.env[PROJECT_ROOT_ENV];
14541
+ if (override2 && override2.trim()) return resolve(override2.trim());
14542
+ return findProjectRoot(filePath);
14543
+ }
14544
+ function candidateProjectRoots(filePath) {
14545
+ const out = [];
14546
+ const override2 = process.env[PROJECT_ROOT_ENV];
14547
+ if (override2 && override2.trim()) out.push(resolve(override2.trim()));
14548
+ out.push(findProjectRoot(filePath));
14549
+ out.push(dirname(filePath));
14550
+ out.push(legacyFindProjectRoot(filePath));
14551
+ const seen = /* @__PURE__ */ new Set();
14552
+ return out.filter((r) => seen.has(r) ? false : (seen.add(r), true));
14553
+ }
14486
14554
  function findProjectRootForDir(dir) {
14487
14555
  return findProjectRoot(join22(dir, "__pretense_probe__"));
14488
14556
  }
@@ -16118,6 +16186,7 @@ var AuditStore = class {
16118
16186
  blocked: 0,
16119
16187
  mutated: 0,
16120
16188
  redacted: 0,
16189
+ reversed: 0,
16121
16190
  error: 0
16122
16191
  };
16123
16192
  for (const row of actionRows) {
@@ -16300,7 +16369,7 @@ import { join as join5 } from "path";
16300
16369
  import { parse as parseYaml } from "yaml";
16301
16370
  import { readFileSync as readFileSync23 } from "fs";
16302
16371
  import { fileURLToPath as fileURLToPath3 } from "url";
16303
- import { dirname as dirname3, join as join23, resolve } from "path";
16372
+ import { dirname as dirname3, join as join23, resolve as resolve2 } from "path";
16304
16373
  import https from "https";
16305
16374
 
16306
16375
  // ../packages/tls/dist/index.js
@@ -16436,7 +16505,7 @@ function versionFromCliPackageJson() {
16436
16505
  }
16437
16506
  } catch {
16438
16507
  }
16439
- const parent = resolve(dir, "..");
16508
+ const parent = resolve2(dir, "..");
16440
16509
  if (parent === dir) break;
16441
16510
  dir = parent;
16442
16511
  }
@@ -16445,11 +16514,11 @@ function versionFromCliPackageJson() {
16445
16514
  return void 0;
16446
16515
  }
16447
16516
  function productVersion() {
16448
- return override ?? (true ? "0.6.15" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
16517
+ return override ?? (true ? "0.6.17" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
16449
16518
  }
16450
16519
  var UNKNOWN_COMMIT = "unknown";
16451
16520
  function bakedCommitSha() {
16452
- return "538df7d688ce6d0c9f2b39578c555bb4b8281a55".length > 0 ? "538df7d688ce6d0c9f2b39578c555bb4b8281a55" : UNKNOWN_COMMIT;
16521
+ return "6f467e565f6f035a63fe420acef1019ddf2455f0".length > 0 ? "6f467e565f6f035a63fe420acef1019ddf2455f0" : UNKNOWN_COMMIT;
16453
16522
  }
16454
16523
  var FEATURE_TIERS = {
16455
16524
  compliance_export: "pro",
@@ -16571,7 +16640,10 @@ var PlanEnforcer = class {
16571
16640
  upgradeMessage,
16572
16641
  upgradeTier,
16573
16642
  upgradeUrl,
16574
- resetDate
16643
+ resetDate,
16644
+ limitMetric: worstMetric,
16645
+ limitCurrent: worstCurrent,
16646
+ limitValue: worstLimit
16575
16647
  };
16576
16648
  }
16577
16649
  if (overallPercent >= 90) {
@@ -17514,20 +17586,26 @@ function createProxy(opts = {}) {
17514
17586
  if (_enforcement.upgradeUrl) c.header("X-Pretense-Upgrade-Url", _enforcement.upgradeUrl);
17515
17587
  }
17516
17588
  if (!_enforcement.allowed) {
17517
- const _planConfig = PLAN_CONFIGS[_effectiveTier];
17518
- const _mutLimit = _planConfig.mutationsPerPeriod ?? 0;
17519
- const _mutCurrent = _periodStats.totalMutationsApplied;
17520
17589
  const _upgradeTier = _enforcement.upgradeTier ?? "pro";
17590
+ const _metric = _enforcement.limitMetric ?? "mutations";
17591
+ const _mutCurrent = _enforcement.limitCurrent ?? _periodStats.totalMutationsApplied;
17592
+ const _mutLimit = _enforcement.limitValue ?? (PLAN_CONFIGS[_effectiveTier].mutationsPerPeriod ?? 0);
17593
+ const _metricLabel = {
17594
+ mutations: "Mutation",
17595
+ scans: "Request",
17596
+ bytes: "Data",
17597
+ seats: "Seat"
17598
+ };
17599
+ const _label = _metricLabel[_metric] ?? "Usage";
17521
17600
  return c.json({
17522
17601
  error: {
17523
17602
  type: "plan_limit_exceeded",
17524
- code: "MUTATION_LIMIT_EXCEEDED",
17525
- // Human-readable error matching the spec format
17603
+ code: `${_metric.toUpperCase()}_LIMIT_EXCEEDED`,
17526
17604
  // 100,000 = PLAN_CONFIGS.pro.mutationsPerPeriod; $29/seat =
17527
17605
  // PLAN_CONFIGS.pro.pricePerSeatCents (monthly rate). Enterprise
17528
17606
  // prints no price and no self-serve `pretense upgrade` path: it
17529
17607
  // is contact-sales only. DO NOT restore "$99/seat".
17530
- message: `\u2717 Mutation limit reached for this 7-day period (${_mutCurrent.toLocaleString()}/${_mutLimit.toLocaleString()})
17608
+ message: `\u2717 ${_label} limit reached for this 7-day period (${_mutCurrent.toLocaleString()}/${_mutLimit.toLocaleString()})
17531
17609
 
17532
17610
  ${_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
17611
  upgrade: {
@@ -17540,7 +17618,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17540
17618
  current: _mutCurrent,
17541
17619
  limit: _mutLimit,
17542
17620
  percent: _enforcement.usagePercent,
17543
- metric: "mutations",
17621
+ metric: _metric,
17544
17622
  resetDate: _enforcement.resetDate
17545
17623
  }
17546
17624
  }
@@ -17681,6 +17759,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17681
17759
  return replaceCodeBlocks(text, blocks, mutatedBlocks);
17682
17760
  });
17683
17761
  const mutDurationMs = Math.round((performance.now() - mutT0) * 100) / 100;
17762
+ const mutationsApplied = tokensMutated + secretsTokenized + piiTokenized;
17684
17763
  const totalChars = fullText.length;
17685
17764
  const identifierExposureRate = totalChars > 0 ? Math.min(1, tokensMutated / Math.max(1, totalChars / 10)) : 0;
17686
17765
  const riskCtx = {
@@ -17709,7 +17788,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17709
17788
  processedSize: 0,
17710
17789
  secretsBlocked: secretsTokenized,
17711
17790
  piiRedacted: piiTokenized,
17712
- mutationsApplied: tokensMutated,
17791
+ mutationsApplied,
17713
17792
  scanDurationMs,
17714
17793
  mutationDurationMs: mutDurationMs,
17715
17794
  action: "blocked",
@@ -17791,7 +17870,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17791
17870
  processedSize,
17792
17871
  secretsBlocked: secretsTokenized,
17793
17872
  piiRedacted: piiTokenized,
17794
- mutationsApplied: tokensMutated,
17873
+ mutationsApplied,
17795
17874
  scanDurationMs,
17796
17875
  mutationDurationMs: mutDurationMs,
17797
17876
  action: "error",
@@ -17828,7 +17907,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
17828
17907
  processedSize,
17829
17908
  secretsBlocked: secretsTokenized,
17830
17909
  piiRedacted: piiTokenized,
17831
- mutationsApplied: tokensMutated,
17910
+ mutationsApplied,
17832
17911
  scanDurationMs,
17833
17912
  mutationDurationMs: mutDurationMs,
17834
17913
  action,
@@ -17904,10 +17983,16 @@ function renderClientEnvLines(port, protocol) {
17904
17983
  " Easiest \u2014 let Pretense wire the env var and run the tool for you:",
17905
17984
  ' pretense run claude "build me X" # or: cursor, gemini, any tool',
17906
17985
  "",
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}`
17986
+ // The manual lines are SAME-SHELL (env var and tool on one command) on
17987
+ // purpose: a user who runs `export …BASE_URL=…` here and then `claude` in
17988
+ // a NEW terminal is unprotected — env vars are per-shell. The warning
17989
+ // below states that, and points back at `pretense run` as the fix.
17990
+ " Or point your AI client at Pretense yourself (SAME terminal as the tool):",
17991
+ ` ANTHROPIC_BASE_URL=${base} claude`,
17992
+ ` OPENAI_BASE_URL=${base}/v1 cursor`,
17993
+ ` GOOGLE_GEMINI_BASE_URL=${base} gemini`,
17994
+ " \u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT",
17995
+ " protected. Simplest: use `pretense run` and skip the export entirely."
17911
17996
  ].join("\n") + "\n";
17912
17997
  }
17913
17998
  function printBanner(port, protocol, tls) {
@@ -17938,14 +18023,14 @@ if (isMain) {
17938
18023
  init_esm_shims();
17939
18024
  import net from "net";
17940
18025
  function isPortFree(port) {
17941
- return new Promise((resolve7) => {
18026
+ return new Promise((resolve8) => {
17942
18027
  const srv = net.createServer();
17943
18028
  srv.once("error", () => {
17944
- srv.close(() => resolve7(false));
17945
- resolve7(false);
18029
+ srv.close(() => resolve8(false));
18030
+ resolve8(false);
17946
18031
  });
17947
18032
  srv.once("listening", () => {
17948
- srv.close(() => resolve7(true));
18033
+ srv.close(() => resolve8(true));
17949
18034
  });
17950
18035
  srv.listen(port);
17951
18036
  });
@@ -18051,6 +18136,16 @@ async function stopProxy(path2 = pidFilePath(), graceMs = 2e3, pollMs = 50) {
18051
18136
  }
18052
18137
 
18053
18138
  // src/commands/start.ts
18139
+ function parsePort(raw2) {
18140
+ if (!/^\d+$/.test(raw2.trim())) {
18141
+ throw new InvalidArgumentError(`"${raw2}" is not a number. Use a port between 1 and 65535.`);
18142
+ }
18143
+ const n = Number(raw2);
18144
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
18145
+ throw new InvalidArgumentError(`${raw2} is out of range. Use a port between 1 and 65535.`);
18146
+ }
18147
+ return String(n);
18148
+ }
18054
18149
  function pidListeningOn(port) {
18055
18150
  try {
18056
18151
  const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
@@ -18084,7 +18179,7 @@ function explainAddrInUse(port) {
18084
18179
  );
18085
18180
  }
18086
18181
  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) => {
18182
+ 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
18183
  const requested = parseInt(opts.port, 10);
18089
18184
  const existing = readProxyRecord();
18090
18185
  if (existing) {
@@ -18092,7 +18187,10 @@ function startCommand() {
18092
18187
  if (existing.port === requested) {
18093
18188
  console.error(
18094
18189
  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"
18190
+ 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
18191
+ // tool through it, and `pretense run` reuses this exact proxy
18192
+ // (no second export, no second terminal). Lead with that.
18193
+ 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
18194
  );
18097
18195
  process.exit(1);
18098
18196
  }
@@ -18155,109 +18253,485 @@ function startCommand() {
18155
18253
 
18156
18254
  // src/commands/stop.ts
18157
18255
  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";
18256
+ import { Command as Command4 } from "commander";
18222
18257
  import chalk4 from "chalk";
18223
- import { readFileSync as readFileSync7, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
18224
18258
  import { execSync as execSync2 } from "child_process";
18225
- import { resolve as resolve2, join as join7, extname as extname2, basename } from "path";
18226
18259
 
18227
- // src/safe-output.ts
18260
+ // src/commands/doctor.ts
18228
18261
  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
- };
18248
- }
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" : ""}`;
18262
+ import { Command as Command3 } from "commander";
18263
+ import chalk3 from "chalk";
18264
+
18265
+ // src/plan-usage.ts
18266
+ init_esm_shims();
18267
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
18268
+ import { join as join7 } from "path";
18269
+ import { homedir as homedir4 } from "os";
18270
+ function credentialsPath() {
18271
+ return join7(homedir4(), ".pretense", "credentials.json");
18254
18272
  }
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;
18273
+ function readApiKey() {
18274
+ const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
18275
+ if (fromEnv) return fromEnv;
18276
+ const path2 = credentialsPath();
18277
+ if (!existsSync8(path2)) return null;
18278
+ try {
18279
+ const creds = JSON.parse(readFileSync7(path2, "utf-8"));
18280
+ const key = creds["apiKey"] ?? creds["api_key"];
18281
+ if (typeof key === "string" && key.trim().length > 0) return key.trim();
18282
+ } catch {
18283
+ }
18284
+ return null;
18285
+ }
18286
+ var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
18287
+ var DEFAULT_TIER_TIMEOUT_MS = 5e3;
18288
+ function getConfiguredApiRoot() {
18289
+ const raw2 = (process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
18290
+ if (!raw2) return DEFAULT_PRETENSE_API_URL;
18291
+ const normalized = /^https?:\/\//i.test(raw2) ? raw2 : `https://${raw2}`;
18292
+ try {
18293
+ const u = new URL(normalized);
18294
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL : u.origin;
18295
+ const path2 = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
18296
+ return `${origin}${path2}`;
18297
+ } catch {
18298
+ return DEFAULT_PRETENSE_API_URL;
18299
+ }
18300
+ }
18301
+ function tierRequestTimeoutMs() {
18302
+ const raw2 = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
18303
+ if (!raw2 || !/^\d+$/.test(raw2)) return DEFAULT_TIER_TIMEOUT_MS;
18304
+ return Math.min(Math.max(parseInt(raw2, 10), 1e3), 12e4);
18305
+ }
18306
+ function planToTier(plan) {
18307
+ switch (String(plan).toUpperCase()) {
18308
+ case "FREE":
18309
+ return "free";
18310
+ case "PRO":
18311
+ return "pro";
18312
+ case "ENTERPRISE":
18313
+ return "enterprise";
18314
+ default:
18315
+ return null;
18316
+ }
18317
+ }
18318
+ async function tierFromBackend(apiKey) {
18319
+ const url = `${getConfiguredApiRoot()}/api/cli/auth/validate`;
18320
+ const controller = new AbortController();
18321
+ const timer = setTimeout(() => controller.abort(), tierRequestTimeoutMs());
18322
+ try {
18323
+ const resp = await fetch(url, {
18324
+ method: "POST",
18325
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
18326
+ body: "{}",
18327
+ signal: controller.signal
18328
+ });
18329
+ if (resp.status === 401 || resp.status === 403) {
18330
+ return {
18331
+ tier: null,
18332
+ source: "unverified",
18333
+ detail: "the server rejected this API key \u2014 run `pretense auth login` again"
18334
+ };
18335
+ }
18336
+ if (!resp.ok) {
18337
+ return {
18338
+ tier: null,
18339
+ source: "unverified",
18340
+ detail: `the plan service returned HTTP ${resp.status}`
18341
+ };
18342
+ }
18343
+ const body = await resp.json();
18344
+ const tier = planToTier(body["plan"]);
18345
+ if (body["valid"] === false || !tier) {
18346
+ return {
18347
+ tier: null,
18348
+ source: "unverified",
18349
+ detail: "the plan service did not report a recognizable plan"
18350
+ };
18351
+ }
18352
+ return { tier, source: "backend" };
18353
+ } catch (err) {
18354
+ const aborted = err?.name === "AbortError";
18355
+ return {
18356
+ tier: null,
18357
+ source: "unverified",
18358
+ detail: aborted ? `could not reach ${url} within ${tierRequestTimeoutMs()}ms` : `could not reach ${url}`
18359
+ };
18360
+ } finally {
18361
+ clearTimeout(timer);
18362
+ }
18363
+ }
18364
+ async function resolvePlanTier(opts = {}) {
18365
+ const apiKey = readApiKey();
18366
+ if (!apiKey) return { tier: "free", source: "no-credentials" };
18367
+ const fromPrefix = tierFromApiKey(apiKey);
18368
+ if (fromPrefix) return { tier: fromPrefix, source: "key-prefix" };
18369
+ if (opts.offline) {
18370
+ return {
18371
+ tier: null,
18372
+ source: "unverified",
18373
+ detail: "--offline was set, and a dashboard key carries no plan of its own"
18374
+ };
18375
+ }
18376
+ return tierFromBackend(apiKey);
18377
+ }
18378
+ var QUOTA_PERIOD_DAYS2 = 7;
18379
+ function collectUsage(dbPath) {
18380
+ const now = /* @__PURE__ */ new Date();
18381
+ const periodMs = QUOTA_PERIOD_DAYS2 * 24 * 60 * 60 * 1e3;
18382
+ const periodStartTs = Date.now() - periodMs;
18383
+ const sinceMs = periodMs;
18384
+ let store = null;
18385
+ try {
18386
+ store = new AuditStore(dbPath);
18387
+ const stats = store.getStats(sinceMs);
18388
+ return {
18389
+ periodStartTs,
18390
+ metrics: {
18391
+ tenantId: "local",
18392
+ // The trailing window these numbers actually cover — previously the
18393
+ // calendar month, which did not match the aggregation OR the quota.
18394
+ periodStart: new Date(periodStartTs).toISOString().split("T")[0],
18395
+ periodEnd: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
18396
+ mutationsApplied: stats.totalMutationsApplied,
18397
+ secretsBlocked: stats.totalSecretsBlocked,
18398
+ piiRedacted: stats.totalPiiRedacted,
18399
+ totalScans: stats.totalRequests,
18400
+ bytesProtected: 0,
18401
+ activeSeats: 1,
18402
+ providers: Object.fromEntries(stats.topProviders.map((p) => [p.provider, p.count]))
18403
+ }
18404
+ };
18405
+ } catch {
18406
+ return null;
18407
+ } finally {
18408
+ store?.close();
18409
+ }
18410
+ }
18411
+ function resetCadenceLabel() {
18412
+ return `every ${QUOTA_PERIOD_DAYS2} days`;
18413
+ }
18414
+ function priceLabel(tier) {
18415
+ const cents = PLAN_CONFIGS[tier].pricePerSeatCents;
18416
+ if (cents === null) return "custom \u2014 contact sales";
18417
+ return cents === 0 ? "free" : `$${cents / 100}/seat/month`;
18418
+ }
18419
+ function tierName(tier) {
18420
+ return { free: "Free", pro: "Pro", enterprise: "Enterprise" }[tier];
18421
+ }
18422
+ function tierLabel(tier) {
18423
+ if (tier === null) return "Unknown (unverified)";
18424
+ if (tier === "free") return "Free";
18425
+ return `${tierName(tier)} (${priceLabel(tier)})`;
18426
+ }
18427
+ function planMismatchHint(resolution) {
18428
+ if (resolution.tier !== "free" || resolution.source !== "backend") return [];
18429
+ return [
18430
+ "Not the plan you expected? An org flagged PRO without an active",
18431
+ "subscription/comp resolves to Free \u2014 see pretense.ai/dashboard/settings or contact support."
18432
+ ];
18433
+ }
18434
+ function unverifiedNote(detail) {
18435
+ return [
18436
+ `Could not confirm your plan: ${detail ?? "unknown reason"}.`,
18437
+ "Limits are not shown because guessing could under- or over-state what you pay for."
18438
+ ];
18439
+ }
18440
+
18441
+ // src/commands/doctor.ts
18442
+ var BASE_URL_ENVS = [
18443
+ { name: "ANTHROPIC_BASE_URL", provider: "Claude / Claude Code" },
18444
+ { name: "OPENAI_BASE_URL", provider: "OpenAI / Cursor / any OpenAI-compatible" },
18445
+ { name: "GEMINI_BASE_URL", provider: "Google Gemini" },
18446
+ { name: "GOOGLE_GEMINI_BASE_URL", provider: "Google Gemini (legacy var)" }
18447
+ ];
18448
+ var COMMON_PORTS = Array.from({ length: 7 }, (_, i) => 9339 + i);
18449
+ async function probePort(port, timeoutMs = 700) {
18450
+ const ctrl = new AbortController();
18451
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
18452
+ try {
18453
+ const res = await fetch(`http://localhost:${port}/health`, { signal: ctrl.signal });
18454
+ if (!res.ok) return { alive: true, isPretense: false };
18455
+ const hasHeader = res.headers.has("x-pretense-version");
18456
+ let body = {};
18457
+ try {
18458
+ body = await res.json();
18459
+ } catch {
18460
+ return { alive: true, isPretense: false };
18461
+ }
18462
+ const looksPretense = hasHeader && "auditAvailable" in body && "version" in body;
18463
+ return {
18464
+ alive: true,
18465
+ isPretense: looksPretense,
18466
+ status: typeof body["status"] === "string" ? body["status"] : void 0,
18467
+ auditAvailable: typeof body["auditAvailable"] === "boolean" ? body["auditAvailable"] : void 0
18468
+ };
18469
+ } catch {
18470
+ return { alive: false, isPretense: false };
18471
+ } finally {
18472
+ clearTimeout(t);
18473
+ }
18474
+ }
18475
+ function portFromBaseUrl(value) {
18476
+ try {
18477
+ const u = new URL(value);
18478
+ if (u.port) return parseInt(u.port, 10);
18479
+ return u.protocol === "https:" ? 443 : 80;
18480
+ } catch {
18481
+ return null;
18482
+ }
18483
+ }
18484
+ function doctorCommand() {
18485
+ 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) => {
18486
+ let problems = 0;
18487
+ const note = (s) => console.log(s);
18488
+ console.log(chalk3.bold.cyan("\n Pretense Doctor\n"));
18489
+ note(chalk3.bold(" Proxy"));
18490
+ const record = readProxyRecord(pidFilePath());
18491
+ let recordedPort = null;
18492
+ if (record) {
18493
+ if (isProcessAlive(record.pid)) {
18494
+ const probe = await probePort(record.port);
18495
+ if (probe.isPretense) {
18496
+ recordedPort = record.port;
18497
+ const degraded = probe.status === "degraded" || probe.auditAvailable === false;
18498
+ note(chalk3.green(` \u2713 Pretense proxy running on :${record.port} (pid ${record.pid}).`));
18499
+ if (degraded) {
18500
+ note(chalk3.yellow(` \u26A0 Audit store unavailable \u2014 requests are still filtered, but ` + chalk3.cyan("pretense audit") + chalk3.yellow(" will be empty this session.")));
18501
+ }
18502
+ } else if (probe.alive) {
18503
+ problems += 1;
18504
+ note(chalk3.yellow(` \u26A0 Pidfile says a proxy is on :${record.port} (pid ${record.pid}), but that port is answering as something ELSE.`));
18505
+ note(chalk3.dim(` Something took the port. Stop the stray process, then ` + chalk3.cyan("pretense start") + chalk3.dim(".")));
18506
+ } else {
18507
+ problems += 1;
18508
+ note(chalk3.yellow(` \u26A0 Pidfile records pid ${record.pid} on :${record.port}, but it is not answering /health.`));
18509
+ note(chalk3.dim(" It may be starting, wedged, or dead. Try ") + chalk3.cyan("pretense stop") + chalk3.dim(" then ") + chalk3.cyan("pretense start") + chalk3.dim("."));
18510
+ }
18511
+ } else {
18512
+ problems += 1;
18513
+ note(chalk3.yellow(` \u26A0 Stale pidfile: pid ${record.pid} (port ${record.port}) is no longer alive.`));
18514
+ note(chalk3.dim(" Harmless, cleared on the next ") + chalk3.cyan("pretense start") + chalk3.dim("."));
18515
+ }
18516
+ } else {
18517
+ note(chalk3.dim(" \u25CB No Pretense proxy recorded (pidfile absent)."));
18518
+ }
18519
+ const stray = [];
18520
+ for (const p of COMMON_PORTS) {
18521
+ if (p === recordedPort) continue;
18522
+ const probe = await probePort(p, 400);
18523
+ if (probe.isPretense) stray.push(p);
18524
+ }
18525
+ if (stray.length > 0) {
18526
+ note(chalk3.yellow(` \u26A0 Other Pretense proxies answering on: ${stray.map((p) => `:${p}`).join(", ")}.`));
18527
+ 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."));
18528
+ }
18529
+ note(chalk3.bold("\n This shell"));
18530
+ const setEnvs = BASE_URL_ENVS.filter((e) => (process.env[e.name] ?? "").trim() !== "");
18531
+ let protectedHere = false;
18532
+ if (setEnvs.length === 0) {
18533
+ problems += 1;
18534
+ note(chalk3.red(" \u2717 No base-URL env var is set in THIS terminal \u2014 your AI tool here is NOT protected."));
18535
+ note(chalk3.dim(" Env vars are per-shell: a proxy running elsewhere does not protect this terminal."));
18536
+ note(chalk3.dim(" Simplest fix (no export, no second terminal):"));
18537
+ note(" " + chalk3.cyan('pretense run claude "\u2026"') + chalk3.dim(" # or cursor, gemini, any tool"));
18538
+ if (recordedPort !== null) {
18539
+ note(chalk3.dim(" Or, IN THIS terminal:"));
18540
+ note(" " + chalk3.cyan(`export ANTHROPIC_BASE_URL=http://localhost:${recordedPort}`));
18541
+ }
18542
+ } else {
18543
+ for (const e of setEnvs) {
18544
+ const val = (process.env[e.name] ?? "").trim();
18545
+ const port = portFromBaseUrl(val);
18546
+ if (port === null) {
18547
+ problems += 1;
18548
+ note(chalk3.red(` \u2717 ${e.name}=${val} is not a valid URL.`));
18549
+ continue;
18550
+ }
18551
+ const probe = await probePort(port);
18552
+ if (probe.isPretense) {
18553
+ protectedHere = true;
18554
+ note(chalk3.green(` \u2713 ${e.name} \u2192 :${port} is a live Pretense proxy (${e.provider}).`));
18555
+ } else if (probe.alive) {
18556
+ problems += 1;
18557
+ note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is answering, but it is NOT a Pretense proxy.`));
18558
+ note(chalk3.dim(" Your traffic is going somewhere, but not through the firewall. Fix the URL or use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
18559
+ } else {
18560
+ problems += 1;
18561
+ note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is set, but nothing is listening there.`));
18562
+ note(chalk3.dim(" Start the proxy (") + chalk3.cyan("pretense start") + chalk3.dim(") or, simplest, use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
18563
+ }
18564
+ }
18565
+ }
18566
+ note(chalk3.bold("\n Plan"));
18567
+ try {
18568
+ const resolution = await resolvePlanTier({ offline: opts.offline });
18569
+ note(chalk3.dim(` Plan: ${tierLabel(resolution.tier)}`));
18570
+ for (const line of planMismatchHint(resolution)) {
18571
+ note(chalk3.dim(` ${line}`));
18572
+ }
18573
+ } catch {
18574
+ note(chalk3.dim(" Plan: could not be resolved (offline or no credentials)."));
18575
+ }
18576
+ if (protectedHere && problems === 0) {
18577
+ note(chalk3.bold.green("\n \u2713 This shell is protected \u2014 traffic routes through Pretense.\n"));
18578
+ process.exitCode = 0;
18579
+ } else if (protectedHere) {
18580
+ note(chalk3.bold.yellow(`
18581
+ \u26A0 This shell is protected, but doctor found ${problems} thing(s) to look at above.
18582
+ `));
18583
+ process.exitCode = 1;
18584
+ } else {
18585
+ 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"));
18586
+ process.exitCode = 1;
18587
+ }
18588
+ });
18589
+ }
18590
+
18591
+ // src/commands/stop.ts
18592
+ function pidListeningOn2(port) {
18593
+ try {
18594
+ const out = execSync2(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
18595
+ encoding: "utf-8",
18596
+ stdio: ["pipe", "pipe", "ignore"]
18597
+ }).trim();
18598
+ const pid = parseInt(out.split("\n")[0] ?? "", 10);
18599
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
18600
+ } catch {
18601
+ return null;
18602
+ }
18603
+ }
18604
+ async function isRecordOurs(record) {
18605
+ const port = record.port;
18606
+ if (port > 0) {
18607
+ const probe = await probePort(port, 700);
18608
+ const listener = pidListeningOn2(port);
18609
+ if (probe.isPretense) {
18610
+ return listener === null || listener === record.pid;
18611
+ }
18612
+ return listener !== null && listener === record.pid;
18613
+ }
18614
+ return true;
18615
+ }
18616
+ function stopCommand() {
18617
+ return new Command4("stop").description("Stop the running Pretense proxy").option("--json", "Machine-readable outcome", false).action(async (opts) => {
18618
+ const path2 = pidFilePath();
18619
+ const record = readProxyRecord(path2);
18620
+ if (record && isProcessAlive(record.pid) && !await isRecordOurs(record)) {
18621
+ removeProxyRecord(path2);
18622
+ const outcome2 = { kind: "foreign", pid: record.pid, port: record.port };
18623
+ if (opts.json) {
18624
+ console.log(JSON.stringify(outcome2, null, 2));
18625
+ process.exit(0);
18626
+ }
18627
+ console.log(
18628
+ chalk4.yellow(`
18629
+ \xB7 Not stopping pid ${record.pid}: it is not the Pretense proxy.`) + chalk4.dim(`
18630
+ The recorded proxy (port ${record.port}) is gone and that pid now`) + chalk4.dim(`
18631
+ belongs to an unrelated process \u2014 Pretense will not signal it.`) + chalk4.dim(`
18632
+ Cleaned up the stale record; nothing was killed.
18633
+ `)
18634
+ );
18635
+ process.exit(0);
18636
+ }
18637
+ const outcome = await stopProxy(path2);
18638
+ if (opts.json) {
18639
+ console.log(JSON.stringify(outcome, null, 2));
18640
+ process.exit(outcome.kind === "refused" || outcome.kind === "not-permitted" ? 1 : 0);
18641
+ }
18642
+ switch (outcome.kind) {
18643
+ case "not-running":
18644
+ console.log(
18645
+ chalk4.dim(`
18646
+ \xB7 No running Pretense proxy recorded (${path2} does not exist).`) + chalk4.dim(`
18647
+ Start one with `) + chalk4.cyan("pretense start") + "\n"
18648
+ );
18649
+ process.exit(0);
18650
+ break;
18651
+ case "stale":
18652
+ console.log(
18653
+ chalk4.yellow(
18654
+ `
18655
+ \xB7 Stale PID file cleaned up${outcome.pid ? ` (pid ${outcome.pid} is not running)` : ""}.`
18656
+ ) + chalk4.dim(`
18657
+ No proxy was running.
18658
+ `)
18659
+ );
18660
+ process.exit(0);
18661
+ break;
18662
+ case "stopped":
18663
+ console.log(
18664
+ chalk4.green(
18665
+ `
18666
+ \u2713 Pretense proxy stopped (pid ${outcome.pid}${outcome.port ? `, port ${outcome.port}` : ""})` + (outcome.forced ? " \u2014 did not exit on SIGTERM, sent SIGKILL" : "") + "\n"
18667
+ )
18668
+ );
18669
+ process.exit(0);
18670
+ break;
18671
+ case "not-permitted":
18672
+ console.error(
18673
+ chalk4.red(`
18674
+ x Cannot stop pid ${outcome.pid}: it belongs to another user.`) + chalk4.dim(`
18675
+ The proxy is still running. Stop it as its owner, or: sudo kill ${outcome.pid}
18676
+ `)
18677
+ );
18678
+ process.exit(1);
18679
+ break;
18680
+ case "refused":
18681
+ console.error(
18682
+ chalk4.red(`
18683
+ x Pid ${outcome.pid} is still alive after SIGTERM and SIGKILL.`) + chalk4.dim(`
18684
+ ${path2} was left in place so you can investigate: ps -p ${outcome.pid}
18685
+ `)
18686
+ );
18687
+ process.exit(1);
18688
+ break;
18689
+ }
18690
+ });
18691
+ }
18692
+
18693
+ // src/commands/scan.ts
18694
+ init_esm_shims();
18695
+ import { Command as Command5, Option, InvalidArgumentError as InvalidArgumentError2 } from "commander";
18696
+ import chalk5 from "chalk";
18697
+ import { readFileSync as readFileSync8, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
18698
+ import { execSync as execSync3 } from "child_process";
18699
+ import { resolve as resolve3, join as join8, extname as extname2, basename } from "path";
18700
+
18701
+ // src/safe-output.ts
18702
+ init_esm_shims();
18703
+ var UNSAFE_VALUES_FLAG = "--unsafe-show-values";
18704
+ function lineColFromOffset2(text, offset) {
18705
+ const clamped = Math.max(0, Math.min(offset, text.length));
18706
+ let line = 1;
18707
+ let lineStart = 0;
18708
+ for (let i = 0; i < clamped; i++) {
18709
+ if (text.charCodeAt(i) === 10) {
18710
+ line++;
18711
+ lineStart = i + 1;
18712
+ }
18713
+ }
18714
+ return { line, column: clamped - lineStart + 1 };
18715
+ }
18716
+ function withLocations(result, content) {
18717
+ const locate = (m) => lineColFromOffset2(content, m.start);
18718
+ return {
18719
+ ...result,
18720
+ matches: result.matches.map((m) => ({ ...m, ...locate(m) }))
18721
+ };
18722
+ }
18723
+ function safeLocator(m) {
18724
+ const len = Math.max(0, m.end - m.start);
18725
+ if (m.deobfuscated && len === 0) return "len=0 unmapped";
18726
+ const pos = m.line !== void 0 && m.column !== void 0 ? `L${m.line}:${m.column} ` : "";
18727
+ return `${pos}len=${len}${m.deobfuscated ? " enc" : ""}`;
18728
+ }
18729
+ var VALUE_FIELDS = ["value", "masked", "deobfuscatedValue"];
18730
+ function stripRow(row) {
18731
+ const out = {};
18732
+ for (const [k, v] of Object.entries(row)) {
18733
+ if (VALUE_FIELDS.includes(k)) continue;
18734
+ out[k] = v;
18261
18735
  }
18262
18736
  out.length = typeof row.end === "number" && typeof row.start === "number" ? row.end - row.start : 0;
18263
18737
  return out;
@@ -18349,7 +18823,7 @@ function isBinaryFile(filePath) {
18349
18823
  }
18350
18824
  var SEVERITY_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
18351
18825
  function collectFiles(target) {
18352
- const abs = resolve2(target);
18826
+ const abs = resolve3(target);
18353
18827
  let stat;
18354
18828
  try {
18355
18829
  stat = statSync3(abs);
@@ -18404,7 +18878,7 @@ function walkDir(dir) {
18404
18878
  } catch (err) {
18405
18879
  if (isSkippableWalkError(err)) {
18406
18880
  const code = err.code;
18407
- console.error(chalk4.yellow(` Skipping unreadable path (${code}): ${dir}`));
18881
+ console.error(chalk5.yellow(` Skipping unreadable path (${code}): ${dir}`));
18408
18882
  return files;
18409
18883
  }
18410
18884
  throw err;
@@ -18412,16 +18886,16 @@ function walkDir(dir) {
18412
18886
  for (const entry of entries) {
18413
18887
  if (entry.isDirectory()) {
18414
18888
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
18415
- files.push(...walkDir(join7(dir, entry.name)));
18889
+ files.push(...walkDir(join8(dir, entry.name)));
18416
18890
  } else if (entry.isFile()) {
18417
- files.push(join7(dir, entry.name));
18891
+ files.push(join8(dir, entry.name));
18418
18892
  }
18419
18893
  }
18420
18894
  return files;
18421
18895
  }
18422
18896
  function gitExecChecked(cmd) {
18423
18897
  try {
18424
- const out = execSync2(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
18898
+ const out = execSync3(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
18425
18899
  return { ok: true, lines: out ? out.split("\n").filter(Boolean) : [] };
18426
18900
  } catch (err) {
18427
18901
  const e = err;
@@ -18468,7 +18942,7 @@ function warnIfLargeForL2(byteLength, filePath) {
18468
18942
  const mb = (byteLength / (1024 * 1024)).toFixed(1);
18469
18943
  const where = filePath ? ` \u2014 ${basename(filePath)}` : "";
18470
18944
  console.error(
18471
- chalk4.yellow(
18945
+ chalk5.yellow(
18472
18946
  ` Note: L2 AST scanning is slow on very large files (${mb}MB${where}); this may take a while.`
18473
18947
  )
18474
18948
  );
@@ -18531,14 +19005,14 @@ async function scanContentRaw(content, opts, filePath) {
18531
19005
  return result;
18532
19006
  }
18533
19007
  async function scanFile(filePath, opts) {
18534
- const content = readFileSync7(filePath, "utf-8");
19008
+ const content = readFileSync8(filePath, "utf-8");
18535
19009
  return scanContent(content, opts, filePath);
18536
19010
  }
18537
19011
  function severityIcon(severity) {
18538
19012
  return severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
18539
19013
  }
18540
19014
  function levelTag(level) {
18541
- return level === 2 ? chalk4.magenta("L2") : chalk4.blue("L1");
19015
+ return level === 2 ? chalk5.magenta("L2") : chalk5.blue("L1");
18542
19016
  }
18543
19017
  function rowTail(match3, opts) {
18544
19018
  if (opts.unsafeShowValues) return match3.masked;
@@ -18549,12 +19023,12 @@ function emitResult(result, opts) {
18549
19023
  }
18550
19024
  function printFindings(filePath, result, opts) {
18551
19025
  if (result.clean) return;
18552
- console.log(chalk4.underline(`
19026
+ console.log(chalk5.underline(`
18553
19027
  ${filePath}`));
18554
19028
  for (const match3 of result.matches) {
18555
- const actionColor = match3.action === "block" ? chalk4.red : match3.action === "redact" ? chalk4.yellow : chalk4.dim;
19029
+ const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
18556
19030
  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))}`
19031
+ ` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts))}`
18558
19032
  );
18559
19033
  }
18560
19034
  }
@@ -18640,7 +19114,7 @@ async function scanFiles(files, opts) {
18640
19114
  let blocked = 0;
18641
19115
  for (const f of files) {
18642
19116
  if (isBinaryFile(f)) {
18643
- console.error(chalk4.dim(` Skipping binary file: ${f}`));
19117
+ console.error(chalk5.dim(` Skipping binary file: ${f}`));
18644
19118
  continue;
18645
19119
  }
18646
19120
  try {
@@ -18682,17 +19156,17 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
18682
19156
  }
18683
19157
  if (files.length === 0) {
18684
19158
  console.error(
18685
- chalk4.yellow(`
18686
- No supported files found in ${label}.`) + chalk4.dim(`
19159
+ chalk5.yellow(`
19160
+ No supported files found in ${label}.`) + chalk5.dim(`
18687
19161
  Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
18688
19162
  `)
18689
19163
  );
18690
19164
  } else {
18691
19165
  console.log(
18692
- chalk4.yellow(`
18693
- No supported source files found.`) + chalk4.dim(`
19166
+ chalk5.yellow(`
19167
+ No supported source files found.`) + chalk5.dim(`
18694
19168
  Currently supports: TypeScript, JavaScript, Python, Go, Java, C#, Ruby, Rust
18695
- `) + chalk4.dim(` (${SUPPORTED_EXTENSIONS_DISPLAY})
19169
+ `) + chalk5.dim(` (${SUPPORTED_EXTENSIONS_DISPLAY})
18696
19170
  `)
18697
19171
  );
18698
19172
  }
@@ -18723,18 +19197,18 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
18723
19197
  if (allResults.length === 0) {
18724
19198
  if (notice) console.log(notice);
18725
19199
  if (policyLine) console.log(policyLine);
18726
- console.log(chalk4.green(` \u2713 No secrets or PII detected`));
19200
+ console.log(chalk5.green(` \u2713 No secrets or PII detected`));
18727
19201
  process.exit(0);
18728
19202
  }
18729
- console.log(chalk4.cyan(`
19203
+ console.log(chalk5.cyan(`
18730
19204
  Pretense Scan \u2014 ${label}
18731
19205
  `));
18732
- console.log(chalk4.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)}
19206
+ console.log(chalk5.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)}
18733
19207
  `));
18734
19208
  if (policyLine) console.log(policyLine + "\n");
18735
19209
  if (notice) console.log(notice + "\n");
18736
19210
  for (const { file, result } of allResults) printFindings(file, result, opts);
18737
- console.log(chalk4.dim(`
19211
+ console.log(chalk5.dim(`
18738
19212
  Total: ${total} findings | ${blocked} blocked
18739
19213
  `));
18740
19214
  process.exit(findingsExitCode);
@@ -18764,7 +19238,7 @@ function levelOption() {
18764
19238
  function parseLevel(v) {
18765
19239
  if (v === "1") return 1;
18766
19240
  if (v === "2") return 2;
18767
- throw new InvalidArgumentError("Detection level must be 1 (regex) or 2 (AST).");
19241
+ throw new InvalidArgumentError2("Detection level must be 1 (regex) or 2 (AST).");
18768
19242
  }
18769
19243
  function levelsLabel(opts) {
18770
19244
  if (opts.level === 1) return "L1 only (--level 1)";
@@ -18776,13 +19250,13 @@ function l2Notice(results, opts) {
18776
19250
  if (isAstCoreAvailable()) {
18777
19251
  const errored = results.filter((r) => r.result.l2?.parseErrors).length;
18778
19252
  if (errored > 0) {
18779
- return chalk4.yellow(
19253
+ return chalk5.yellow(
18780
19254
  ` ! L2 parsed ${errored} file(s) with syntax errors \u2014 AST findings may be incomplete. L1 results are unaffected.`
18781
19255
  );
18782
19256
  }
18783
19257
  return null;
18784
19258
  }
18785
- return chalk4.yellow(
19259
+ return chalk5.yellow(
18786
19260
  " ! 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
19261
  );
18788
19262
  }
@@ -18794,9 +19268,9 @@ function validateScanFlags(opts) {
18794
19268
  const raw2 = String(opts.format).trim().toLowerCase();
18795
19269
  if (!VALID_FORMATS.includes(raw2)) {
18796
19270
  console.error(
18797
- chalk4.red(`
19271
+ chalk5.red(`
18798
19272
  x Unknown --format: "${opts.format}"
18799
- `) + chalk4.dim(` Valid formats: ${VALID_FORMATS.join(", ")}.
19273
+ `) + chalk5.dim(` Valid formats: ${VALID_FORMATS.join(", ")}.
18800
19274
  `)
18801
19275
  );
18802
19276
  process.exit(USAGE_EXIT);
@@ -18807,9 +19281,9 @@ function validateScanFlags(opts) {
18807
19281
  const raw2 = String(opts.severity).trim().toLowerCase();
18808
19282
  if (!VALID_SEVERITIES.includes(raw2)) {
18809
19283
  console.error(
18810
- chalk4.red(`
19284
+ chalk5.red(`
18811
19285
  x Unknown --severity: "${opts.severity}"
18812
- `) + chalk4.dim(` Valid severities: ${VALID_SEVERITIES.join(", ")}.
19286
+ `) + chalk5.dim(` Valid severities: ${VALID_SEVERITIES.join(", ")}.
18813
19287
  `)
18814
19288
  );
18815
19289
  process.exit(USAGE_EXIT);
@@ -18826,7 +19300,7 @@ function applyPolicy(opts) {
18826
19300
  if (resolved.scannerPreset) opts.presetName = resolved.scannerPreset;
18827
19301
  }
18828
19302
  } catch (err) {
18829
- console.error(chalk4.red(`
19303
+ console.error(chalk5.red(`
18830
19304
  x ${err.message}
18831
19305
  `));
18832
19306
  process.exit(USAGE_EXIT);
@@ -18836,15 +19310,15 @@ function applyPolicy(opts) {
18836
19310
  function policyNotice(opts) {
18837
19311
  if (!opts.policyResolved) return null;
18838
19312
  const p = opts.policyResolved;
18839
- const line = chalk4.dim(` ${describePolicy(p)}`);
18840
- return p.scannerPreset ? line : chalk4.yellow(` ! ${describePolicy(p)}`);
19313
+ const line = chalk5.dim(` ${describePolicy(p)}`);
19314
+ return p.scannerPreset ? line : chalk5.yellow(` ! ${describePolicy(p)}`);
18841
19315
  }
18842
19316
  function inheritedScanOpts(cmd) {
18843
19317
  return cmd.optsWithGlobals();
18844
19318
  }
18845
19319
  function ciSubcommand() {
18846
19320
  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)
19321
+ 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
19322
  ).action(async (target, opts, cmd) => {
18849
19323
  opts = inheritedScanOpts(cmd);
18850
19324
  applyPolicy(opts);
@@ -18852,8 +19326,8 @@ function ciSubcommand() {
18852
19326
  const files2 = collectFiles(target);
18853
19327
  if (files2.length === 0) {
18854
19328
  console.error(
18855
- chalk4.red(`
18856
- x Path not found or empty: ${target}`) + chalk4.dim(`
19329
+ chalk5.red(`
19330
+ x Path not found or empty: ${target}`) + chalk5.dim(`
18857
19331
  Try: pretense scan ci <file-or-directory>, or omit the path to scan git-tracked files.
18858
19332
  `)
18859
19333
  );
@@ -18865,11 +19339,11 @@ function ciSubcommand() {
18865
19339
  const tracked = gitExecChecked("git ls-files");
18866
19340
  if (!tracked.ok) {
18867
19341
  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(`
19342
+ chalk5.red(`
19343
+ x scan ci could not enumerate git-tracked files \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
19344
+ git: ${tracked.error}`) + chalk5.dim(`
19345
+ This is a scan FAILURE (exit 1), not a clean result. Reporting 0 here`) + chalk5.dim(`
19346
+ would mark a CI gate green over a tree that was never read.`) + chalk5.dim(`
18873
19347
  Fix the checkout, or scan a path directly: pretense scan ci <path>
18874
19348
  `)
18875
19349
  );
@@ -18885,7 +19359,7 @@ function ciSubcommand() {
18885
19359
  }
18886
19360
  function commitRangeSubcommand() {
18887
19361
  return addScanOpts(
18888
- new Command4("commit-range").description("Scan files changed between two commits").argument("<range>", "Commit range (e.g. base..head)")
19362
+ new Command5("commit-range").description("Scan files changed between two commits").argument("<range>", "Commit range (e.g. base..head)")
18889
19363
  ).action(async (range, opts, cmd) => {
18890
19364
  opts = inheritedScanOpts(cmd);
18891
19365
  applyPolicy(opts);
@@ -18893,7 +19367,7 @@ function commitRangeSubcommand() {
18893
19367
  if (files.length === 0) {
18894
19368
  const fmt2 = resolveFormat(opts);
18895
19369
  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"));
19370
+ else console.log(chalk5.green(" \u2713 No changed files in range"));
18897
19371
  process.exit(0);
18898
19372
  }
18899
19373
  await outputMultiFileResults(`commit-range ${range}`, files, opts);
@@ -18901,7 +19375,7 @@ function commitRangeSubcommand() {
18901
19375
  }
18902
19376
  function preCommitSubcommand() {
18903
19377
  return addScanOpts(
18904
- new Command4("pre-commit").description("Scan git staged files")
19378
+ new Command5("pre-commit").description("Scan git staged files")
18905
19379
  ).action(async (opts, cmd) => {
18906
19380
  opts = inheritedScanOpts(cmd);
18907
19381
  applyPolicy(opts);
@@ -18909,23 +19383,79 @@ function preCommitSubcommand() {
18909
19383
  if (files.length === 0) {
18910
19384
  const fmt2 = resolveFormat(opts);
18911
19385
  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"));
19386
+ else console.log(chalk5.green(" \u2713 No staged files to scan"));
18913
19387
  process.exit(0);
18914
19388
  }
18915
19389
  await outputMultiFileResults(`pre-commit (${files.length} staged files)`, files, opts);
18916
19390
  });
18917
19391
  }
19392
+ function isZeroOid(oid) {
19393
+ return /^0+$/.test(oid.trim());
19394
+ }
19395
+ function emptyTreeOid() {
19396
+ const r = gitExecChecked("git hash-object -t tree /dev/null");
19397
+ return r.ok && r.lines[0] ? r.lines[0] : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
19398
+ }
19399
+ function changedFilesForPush(localSha, remoteSha) {
19400
+ if (isZeroOid(localSha)) return { ok: true, lines: [] };
19401
+ if (!isZeroOid(remoteSha)) {
19402
+ return gitExecChecked(`git diff --name-only ${remoteSha} ${localSha}`);
19403
+ }
19404
+ const revs = gitExecChecked(`git rev-list ${localSha} --not --remotes`);
19405
+ if (revs.ok && revs.lines.length > 0) {
19406
+ const oldest = revs.lines[revs.lines.length - 1];
19407
+ const parent = gitExecChecked(`git rev-parse --verify --quiet ${oldest}^`);
19408
+ const base = parent.ok && parent.lines[0] ? parent.lines[0] : emptyTreeOid();
19409
+ return gitExecChecked(`git diff --name-only ${base} ${localSha}`);
19410
+ }
19411
+ if (!revs.ok) return revs;
19412
+ return gitExecChecked(`git diff --name-only ${emptyTreeOid()} ${localSha}`);
19413
+ }
19414
+ async function readStdin() {
19415
+ if (process.stdin.isTTY) return "";
19416
+ const chunks = [];
19417
+ try {
19418
+ for await (const c of process.stdin) chunks.push(c);
19419
+ } catch {
19420
+ return "";
19421
+ }
19422
+ return Buffer.concat(chunks).toString("utf-8");
19423
+ }
18918
19424
  function prePushSubcommand() {
18919
19425
  return addScanOpts(
18920
- new Command4("pre-push").description("Scan files changed between local and remote")
19426
+ new Command5("pre-push").description("Scan the commits being pushed (reads git's pre-push ref lines on stdin)")
18921
19427
  ).action(async (opts, cmd) => {
18922
19428
  opts = inheritedScanOpts(cmd);
18923
19429
  applyPolicy(opts);
18924
- const files = filterExisting(gitExec("git diff --name-only @{push}..HEAD"));
19430
+ const stdin = await readStdin();
19431
+ const refLines = stdin.split("\n").map((l) => l.trim()).filter(Boolean);
19432
+ const changed = /* @__PURE__ */ new Set();
19433
+ if (refLines.length > 0) {
19434
+ for (const line of refLines) {
19435
+ const [, localSha, , remoteSha] = line.split(/\s+/);
19436
+ if (!localSha || !remoteSha) continue;
19437
+ const r = changedFilesForPush(localSha, remoteSha);
19438
+ if (!r.ok) {
19439
+ console.error(
19440
+ chalk5.red(`
19441
+ x pre-push: could not determine what is being pushed \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
19442
+ git: ${r.error}`) + chalk5.dim(`
19443
+ Blocking the push (exit 1) rather than letting an unscanned commit through.`) + chalk5.dim(`
19444
+ Override at your own risk with: git push --no-verify
19445
+ `)
19446
+ );
19447
+ process.exit(1);
19448
+ }
19449
+ for (const f of r.lines) changed.add(f);
19450
+ }
19451
+ } else {
19452
+ for (const f of gitExec("git diff --name-only @{push}..HEAD")) changed.add(f);
19453
+ }
19454
+ const files = filterExisting([...changed]);
18925
19455
  if (files.length === 0) {
18926
19456
  const fmt2 = resolveFormat(opts);
18927
19457
  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"));
19458
+ else console.log(chalk5.green(" \u2713 No changed files to push"));
18929
19459
  process.exit(0);
18930
19460
  }
18931
19461
  await outputMultiFileResults(`pre-push (${files.length} changed files)`, files, opts);
@@ -18933,15 +19463,15 @@ function prePushSubcommand() {
18933
19463
  }
18934
19464
  function pathSubcommand() {
18935
19465
  return addScanOpts(
18936
- new Command4("path").description("Scan a file or directory (recursive)").argument("<target>", "File or directory to scan")
19466
+ new Command5("path").description("Scan a file or directory (recursive)").argument("<target>", "File or directory to scan")
18937
19467
  ).action(async (target, opts, cmd) => {
18938
19468
  opts = inheritedScanOpts(cmd);
18939
19469
  applyPolicy(opts);
18940
19470
  const files = collectFiles(target);
18941
19471
  if (files.length === 0) {
18942
19472
  console.error(
18943
- chalk4.red(`
18944
- x Path not found or empty: ${target}`) + chalk4.dim(`
19473
+ chalk5.red(`
19474
+ x Path not found or empty: ${target}`) + chalk5.dim(`
18945
19475
  Try: pretense scan path <file-or-directory>
18946
19476
  `)
18947
19477
  );
@@ -18951,7 +19481,7 @@ function pathSubcommand() {
18951
19481
  });
18952
19482
  }
18953
19483
  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(
19484
+ 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
19485
  "--policy <preset>",
18956
19486
  // A preset RE-WEIGHTS detector severity/action for that framework
18957
19487
  // (see compliance-preset.ts `actionOverrides`). It does NOT narrow the
@@ -18974,8 +19504,8 @@ function scanCommand() {
18974
19504
  const allFiles = collectFiles(file);
18975
19505
  if (allFiles.length === 0) {
18976
19506
  console.error(
18977
- chalk4.yellow(`
18978
- No supported files found in ${file}.`) + chalk4.dim(`
19507
+ chalk5.yellow(`
19508
+ No supported files found in ${file}.`) + chalk5.dim(`
18979
19509
  Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
18980
19510
  `)
18981
19511
  );
@@ -18986,17 +19516,17 @@ function scanCommand() {
18986
19516
  } catch {
18987
19517
  }
18988
19518
  if (isBinaryFile(file)) {
18989
- console.error(chalk4.yellow(`
19519
+ console.error(chalk5.yellow(`
18990
19520
  Skipping binary file: ${file}
18991
19521
  `));
18992
19522
  process.exit(0);
18993
19523
  }
18994
19524
  try {
18995
- content = readFileSync7(file, "utf-8");
19525
+ content = readFileSync8(file, "utf-8");
18996
19526
  } catch {
18997
19527
  console.error(
18998
- chalk4.red(`
18999
- x Cannot read file: ${file}`) + chalk4.dim(`
19528
+ chalk5.red(`
19529
+ x Cannot read file: ${file}`) + chalk5.dim(`
19000
19530
  Try: check the file exists and you have read permission.
19001
19531
  `)
19002
19532
  );
@@ -19005,7 +19535,7 @@ function scanCommand() {
19005
19535
  } else {
19006
19536
  if (process.stdin.isTTY) {
19007
19537
  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"
19538
+ 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
19539
  );
19010
19540
  process.exit(USAGE_EXIT);
19011
19541
  }
@@ -19014,7 +19544,7 @@ function scanCommand() {
19014
19544
  content = Buffer.concat(chunks).toString("utf-8");
19015
19545
  if (content.trim() === "") {
19016
19546
  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"
19547
+ 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"
19018
19548
  );
19019
19549
  process.exit(USAGE_EXIT);
19020
19550
  }
@@ -19032,466 +19562,295 @@ function scanCommand() {
19032
19562
  console.log(JSON.stringify(toSarif(fileResults, opts ?? {}), null, 2));
19033
19563
  process.exit(result.clean ? 0 : 1);
19034
19564
  }
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);
19225
- }
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}
19565
+ if (fmt2 === "csv") {
19566
+ const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
19567
+ console.log(toCsv(fileResults, opts ?? {}));
19568
+ process.exit(result.clean ? 0 : 1);
19569
+ }
19570
+ console.log(chalk5.cyan(`
19571
+ Pretense Scan \u2014 ${file ?? "stdin"}
19237
19572
  `));
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
19573
+ console.log(chalk5.dim(` Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)} | Duration: ${result.durationMs}ms
19245
19574
  `));
19246
- } else {
19247
- cmd.help();
19575
+ {
19576
+ const policyLine = policyNotice(opts);
19577
+ if (policyLine) console.log(policyLine + "\n");
19578
+ }
19579
+ if (opts.level !== 1 && result.l2 && !result.l2.ran && result.l2.reason) {
19580
+ console.log(chalk5.yellow(` ! ${result.l2.reason}`));
19581
+ console.log(chalk5.dim(" L1 (regex) ran in full; no finding was dropped.\n"));
19582
+ }
19583
+ if (result.clean) {
19584
+ console.log(chalk5.green(" \u2713 No secrets or PII detected\n"));
19585
+ process.exit(0);
19586
+ }
19587
+ for (const match3 of result.matches) {
19588
+ const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
19589
+ console.log(
19590
+ ` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts ?? {}))}`
19591
+ );
19248
19592
  }
19593
+ const blocked = result.matches.filter((m) => m.action === "block").length;
19594
+ const redacted = result.matches.filter((m) => m.action === "redact").length;
19595
+ console.log(chalk5.dim(`
19596
+ Total: ${result.matches.length} findings | ${blocked} blocked, ${redacted} redacted
19597
+ `));
19598
+ process.exit(1);
19599
+ });
19600
+ cmd.addCommand(ciSubcommand());
19601
+ cmd.addCommand(commitRangeSubcommand());
19602
+ cmd.addCommand(preCommitSubcommand());
19603
+ cmd.addCommand(prePushSubcommand());
19604
+ cmd.addCommand(pathSubcommand());
19605
+ cmd.exitOverride((err) => {
19606
+ process.exit(err.exitCode === 0 ? 0 : 3);
19249
19607
  });
19250
19608
  return cmd;
19251
19609
  }
19252
19610
 
19253
- // src/commands/logs.ts
19611
+ // src/commands/status.ts
19254
19612
  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;
19613
+ import { Command as Command6 } from "commander";
19614
+ import chalk6 from "chalk";
19615
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
19616
+ import { join as join9 } from "path";
19617
+ function statusCommand() {
19618
+ return new Command6("status").description("Show Pretense protection status").action(async () => {
19619
+ const dir = process.cwd();
19620
+ const fingerprintPath = join9(dir, ".pretense", "fingerprint.json");
19621
+ const configPath = join9(dir, "pretense.yaml");
19622
+ console.log(chalk6.cyan("\n Pretense Status\n"));
19623
+ const record = readProxyRecord(pidFilePath());
19624
+ if (record && !isProcessAlive(record.pid)) {
19625
+ console.log(
19626
+ chalk6.yellow(
19627
+ ` \u25CB Proxy not running ${chalk6.dim(`(stale record for pid ${record.pid}; run: pretense start)`)}`
19628
+ )
19629
+ );
19630
+ } else {
19631
+ const port = record && record.port > 0 ? record.port : 9339;
19632
+ try {
19633
+ const resp = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
19634
+ if (!resp.ok) throw new Error(`health returned ${resp.status}`);
19635
+ const health = await resp.json();
19636
+ console.log(chalk6.green(` \u2713 Proxy running on :${port} (uptime: ${health["uptime"]}s)`));
19637
+ const stats = health["stats"];
19638
+ console.log(chalk6.dim(` Requests: ${stats["requestsProcessed"] ?? 0} | Mutations: ${stats["mutationsApplied"] ?? 0} | Blocked: ${stats["secretsBlocked"] ?? 0}`));
19639
+ } catch {
19640
+ console.log(chalk6.yellow(` \u25CB Proxy not running ${chalk6.dim("(run: pretense start)")}`));
19272
19641
  }
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
19642
  }
19277
- if (opts.json) {
19278
- console.log(JSON.stringify(entries, null, 2));
19279
- return;
19643
+ if (existsSync9(configPath)) {
19644
+ console.log(chalk6.green(` \u2713 Config: pretense.yaml`));
19645
+ } else {
19646
+ console.log(chalk6.yellow(` \u25CB No config ${chalk6.dim("(run: pretense config)")}`));
19280
19647
  }
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;
19648
+ if (existsSync9(fingerprintPath)) {
19649
+ try {
19650
+ const fp = JSON.parse(readFileSync9(fingerprintPath, "utf-8"));
19651
+ const age = Math.round((Date.now() - fp.scannedAt) / (60 * 1e3));
19652
+ console.log(chalk6.green(` \u2713 Fingerprint: ${fp.filesScanned} files, ${fp.functions.length + fp.classes.length + fp.constants.length} identifiers`));
19653
+ console.log(chalk6.dim(` Last scanned: ${age} minutes ago`));
19654
+ } catch {
19655
+ console.log(chalk6.yellow(" \u26A0 Fingerprint corrupted (run: pretense init)"));
19656
+ }
19657
+ } else {
19658
+ console.log(chalk6.yellow(` \u25CB No fingerprint ${chalk6.dim("(run: pretense init)")}`));
19284
19659
  }
19285
- console.log(chalk7.cyan(`
19286
- Pretense Audit Log \u2014 last ${entries.length} entries
19660
+ console.log(chalk6.dim(`
19661
+ Scanner: ${getPatternCount()} patterns active
19287
19662
  `));
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
19663
  });
19299
19664
  }
19300
19665
 
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
19666
+ // src/commands/config.ts
19309
19667
  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;
19428
- 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
- };
19668
+ import { Command as Command7 } from "commander";
19669
+ import chalk7 from "chalk";
19670
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
19671
+ import { join as join10 } from "path";
19672
+ import YAML2 from "yaml";
19673
+ var DEFAULT_CONFIG = {
19674
+ version: 1,
19675
+ languages: ["typescript", "python", "java", "go", "ruby", "rust"],
19676
+ exclude_paths: ["node_modules", ".git", "dist"],
19677
+ mutation_mode: "full",
19678
+ output_format: "interactive"
19679
+ };
19680
+ function findConfigPath() {
19681
+ const cwd = process.cwd();
19682
+ const candidates = [".pretense.yaml", "pretense.yaml"];
19683
+ for (const name of candidates) {
19684
+ const full = join10(cwd, name);
19685
+ if (existsSync10(full)) return full;
19686
+ }
19687
+ return null;
19688
+ }
19689
+ function loadConfig2() {
19690
+ const path2 = findConfigPath();
19691
+ if (!path2) return { ...DEFAULT_CONFIG };
19692
+ try {
19693
+ const raw2 = readFileSync10(path2, "utf-8");
19694
+ return YAML2.parse(raw2) ?? { ...DEFAULT_CONFIG };
19448
19695
  } catch {
19449
- return null;
19450
- } finally {
19451
- store?.close();
19696
+ return { ...DEFAULT_CONFIG };
19452
19697
  }
19453
19698
  }
19454
- function resetCadenceLabel() {
19455
- return `every ${QUOTA_PERIOD_DAYS2} days`;
19699
+ function configFilePath() {
19700
+ return findConfigPath() ?? join10(process.cwd(), "pretense.yaml");
19456
19701
  }
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`;
19702
+ function getNestedValue(obj, key) {
19703
+ const parts = key.split(".");
19704
+ let cur = obj;
19705
+ for (const p of parts) {
19706
+ if (cur == null || typeof cur !== "object") return void 0;
19707
+ cur = cur[p];
19708
+ }
19709
+ return cur;
19461
19710
  }
19462
- function tierName(tier) {
19463
- return { free: "Free", pro: "Pro", enterprise: "Enterprise" }[tier];
19711
+ function setNestedValue(obj, key, value) {
19712
+ const parts = key.split(".");
19713
+ const leaf = parts.pop();
19714
+ if (leaf === void 0) return;
19715
+ let cur = obj;
19716
+ for (const p of parts) {
19717
+ const child = cur[p];
19718
+ if (child == null || typeof child !== "object" || Array.isArray(child)) {
19719
+ cur[p] = {};
19720
+ }
19721
+ cur = cur[p];
19722
+ }
19723
+ cur[leaf] = value;
19464
19724
  }
19465
- function tierLabel(tier) {
19466
- if (tier === null) return "Unknown (unverified)";
19467
- if (tier === "free") return "Free";
19468
- return `${tierName(tier)} (${priceLabel(tier)})`;
19725
+ function parseValue(raw2) {
19726
+ if (raw2.startsWith("[") && raw2.endsWith("]")) {
19727
+ return raw2.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
19728
+ }
19729
+ if (raw2 === "true") return true;
19730
+ if (raw2 === "false") return false;
19731
+ const n = Number(raw2);
19732
+ if (!Number.isNaN(n) && raw2.length > 0) return n;
19733
+ return raw2;
19469
19734
  }
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
- ];
19735
+ function configCommand() {
19736
+ const cmd = new Command7("config").description("Manage pretense configuration");
19737
+ cmd.command("list").description("Print current configuration").action(() => {
19738
+ const config = loadConfig2();
19739
+ const path2 = findConfigPath();
19740
+ console.log(
19741
+ chalk7.cyan(`
19742
+ Pretense Config`) + chalk7.dim(` (${path2 ?? "defaults - no config file found"})
19743
+ `)
19744
+ );
19745
+ console.log(YAML2.stringify(config).replace(/^/gm, " "));
19746
+ });
19747
+ cmd.command("get").description("Print a single config value").argument("<key>", "Config key (supports dotted paths)").action((key) => {
19748
+ const config = loadConfig2();
19749
+ const val = getNestedValue(config, key);
19750
+ if (val === void 0) {
19751
+ console.error(chalk7.red(`
19752
+ \u2717 Key not found: ${key}
19753
+ `));
19754
+ process.exit(1);
19755
+ }
19756
+ const display = typeof val === "object" ? JSON.stringify(val) : String(val);
19757
+ console.log(display);
19758
+ });
19759
+ cmd.command("set").description("Set a config value").argument("<key>", "Config key (supports dotted paths)").argument("<value>", "Value to set").action((key, rawValue) => {
19760
+ const config = loadConfig2();
19761
+ const value = parseValue(rawValue);
19762
+ setNestedValue(config, key, value);
19763
+ const outPath = configFilePath();
19764
+ writeFileSync4(outPath, YAML2.stringify(config));
19765
+ console.log(chalk7.green(`
19766
+ \u2713 Set ${key} = ${JSON.stringify(value)} in ${outPath}
19767
+ `));
19768
+ });
19769
+ cmd.option("--force", "Overwrite existing config with defaults", false).action((opts) => {
19770
+ if (opts.force) {
19771
+ const outPath = join10(process.cwd(), "pretense.yaml");
19772
+ writeFileSync4(outPath, YAML2.stringify(DEFAULT_CONFIG));
19773
+ console.log(chalk7.green(`
19774
+ \u2713 Created pretense.yaml
19775
+ `));
19776
+ } else {
19777
+ cmd.help();
19778
+ }
19779
+ });
19780
+ return cmd;
19781
+ }
19782
+
19783
+ // src/commands/logs.ts
19784
+ init_esm_shims();
19785
+ import { Command as Command8 } from "commander";
19786
+ import chalk8 from "chalk";
19787
+ import { homedir as homedir5 } from "os";
19788
+ import { join as join11 } from "path";
19789
+ function logsCommand() {
19790
+ 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) => {
19791
+ const limit = parseInt(opts.limit, 10);
19792
+ let store;
19793
+ let entries;
19794
+ try {
19795
+ store = new AuditStore(join11(homedir5(), ".pretense", "audit.db"));
19796
+ entries = store.getRecentEntries(limit);
19797
+ store.close();
19798
+ } catch {
19799
+ if (opts.json) {
19800
+ console.log("[]");
19801
+ return;
19802
+ }
19803
+ console.error(chalk8.dim("\n No audit data available. The audit database has not been initialized yet."));
19804
+ console.error(chalk8.dim(" Run " + chalk8.cyan("pretense start") + " and make some requests first.\n"));
19805
+ return;
19806
+ }
19807
+ if (opts.json) {
19808
+ console.log(JSON.stringify(entries, null, 2));
19809
+ return;
19810
+ }
19811
+ if (entries.length === 0) {
19812
+ console.log(chalk8.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
19813
+ return;
19814
+ }
19815
+ console.log(chalk8.cyan(`
19816
+ Pretense Audit Log \u2014 last ${entries.length} entries
19817
+ `));
19818
+ console.log(chalk8.dim(" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATIONS".padEnd(12) + "BLOCKED".padEnd(10) + "MS"));
19819
+ console.log(chalk8.dim(" " + "\u2500".repeat(70)));
19820
+ for (const entry of entries) {
19821
+ const time = new Date(entry.timestamp).toTimeString().slice(0, 8);
19822
+ const actionColor = entry.action === "blocked" ? chalk8.red : entry.action === "mutated" ? chalk8.green : entry.action === "redacted" ? chalk8.yellow : entry.action === "error" ? chalk8.red : chalk8.dim;
19823
+ console.log(
19824
+ " " + 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))
19825
+ );
19826
+ }
19827
+ console.log();
19828
+ });
19475
19829
  }
19476
19830
 
19477
19831
  // src/commands/usage.ts
19832
+ init_esm_shims();
19833
+ import { Command as Command9 } from "commander";
19834
+ import chalk9 from "chalk";
19835
+ import { join as join12 } from "path";
19836
+ import { homedir as homedir6 } from "os";
19478
19837
  function renderBar(percent, width = 30) {
19479
19838
  const filled = Math.min(Math.round(percent / 100 * width), width);
19480
19839
  const empty = width - filled;
19481
19840
  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);
19841
+ if (percent >= 100) return chalk9.red(bar2);
19842
+ if (percent >= 90) return chalk9.yellow(bar2);
19843
+ if (percent >= 80) return chalk9.yellow(bar2);
19844
+ return chalk9.green(bar2);
19486
19845
  }
19487
19846
  function formatMetric(value, limit) {
19488
19847
  if (limit === null) return `${value.toLocaleString("en-US")} / unlimited`;
19489
19848
  return `${value.toLocaleString("en-US")} / ${limit.toLocaleString("en-US")}`;
19490
19849
  }
19491
19850
  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}%`);
19851
+ if (percent >= 100) return chalk9.red("OVER LIMIT");
19852
+ if (percent >= 80) return chalk9.yellow(`${percent}%`);
19853
+ return chalk9.green(`${percent}%`);
19495
19854
  }
19496
19855
  function emptyUsagePayload(resolution) {
19497
19856
  const tier = resolution.tier;
@@ -19513,7 +19872,7 @@ function emptyUsagePayload(resolution) {
19513
19872
  };
19514
19873
  }
19515
19874
  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) => {
19875
+ 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
19876
  const resolution = await resolvePlanTier({ offline: opts.offline });
19518
19877
  const tier = resolution.tier;
19519
19878
  const collected = collectUsage(opts.db);
@@ -19524,7 +19883,7 @@ function usageCommand() {
19524
19883
  return;
19525
19884
  }
19526
19885
  console.log(
19527
- chalk8.yellow("\n No audit data found. Run ") + chalk8.cyan("pretense start") + chalk8.yellow(" and make some requests first.\n")
19886
+ chalk9.yellow("\n No audit data found. Run ") + chalk9.cyan("pretense start") + chalk9.yellow(" and make some requests first.\n")
19528
19887
  );
19529
19888
  return;
19530
19889
  }
@@ -19567,7 +19926,7 @@ function usageCommand() {
19567
19926
  }
19568
19927
  const resetDate = resetCadenceLabel();
19569
19928
  console.log(
19570
- chalk8.cyan(
19929
+ chalk9.cyan(
19571
19930
  "\n Pretense Usage \u2014 " + // Header shows today's date, NOT a billing month: quotas roll on a
19572
19931
  // 7-day window anchored to signup, so a month name would imply a
19573
19932
  // reset boundary that does not exist.
@@ -19578,11 +19937,14 @@ function usageCommand() {
19578
19937
  })
19579
19938
  )
19580
19939
  );
19581
- console.log(chalk8.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetDate}
19940
+ console.log(chalk9.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetDate}
19582
19941
  `));
19942
+ for (const line of planMismatchHint(resolution)) {
19943
+ console.log(chalk9.dim(` ${line}`));
19944
+ }
19583
19945
  if (!tier) {
19584
19946
  for (const line of unverifiedNote(resolution.detail)) {
19585
- console.log(chalk8.yellow(` ${line}`));
19947
+ console.log(chalk9.yellow(` ${line}`));
19586
19948
  }
19587
19949
  console.log();
19588
19950
  console.log(" " + "Mutations".padEnd(20) + usageMetrics.mutationsApplied.toLocaleString("en-US"));
@@ -19595,42 +19957,42 @@ function usageCommand() {
19595
19957
  const overall = maxUsagePercent(usageMetrics, tier);
19596
19958
  const nextTier = recommendedTier(usageMetrics, tier);
19597
19959
  console.log(
19598
- " " + chalk8.bold("Metric".padEnd(20)) + chalk8.bold("Used / Limit".padEnd(30)) + chalk8.bold("Progress")
19960
+ " " + chalk9.bold("Metric".padEnd(20)) + chalk9.bold("Used / Limit".padEnd(30)) + chalk9.bold("Progress")
19599
19961
  );
19600
- console.log(" " + chalk8.dim("\u2500".repeat(72)));
19962
+ console.log(" " + chalk9.dim("\u2500".repeat(72)));
19601
19963
  const rows = [
19602
19964
  { label: "Mutations", value: usageMetrics.mutationsApplied, limit: planConfig.mutationsPerPeriod },
19603
19965
  { label: "API Requests", value: usageMetrics.totalScans, limit: planConfig.scansPerPeriod }
19604
19966
  ];
19605
19967
  for (const row of rows) {
19606
19968
  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);
19969
+ const bar2 = row.limit === null ? chalk9.dim(" unlimited ".padEnd(32)) : renderBar(percent);
19608
19970
  console.log(
19609
19971
  " " + row.label.padEnd(20) + formatMetric(row.value, row.limit).padEnd(30) + bar2 + " " + (row.limit === null ? "" : percentLabel(percent))
19610
19972
  );
19611
19973
  }
19612
19974
  console.log(
19613
- " " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) + chalk8.dim("(no limit)")
19975
+ " " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) + chalk9.dim("(no limit)")
19614
19976
  );
19615
- console.log(" " + chalk8.dim("\u2500".repeat(72)));
19977
+ console.log(" " + chalk9.dim("\u2500".repeat(72)));
19616
19978
  if (tier === "enterprise") {
19617
- console.log(chalk8.green("\n Enterprise plan \u2014 all limits are unlimited.\n"));
19979
+ console.log(chalk9.green("\n Enterprise plan \u2014 all limits are unlimited.\n"));
19618
19980
  } else if (overall >= 100) {
19619
- console.log(chalk8.red(`
19981
+ console.log(chalk9.red(`
19620
19982
  LIMIT REACHED (${overall}% of ${tierLabel(tier)} plan used).`));
19621
19983
  } else if (overall >= 90) {
19622
19984
  console.log(
19623
- chalk8.yellow(
19985
+ chalk9.yellow(
19624
19986
  `
19625
19987
  WARNING: ${overall}% of your ${tierLabel(tier)} plan used \u2014 approaching limit.`
19626
19988
  )
19627
19989
  );
19628
19990
  } else if (overall >= 80) {
19629
- console.log(chalk8.yellow(`
19991
+ console.log(chalk9.yellow(`
19630
19992
  NOTICE: ${overall}% of your ${tierLabel(tier)} plan used.`));
19631
19993
  } else {
19632
19994
  console.log(
19633
- chalk8.green(`
19995
+ chalk9.green(`
19634
19996
  ${overall}% of your ${tierLabel(tier)} plan used \u2014 you're in good shape.`)
19635
19997
  );
19636
19998
  }
@@ -19642,12 +20004,12 @@ function usageCommand() {
19642
20004
  enterprise: "Unlimited mutations + SSO + on-prem + SIEM"
19643
20005
  };
19644
20006
  console.log(
19645
- chalk8.cyan(`
20007
+ chalk9.cyan(`
19646
20008
  Upgrade to ${tierName(nextTier)} (${priceLabel(nextTier)}):`)
19647
20009
  );
19648
- console.log(chalk8.dim(` ${upgradeBenefit[nextTier]}`));
20010
+ console.log(chalk9.dim(` ${upgradeBenefit[nextTier]}`));
19649
20011
  console.log(
19650
- chalk8.bold(`
20012
+ chalk9.bold(`
19651
20013
  \u2192 https://pretense.ai/pricing?ref=cli_usage&from=${tier}
19652
20014
  `)
19653
20015
  );
@@ -19659,12 +20021,12 @@ function usageCommand() {
19659
20021
 
19660
20022
  // src/commands/auth.ts
19661
20023
  init_esm_shims();
19662
- import { Command as Command9 } from "commander";
19663
- import chalk9 from "chalk";
20024
+ import { Command as Command10 } from "commander";
20025
+ import chalk10 from "chalk";
19664
20026
  import { existsSync as existsSync11, readFileSync as readFileSync11, unlinkSync as unlinkSync2 } from "fs";
19665
20027
  import { join as join13 } from "path";
19666
20028
  import { homedir as homedir7 } from "os";
19667
- import { execSync as execSync3 } from "child_process";
20029
+ import { execSync as execSync4 } from "child_process";
19668
20030
  var CREDENTIALS_DIR = join13(homedir7(), ".pretense");
19669
20031
  var CREDENTIALS_PATH = join13(CREDENTIALS_DIR, "credentials.json");
19670
20032
  async function verifyApiKey(apiKey) {
@@ -19708,7 +20070,7 @@ async function verifyApiKey(apiKey) {
19708
20070
  }
19709
20071
  }
19710
20072
  function storageNotice() {
19711
- return chalk9.dim(` Stored: ${CREDENTIALS_PATH}`) + chalk9.dim(`
20073
+ return chalk10.dim(` Stored: ${CREDENTIALS_PATH}`) + chalk10.dim(`
19712
20074
  Mode: 0600 (file), 0700 (~/.pretense) \u2014 plaintext, not encrypted
19713
20075
  `);
19714
20076
  }
@@ -19731,15 +20093,15 @@ function maskApiKey(key) {
19731
20093
  function openUrl(url) {
19732
20094
  try {
19733
20095
  if (process.platform === "darwin") {
19734
- execSync3(`open ${url}`, { stdio: "ignore" });
20096
+ execSync4(`open ${url}`, { stdio: "ignore" });
19735
20097
  } else {
19736
- execSync3(`xdg-open ${url}`, { stdio: "ignore" });
20098
+ execSync4(`xdg-open ${url}`, { stdio: "ignore" });
19737
20099
  }
19738
20100
  } catch {
19739
20101
  }
19740
20102
  }
19741
20103
  function authCommand() {
19742
- const auth = new Command9("auth").description("Manage CLI authentication");
20104
+ const auth = new Command10("auth").description("Manage CLI authentication");
19743
20105
  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
20106
  "--offline",
19745
20107
  "Skip server verification and store the key unverified (says so in the output)",
@@ -19758,9 +20120,9 @@ function authCommand() {
19758
20120
  const isValidFormat = validPrefixes.some((prefix) => opts.apiKey.startsWith(prefix));
19759
20121
  if (!isValidFormat) {
19760
20122
  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(`
20123
+ chalk10.red(`
20124
+ x Invalid API key format.`) + chalk10.dim(`
20125
+ Key must start with prtns_live_, prtns_test_, or a legacy pt-free-/pt-pro-/pt-ent- prefix`) + chalk10.dim(`
19764
20126
  Try: pretense auth login (opens browser to get a valid key)
19765
20127
  `)
19766
20128
  );
@@ -19775,10 +20137,10 @@ function authCommand() {
19775
20137
  const existing = readCredentials();
19776
20138
  if (existing?.verified === true) {
19777
20139
  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(`
20140
+ chalk10.red("\n x The API key was rejected \u2014 your existing key was kept.") + chalk10.dim(`
20141
+ Rejected: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20142
+ Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) + chalk10.dim(`
20143
+ Server: ${verification.url}`) + chalk10.dim(`
19782
20144
  Reason: ${verification.detail}
19783
20145
  `)
19784
20146
  );
@@ -19793,10 +20155,10 @@ function authCommand() {
19793
20155
  rejected_by: verification.url
19794
20156
  });
19795
20157
  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(`
20158
+ chalk10.red("\n x The Pretense API REJECTED this key. It will not work.") + chalk10.dim(`
20159
+ Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20160
+ Server: ${verification.url}`) + chalk10.dim(`
20161
+ Reason: ${verification.detail}`) + chalk10.dim(`
19800
20162
  Get a current key at https://pretense.ai/auth
19801
20163
  `) + storageNotice()
19802
20164
  );
@@ -19814,19 +20176,22 @@ function authCommand() {
19814
20176
  };
19815
20177
  writeCredentials(creds);
19816
20178
  if (verified) {
20179
+ const planIsFree = String(verification.plan ?? "").trim().toLowerCase() === "free";
19817
20180
  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()
20181
+ chalk10.green("\n Authenticated. The key was verified by the Pretense API.") + chalk10.dim(`
20182
+ Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20183
+ Server: ${verification.url}`) + (verification.plan ? chalk10.dim(`
20184
+ Plan: ${verification.plan}`) : "") + (planIsFree ? chalk10.dim(
20185
+ "\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."
20186
+ ) : "") + "\n" + storageNotice()
19822
20187
  );
19823
20188
  return;
19824
20189
  }
19825
20190
  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(
20191
+ chalk10.yellow("\n Key saved \u2014 NOT verified.") + chalk10.dim(`
20192
+ Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
20193
+ Server: ${verification.url}`) + chalk10.dim(`
20194
+ Reason: ${verification.detail}`) + chalk10.dim(
19830
20195
  `
19831
20196
  The key's format is valid; nothing has confirmed the key itself.
19832
20197
  Re-run \`pretense auth login --key <key>\` when online to verify it.
@@ -19836,58 +20201,58 @@ function authCommand() {
19836
20201
  return;
19837
20202
  }
19838
20203
  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"));
20204
+ console.log(chalk10.cyan("\n Opening browser to https://pretense.ai/auth"));
20205
+ console.log(chalk10.white(" 1. Sign in or create your account"));
20206
+ console.log(chalk10.white(" 2. Copy your API key from the page"));
20207
+ console.log(chalk10.white(" 3. Return here and run:"));
20208
+ console.log(chalk10.green(" pretense auth login --api-key <your-api-key>\n"));
19844
20209
  openUrl(authUrl);
19845
20210
  });
19846
20211
  auth.command("logout").description("Remove stored credentials").action(() => {
19847
20212
  if (!existsSync11(CREDENTIALS_PATH)) {
19848
- console.log(chalk9.yellow("\n No credentials found. Already logged out.\n"));
20213
+ console.log(chalk10.yellow("\n No credentials found. Already logged out.\n"));
19849
20214
  return;
19850
20215
  }
19851
20216
  unlinkSync2(CREDENTIALS_PATH);
19852
- console.log(chalk9.green("\n Logged out. Credentials removed.\n"));
20217
+ console.log(chalk10.green("\n Logged out. Credentials removed.\n"));
19853
20218
  });
19854
20219
  auth.command("status").description("Show current authentication state").action(() => {
19855
20220
  const creds = readCredentials();
19856
20221
  if (!creds) {
19857
- console.log(chalk9.yellow("\n Not authenticated."));
20222
+ console.log(chalk10.yellow("\n Not authenticated."));
19858
20223
  console.log(
19859
- chalk9.dim(" Run ") + chalk9.cyan("pretense auth login --api-key <key>") + chalk9.dim(" to authenticate.\n")
20224
+ chalk10.dim(" Run ") + chalk10.cyan("pretense auth login --api-key <key>") + chalk10.dim(" to authenticate.\n")
19860
20225
  );
19861
20226
  return;
19862
20227
  }
19863
20228
  if (creds.verified === true) {
19864
- console.log(chalk9.green("\n Authenticated (key verified by the Pretense API)"));
20229
+ console.log(chalk10.green("\n Authenticated (key verified by the Pretense API)"));
19865
20230
  } else if (creds.rejected_at) {
19866
- console.log(chalk9.red("\n Key stored \u2014 REJECTED by the Pretense API"));
20231
+ console.log(chalk10.red("\n Key stored \u2014 REJECTED by the Pretense API"));
19867
20232
  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")
20233
+ 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
20234
  );
19870
20235
  } else {
19871
- console.log(chalk9.yellow("\n Key stored \u2014 NOT verified"));
20236
+ console.log(chalk10.yellow("\n Key stored \u2014 NOT verified"));
19872
20237
  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.")
20238
+ chalk10.dim(" No server has confirmed this key. Run ") + chalk10.cyan("pretense auth login --key <key>") + chalk10.dim(" while online to verify it.")
19874
20239
  );
19875
20240
  }
19876
- console.log(chalk9.dim(` API Key: ${maskApiKey(creds.api_key)}`));
20241
+ console.log(chalk10.dim(` API Key: ${maskApiKey(creds.api_key)}`));
19877
20242
  if (creds.authenticated_at) {
19878
- console.log(chalk9.dim(` Saved: ${creds.authenticated_at}`));
20243
+ console.log(chalk10.dim(` Saved: ${creds.authenticated_at}`));
19879
20244
  }
19880
20245
  if (creds.verified_at) {
19881
- console.log(chalk9.dim(` Verified: ${creds.verified_at} by ${creds.verified_by ?? "?"}`));
20246
+ console.log(chalk10.dim(` Verified: ${creds.verified_at} by ${creds.verified_by ?? "?"}`));
19882
20247
  }
19883
20248
  if (creds.plan) {
19884
- console.log(chalk9.dim(` Plan: ${creds.plan}`));
20249
+ console.log(chalk10.dim(` Plan: ${creds.plan}`));
19885
20250
  }
19886
- console.log(chalk9.dim(` Stored: ${CREDENTIALS_PATH} (mode 0600, plaintext)`));
20251
+ console.log(chalk10.dim(` Stored: ${CREDENTIALS_PATH} (mode 0600, plaintext)`));
19887
20252
  if (creds.expires_at) {
19888
20253
  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})`));
20254
+ const label = expired ? chalk10.red("Expired") : chalk10.green("Valid");
20255
+ console.log(chalk10.dim(` Expires: ${creds.expires_at} (${label})`));
19891
20256
  }
19892
20257
  console.log();
19893
20258
  });
@@ -19896,8 +20261,8 @@ function authCommand() {
19896
20261
 
19897
20262
  // src/commands/review.ts
19898
20263
  init_esm_shims();
19899
- import { Command as Command10 } from "commander";
19900
- import chalk10 from "chalk";
20264
+ import { Command as Command11 } from "commander";
20265
+ import chalk11 from "chalk";
19901
20266
  import { readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
19902
20267
  import { join as join14, extname as extname3, relative, isAbsolute } from "path";
19903
20268
  var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -20072,37 +20437,37 @@ function formatInteractive(results) {
20072
20437
  const filesWithFindings = results.filter((r) => r.mutations.length > 0 || r.secrets.length > 0);
20073
20438
  const totalMutations = results.reduce((sum, r) => sum + r.mutations.length, 0);
20074
20439
  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}
20440
+ console.log(chalk11.cyan("\n Pretense Review\n"));
20441
+ console.log(chalk11.dim(` Files scanned: ${results.length} | With findings: ${filesWithFindings.length}
20077
20442
  `));
20078
20443
  if (totalMutations === 0 && totalSecrets === 0) {
20079
- console.log(chalk10.green(" No mutations or secrets detected.\n"));
20444
+ console.log(chalk11.green(" No mutations or secrets detected.\n"));
20080
20445
  return;
20081
20446
  }
20082
- console.log(chalk10.bold(` ${totalMutations} mutation(s) | ${totalSecrets} secret(s)
20447
+ console.log(chalk11.bold(` ${totalMutations} mutation(s) | ${totalSecrets} secret(s)
20083
20448
  `));
20084
20449
  for (const r of filesWithFindings) {
20085
- console.log(chalk10.bold(` ${r.file}`));
20450
+ console.log(chalk11.bold(` ${r.file}`));
20086
20451
  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}`)}`);
20452
+ const catColor = m.category === "function" ? chalk11.blue : m.category === "class" ? chalk11.magenta : m.category === "constant" ? chalk11.yellow : chalk11.white;
20453
+ console.log(` ${catColor(m.category.padEnd(10))} ${chalk11.red(m.original)} ${chalk11.dim("->")} ${chalk11.green(m.mutated)} ${chalk11.dim(`L${m.line}`)}`);
20089
20454
  }
20090
20455
  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)}`);
20456
+ const icon = s.severity === "critical" ? chalk11.red("CRIT") : s.severity === "high" ? chalk11.yellow("HIGH") : chalk11.dim(s.severity.toUpperCase());
20457
+ console.log(` ${icon.padEnd(10)} ${chalk11.bold(s.type)} ${chalk11.dim(s.locator)}`);
20093
20458
  }
20094
20459
  console.log();
20095
20460
  }
20096
20461
  }
20097
20462
  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) => {
20463
+ 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
20464
  const targetDir = path2 ? isAbsolute(path2) ? path2 : join14(process.cwd(), path2) : process.cwd();
20100
20465
  const files = discoverFiles(targetDir);
20101
20466
  if (files.length === 0) {
20102
20467
  if (opts?.json || opts?.agent) {
20103
20468
  console.log("[]");
20104
20469
  } else {
20105
- console.error(chalk10.yellow(" No supported source files found.\n"));
20470
+ console.error(chalk11.yellow(" No supported source files found.\n"));
20106
20471
  }
20107
20472
  return;
20108
20473
  }
@@ -20129,8 +20494,8 @@ function reviewCommand() {
20129
20494
 
20130
20495
  // src/commands/install.ts
20131
20496
  init_esm_shims();
20132
- import { Command as Command11 } from "commander";
20133
- import chalk11 from "chalk";
20497
+ import { Command as Command12 } from "commander";
20498
+ import chalk12 from "chalk";
20134
20499
  import {
20135
20500
  existsSync as existsSync12,
20136
20501
  writeFileSync as writeFileSync5,
@@ -20138,7 +20503,7 @@ import {
20138
20503
  readFileSync as readFileSync13,
20139
20504
  mkdirSync as mkdirSync3
20140
20505
  } from "fs";
20141
- import { join as join15, resolve as resolve3 } from "path";
20506
+ import { join as join15, resolve as resolve4 } from "path";
20142
20507
  var HOOK_SCRIPTS = {
20143
20508
  "pre-commit": `#!/bin/sh
20144
20509
  # Pretense AI Firewall \u2014 pre-commit hook
@@ -20176,16 +20541,16 @@ function installHook(hooksDir, mode, force) {
20176
20541
  return { installed: true, path: hookPath };
20177
20542
  }
20178
20543
  function installCommand() {
20179
- return new Command11("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option(
20544
+ return new Command12("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option(
20180
20545
  "-m, --mode <mode>",
20181
20546
  "Hook mode: pre-commit, pre-push (omit to install both)"
20182
20547
  ).option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
20183
- const dir = resolve3(opts.dir);
20548
+ const dir = resolve4(opts.dir);
20184
20549
  const force = opts.force;
20185
20550
  const modeArg = opts.mode;
20186
20551
  if (modeArg && !VALID_MODES.includes(modeArg)) {
20187
20552
  console.error(
20188
- chalk11.red(
20553
+ chalk12.red(
20189
20554
  `
20190
20555
  Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
20191
20556
  `
@@ -20197,7 +20562,7 @@ function installCommand() {
20197
20562
  const gitDir = findGitDir(dir);
20198
20563
  if (!gitDir) {
20199
20564
  console.error(
20200
- chalk11.red(
20565
+ chalk12.red(
20201
20566
  `
20202
20567
  Error: No .git directory found in ${dir}
20203
20568
  Run this command from a git repository root.
@@ -20208,23 +20573,23 @@ function installCommand() {
20208
20573
  }
20209
20574
  const hooksDir = join15(gitDir, "hooks");
20210
20575
  mkdirSync3(hooksDir, { recursive: true });
20211
- console.log(chalk11.cyan("\n Pretense hook installer\n"));
20576
+ console.log(chalk12.cyan("\n Pretense hook installer\n"));
20212
20577
  let hasError = false;
20213
20578
  for (const mode of modes) {
20214
20579
  const result = installHook(hooksDir, mode, force);
20215
20580
  if (result.installed) {
20216
20581
  console.log(
20217
- chalk11.green(` Installed ${mode} hook at ${result.path}`)
20582
+ chalk12.green(` Installed ${mode} hook at ${result.path}`)
20218
20583
  );
20219
20584
  } else if (result.skipped) {
20220
20585
  console.log(
20221
- chalk11.dim(
20586
+ chalk12.dim(
20222
20587
  ` Pretense ${mode} hook already installed at ${result.path}`
20223
20588
  )
20224
20589
  );
20225
20590
  } else {
20226
20591
  console.error(
20227
- chalk11.yellow(
20592
+ chalk12.yellow(
20228
20593
  ` Hook already exists at ${result.path}. Use --force to overwrite.`
20229
20594
  )
20230
20595
  );
@@ -20235,14 +20600,14 @@ function installCommand() {
20235
20600
  console.log("");
20236
20601
  process.exit(1);
20237
20602
  }
20238
- console.log(chalk11.bold.green("\n Git hooks installed successfully.\n"));
20603
+ console.log(chalk12.bold.green("\n Git hooks installed successfully.\n"));
20239
20604
  });
20240
20605
  }
20241
20606
 
20242
20607
  // src/commands/ignore.ts
20243
20608
  init_esm_shims();
20244
- import { Command as Command12 } from "commander";
20245
- import chalk12 from "chalk";
20609
+ import { Command as Command13 } from "commander";
20610
+ import chalk13 from "chalk";
20246
20611
  import { appendFileSync as appendFileSync3, readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
20247
20612
  import { join as join16 } from "path";
20248
20613
  import { homedir as homedir8 } from "os";
@@ -20261,7 +20626,7 @@ function appendPattern(pattern) {
20261
20626
  const content = existsSync13(p) ? readFileSync14(p, "utf-8") : "";
20262
20627
  const lines = content.split("\n").filter((l) => l.length > 0);
20263
20628
  if (lines.includes(pattern)) {
20264
- console.log(chalk12.yellow(`
20629
+ console.log(chalk13.yellow(`
20265
20630
  Pattern already exists: ${pattern}
20266
20631
  `));
20267
20632
  return;
@@ -20269,26 +20634,26 @@ function appendPattern(pattern) {
20269
20634
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
20270
20635
  appendFileSync3(p, `${prefix}${pattern}
20271
20636
  `);
20272
- console.log(chalk12.green(`
20637
+ console.log(chalk13.green(`
20273
20638
  \u2713 Added to ${IGNORE_FILE}: ${pattern}
20274
20639
  `));
20275
20640
  }
20276
20641
  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) => {
20642
+ 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
20643
  if (opts.list) {
20279
20644
  const patterns = readIgnoreFile();
20280
20645
  if (patterns.length === 0) {
20281
- console.log(chalk12.dim(`
20646
+ console.log(chalk13.dim(`
20282
20647
  No ${IGNORE_FILE} found or file is empty.
20283
20648
  `));
20284
20649
  return;
20285
20650
  }
20286
- console.log(chalk12.cyan(`
20651
+ console.log(chalk13.cyan(`
20287
20652
  ${IGNORE_FILE}:
20288
20653
  `));
20289
20654
  for (const line of patterns) {
20290
20655
  if (line.startsWith("#")) {
20291
- console.log(chalk12.dim(` ${line}`));
20656
+ console.log(chalk13.dim(` ${line}`));
20292
20657
  } else {
20293
20658
  console.log(` ${line}`);
20294
20659
  }
@@ -20298,10 +20663,10 @@ function ignoreCommand() {
20298
20663
  }
20299
20664
  if (opts.lastFound) {
20300
20665
  if (!existsSync13(LAST_SCAN_PATH)) {
20301
- console.error(chalk12.red(`
20666
+ console.error(chalk13.red(`
20302
20667
  \u2717 No last scan results found at ${LAST_SCAN_PATH}
20303
20668
  `));
20304
- console.error(chalk12.dim(" Run 'pretense scan <file>' first.\n"));
20669
+ console.error(chalk13.dim(" Run 'pretense scan <file>' first.\n"));
20305
20670
  process.exit(1);
20306
20671
  }
20307
20672
  try {
@@ -20309,7 +20674,7 @@ function ignoreCommand() {
20309
20674
  const data = JSON.parse(raw2);
20310
20675
  const matches = data.matches ?? [];
20311
20676
  if (matches.length === 0) {
20312
- console.log(chalk12.green("\n \u2713 Last scan was clean - nothing to ignore.\n"));
20677
+ console.log(chalk13.green("\n \u2713 Last scan was clean - nothing to ignore.\n"));
20313
20678
  return;
20314
20679
  }
20315
20680
  let added = 0;
@@ -20320,11 +20685,11 @@ function ignoreCommand() {
20320
20685
  added++;
20321
20686
  }
20322
20687
  }
20323
- console.log(chalk12.green(`
20688
+ console.log(chalk13.green(`
20324
20689
  \u2713 Added ${added} pattern(s) from last scan.
20325
20690
  `));
20326
20691
  } catch {
20327
- console.error(chalk12.red(`
20692
+ console.error(chalk13.red(`
20328
20693
  \u2717 Failed to parse ${LAST_SCAN_PATH}
20329
20694
  `));
20330
20695
  process.exit(1);
@@ -20332,7 +20697,7 @@ function ignoreCommand() {
20332
20697
  return;
20333
20698
  }
20334
20699
  if (!pattern) {
20335
- console.error(chalk12.red("\n \u2717 Please provide a pattern or use --last-found\n"));
20700
+ console.error(chalk13.red("\n \u2717 Please provide a pattern or use --last-found\n"));
20336
20701
  process.exit(1);
20337
20702
  }
20338
20703
  appendPattern(pattern);
@@ -20341,7 +20706,7 @@ function ignoreCommand() {
20341
20706
 
20342
20707
  // src/commands/completion.ts
20343
20708
  init_esm_shims();
20344
- import { Command as Command13 } from "commander";
20709
+ import { Command as Command14 } from "commander";
20345
20710
  var ZSH_COMPLETION = `
20346
20711
  # Pretense zsh completion
20347
20712
  _pretense() {
@@ -20533,7 +20898,7 @@ complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a bash -d "
20533
20898
  complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a fish -d "fish completion script"
20534
20899
  `.trim();
20535
20900
  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) => {
20901
+ return new Command14("completion").description("Print shell tab-completion script").argument("<shell>", "Shell to generate completion for: zsh, bash, fish").action((shell) => {
20537
20902
  switch (shell) {
20538
20903
  case "zsh":
20539
20904
  console.log(ZSH_COMPLETION);
@@ -20553,10 +20918,10 @@ function completionCommand() {
20553
20918
 
20554
20919
  // src/commands/quickstart.ts
20555
20920
  init_esm_shims();
20556
- import { Command as Command14 } from "commander";
20557
- import chalk13 from "chalk";
20921
+ import { Command as Command15 } from "commander";
20922
+ import chalk14 from "chalk";
20558
20923
  import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "fs";
20559
- import { join as join17, resolve as resolve4 } from "path";
20924
+ import { join as join17, resolve as resolve5 } from "path";
20560
20925
  var DEFAULT_CONFIG_YAML = `scan:
20561
20926
  secrets: block
20562
20927
  pii: redact
@@ -20582,63 +20947,65 @@ customPatterns: []
20582
20947
  verbose: false
20583
20948
  `;
20584
20949
  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) => {
20586
- 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..."));
20950
+ 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) => {
20951
+ const dir = resolve5(opts.dir);
20952
+ console.log(chalk14.bold.cyan("\n Pretense Quickstart\n"));
20953
+ console.log(chalk14.dim(" Setting up AI firewall for this project...\n"));
20954
+ console.log(chalk14.dim(" [1/4] Scanning codebase..."));
20590
20955
  let fp;
20591
20956
  try {
20592
20957
  fp = await learn({ dir, minOccurrences: 1 });
20593
- console.log(chalk13.green(` ${fp.filesScanned} files | ${fp.functions.length} functions | ${fp.classes.length} classes`));
20958
+ console.log(chalk14.green(` ${fp.filesScanned} files | ${fp.functions.length} functions | ${fp.classes.length} classes`));
20594
20959
  } catch (err) {
20595
- console.error(chalk13.red(` \u2717 Scan failed: ${err.message}`));
20960
+ console.error(chalk14.red(` \u2717 Scan failed: ${err.message}`));
20596
20961
  process.exit(1);
20597
20962
  }
20598
- console.log(chalk13.dim(" [2/4] Writing config..."));
20963
+ console.log(chalk14.dim(" [2/4] Writing config..."));
20599
20964
  const configPath = join17(dir, "pretense.yaml");
20600
20965
  if (!opts.dryRun) {
20601
20966
  if (!existsSync14(configPath)) {
20602
20967
  writeFileSync6(configPath, DEFAULT_CONFIG_YAML);
20603
- console.log(chalk13.green(" Created pretense.yaml"));
20968
+ console.log(chalk14.green(" Created pretense.yaml"));
20604
20969
  } else {
20605
- console.log(chalk13.dim(" pretense.yaml already exists, skipping"));
20970
+ console.log(chalk14.dim(" pretense.yaml already exists, skipping"));
20606
20971
  }
20607
20972
  }
20608
- console.log(chalk13.dim(" [3/4] Updating .gitignore..."));
20973
+ console.log(chalk14.dim(" [3/4] Updating .gitignore..."));
20609
20974
  if (!opts.dryRun) {
20610
20975
  const gitignorePath = join17(dir, ".gitignore");
20611
20976
  let content = existsSync14(gitignorePath) ? (await import("fs")).default.readFileSync(gitignorePath, "utf-8") : "";
20612
20977
  if (!content.includes(".pretense/")) {
20613
20978
  content = content + (content.endsWith("\n") ? "" : "\n") + ".pretense/\n";
20614
20979
  writeFileSync6(gitignorePath, content);
20615
- console.log(chalk13.green(" Added .pretense/ to .gitignore"));
20980
+ console.log(chalk14.green(" Added .pretense/ to .gitignore"));
20616
20981
  } 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 ...")}
20982
+ console.log(chalk14.dim(" .gitignore already includes .pretense/"));
20983
+ }
20984
+ }
20985
+ console.log(chalk14.dim(" [4/4] Done.\n"));
20986
+ console.log(chalk14.bold(" Next steps:\n"));
20987
+ console.log(` ${chalk14.green("1.")} ${chalk14.cyan('pretense run claude "build me X"')} Run any AI tool through the proxy \u2014 zero setup`);
20988
+ console.log(` ${chalk14.dim("(auto-starts the proxy on a free port and sets the base-URL env var for you)")}`);
20989
+ console.log(` ${chalk14.dim("Works the same for any tool: ")}${chalk14.cyan("pretense run cursor")}${chalk14.dim(", ")}${chalk14.cyan("pretense run gemini")}`);
20990
+ console.log(` ${chalk14.green("2.")} ${chalk14.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
20991
+ console.log(` ${chalk14.green("3.")} ${chalk14.dim("Prefer to wire it by hand? Set the base URL in the SAME command as the tool:")}`);
20992
+ console.log(` ${chalk14.dim("ANTHROPIC_BASE_URL=http://localhost:9339 claude")}`);
20993
+ console.log(` ${chalk14.dim("OPENAI_BASE_URL=http://localhost:9339/v1 cursor ...")}`);
20994
+ console.log(` ${chalk14.dim("GOOGLE_GEMINI_BASE_URL=http://localhost:9339 gemini ...")}`);
20995
+ console.log(` ${chalk14.bold.yellow("\u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT protected.")}`);
20996
+ console.log(` ${chalk14.bold.yellow(" Simplest: use ")}${chalk14.cyan("pretense run")}${chalk14.bold.yellow(" and skip the export entirely.")}
20630
20997
  `);
20631
- console.log(chalk13.bold.green(" Protected. Happy coding.\n"));
20998
+ console.log(chalk14.bold.green(" Protected. Happy coding.\n"));
20632
20999
  });
20633
21000
  }
20634
21001
 
20635
21002
  // src/commands/mutate.ts
20636
21003
  init_esm_shims();
20637
- import { Command as Command15, Option as Option2 } from "commander";
20638
- import chalk14 from "chalk";
21004
+ import { Command as Command16, Option as Option2 } from "commander";
21005
+ import chalk16 from "chalk";
20639
21006
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, chmodSync as chmodSync4, existsSync as existsSync15 } from "fs";
20640
21007
  import { createHash as createHash3 } from "crypto";
20641
- import { resolve as resolve5, extname as extname4 } from "path";
21008
+ import { resolve as resolve6, extname as extname4 } from "path";
20642
21009
 
20643
21010
  // src/secret-crypto.ts
20644
21011
  init_esm_shims();
@@ -20761,6 +21128,48 @@ function ensureDefaultMapPath(absFilePath, override2) {
20761
21128
  return defaultMapPath(absFilePath, override2);
20762
21129
  }
20763
21130
 
21131
+ // src/local-audit.ts
21132
+ init_esm_shims();
21133
+ import { homedir as homedir10 } from "os";
21134
+ import { join as join21 } from "path";
21135
+ import { randomUUID } from "crypto";
21136
+ import chalk15 from "chalk";
21137
+ function localAuditDbPath() {
21138
+ return join21(homedir10(), ".pretense", "audit.db");
21139
+ }
21140
+ function recordLocalAudit(rec, dbPath = localAuditDbPath()) {
21141
+ let store = null;
21142
+ try {
21143
+ store = new AuditStore(dbPath);
21144
+ store.logEntry({
21145
+ timestamp: Date.now(),
21146
+ requestId: randomUUID(),
21147
+ provider: "cli",
21148
+ model: "-",
21149
+ direction: "outbound",
21150
+ originalSize: rec.originalSize,
21151
+ processedSize: rec.processedSize,
21152
+ secretsBlocked: rec.secretsBlocked,
21153
+ piiRedacted: rec.piiRedacted,
21154
+ mutationsApplied: rec.mutationsApplied,
21155
+ scanDurationMs: 0,
21156
+ mutationDurationMs: rec.durationMs,
21157
+ action: rec.action,
21158
+ details: { source: rec.action === "reversed" ? "cli-reverse" : "cli-mutate", file: rec.file },
21159
+ ...rec.detectorKinds && rec.detectorKinds.length > 0 ? { detectorKinds: rec.detectorKinds } : {}
21160
+ });
21161
+ return true;
21162
+ } catch (err) {
21163
+ console.error(
21164
+ chalk15.yellow(` ! Could not record this ${rec.action === "reversed" ? "reversal" : "mutation"} in the audit log`) + chalk15.dim(` (${err.message}).`) + chalk15.dim(`
21165
+ The file was still ${rec.action === "reversed" ? "restored" : "mutated"}; only the audit entry is missing.`)
21166
+ );
21167
+ return false;
21168
+ } finally {
21169
+ store?.close();
21170
+ }
21171
+ }
21172
+
20764
21173
  // src/commands/mutate.ts
20765
21174
  var REVERSAL_MAP_VERSION = 3;
20766
21175
  function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
@@ -20781,7 +21190,7 @@ function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
20781
21190
  to overwrite it deliberately.`
20782
21191
  };
20783
21192
  }
20784
- const existingFile = existing.file ? resolve5(existing.file) : void 0;
21193
+ const existingFile = existing.file ? resolve6(existing.file) : void 0;
20785
21194
  if (existingFile && existingFile !== absFile) {
20786
21195
  if (force) {
20787
21196
  return { action: "write", generation: 1, supersededSha: existing.originalSha };
@@ -20857,8 +21266,10 @@ async function collectMutationFindings(content, file, level, presetName) {
20857
21266
  const cannotMutate = (type) => {
20858
21267
  unmutatable.set(type, (unmutatable.get(type) ?? 0) + 1);
20859
21268
  };
21269
+ const categoryByType = /* @__PURE__ */ new Map();
20860
21270
  for (const m of result.matches) {
20861
21271
  if (level === 2 && m.level !== 2) continue;
21272
+ if (!categoryByType.has(m.type)) categoryByType.set(m.type, m.category);
20862
21273
  const offset = m.start;
20863
21274
  const length = m.end - m.start;
20864
21275
  if (length <= 0) {
@@ -20871,18 +21282,18 @@ async function collectMutationFindings(content, file, level, presetName) {
20871
21282
  }
20872
21283
  findings.push({ value: m.value, offset, length, type: m.type });
20873
21284
  }
20874
- return { findings, unmutatable };
21285
+ return { findings, unmutatable, categoryByType };
20875
21286
  }
20876
21287
  function reportUnmutatable(unmutatable) {
20877
21288
  if (unmutatable.size === 0) return;
20878
21289
  const total = [...unmutatable.values()].reduce((a, b) => a + b, 0);
20879
21290
  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
21291
  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(`
21292
+ chalk16.yellow(`
21293
+ ! ${total} detected value${total === 1 ? "" : "s"} could NOT be mutated: ${kinds}`) + chalk16.dim(`
21294
+ These were found only inside an encoded form (base64/hex) or at a span`) + chalk16.dim(`
21295
+ that does not map back to the file's bytes, so there is no range to`) + chalk16.dim(`
21296
+ rewrite. They are STILL PRESENT in the output. Run \`pretense scan\` on`) + chalk16.dim(`
20886
21297
  this file to see them, and remove or re-encode them by hand.
20887
21298
  `)
20888
21299
  );
@@ -20892,9 +21303,9 @@ function writeReversalMap(mapPath, mapFile) {
20892
21303
  writeFileSync7(mapPath, JSON.stringify(mapFile, null, 2), { mode: 384 });
20893
21304
  } catch (e) {
20894
21305
  console.error(
20895
- chalk14.red(`
20896
- x Cannot write reversal map: ${mapPath}`) + chalk14.dim(`
20897
- ${e.message}`) + chalk14.dim(`
21306
+ chalk16.red(`
21307
+ x Cannot write reversal map: ${mapPath}`) + chalk16.dim(`
21308
+ ${e.message}`) + chalk16.dim(`
20898
21309
  Nothing was mutated \u2014 without a map the original could not be restored.
20899
21310
  `)
20900
21311
  );
@@ -20905,7 +21316,7 @@ function writeReversalMap(mapPath, mapFile) {
20905
21316
  } catch {
20906
21317
  }
20907
21318
  console.error(
20908
- chalk14.green(` \u2713 Wrote encrypted reversal map \u2192 ${mapPath}`) + chalk14.dim(" (AES-256-GCM, mode 0600)")
21319
+ chalk16.green(` \u2713 Wrote encrypted reversal map \u2192 ${mapPath}`) + chalk16.dim(" (AES-256-GCM, mode 0600)")
20909
21320
  );
20910
21321
  return 0;
20911
21322
  }
@@ -20914,7 +21325,7 @@ async function runMutate(file, opts = {}) {
20914
21325
  try {
20915
21326
  raw2 = readFileSync15(file);
20916
21327
  } catch {
20917
- console.error(chalk14.red(`
21328
+ console.error(chalk16.red(`
20918
21329
  x Cannot read file: ${file}
20919
21330
  `));
20920
21331
  return 1;
@@ -20922,9 +21333,9 @@ async function runMutate(file, opts = {}) {
20922
21333
  const content = raw2.toString("utf-8");
20923
21334
  if (!Buffer.from(content, "utf-8").equals(raw2)) {
20924
21335
  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(`
21336
+ chalk16.red(`
21337
+ x Not valid UTF-8: ${file}`) + chalk16.dim(`
21338
+ Mutating would replace the undecodable bytes with U+FFFD and the`) + chalk16.dim(`
20928
21339
  original content could not be restored. File left untouched.
20929
21340
  `)
20930
21341
  );
@@ -20932,7 +21343,8 @@ async function runMutate(file, opts = {}) {
20932
21343
  }
20933
21344
  const preview = opts.stdout === true;
20934
21345
  const inPlace = !preview && !opts.output;
20935
- const { findings, unmutatable } = await collectMutationFindings(
21346
+ const mutateStartMs = Date.now();
21347
+ const { findings, unmutatable, categoryByType } = await collectMutationFindings(
20936
21348
  content,
20937
21349
  file,
20938
21350
  opts.level,
@@ -20940,7 +21352,7 @@ async function runMutate(file, opts = {}) {
20940
21352
  );
20941
21353
  if (findings.length === 0) {
20942
21354
  if (unmutatable.size === 0) {
20943
- console.error(chalk14.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
21355
+ console.error(chalk16.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
20944
21356
  } else {
20945
21357
  reportUnmutatable(unmutatable);
20946
21358
  }
@@ -20948,8 +21360,8 @@ async function runMutate(file, opts = {}) {
20948
21360
  return 0;
20949
21361
  }
20950
21362
  reportUnmutatable(unmutatable);
20951
- const absFile = resolve5(file);
20952
- const projectRoot = findProjectRoot(absFile);
21363
+ const absFile = resolve6(file);
21364
+ const projectRoot = resolveProjectRoot(absFile);
20953
21365
  const result = mutateSecrets(content, findings, { projectRoot, keysDir: opts.keysDir });
20954
21366
  let effectiveMap;
20955
21367
  if (preview) {
@@ -20964,9 +21376,9 @@ async function runMutate(file, opts = {}) {
20964
21376
  const pre = preflightReversalMap(effectiveMap, absFile, content, contentSha, opts.forceMap === true);
20965
21377
  if (pre.action === "refuse") {
20966
21378
  console.error(
20967
- chalk14.red(`
20968
- x ${pre.code}: refusing to overwrite an existing reversal map.`) + chalk14.dim(`
20969
- ${pre.message}`) + chalk14.dim(`
21379
+ chalk16.red(`
21380
+ x ${pre.code}: refusing to overwrite an existing reversal map.`) + chalk16.dim(`
21381
+ ${pre.message}`) + chalk16.dim(`
20970
21382
  Nothing was mutated. Both the file and the map are untouched.
20971
21383
  `)
20972
21384
  );
@@ -20976,8 +21388,8 @@ async function runMutate(file, opts = {}) {
20976
21388
  const isNoop = pre.action === "keep" && pre.expectedMutatedSha === mutatedSha;
20977
21389
  if (isNoop) {
20978
21390
  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(`
21391
+ chalk16.dim(` = Reversal map ${effectiveMap} already describes ${file} at this exact content`) + chalk16.dim(`
21392
+ hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) + chalk16.dim(`
20981
21393
  byte-identical).`)
20982
21394
  );
20983
21395
  } else {
@@ -21003,10 +21415,10 @@ async function runMutate(file, opts = {}) {
21003
21415
  if (written !== 0) return written;
21004
21416
  if (pre.action === "write" && pre.generation > 1) {
21005
21417
  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(`
21418
+ chalk16.yellow(`
21419
+ ! --force-map: this map is mutation GENERATION ${pre.generation}.`) + chalk16.dim(`
21420
+ Its "original" values are synthetics from the previous round, so`) + chalk16.dim(`
21421
+ reversing with it returns the EARLIER MUTATED file, not your real`) + chalk16.dim(`
21010
21422
  secrets. \`pretense reverse\` will refuse it unless --allow-partial.
21011
21423
  `)
21012
21424
  );
@@ -21015,8 +21427,8 @@ async function runMutate(file, opts = {}) {
21015
21427
  }
21016
21428
  if (inPlace && !effectiveMap) {
21017
21429
  console.error(
21018
- chalk14.red(`
21019
- x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk14.dim(`
21430
+ chalk16.red(`
21431
+ x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk16.dim(`
21020
21432
  Pass --map <path>, or report this as a bug.
21021
21433
  `)
21022
21434
  );
@@ -21025,29 +21437,48 @@ async function runMutate(file, opts = {}) {
21025
21437
  try {
21026
21438
  if (inPlace) {
21027
21439
  writeFileAtomicSync(file, result.content);
21028
- console.error(chalk14.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
21440
+ console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
21029
21441
  } else if (opts.output) {
21030
21442
  writeFileAtomicSync(opts.output, result.content);
21031
- console.error(chalk14.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
21443
+ console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
21032
21444
  } else {
21033
21445
  process.stdout.write(result.content);
21034
21446
  }
21035
21447
  } catch (e) {
21036
21448
  console.error(
21037
- chalk14.red(`
21038
- x Cannot write mutated content: ${opts.output ?? file}`) + chalk14.dim(`
21449
+ chalk16.red(`
21450
+ x Cannot write mutated content: ${opts.output ?? file}`) + chalk16.dim(`
21039
21451
  ${e.message}
21040
21452
  `)
21041
21453
  );
21042
21454
  return 1;
21043
21455
  }
21456
+ if (!preview && result.mutations.length > 0) {
21457
+ let secretsBlocked = 0;
21458
+ let piiRedacted = 0;
21459
+ for (const m of result.mutations) {
21460
+ if (categoryByType.get(m.type) === "pii") piiRedacted += 1;
21461
+ else secretsBlocked += 1;
21462
+ }
21463
+ recordLocalAudit({
21464
+ action: "mutated",
21465
+ mutationsApplied: result.mutations.length,
21466
+ secretsBlocked,
21467
+ piiRedacted,
21468
+ originalSize: Buffer.byteLength(content, "utf-8"),
21469
+ processedSize: Buffer.byteLength(result.content, "utf-8"),
21470
+ durationMs: Date.now() - mutateStartMs,
21471
+ detectorKinds: [...new Set(result.mutations.map((m) => m.type))],
21472
+ file: absFile
21473
+ });
21474
+ }
21044
21475
  if (opts.showMap) {
21045
21476
  console.error(JSON.stringify(result.mutations, null, 2));
21046
21477
  }
21047
21478
  return 0;
21048
21479
  }
21049
21480
  function mutateCommand() {
21050
- return new Command15("mutate").description(
21481
+ return new Command16("mutate").description(
21051
21482
  "Mutate secrets IN PLACE and auto-write an encrypted reversal map (reverse with `pretense reverse <file>`; --stdout to preview instead)"
21052
21483
  ).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
21484
  "--map <path>",
@@ -21068,7 +21499,7 @@ function mutateCommand() {
21068
21499
  const resolved = resolvePolicy(opts.policy);
21069
21500
  if (resolved?.scannerPreset) opts.presetName = resolved.scannerPreset;
21070
21501
  } catch (err) {
21071
- console.error(chalk14.red(`
21502
+ console.error(chalk16.red(`
21072
21503
  x ${err.message}
21073
21504
  `));
21074
21505
  process.exit(1);
@@ -21080,10 +21511,10 @@ function mutateCommand() {
21080
21511
 
21081
21512
  // src/commands/reverse.ts
21082
21513
  init_esm_shims();
21083
- import { Command as Command16 } from "commander";
21084
- import chalk15 from "chalk";
21514
+ import { Command as Command17 } from "commander";
21515
+ import chalk17 from "chalk";
21085
21516
  import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync6 } from "fs";
21086
- import { resolve as resolve6 } from "path";
21517
+ import { resolve as resolve7 } from "path";
21087
21518
  import { createHash as createHash4 } from "crypto";
21088
21519
  function sha256Hex3(s) {
21089
21520
  return createHash4("sha256").update(s, "utf8").digest("hex");
@@ -21115,14 +21546,14 @@ function loadReversalMap(mapPath) {
21115
21546
  try {
21116
21547
  parsed = JSON.parse(readFileSync16(mapPath, "utf-8"));
21117
21548
  } catch {
21118
- console.error(chalk15.red(`
21549
+ console.error(chalk17.red(`
21119
21550
  x Cannot read reversal map: ${mapPath}
21120
21551
  `));
21121
21552
  process.exit(1);
21122
21553
  }
21123
21554
  const map = parsed;
21124
21555
  if (!map || !Array.isArray(map.mutations)) {
21125
- console.error(chalk15.red(`
21556
+ console.error(chalk17.red(`
21126
21557
  x Malformed reversal map (missing "mutations"): ${mapPath}
21127
21558
  `));
21128
21559
  process.exit(1);
@@ -21130,9 +21561,9 @@ function loadReversalMap(mapPath) {
21130
21561
  const version = typeof map.version === "number" ? map.version : 1;
21131
21562
  if (version > REVERSAL_MAP_VERSION) {
21132
21563
  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(`
21564
+ chalk17.red(`
21565
+ x Reversal map is version ${version}; this build understands up to ${REVERSAL_MAP_VERSION}.`) + chalk17.dim(`
21566
+ It was written by a newer Pretense. Upgrade the CLI to reverse it.`) + chalk17.dim(`
21136
21567
  File left untouched.
21137
21568
  `)
21138
21569
  );
@@ -21141,9 +21572,10 @@ function loadReversalMap(mapPath) {
21141
21572
  return map;
21142
21573
  }
21143
21574
  function runReverse(file, opts = {}) {
21144
- const absPath = resolve6(file);
21575
+ const reverseStartMs = Date.now();
21576
+ const absPath = resolve7(file);
21145
21577
  if (!existsSync16(absPath) || !statSync6(absPath).isFile()) {
21146
- console.error(chalk15.red(`
21578
+ console.error(chalk17.red(`
21147
21579
  x File not found: ${file}
21148
21580
  `));
21149
21581
  return 1;
@@ -21151,11 +21583,11 @@ function runReverse(file, opts = {}) {
21151
21583
  const mapPath = opts.map ?? defaultMapPath(absPath, opts.mapsDir);
21152
21584
  if (!opts.map && !existsSync16(mapPath)) {
21153
21585
  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.
21586
+ chalk17.red(`
21587
+ x No reversal map found for ${absPath}`) + chalk17.dim(`
21588
+ Looked in the default location: ${mapPath}`) + chalk17.dim(`
21589
+ Mutate the file first with ${chalk17.cyan(`pretense mutate ${file}`)}, or pass`) + chalk17.dim(`
21590
+ an explicit ${chalk17.cyan("--map <path>")} if you wrote the map somewhere else.
21159
21591
  `)
21160
21592
  );
21161
21593
  return 1;
@@ -21163,30 +21595,41 @@ function runReverse(file, opts = {}) {
21163
21595
  const mapFile = loadReversalMap(mapPath);
21164
21596
  let plainMutations;
21165
21597
  if (mapFile.encrypted) {
21166
- const projectRoot = findProjectRoot(mapFile.file ?? absPath);
21167
- const key = readProjectKey(projectRoot, opts.keysDir);
21168
- if (!key) {
21169
- console.error(
21170
- chalk15.red(`
21171
- x Encryption key not found for this project \u2014 cannot decrypt the reversal map.`) + chalk15.dim(`
21172
- Expected a key derived from ${projectRoot}. Reverse must run where the map was created.
21173
- `)
21174
- );
21175
- return 1;
21598
+ const keySource = mapFile.file ?? absPath;
21599
+ const roots = candidateProjectRoots(keySource);
21600
+ let decrypted;
21601
+ let anyKeyPresent = false;
21602
+ for (const root of roots) {
21603
+ const key = readProjectKey(root, opts.keysDir);
21604
+ if (!key) continue;
21605
+ anyKeyPresent = true;
21606
+ try {
21607
+ decrypted = mapFile.mutations.map((m) => ({
21608
+ original: decryptSecret(m.original, key),
21609
+ replacement: m.replacement
21610
+ }));
21611
+ break;
21612
+ } catch {
21613
+ decrypted = void 0;
21614
+ }
21176
21615
  }
21177
- try {
21178
- plainMutations = mapFile.mutations.map((m) => ({
21179
- original: decryptSecret(m.original, key),
21180
- replacement: m.replacement
21181
- }));
21182
- } catch {
21616
+ if (!decrypted) {
21617
+ const looked = roots.join("\n ");
21183
21618
  console.error(
21184
- chalk15.red(`
21185
- x Failed to decrypt the reversal map (wrong key or corrupt data). File left untouched.
21619
+ chalk17.red(`
21620
+ x Encryption key not found for this file \u2014 cannot decrypt the reversal map.`) + (anyKeyPresent ? chalk17.dim(`
21621
+ A key was present but did not decrypt this map (wrong key or corrupt data).`) : chalk17.dim(`
21622
+ No key was found under any known project location for this file.`)) + chalk17.dim(`
21623
+ Looked under these project roots (keys dir ${opts.keysDir ?? "~/.pretense/keys"}):`) + chalk17.dim(`
21624
+ ${looked}`) + chalk17.dim(`
21625
+ Run \`reverse\` on the SAME machine and HOME where you mutated the file,`) + chalk17.dim(`
21626
+ or set ${chalk17.cyan("PRETENSE_PROJECT_ROOT")} to the project root used at mutate time.`) + chalk17.dim(`
21627
+ File left untouched.
21186
21628
  `)
21187
21629
  );
21188
21630
  return 1;
21189
21631
  }
21632
+ plainMutations = decrypted;
21190
21633
  } else {
21191
21634
  plainMutations = mapFile.mutations.map((m) => ({ original: m.original, replacement: m.replacement }));
21192
21635
  }
@@ -21195,10 +21638,10 @@ function runReverse(file, opts = {}) {
21195
21638
  const absentCount = plainMutations.length - appliedCount;
21196
21639
  if (typeof mapFile.mutatedSha === "string" && mapFile.mutatedSha !== sha256Hex3(mutatedContent)) {
21197
21640
  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(`
21641
+ chalk17.yellow(`
21642
+ ! This file is not the exact output this map was written for.`) + chalk17.dim(`
21643
+ map expects sha256 ${mapFile.mutatedSha}`) + chalk17.dim(`
21644
+ file is sha256 ${sha256Hex3(mutatedContent)}`) + chalk17.dim(`
21202
21645
  ${absentCount} of ${plainMutations.length} mapped tokens are not present in it.
21203
21646
  `)
21204
21647
  );
@@ -21208,8 +21651,8 @@ function runReverse(file, opts = {}) {
21208
21651
  restored = applyReverse(mutatedContent, plainMutations);
21209
21652
  } catch (e) {
21210
21653
  console.error(
21211
- chalk15.red(`
21212
- x ${e.message}`) + chalk15.dim(`
21654
+ chalk17.red(`
21655
+ x ${e.message}`) + chalk17.dim(`
21213
21656
  File left untouched.
21214
21657
  `)
21215
21658
  );
@@ -21217,8 +21660,8 @@ function runReverse(file, opts = {}) {
21217
21660
  }
21218
21661
  if (!mapFile.originalSha) {
21219
21662
  console.error(
21220
- chalk15.red(`
21221
- x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) + chalk15.dim(`
21663
+ chalk17.red(`
21664
+ x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) + chalk17.dim(`
21222
21665
  Refusing to write. File left untouched.
21223
21666
  `)
21224
21667
  );
@@ -21227,10 +21670,10 @@ function runReverse(file, opts = {}) {
21227
21670
  const restoredSha = sha256Hex3(restored);
21228
21671
  if (restoredSha !== mapFile.originalSha) {
21229
21672
  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(`
21673
+ chalk17.red(`
21674
+ x Reversal integrity check failed for ${absPath}`) + chalk17.dim(`
21675
+ expected sha256 ${mapFile.originalSha}`) + chalk17.dim(`
21676
+ got sha256 ${restoredSha}`) + chalk17.dim(`
21234
21677
  File left untouched.
21235
21678
  `)
21236
21679
  );
@@ -21241,30 +21684,30 @@ function runReverse(file, opts = {}) {
21241
21684
  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
21685
  if (!opts.allowPartial) {
21243
21686
  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(`
21687
+ chalk17.red(`
21688
+ x NOT A RESTORE: ${what}`) + chalk17.dim(`
21689
+ Its originals are themselves synthetics from an earlier mutation of this`) + chalk17.dim(`
21690
+ file, so applying it would return the PREVIOUS MUTATED VERSION, not your`) + chalk17.dim(`
21691
+ real secrets \u2014 while every integrity check passed, because the map is`) + chalk17.dim(`
21692
+ self-consistent with the file. Reverse with the map from the FIRST`) + chalk17.dim(`
21693
+ mutation round instead.`) + chalk17.dim(`
21694
+ --allow-partial writes it anyway (useful only to walk a chain back one`) + chalk17.dim(`
21695
+ generation at a time).`) + chalk17.dim(`
21253
21696
  File left untouched.
21254
21697
  `)
21255
21698
  );
21256
21699
  return 1;
21257
21700
  }
21258
21701
  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(`
21702
+ chalk17.yellow(`
21703
+ ! PARTIAL RESTORE (--allow-partial): ${what}`) + chalk17.dim(`
21704
+ The output is the PREVIOUS MUTATED GENERATION of this file, NOT the`) + chalk17.dim(`
21705
+ original. ${lineage.syntheticCount || "Some"} restored value(s) are still synthetic. Reverse again with`) + chalk17.dim(`
21263
21706
  the earlier round's map to reach the real secrets.
21264
21707
  `)
21265
21708
  );
21266
21709
  }
21267
- const mark = lineage.ok ? chalk15.green(" \u2713 Restored") : chalk15.yellow(" ! Partially restored");
21710
+ const mark = lineage.ok ? chalk17.green(" \u2713 Restored") : chalk17.yellow(" ! Partially restored");
21268
21711
  const detail = `${appliedCount} of ${plainMutations.length} mapped tokens` + (lineage.ok ? "" : " \u2014 to an earlier MUTATED generation, not the original");
21269
21712
  const preview = opts.stdout === true;
21270
21713
  const inPlace = !preview && !opts.output;
@@ -21277,6 +21720,19 @@ function runReverse(file, opts = {}) {
21277
21720
  } else {
21278
21721
  process.stdout.write(restored);
21279
21722
  }
21723
+ if (!preview && appliedCount > 0) {
21724
+ recordLocalAudit({
21725
+ action: "reversed",
21726
+ mutationsApplied: appliedCount,
21727
+ secretsBlocked: 0,
21728
+ piiRedacted: 0,
21729
+ originalSize: Buffer.byteLength(mutatedContent, "utf-8"),
21730
+ processedSize: Buffer.byteLength(restored, "utf-8"),
21731
+ durationMs: Date.now() - reverseStartMs,
21732
+ detectorKinds: [...new Set(mapFile.mutations.map((m) => m.type).filter((t) => typeof t === "string"))],
21733
+ file: absPath
21734
+ });
21735
+ }
21280
21736
  if (opts.json) {
21281
21737
  console.error(
21282
21738
  JSON.stringify({
@@ -21294,7 +21750,7 @@ function runReverse(file, opts = {}) {
21294
21750
  return 0;
21295
21751
  }
21296
21752
  function reverseCommand() {
21297
- return new Command16("reverse").description(
21753
+ return new Command17("reverse").description(
21298
21754
  "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
21755
  ).argument("<file>", "Mutated file to restore (must be byte-identical to the mutate output)").option(
21300
21756
  "--map <path>",
@@ -21309,8 +21765,8 @@ function reverseCommand() {
21309
21765
 
21310
21766
  // src/commands/run.ts
21311
21767
  init_esm_shims();
21312
- import { Command as Command17 } from "commander";
21313
- import chalk16 from "chalk";
21768
+ import { Command as Command18 } from "commander";
21769
+ import chalk18 from "chalk";
21314
21770
  import { spawn } from "child_process";
21315
21771
  import { basename as basename3 } from "path";
21316
21772
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -21384,20 +21840,23 @@ async function waitHealthy(port, deadlineMs, pollMs = 150) {
21384
21840
  }
21385
21841
  function spawnProxy(port) {
21386
21842
  const cliEntry = process.argv[1] ?? fileURLToPath4(import.meta.url);
21387
- return spawn(process.execPath, [cliEntry, "start", "--port", String(port), "--strict-port"], {
21843
+ const startArgs = ["start", "--port", String(port), "--strict-port"];
21844
+ const isCompiledBinary = cliEntry.includes("$bunfs") || cliEntry.includes("~BUN");
21845
+ const spawnArgs = isCompiledBinary ? startArgs : [cliEntry, ...startArgs];
21846
+ return spawn(process.execPath, spawnArgs, {
21388
21847
  stdio: ["ignore", "ignore", "pipe"],
21389
21848
  env: process.env
21390
21849
  });
21391
21850
  }
21392
21851
  function runCommand() {
21393
- return new Command17("run").alias("with").description(
21852
+ return new Command18("run").alias("with").description(
21394
21853
  "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
21854
  ).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
21855
  let provider;
21397
21856
  try {
21398
21857
  provider = resolveProvider(tool, opts.provider);
21399
21858
  } catch (err) {
21400
- console.error(chalk16.red(`
21859
+ console.error(chalk18.red(`
21401
21860
  x ${err.message}
21402
21861
  `));
21403
21862
  process.exit(1);
@@ -21408,13 +21867,13 @@ function runCommand() {
21408
21867
  const rec = readProxyRecord();
21409
21868
  if (rec && isProcessAlive(rec.pid) && await isProxyHealthy(rec.port)) {
21410
21869
  port = rec.port;
21411
- console.error(chalk16.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
21870
+ console.error(chalk18.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
21412
21871
  } else {
21413
21872
  const preferred = opts.port ? parseInt(opts.port, 10) : 9339;
21414
21873
  try {
21415
21874
  port = await findFreePort(preferred);
21416
21875
  } catch (err) {
21417
- console.error(chalk16.red(`
21876
+ console.error(chalk18.red(`
21418
21877
  x ${err.message}
21419
21878
  `));
21420
21879
  process.exit(1);
@@ -21428,14 +21887,14 @@ function runCommand() {
21428
21887
  const ready = await waitHealthy(port, 15e3);
21429
21888
  if (!ready) {
21430
21889
  console.error(
21431
- chalk16.red(`
21432
- x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk16.dim(`
21890
+ chalk18.red(`
21891
+ x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk18.dim(`
21433
21892
  ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21434
21893
  );
21435
21894
  proxyChild.kill("SIGTERM");
21436
21895
  process.exit(1);
21437
21896
  }
21438
- console.error(chalk16.green(` \u2713 Started Pretense proxy on port ${port}.`));
21897
+ console.error(chalk18.green(` \u2713 Started Pretense proxy on port ${port}.`));
21439
21898
  }
21440
21899
  const stopStartedProxy = () => {
21441
21900
  if (startedByUs && proxyChild && !proxyChild.killed) {
@@ -21444,7 +21903,7 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21444
21903
  };
21445
21904
  const inject = injectedEnvFor(provider, port);
21446
21905
  console.error(
21447
- chalk16.dim(
21906
+ chalk18.dim(
21448
21907
  ` \u2192 Routing ${basename3(tool)} through the proxy (${primaryEnvVar(provider)}=http://localhost:${port}).
21449
21908
  `
21450
21909
  )
@@ -21463,15 +21922,15 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21463
21922
  child.on("error", (err) => {
21464
21923
  if (err.code === "ENOENT") {
21465
21924
  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(`
21925
+ chalk18.red(`
21926
+ x "${tool}" is not on your PATH.`) + chalk18.dim(`
21927
+ Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) + chalk18.dim(`
21928
+ You can still point a client at the proxy yourself:`) + chalk18.dim(`
21470
21929
  ${primaryEnvVar(provider)}=http://localhost:${port}
21471
21930
  `)
21472
21931
  );
21473
21932
  } else {
21474
- console.error(chalk16.red(`
21933
+ console.error(chalk18.red(`
21475
21934
  x Could not run "${tool}": ${err.message}
21476
21935
  `));
21477
21936
  }
@@ -21490,8 +21949,8 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21490
21949
 
21491
21950
  // src/commands/policy.ts
21492
21951
  init_esm_shims();
21493
- import { Command as Command18 } from "commander";
21494
- import chalk17 from "chalk";
21952
+ import { Command as Command19 } from "commander";
21953
+ import chalk19 from "chalk";
21495
21954
  function listAction(opts) {
21496
21955
  const ids = listPresetIds();
21497
21956
  if (opts.json) {
@@ -21514,21 +21973,21 @@ function listAction(opts) {
21514
21973
  );
21515
21974
  return;
21516
21975
  }
21517
- console.log(chalk17.cyan("\n Pretense Compliance Frameworks\n"));
21976
+ console.log(chalk19.cyan("\n Pretense Compliance Frameworks\n"));
21518
21977
  console.log(
21519
- chalk17.dim(
21978
+ chalk19.dim(
21520
21979
  " " + "ID".padEnd(8) + "NAME".padEnd(14) + "TIER".padEnd(12) + "AUDIT".padEnd(8) + "DETECTORS"
21521
21980
  )
21522
21981
  );
21523
- console.log(chalk17.dim(" " + "\u2500".repeat(64)));
21982
+ console.log(chalk19.dim(" " + "\u2500".repeat(64)));
21524
21983
  for (const id of ids) {
21525
21984
  const p = COMPLIANCE_PRESETS[id];
21526
21985
  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`)
21986
+ " " + 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
21987
  );
21529
21988
  }
21530
- console.log(chalk17.dim(`
21531
- Run ${chalk17.cyan("pretense policy show <id>")} for detector-level detail.
21989
+ console.log(chalk19.dim(`
21990
+ Run ${chalk19.cyan("pretense policy show <id>")} for detector-level detail.
21532
21991
  `));
21533
21992
  }
21534
21993
  function showAction(id, opts) {
@@ -21537,8 +21996,8 @@ function showAction(id, opts) {
21537
21996
  preset = loadPreset(id);
21538
21997
  } catch (err) {
21539
21998
  console.error(
21540
- chalk17.red(`
21541
- x ${err.message}`) + chalk17.dim(`
21999
+ chalk19.red(`
22000
+ x ${err.message}`) + chalk19.dim(`
21542
22001
  Valid frameworks: ${listPresetIds().join(", ")}
21543
22002
  `)
21544
22003
  );
@@ -21548,24 +22007,24 @@ function showAction(id, opts) {
21548
22007
  console.log(JSON.stringify(preset, null, 2));
21549
22008
  return;
21550
22009
  }
21551
- console.log(chalk17.cyan(`
21552
- ${preset.name} `) + chalk17.dim(`(${preset.id})`));
21553
- console.log(chalk17.dim(` ${preset.description}
22010
+ console.log(chalk19.cyan(`
22011
+ ${preset.name} `) + chalk19.dim(`(${preset.id})`));
22012
+ console.log(chalk19.dim(` ${preset.description}
21554
22013
  `));
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(", "))}
22014
+ console.log(` Minimum tier: ${chalk19.bold(preset.minTier)}`);
22015
+ console.log(` Audit required: ${preset.auditRequired ? chalk19.green("yes") : chalk19.dim("no")}`);
22016
+ console.log(` Detector sets: ${chalk19.dim(preset.detectorSets.join(", "))}
21558
22017
  `);
21559
22018
  const overrides = Object.entries(preset.actionOverrides);
21560
- console.log(chalk17.dim(` Detector actions (${overrides.length}):`));
22019
+ console.log(chalk19.dim(` Detector actions (${overrides.length}):`));
21561
22020
  for (const [detector, action] of overrides) {
21562
- const color = action === "block" ? chalk17.red : action === "redact" ? chalk17.yellow : chalk17.dim;
22021
+ const color = action === "block" ? chalk19.red : action === "redact" ? chalk19.yellow : chalk19.dim;
21563
22022
  console.log(` ${detector.padEnd(24)} ${color(action)}`);
21564
22023
  }
21565
22024
  console.log();
21566
22025
  }
21567
22026
  function policyCommand() {
21568
- const cmd = new Command18("policy").description(
22027
+ const cmd = new Command19("policy").description(
21569
22028
  "List and inspect the built-in compliance presets (5: hipaa, gdpr, soc2, nist, pci)"
21570
22029
  );
21571
22030
  cmd.command("list").description("List the 5 built-in compliance presets").option("--json", "Output as JSON").action((opts) => listAction(opts));
@@ -21576,10 +22035,10 @@ function policyCommand() {
21576
22035
 
21577
22036
  // src/commands/audit.ts
21578
22037
  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";
22038
+ import { Command as Command20 } from "commander";
22039
+ import chalk20 from "chalk";
22040
+ import { homedir as homedir11 } from "os";
22041
+ import { join as join24 } from "path";
21583
22042
  var VALID_FORMATS2 = ["table", "csv", "json", "jsonl"];
21584
22043
  function toSafeAuditRow(e) {
21585
22044
  return {
@@ -21650,18 +22109,18 @@ function resolveFormat2(opts) {
21650
22109
  const raw2 = opts.format?.trim().toLowerCase();
21651
22110
  if (raw2 !== void 0 && !VALID_FORMATS2.includes(raw2)) {
21652
22111
  console.error(
21653
- chalk18.red(`
22112
+ chalk20.red(`
21654
22113
  x Unknown --format: "${opts.format}"
21655
- `) + chalk18.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
22114
+ `) + chalk20.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
21656
22115
  );
21657
22116
  return null;
21658
22117
  }
21659
22118
  const fromFormat = raw2 === void 0 ? void 0 : raw2 === "jsonl" ? "json" : raw2;
21660
22119
  if (opts.json && fromFormat !== void 0 && fromFormat !== "json") {
21661
22120
  console.error(
21662
- chalk18.red(`
22121
+ chalk20.red(`
21663
22122
  x --json and --format ${fromFormat} request different outputs.
21664
- `) + chalk18.dim(" Pick one.\n")
22123
+ `) + chalk20.dim(" Pick one.\n")
21665
22124
  );
21666
22125
  return null;
21667
22126
  }
@@ -21669,17 +22128,17 @@ function resolveFormat2(opts) {
21669
22128
  return opts.json ? "json" : "table";
21670
22129
  }
21671
22130
  function auditCommand() {
21672
- return new Command19("audit").description(
22131
+ return new Command20("audit").description(
21673
22132
  "Show recent mutation/tokenization audit entries (leak-proof; JSONL with --json, RFC 4180 CSV with --format csv)"
21674
22133
  ).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
22134
  "--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."
22135
+ "Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see them all."
21677
22136
  ).option("--list-frameworks", "List the compliance frameworks that can be filtered on", false).option(
21678
22137
  "--format <fmt>",
21679
22138
  "Output format: table (default), csv (RFC 4180 compliance export), json (JSONL)"
21680
22139
  ).action((opts) => {
21681
22140
  if (opts.listFrameworks) {
21682
- for (const f of COMPLIANCE_FRAMEWORKS) console.log(f);
22141
+ for (const f of POLICY_FRAMEWORKS) console.log(f);
21683
22142
  return;
21684
22143
  }
21685
22144
  const format = resolveFormat2(opts);
@@ -21689,21 +22148,21 @@ function auditCommand() {
21689
22148
  framework = resolveFrameworkName(opts.framework);
21690
22149
  if (!framework) {
21691
22150
  console.error(
21692
- chalk18.red(`
22151
+ chalk20.red(`
21693
22152
  x Unknown framework: "${opts.framework}"
21694
- `) + chalk18.dim(" Run ") + chalk18.cyan("pretense audit --list-frameworks") + chalk18.dim(" to see all 36.\n")
22153
+ `) + chalk20.dim(" Run ") + chalk20.cyan("pretense audit --list-frameworks") + chalk20.dim(" to see them all.\n")
21695
22154
  );
21696
22155
  process.exit(1);
21697
22156
  }
21698
22157
  }
21699
22158
  const limit = parseInt(opts.limit ?? "20", 10);
21700
22159
  if (!Number.isFinite(limit) || limit <= 0) {
21701
- console.error(chalk18.red(`
22160
+ console.error(chalk20.red(`
21702
22161
  x Invalid --limit: "${opts.limit}". Provide a positive integer.
21703
22162
  `));
21704
22163
  process.exit(1);
21705
22164
  }
21706
- const dbPath = opts.db ?? join21(homedir10(), ".pretense", "audit.db");
22165
+ const dbPath = opts.db ?? join24(homedir11(), ".pretense", "audit.db");
21707
22166
  let rows;
21708
22167
  try {
21709
22168
  const store = new AuditStore(dbPath);
@@ -21723,8 +22182,8 @@ function auditCommand() {
21723
22182
  console.error("pretense: no audit data \u2014 the audit database has not been initialized yet.");
21724
22183
  return;
21725
22184
  }
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"));
22185
+ console.log(chalk20.dim("\n No audit data available. The audit database has not been initialized yet."));
22186
+ console.log(chalk20.dim(" Run " + chalk20.cyan("pretense start") + " and make some requests first.\n"));
21728
22187
  return;
21729
22188
  }
21730
22189
  if (format === "csv") {
@@ -21736,23 +22195,23 @@ function auditCommand() {
21736
22195
  return;
21737
22196
  }
21738
22197
  if (rows.length === 0) {
21739
- console.log(chalk18.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
22198
+ console.log(chalk20.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
21740
22199
  return;
21741
22200
  }
21742
- console.log(chalk18.cyan(`
22201
+ console.log(chalk20.cyan(`
21743
22202
  Pretense Mutation Audit \u2014 last ${rows.length} entries
21744
22203
  `));
21745
22204
  console.log(
21746
- chalk18.dim(
22205
+ chalk20.dim(
21747
22206
  " " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATED".padEnd(10) + "BLOCKED".padEnd(10) + "PII".padEnd(8) + "MS"
21748
22207
  )
21749
22208
  );
21750
- console.log(chalk18.dim(" " + "\u2500".repeat(72)));
22209
+ console.log(chalk20.dim(" " + "\u2500".repeat(72)));
21751
22210
  for (const row of rows) {
21752
22211
  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;
22212
+ 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
22213
  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))
22214
+ " " + 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
22215
  );
21757
22216
  }
21758
22217
  console.log();
@@ -21761,10 +22220,10 @@ function auditCommand() {
21761
22220
 
21762
22221
  // src/commands/credits.ts
21763
22222
  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";
22223
+ import { Command as Command21 } from "commander";
22224
+ import chalk21 from "chalk";
22225
+ import { join as join25 } from "path";
22226
+ import { homedir as homedir12 } from "os";
21768
22227
  function remaining(used, limit) {
21769
22228
  if (limit === null) return null;
21770
22229
  return Math.max(0, limit - used);
@@ -21773,16 +22232,16 @@ function fmt(n) {
21773
22232
  return n === null ? "unlimited" : n.toLocaleString("en-US");
21774
22233
  }
21775
22234
  function bar(used, limit, width = 24) {
21776
- if (limit === null) return chalk19.green("\u2588".repeat(width));
22235
+ if (limit === null) return chalk21.green("\u2588".repeat(width));
21777
22236
  const pct = limit <= 0 ? 1 : Math.min(1, used / limit);
21778
22237
  const filled = Math.min(width, Math.round(pct * width));
21779
22238
  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);
22239
+ if (pct >= 1) return chalk21.red(s);
22240
+ if (pct >= 0.8) return chalk21.yellow(s);
22241
+ return chalk21.green(s);
21783
22242
  }
21784
22243
  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) => {
22244
+ 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
22245
  const resolution = await resolvePlanTier({ offline: opts.offline });
21787
22246
  const tier = resolution.tier;
21788
22247
  const collected = collectUsage(opts.db);
@@ -21795,7 +22254,7 @@ function creditsCommand() {
21795
22254
  periodEnd: null
21796
22255
  };
21797
22256
  if (!collected && !opts.json) {
21798
- console.log(chalk19.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
22257
+ console.log(chalk21.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
21799
22258
  }
21800
22259
  const mutationLimit = tier ? PLAN_CONFIGS[tier].mutationsPerPeriod : null;
21801
22260
  const scanLimit = tier ? PLAN_CONFIGS[tier].scansPerPeriod : null;
@@ -21827,12 +22286,12 @@ function creditsCommand() {
21827
22286
  );
21828
22287
  return;
21829
22288
  }
21830
- console.log(chalk19.cyan("\n Pretense Credits"));
21831
- console.log(chalk19.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
22289
+ console.log(chalk21.cyan("\n Pretense Credits"));
22290
+ console.log(chalk21.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
21832
22291
  `));
21833
22292
  if (!tier) {
21834
22293
  for (const line of unverifiedNote(resolution.detail)) {
21835
- console.log(chalk19.yellow(` ${line}`));
22294
+ console.log(chalk21.yellow(` ${line}`));
21836
22295
  }
21837
22296
  console.log();
21838
22297
  console.log(" " + "Mutations used".padEnd(20) + m.mutationsApplied.toLocaleString("en-US"));
@@ -21841,18 +22300,18 @@ function creditsCommand() {
21841
22300
  return;
21842
22301
  }
21843
22302
  console.log(
21844
- " " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk19.bold(fmt(mutationsLeft)) + chalk19.dim(` left of ${fmt(mutationLimit)}`)
22303
+ " " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk21.bold(fmt(mutationsLeft)) + chalk21.dim(` left of ${fmt(mutationLimit)}`)
21845
22304
  );
21846
22305
  console.log(
21847
- " " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk19.bold(fmt(scansLeft)) + chalk19.dim(` left of ${fmt(scanLimit)}`)
22306
+ " " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk21.bold(fmt(scansLeft)) + chalk21.dim(` left of ${fmt(scanLimit)}`)
21848
22307
  );
21849
22308
  if (tier !== "enterprise" && mutationLimit !== null && mutationsLeft === 0) {
21850
22309
  console.log(
21851
- chalk19.red("\n Mutation budget exhausted for this period.") + chalk19.dim(" Run ") + chalk19.cyan("pretense upgrade") + chalk19.dim(" to compare plans.")
22310
+ chalk21.red("\n Mutation budget exhausted for this period.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
21852
22311
  );
21853
22312
  } else if (tier !== "enterprise" && mutationLimit !== null && m.mutationsApplied >= mutationLimit * 0.8) {
21854
22313
  console.log(
21855
- chalk19.yellow("\n Running low.") + chalk19.dim(" Run ") + chalk19.cyan("pretense upgrade") + chalk19.dim(" to compare plans.")
22314
+ chalk21.yellow("\n Running low.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
21856
22315
  );
21857
22316
  }
21858
22317
  console.log();
@@ -21861,21 +22320,24 @@ function creditsCommand() {
21861
22320
 
21862
22321
  // src/commands/upgrade.ts
21863
22322
  init_esm_shims();
21864
- import { Command as Command21 } from "commander";
21865
- import chalk20 from "chalk";
22323
+ import { Command as Command22 } from "commander";
22324
+ import chalk22 from "chalk";
21866
22325
  var PRICING_URL = "https://pretense.ai/pricing";
21867
22326
  var TIERS = ["free", "pro", "enterprise"];
21868
22327
  function quota(n) {
21869
22328
  return n === null ? "unlimited" : n.toLocaleString();
21870
22329
  }
21871
22330
  function seats(n) {
21872
- return n === null ? "unlimited" : String(n);
22331
+ return n === null ? "up to 100" : String(n);
22332
+ }
22333
+ function comingSoonOrDash(enabled) {
22334
+ return enabled ? "coming soon" : "\u2014";
21873
22335
  }
21874
22336
  function yesNo(b) {
21875
22337
  return b ? "yes" : "\u2014";
21876
22338
  }
21877
22339
  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) => {
22340
+ 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
22341
  const resolution = await resolvePlanTier({ offline: opts.offline });
21880
22342
  const current = resolution.tier;
21881
22343
  if (opts.json) {
@@ -21903,40 +22365,41 @@ function upgradeCommand() {
21903
22365
  );
21904
22366
  return;
21905
22367
  }
21906
- console.log(chalk20.cyan("\n Pretense Plans\n"));
22368
+ console.log(chalk22.cyan("\n Pretense Plans\n"));
21907
22369
  const col = 16;
21908
22370
  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)));
22371
+ console.log(chalk22.bold(header));
22372
+ console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
21911
22373
  const rows = [
21912
22374
  ["Price", (t) => priceLabel(t)],
21913
22375
  ["Mutations/7d", (t) => quota(PLAN_CONFIGS[t].mutationsPerPeriod)],
21914
22376
  ["Requests/7d", (t) => quota(PLAN_CONFIGS[t].scansPerPeriod)],
21915
22377
  ["Seats", (t) => seats(PLAN_CONFIGS[t].maxSeats)],
21916
- ["SSO", (t) => yesNo(PLAN_CONFIGS[t].ssoEnabled)],
22378
+ ["SSO", (t) => comingSoonOrDash(PLAN_CONFIGS[t].ssoEnabled)],
22379
+ // On-prem IS truthful — Docker + Helm ship — so it stays yes/—.
21917
22380
  ["On-prem", (t) => yesNo(PLAN_CONFIGS[t].onPremEnabled)]
21918
22381
  ];
21919
22382
  for (const [label, cell] of rows) {
21920
22383
  console.log(" " + label.padEnd(18) + TIERS.map((t) => cell(t).padEnd(col)).join(""));
21921
22384
  }
21922
- console.log(" " + chalk20.dim("\u2500".repeat(18 + col * TIERS.length)));
22385
+ console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
21923
22386
  if (current) {
21924
- console.log(chalk20.dim(`
22387
+ console.log(chalk22.dim(`
21925
22388
  Current plan: ${tierName(current)}`));
21926
22389
  if (current === "enterprise") {
21927
- console.log(chalk20.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
22390
+ console.log(chalk22.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
21928
22391
  return;
21929
22392
  }
21930
22393
  } else {
21931
22394
  console.log(
21932
- chalk20.yellow(
22395
+ chalk22.yellow(
21933
22396
  `
21934
22397
  Could not confirm your current plan: ${resolution.detail ?? "unknown reason"}.`
21935
22398
  )
21936
22399
  );
21937
22400
  }
21938
22401
  const from = current ?? "unknown";
21939
- console.log(chalk20.bold(`
22402
+ console.log(chalk22.bold(`
21940
22403
  \u2192 ${PRICING_URL}?ref=cli_upgrade&from=${from}
21941
22404
  `));
21942
22405
  });
@@ -21969,35 +22432,36 @@ function findClosestCommand(input, knownCommands) {
21969
22432
  }
21970
22433
  return bestDist <= 3 ? best : null;
21971
22434
  }
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
22435
+ var program = new Command23();
22436
+ program.name("pretense").enablePositionalOptions().description(chalk23.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
22437
+ ${chalk23.bold("Quick start:")}
22438
+ ${chalk23.cyan("pretense run claude")} ${chalk23.dim('"build X"')} Run an AI tool through the proxy (zero setup)
22439
+ ${chalk23.cyan("pretense mutate")} ${chalk23.dim("<file>")} Mutate secrets in place (reverse with the same path)
22440
+ ${chalk23.cyan("pretense scan")} ${chalk23.dim("<file|dir>")} Scan for secrets and PII
21978
22441
 
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
22442
+ ${chalk23.dim("All commands:")}
22443
+ ${chalk23.cyan("pretense init")} Scan codebase and build protection profile
22444
+ ${chalk23.cyan("pretense run <tool>")} Run an AI tool through the proxy, no manual export
22445
+ ${chalk23.cyan("pretense start")} Start proxy on localhost:9339
22446
+ ${chalk23.cyan("pretense stop")} Stop the running proxy
22447
+ ${chalk23.cyan("pretense scan src/api.ts")} Scan file for secrets
22448
+ ${chalk23.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
22449
+ ${chalk23.cyan("pretense status")} Show protection status
22450
+ ${chalk23.cyan("pretense doctor")} Diagnose why the proxy isn't catching traffic
22451
+ ${chalk23.cyan("pretense review")} Preview mutations that would be applied
22452
+ ${chalk23.cyan("pretense review --json")} Output review as JSON
22453
+ ${chalk23.cyan("pretense mutate <file>")} Mutate secrets in place + auto-write reversal map
22454
+ ${chalk23.cyan("pretense reverse <file>")} Restore a mutated file (auto-finds its map)
22455
+ ${chalk23.cyan("pretense policy list")} List the 5 compliance presets
22456
+ ${chalk23.cyan("pretense audit")} Show mutation audit log
22457
+ ${chalk23.cyan("pretense usage")} Plan usage vs limits
22458
+ ${chalk23.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
22459
+ ${chalk23.cyan("pretense upgrade")} Compare plans / upgrade
22460
+ ${chalk23.cyan("pretense auth login")} Authenticate with Pretense
22461
+ ${chalk23.cyan("pretense install")} Install git hooks
22462
+ ${chalk23.cyan("pretense logs --limit 20")} Show recent audit entries
21999
22463
 
22000
- ${chalk21.dim("Run")} ${chalk21.cyan("pretense <command> --help")} ${chalk21.dim("for command-specific help.")}
22464
+ ${chalk23.dim("Run")} ${chalk23.cyan("pretense <command> --help")} ${chalk23.dim("for command-specific help.")}
22001
22465
  `);
22002
22466
  var KNOWN_COMMANDS = [
22003
22467
  "init",
@@ -22025,7 +22489,8 @@ var KNOWN_COMMANDS = [
22025
22489
  "logout",
22026
22490
  "credits",
22027
22491
  "tokens",
22028
- "upgrade"
22492
+ "upgrade",
22493
+ "doctor"
22029
22494
  ];
22030
22495
  program.command("version").description("Print Pretense CLI version").action(() => {
22031
22496
  console.log(`@pretense/cli v${program.version()}`);
@@ -22058,16 +22523,17 @@ program.addCommand(policyCommand());
22058
22523
  program.addCommand(auditCommand());
22059
22524
  program.addCommand(creditsCommand());
22060
22525
  program.addCommand(upgradeCommand());
22526
+ program.addCommand(doctorCommand());
22061
22527
  program.on("command:*", (operands) => {
22062
22528
  const unknown = operands[0] ?? "";
22063
22529
  const suggestion = findClosestCommand(unknown, KNOWN_COMMANDS);
22064
- console.error(chalk21.red(`
22530
+ console.error(chalk23.red(`
22065
22531
  x Unknown command: "${unknown}"`));
22066
22532
  if (suggestion) {
22067
- console.error(chalk21.yellow(` Did you mean: ${chalk21.bold(`pretense ${suggestion}`)}`));
22533
+ console.error(chalk23.yellow(` Did you mean: ${chalk23.bold(`pretense ${suggestion}`)}`));
22068
22534
  }
22069
- console.error(chalk21.dim(`
22070
- Run ${chalk21.cyan("pretense --help")} to see all available commands.
22535
+ console.error(chalk23.dim(`
22536
+ Run ${chalk23.cyan("pretense --help")} to see all available commands.
22071
22537
  `));
22072
22538
  process.exit(1);
22073
22539
  });