@pretense/cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import chalk from "chalk";
6
- import { readFileSync as readFileSync4, existsSync as existsSync4, readdirSync, statSync } from "fs";
7
- import { resolve as resolve2, extname, join as join3 } from "path";
5
+ import chalk7 from "chalk";
6
+ import { readFileSync as readFileSync6, existsSync as existsSync6, readdirSync, statSync, openSync, readSync as readSync2, closeSync } from "fs";
7
+ import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
8
+ import { resolve as resolve2, extname, join as join5, basename } from "path";
9
+ import os from "os";
8
10
 
9
11
  // src/types.ts
10
12
  var DEFAULT_CONFIG = {
11
13
  port: 9339,
12
14
  targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
13
- languages: ["typescript", "javascript", "python", "go", "java"],
15
+ languages: ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"],
14
16
  mutationRules: [
15
17
  { tokenType: "Variable" /* Variable */, prefix: "_v" },
16
18
  { tokenType: "Function" /* Function */, prefix: "_fn" },
@@ -262,24 +264,186 @@ var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
262
264
  "permits",
263
265
  "yield"
264
266
  ]);
267
+ var CSHARP_KEYWORDS = /* @__PURE__ */ new Set([
268
+ "class",
269
+ "interface",
270
+ "namespace",
271
+ "public",
272
+ "private",
273
+ "protected",
274
+ "internal",
275
+ "static",
276
+ "void",
277
+ "return",
278
+ "new",
279
+ "using",
280
+ "override",
281
+ "sealed",
282
+ "abstract",
283
+ "virtual",
284
+ "readonly",
285
+ "const",
286
+ "var",
287
+ "int",
288
+ "string",
289
+ "bool",
290
+ "double",
291
+ "float",
292
+ "object",
293
+ "null",
294
+ "true",
295
+ "false",
296
+ "if",
297
+ "else",
298
+ "for",
299
+ "foreach",
300
+ "while",
301
+ "do",
302
+ "switch",
303
+ "case",
304
+ "break",
305
+ "continue",
306
+ "try",
307
+ "catch",
308
+ "finally",
309
+ "throw",
310
+ "async",
311
+ "await",
312
+ "get",
313
+ "set",
314
+ "partial",
315
+ "enum",
316
+ "struct",
317
+ "delegate",
318
+ "event"
319
+ ]);
320
+ var RUBY_KEYWORDS = /* @__PURE__ */ new Set([
321
+ "def",
322
+ "class",
323
+ "module",
324
+ "end",
325
+ "do",
326
+ "if",
327
+ "else",
328
+ "elsif",
329
+ "unless",
330
+ "while",
331
+ "until",
332
+ "for",
333
+ "begin",
334
+ "rescue",
335
+ "ensure",
336
+ "raise",
337
+ "return",
338
+ "yield",
339
+ "self",
340
+ "super",
341
+ "true",
342
+ "false",
343
+ "nil",
344
+ "and",
345
+ "or",
346
+ "not",
347
+ "in",
348
+ "then",
349
+ "case",
350
+ "when",
351
+ "puts",
352
+ "print",
353
+ "require",
354
+ "require_relative",
355
+ "include",
356
+ "extend",
357
+ "attr_accessor",
358
+ "attr_reader",
359
+ "attr_writer",
360
+ "initialize",
361
+ "new",
362
+ "public",
363
+ "private",
364
+ "protected"
365
+ ]);
366
+ var RUST_KEYWORDS = /* @__PURE__ */ new Set([
367
+ "fn",
368
+ "struct",
369
+ "enum",
370
+ "trait",
371
+ "impl",
372
+ "mod",
373
+ "pub",
374
+ "let",
375
+ "mut",
376
+ "const",
377
+ "type",
378
+ "use",
379
+ "crate",
380
+ "self",
381
+ "super",
382
+ "move",
383
+ "ref",
384
+ "match",
385
+ "if",
386
+ "else",
387
+ "loop",
388
+ "while",
389
+ "for",
390
+ "return",
391
+ "break",
392
+ "continue",
393
+ "where",
394
+ "as",
395
+ "in",
396
+ "true",
397
+ "false",
398
+ "unsafe",
399
+ "extern",
400
+ "dyn",
401
+ "box",
402
+ "static",
403
+ "async",
404
+ "await",
405
+ "Box",
406
+ "String",
407
+ "Vec",
408
+ "Option",
409
+ "Result",
410
+ "Ok",
411
+ "Err",
412
+ "Some",
413
+ "None"
414
+ ]);
265
415
  function detectLanguage(code) {
266
416
  if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
267
417
  if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
268
418
  if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
269
419
  if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
420
+ if (/\bnamespace\s+\w+/.test(code) || /\busing\s+System/.test(code)) return "csharp";
421
+ if (/\bfn\s+\w+\s*\(/.test(code) || /\blet\s+mut\s+\w+/.test(code)) return "rust";
422
+ if (/\bdef\s+\w+/.test(code) && /\bend\b/.test(code)) return "ruby";
270
423
  if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
271
424
  return "typescript";
272
425
  }
273
- function lineAt(code, index) {
274
- let line = 1;
275
- for (let i = 0; i < index && i < code.length; i++) {
276
- if (code[i] === "\n") line++;
426
+ function buildLineIndex(code) {
427
+ const offsets = [];
428
+ for (let i = 0; i < code.length; i++) {
429
+ if (code.charCodeAt(i) === 10) offsets.push(i);
277
430
  }
278
- return line;
431
+ return offsets;
432
+ }
433
+ function lineFromIndex(lineOffsets, index) {
434
+ let lo = 0;
435
+ let hi = lineOffsets.length;
436
+ while (lo < hi) {
437
+ const mid = lo + hi >> 1;
438
+ if (lineOffsets[mid] < index) lo = mid + 1;
439
+ else hi = mid;
440
+ }
441
+ return lo + 1;
279
442
  }
280
443
  function scanTypeScript(code) {
281
444
  const tokens = [];
282
445
  if (!code.trim()) return tokens;
446
+ const lineOffsets = buildLineIndex(code);
283
447
  const patterns = [
284
448
  // Single-line comment
285
449
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
@@ -322,7 +486,7 @@ function scanTypeScript(code) {
322
486
  value: captureValue,
323
487
  start: captureStart,
324
488
  end: captureStart + captureValue.length,
325
- line: lineAt(code, captureStart)
489
+ line: lineFromIndex(lineOffsets, captureStart)
326
490
  });
327
491
  }
328
492
  }
@@ -331,6 +495,7 @@ function scanTypeScript(code) {
331
495
  function scanPython(code) {
332
496
  const tokens = [];
333
497
  if (!code.trim()) return tokens;
498
+ const lineOffsets = buildLineIndex(code);
334
499
  const patterns = [
335
500
  // Triple-quoted docstrings
336
501
  { re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
@@ -368,7 +533,7 @@ function scanPython(code) {
368
533
  value: captureValue,
369
534
  start: captureStart,
370
535
  end: captureStart + captureValue.length,
371
- line: lineAt(code, captureStart)
536
+ line: lineFromIndex(lineOffsets, captureStart)
372
537
  });
373
538
  }
374
539
  }
@@ -377,6 +542,7 @@ function scanPython(code) {
377
542
  function scanGo(code) {
378
543
  const tokens = [];
379
544
  if (!code.trim()) return tokens;
545
+ const lineOffsets = buildLineIndex(code);
380
546
  const patterns = [
381
547
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
382
548
  { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
@@ -413,7 +579,7 @@ function scanGo(code) {
413
579
  value: captureValue,
414
580
  start: captureStart,
415
581
  end: captureStart + captureValue.length,
416
- line: lineAt(code, captureStart)
582
+ line: lineFromIndex(lineOffsets, captureStart)
417
583
  });
418
584
  }
419
585
  }
@@ -422,6 +588,7 @@ function scanGo(code) {
422
588
  function scanJava(code) {
423
589
  const tokens = [];
424
590
  if (!code.trim()) return tokens;
591
+ const lineOffsets = buildLineIndex(code);
425
592
  const patterns = [
426
593
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
427
594
  { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
@@ -454,7 +621,137 @@ function scanJava(code) {
454
621
  value: captureValue,
455
622
  start: captureStart,
456
623
  end: captureStart + captureValue.length,
457
- line: lineAt(code, captureStart)
624
+ line: lineFromIndex(lineOffsets, captureStart)
625
+ });
626
+ }
627
+ }
628
+ return deduplicate(tokens);
629
+ }
630
+ function scanCSharp(code) {
631
+ const tokens = [];
632
+ if (!code.trim()) return tokens;
633
+ const lineOffsets = buildLineIndex(code);
634
+ const patterns = [
635
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
636
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
637
+ // Namespace/using import
638
+ { re: /\busing\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
639
+ // Class/interface/struct/enum names
640
+ { re: /\b(?:class|interface|struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
641
+ // Method/property declaration
642
+ { re: /\b(?:public|private|protected|internal|static|virtual|override|async)\s+(?:\w+\s+)+([a-zA-Z_]\w*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
643
+ // Local variable declarations
644
+ { re: /\b(?:var|int|string|bool|double|float|object)\s+([a-zA-Z_]\w*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
645
+ // Double-quoted strings
646
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
647
+ // Verbatim strings
648
+ { re: /@"(?:[^"]|"")*"/g, type: "String" /* String */ }
649
+ ];
650
+ for (const { re, type, groupIndex } of patterns) {
651
+ re.lastIndex = 0;
652
+ let m;
653
+ while ((m = re.exec(code)) !== null) {
654
+ const fullMatch = m[0];
655
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
656
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
657
+ if (!captureValue || captureValue.trim() === "") continue;
658
+ if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
659
+ if (CSHARP_KEYWORDS.has(captureValue)) continue;
660
+ if (captureValue.length <= 1) continue;
661
+ if (/^(String|Object|Integer|Boolean|Double|Float|List|Dictionary|Array|Exception|Task|Console|Math|Enum|Delegate)$/.test(captureValue)) continue;
662
+ }
663
+ tokens.push({
664
+ type,
665
+ value: captureValue,
666
+ start: captureStart,
667
+ end: captureStart + captureValue.length,
668
+ line: lineFromIndex(lineOffsets, captureStart)
669
+ });
670
+ }
671
+ }
672
+ return deduplicate(tokens);
673
+ }
674
+ function scanRuby(code) {
675
+ const tokens = [];
676
+ if (!code.trim()) return tokens;
677
+ const lineOffsets = buildLineIndex(code);
678
+ const patterns = [
679
+ // Single-line comment
680
+ { re: /#[^\n]*/g, type: "Comment" /* Comment */ },
681
+ // Method names
682
+ { re: /\bdef\s+([a-zA-Z_]\w*[!?]?)/g, type: "Function" /* Function */, groupIndex: 1 },
683
+ // Class and module names
684
+ { re: /\b(?:class|module)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
685
+ // Instance variables
686
+ { re: /@([a-zA-Z_]\w*)/g, type: "Property" /* Property */, groupIndex: 1 },
687
+ // All-caps constants (3+ chars to avoid single-letter constants)
688
+ { re: /\b([A-Z][A-Z0-9_]{2,})\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
689
+ // Double-quoted strings
690
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
691
+ // Single-quoted strings
692
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
693
+ ];
694
+ for (const { re, type, groupIndex } of patterns) {
695
+ re.lastIndex = 0;
696
+ let m;
697
+ while ((m = re.exec(code)) !== null) {
698
+ const fullMatch = m[0];
699
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
700
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
701
+ if (!captureValue || captureValue.trim() === "") continue;
702
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Property" /* Property */) {
703
+ if (RUBY_KEYWORDS.has(captureValue)) continue;
704
+ if (captureValue.length <= 1) continue;
705
+ }
706
+ tokens.push({
707
+ type,
708
+ value: captureValue,
709
+ start: captureStart,
710
+ end: captureStart + captureValue.length,
711
+ line: lineFromIndex(lineOffsets, captureStart)
712
+ });
713
+ }
714
+ }
715
+ return deduplicate(tokens);
716
+ }
717
+ function scanRust(code) {
718
+ const tokens = [];
719
+ if (!code.trim()) return tokens;
720
+ const lineOffsets = buildLineIndex(code);
721
+ const patterns = [
722
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
723
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
724
+ // Use declarations
725
+ { re: /\buse\s+([\w:]+)/g, type: "Import" /* Import */, groupIndex: 1 },
726
+ // Named type declarations (struct, enum, trait)
727
+ { re: /\b(?:struct|enum|trait)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
728
+ // Function names
729
+ { re: /\bfn\s+([a-zA-Z_]\w*)/g, type: "Function" /* Function */, groupIndex: 1 },
730
+ // Constants (ALL_CAPS)
731
+ { re: /\bconst\s+([A-Z_][A-Z0-9_]*)\s*:/g, type: "Variable" /* Variable */, groupIndex: 1 },
732
+ // Variable bindings
733
+ { re: /\blet\s+(?:mut\s+)?([a-zA-Z_]\w*)/g, type: "Variable" /* Variable */, groupIndex: 1 },
734
+ // Double-quoted strings
735
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
736
+ ];
737
+ for (const { re, type, groupIndex } of patterns) {
738
+ re.lastIndex = 0;
739
+ let m;
740
+ while ((m = re.exec(code)) !== null) {
741
+ const fullMatch = m[0];
742
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
743
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
744
+ if (!captureValue || captureValue.trim() === "") continue;
745
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
746
+ if (RUST_KEYWORDS.has(captureValue)) continue;
747
+ if (captureValue.length <= 1) continue;
748
+ }
749
+ tokens.push({
750
+ type,
751
+ value: captureValue,
752
+ start: captureStart,
753
+ end: captureStart + captureValue.length,
754
+ line: lineFromIndex(lineOffsets, captureStart)
458
755
  });
459
756
  }
460
757
  }
@@ -498,12 +795,61 @@ function scan(code, language) {
498
795
  case "java":
499
796
  tokens = scanJava(code);
500
797
  break;
798
+ case "csharp":
799
+ case "cs":
800
+ tokens = scanCSharp(code);
801
+ break;
802
+ case "ruby":
803
+ case "rb":
804
+ tokens = scanRuby(code);
805
+ break;
806
+ case "rust":
807
+ case "rs":
808
+ tokens = scanRust(code);
809
+ break;
501
810
  default:
502
811
  tokens = scanTypeScript(code);
503
812
  }
504
813
  return { tokens, language: lang };
505
814
  }
506
815
 
816
+ // src/salt.ts
817
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
818
+ import { join } from "path";
819
+ import { homedir } from "os";
820
+ import { randomBytes } from "crypto";
821
+ var SALT_BYTES = 32;
822
+ function getSaltPath() {
823
+ const dir = join(homedir(), ".pretense");
824
+ return { dir, path: join(dir, "mutation.salt") };
825
+ }
826
+ var _cachedSalt = null;
827
+ function getMutationSalt() {
828
+ if (_cachedSalt) return _cachedSalt;
829
+ const { dir, path } = getSaltPath();
830
+ if (existsSync(path)) {
831
+ try {
832
+ const raw = readFileSync(path, "utf-8").trim();
833
+ if (/^[0-9a-f]{64}$/i.test(raw)) {
834
+ _cachedSalt = raw.toLowerCase();
835
+ return _cachedSalt;
836
+ }
837
+ } catch {
838
+ }
839
+ }
840
+ const salt = randomBytes(SALT_BYTES).toString("hex");
841
+ try {
842
+ mkdirSync(dir, { recursive: true });
843
+ writeFileSync(path, salt, { mode: 384 });
844
+ } catch {
845
+ }
846
+ _cachedSalt = salt;
847
+ return salt;
848
+ }
849
+ function buildSaltedSeed(baseSeed) {
850
+ return `${baseSeed}:${getMutationSalt()}`;
851
+ }
852
+
507
853
  // src/mutator.ts
508
854
  function hash32(str) {
509
855
  let h = 5381;
@@ -553,6 +899,7 @@ function mutate(code, language, seed = "pretense") {
553
899
  stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
554
900
  };
555
901
  }
902
+ const effectiveSeed = seed === "pretense" ? buildSaltedSeed(seed) : seed;
556
903
  const { tokens } = scan(code, lang);
557
904
  const map = /* @__PURE__ */ new Map();
558
905
  for (const token of tokens) {
@@ -567,7 +914,7 @@ function mutate(code, language, seed = "pretense") {
567
914
  continue;
568
915
  }
569
916
  const prefix = prefixForType(token.type);
570
- const synthetic = deterministicId(token.value, seed, prefix);
917
+ const synthetic = deterministicId(token.value, effectiveSeed, prefix);
571
918
  map.set(token.value, synthetic);
572
919
  }
573
920
  const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
@@ -654,7 +1001,15 @@ var SECRET_PATTERNS = [
654
1001
  { name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
655
1002
  { name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
656
1003
  { name: "algolia-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /[a-f0-9]{32}/g, validate: (m) => /ALGOLIA|algolia/.test(m) },
657
- { name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g }
1004
+ { name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g },
1005
+ // ─── Extended patterns (v0.6 unit 8) ─────────────────────────────────────────
1006
+ { name: "google-oauth-refresh", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bGOCSPX-[A-Za-z0-9_-]{20,}\b/g },
1007
+ { name: "slack-webhook-url", category: "secret", severity: "critical", defaultAction: "block", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]{20,}/g },
1008
+ { name: "sentry-dsn", category: "secret", severity: "high", defaultAction: "redact", pattern: /https:\/\/[a-f0-9]{32}@o\d+\.ingest\.sentry\.io\/\d+/g },
1009
+ { name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
1010
+ { name: "launchdarkly-sdk-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bsdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
1011
+ { name: "launchdarkly-mobile-key", category: "secret", severity: "high", defaultAction: "block", pattern: /\bmob-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
1012
+ { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{32,}\b/g }
658
1013
  ];
659
1014
  var PII_PATTERNS = [
660
1015
  { name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
@@ -712,6 +1067,7 @@ function maskValue(value, type) {
712
1067
  return "***";
713
1068
  }
714
1069
  var matchCounter = 0;
1070
+ var ENTROPY_SCAN_MAX_BYTES = 512 * 1024;
715
1071
  function scan2(text, actionOverrides) {
716
1072
  const start = performance.now();
717
1073
  const matches = [];
@@ -737,28 +1093,44 @@ function scan2(text, actionOverrides) {
737
1093
  });
738
1094
  }
739
1095
  }
740
- const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
741
- highEntropyPattern.lastIndex = 0;
742
- let hem;
743
- while ((hem = highEntropyPattern.exec(text)) !== null) {
744
- const value = hem[0];
745
- const alreadyMatched = matches.some(
746
- (m) => m.start <= hem.index && m.end >= hem.index + value.length
747
- );
748
- if (alreadyMatched) continue;
749
- if (isHighEntropy(value)) {
750
- matchCounter++;
751
- matches.push({
752
- id: `scan_${matchCounter}`,
753
- category: "secret",
754
- type: "high-entropy-string",
755
- value,
756
- masked: value.slice(0, 6) + "..." + value.slice(-4),
757
- start: hem.index,
758
- end: hem.index + value.length,
759
- severity: "medium",
760
- action: actionOverrides?.["high-entropy-string"] ?? "warn"
761
- });
1096
+ if (text.length <= ENTROPY_SCAN_MAX_BYTES) {
1097
+ const sortedByStart = [...matches].sort((a, b) => a.start - b.start);
1098
+ const coveredEnd = (idx) => {
1099
+ let lo = 0;
1100
+ let hi = sortedByStart.length - 1;
1101
+ let best = -1;
1102
+ while (lo <= hi) {
1103
+ const mid = lo + hi >> 1;
1104
+ if (sortedByStart[mid].start <= idx) {
1105
+ best = mid;
1106
+ lo = mid + 1;
1107
+ } else {
1108
+ hi = mid - 1;
1109
+ }
1110
+ }
1111
+ return best >= 0 ? sortedByStart[best].end : -1;
1112
+ };
1113
+ const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
1114
+ highEntropyPattern.lastIndex = 0;
1115
+ let hem;
1116
+ while ((hem = highEntropyPattern.exec(text)) !== null) {
1117
+ const value = hem[0];
1118
+ const end = hem.index + value.length;
1119
+ if (coveredEnd(hem.index) >= end) continue;
1120
+ if (isHighEntropy(value)) {
1121
+ matchCounter++;
1122
+ matches.push({
1123
+ id: `scan_${matchCounter}`,
1124
+ category: "secret",
1125
+ type: "high-entropy-string",
1126
+ value,
1127
+ masked: value.slice(0, 6) + "..." + value.slice(-4),
1128
+ start: hem.index,
1129
+ end,
1130
+ severity: "medium",
1131
+ action: actionOverrides?.["high-entropy-string"] ?? "warn"
1132
+ });
1133
+ }
762
1134
  }
763
1135
  }
764
1136
  const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
@@ -790,7 +1162,7 @@ function applyRedactions(text, matches) {
790
1162
  }
791
1163
 
792
1164
  // src/store.ts
793
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1165
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
794
1166
  import { dirname } from "path";
795
1167
  var MutationStore = class {
796
1168
  entries = /* @__PURE__ */ new Map();
@@ -855,20 +1227,20 @@ var MutationStore = class {
855
1227
  */
856
1228
  persist() {
857
1229
  const dir = dirname(this.filePath);
858
- if (!existsSync(dir)) {
859
- mkdirSync(dir, { recursive: true });
1230
+ if (!existsSync2(dir)) {
1231
+ mkdirSync2(dir, { recursive: true });
860
1232
  }
861
1233
  const data = JSON.stringify([...this.entries.values()], null, 2);
862
- writeFileSync(this.filePath, data, "utf-8");
1234
+ writeFileSync2(this.filePath, data, "utf-8");
863
1235
  }
864
1236
  /**
865
1237
  * Load entries from the JSON file into memory.
866
1238
  * No-ops if the file doesn't exist.
867
1239
  */
868
1240
  load() {
869
- if (!existsSync(this.filePath)) return;
1241
+ if (!existsSync2(this.filePath)) return;
870
1242
  try {
871
- const raw = readFileSync(this.filePath, "utf-8");
1243
+ const raw = readFileSync2(this.filePath, "utf-8");
872
1244
  const parsed = JSON.parse(raw);
873
1245
  this.entries.clear();
874
1246
  for (const entry of parsed) {
@@ -901,8 +1273,9 @@ var MutationStore = class {
901
1273
  };
902
1274
 
903
1275
  // src/config.ts
904
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
905
- import { join, resolve } from "path";
1276
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1277
+ import { join as join2, resolve } from "path";
1278
+ import { createHmac } from "crypto";
906
1279
  var CONFIG_DIR_NAME = ".pretense";
907
1280
  var CONFIG_FILE = "config.json";
908
1281
  var MUTATION_MAP_FILE = "mutation-map.json";
@@ -914,30 +1287,33 @@ function getConfigDir(baseDir) {
914
1287
  }
915
1288
  function initConfig(dir) {
916
1289
  const configDir = getConfigDir(dir);
917
- if (!existsSync2(configDir)) {
918
- mkdirSync2(configDir, { recursive: true });
1290
+ if (!existsSync3(configDir)) {
1291
+ mkdirSync3(configDir, { recursive: true });
919
1292
  }
920
- const configPath = join(configDir, CONFIG_FILE);
921
- if (!existsSync2(configPath)) {
1293
+ const configPath = join2(configDir, CONFIG_FILE);
1294
+ if (!existsSync3(configPath)) {
922
1295
  const config = {
923
1296
  ...DEFAULT_CONFIG,
924
1297
  storePath: configDir
925
1298
  };
926
- writeFileSync2(configPath, JSON.stringify(config, null, 2), "utf-8");
927
- }
928
- const mapPath = join(configDir, MUTATION_MAP_FILE);
929
- if (!existsSync2(mapPath)) {
930
- writeFileSync2(mapPath, "[]", "utf-8");
1299
+ writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
931
1300
  }
932
- const auditPath = join(configDir, AUDIT_LOG_FILE);
933
- if (!existsSync2(auditPath)) {
934
- writeFileSync2(auditPath, "", "utf-8");
1301
+ const mapPath = join2(configDir, MUTATION_MAP_FILE);
1302
+ if (!existsSync3(mapPath)) {
1303
+ writeFileSync3(mapPath, "[]", "utf-8");
935
1304
  }
936
- const usagePath = join(configDir, USAGE_FILE);
937
- if (!existsSync2(usagePath)) {
938
- writeFileSync2(
1305
+ const auditPath = join2(configDir, AUDIT_LOG_FILE);
1306
+ if (!existsSync3(auditPath)) {
1307
+ writeFileSync3(auditPath, "", "utf-8");
1308
+ }
1309
+ const usagePath = join2(configDir, USAGE_FILE);
1310
+ if (!existsSync3(usagePath)) {
1311
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1312
+ const month = today.slice(0, 7);
1313
+ const usageData = { month, mutations: 0, firstUseDate: today };
1314
+ writeFileSync3(
939
1315
  usagePath,
940
- JSON.stringify({ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), mutations: 0, firstUseDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }, null, 2),
1316
+ JSON.stringify({ ...usageData, date: today, checksum: computeUsageChecksum(usageData) }, null, 2),
941
1317
  "utf-8"
942
1318
  );
943
1319
  }
@@ -945,71 +1321,111 @@ function initConfig(dir) {
945
1321
  }
946
1322
  function loadConfig(dir) {
947
1323
  const configDir = getConfigDir(dir);
948
- const configPath = join(configDir, CONFIG_FILE);
949
- if (!existsSync2(configPath)) {
1324
+ const configPath = join2(configDir, CONFIG_FILE);
1325
+ if (!existsSync3(configPath)) {
950
1326
  return { ...DEFAULT_CONFIG };
951
1327
  }
952
1328
  try {
953
- const raw = readFileSync2(configPath, "utf-8");
1329
+ const raw = readFileSync3(configPath, "utf-8");
954
1330
  const parsed = JSON.parse(raw);
955
1331
  return { ...DEFAULT_CONFIG, ...parsed };
956
1332
  } catch {
957
1333
  return { ...DEFAULT_CONFIG };
958
1334
  }
959
1335
  }
1336
+ var USAGE_HMAC_SECRET = "pretense_usage_integrity_v1";
1337
+ function getUsageSecret() {
1338
+ return process.env["PRETENSE_USAGE_SECRET"] ?? USAGE_HMAC_SECRET;
1339
+ }
1340
+ function computeUsageChecksum(data) {
1341
+ const payload = JSON.stringify({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate });
1342
+ return createHmac("sha256", getUsageSecret()).update(payload).digest("hex");
1343
+ }
1344
+ function verifyUsageChecksum(data) {
1345
+ if (!data.checksum) return false;
1346
+ return data.checksum === computeUsageChecksum(data);
1347
+ }
960
1348
  function loadUsage(dir) {
961
1349
  const configDir = getConfigDir(dir);
962
- const usagePath = join(configDir, USAGE_FILE);
1350
+ const usagePath = join2(configDir, USAGE_FILE);
963
1351
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
964
- if (!existsSync2(usagePath)) {
965
- return { date: today, mutations: 0, firstUseDate: today };
1352
+ const currentMonth = today.slice(0, 7);
1353
+ if (!existsSync3(usagePath)) {
1354
+ return { month: currentMonth, mutations: 0, firstUseDate: today };
966
1355
  }
967
1356
  try {
968
- const raw = readFileSync2(usagePath, "utf-8");
1357
+ const raw = readFileSync3(usagePath, "utf-8");
969
1358
  const data = JSON.parse(raw);
970
- if (data.date !== today) {
971
- return { date: today, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1359
+ if (data.date && !data.month) {
1360
+ const legacyDate = data.date;
1361
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? legacyDate };
1362
+ }
1363
+ if (!verifyUsageChecksum({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate, checksum: data.checksum })) {
1364
+ process.stderr.write("[PRETENSE] Warning: usage.json integrity check failed. Resetting usage counter.\n");
1365
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
972
1366
  }
973
- return data;
1367
+ if (data.month !== currentMonth) {
1368
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1369
+ }
1370
+ return { month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate };
974
1371
  } catch {
975
- return { date: today, mutations: 0, firstUseDate: today };
1372
+ return { month: currentMonth, mutations: 0, firstUseDate: today };
976
1373
  }
977
1374
  }
978
1375
  function saveUsage(usage, dir) {
979
1376
  const configDir = getConfigDir(dir);
980
- if (!existsSync2(configDir)) {
981
- mkdirSync2(configDir, { recursive: true });
1377
+ if (!existsSync3(configDir)) {
1378
+ mkdirSync3(configDir, { recursive: true });
982
1379
  }
983
- const usagePath = join(configDir, USAGE_FILE);
984
- writeFileSync2(usagePath, JSON.stringify(usage, null, 2), "utf-8");
1380
+ const usagePath = join2(configDir, USAGE_FILE);
1381
+ const checksum = computeUsageChecksum(usage);
1382
+ writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
1383
+ }
1384
+ var MIN_LICENSE_KEY_LENGTH = 40;
1385
+ var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
1386
+ function isValidLicenseKey(key, prefix) {
1387
+ if (key.length < MIN_LICENSE_KEY_LENGTH) return false;
1388
+ const afterPrefix = key.slice(prefix.length);
1389
+ const hmacSecret = process.env["PRETENSE_LICENSE_SECRET"];
1390
+ if (hmacSecret) {
1391
+ const lastUnderscore = afterPrefix.lastIndexOf("_");
1392
+ if (lastUnderscore === -1) return false;
1393
+ const payload = afterPrefix.slice(0, lastUnderscore);
1394
+ const providedHmac = afterPrefix.slice(lastUnderscore + 1);
1395
+ if (!payload || !providedHmac) return false;
1396
+ if (!LICENSE_PAYLOAD_REGEX.test(payload)) return false;
1397
+ const expectedHmac = createHmac("sha256", hmacSecret).update(payload).digest("hex").slice(0, 16);
1398
+ return providedHmac === expectedHmac;
1399
+ }
1400
+ return LICENSE_PAYLOAD_REGEX.test(afterPrefix);
985
1401
  }
986
1402
  function detectTier() {
987
1403
  const key = process.env["PRETENSE_LICENSE_KEY"] ?? "";
988
- if (key.startsWith("pre_ent_")) return "enterprise";
989
- if (key.startsWith("pre_pro_")) return "pro";
1404
+ if (key.startsWith("pre_ent_") && isValidLicenseKey(key, "pre_ent_")) return "enterprise";
1405
+ if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
990
1406
  return "free";
991
1407
  }
992
1408
  function getTierLimits(tier) {
993
1409
  switch (tier) {
994
1410
  case "enterprise":
995
- return { dailyMutations: Infinity, auditRetentionDays: Infinity };
1411
+ return { monthlyMutations: Infinity, auditRetentionDays: Infinity };
996
1412
  case "pro":
997
- return { dailyMutations: Infinity, auditRetentionDays: 90 };
1413
+ return { monthlyMutations: Infinity, auditRetentionDays: 90 };
998
1414
  case "free":
999
1415
  default:
1000
- return { dailyMutations: 500, auditRetentionDays: 3 };
1416
+ return { monthlyMutations: 1e3, auditRetentionDays: 30 };
1001
1417
  }
1002
1418
  }
1003
1419
 
1004
1420
  // src/audit.ts
1005
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync3 } from "fs";
1006
- import { join as join2, dirname as dirname2 } from "path";
1421
+ import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync4 } from "fs";
1422
+ import { join as join3, dirname as dirname2 } from "path";
1007
1423
  function writeAuditEntry(entry, dir) {
1008
1424
  const configDir = getConfigDir(dir);
1009
- const auditPath = join2(configDir, "audit.log");
1425
+ const auditPath = join3(configDir, "audit.log");
1010
1426
  const logDir = dirname2(auditPath);
1011
- if (!existsSync3(logDir)) {
1012
- mkdirSync3(logDir, { recursive: true });
1427
+ if (!existsSync4(logDir)) {
1428
+ mkdirSync4(logDir, { recursive: true });
1013
1429
  }
1014
1430
  const line = JSON.stringify(entry) + "\n";
1015
1431
  appendFileSync(auditPath, line, "utf-8");
@@ -1028,11 +1444,11 @@ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, l
1028
1444
  function readAuditLog(options = {}) {
1029
1445
  const { limit, dir } = options;
1030
1446
  const configDir = getConfigDir(dir);
1031
- const auditPath = join2(configDir, "audit.log");
1032
- if (!existsSync3(auditPath)) {
1447
+ const auditPath = join3(configDir, "audit.log");
1448
+ if (!existsSync4(auditPath)) {
1033
1449
  return [];
1034
1450
  }
1035
- const raw = readFileSync3(auditPath, "utf-8");
1451
+ const raw = readFileSync4(auditPath, "utf-8");
1036
1452
  const lines = raw.trim().split("\n").filter(Boolean);
1037
1453
  let entries = [];
1038
1454
  for (const line of lines) {
@@ -1095,17 +1511,100 @@ function formatAudit(entries, format = "text") {
1095
1511
  import { Hono } from "hono";
1096
1512
  import { serve } from "@hono/node-server";
1097
1513
  import { nanoid } from "nanoid";
1514
+ import { createServer } from "net";
1515
+
1516
+ // src/auth.ts
1517
+ import { createHash } from "crypto";
1518
+ var AUTH_HEADERS = ["authorization", "x-api-key", "x-goog-api-key"];
1519
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set([
1520
+ "/",
1521
+ "/health",
1522
+ "/stats",
1523
+ "/audit",
1524
+ "/tokens"
1525
+ ]);
1526
+ function extractApiKey(c) {
1527
+ for (const h of AUTH_HEADERS) {
1528
+ const v = c.req.header(h);
1529
+ if (v && v.length > 0) {
1530
+ return v.replace(/^Bearer\s+/i, "").trim();
1531
+ }
1532
+ }
1533
+ return null;
1534
+ }
1535
+ function sessionHash(apiKey) {
1536
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
1537
+ }
1538
+ var requireApiKey = async (c, next) => {
1539
+ if (PUBLIC_PATHS.has(c.req.path)) {
1540
+ return next();
1541
+ }
1542
+ const key = extractApiKey(c);
1543
+ if (!key) {
1544
+ return c.json(
1545
+ {
1546
+ error: {
1547
+ type: "missing_api_key",
1548
+ message: "Authorization header required (Authorization, x-api-key, or x-goog-api-key)"
1549
+ }
1550
+ },
1551
+ 401
1552
+ );
1553
+ }
1554
+ c.set("sessionHash", sessionHash(key));
1555
+ return next();
1556
+ };
1557
+
1558
+ // src/session-store.ts
1559
+ var sessions = /* @__PURE__ */ new Map();
1560
+ function getSession(hash) {
1561
+ let s = sessions.get(hash);
1562
+ if (!s) {
1563
+ s = {
1564
+ mutationMap: /* @__PURE__ */ new Map(),
1565
+ reverseMap: /* @__PURE__ */ new Map(),
1566
+ createdAt: Date.now(),
1567
+ lastUsed: Date.now()
1568
+ };
1569
+ sessions.set(hash, s);
1570
+ }
1571
+ s.lastUsed = Date.now();
1572
+ return s;
1573
+ }
1574
+ function sessionCount() {
1575
+ return sessions.size;
1576
+ }
1577
+
1578
+ // src/proxy.ts
1579
+ function anthropicUpstream() {
1580
+ return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
1581
+ }
1582
+ function openaiUpstream() {
1583
+ return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
1584
+ }
1585
+ function geminiUpstream() {
1586
+ return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
1587
+ }
1588
+ function ollamaUpstream() {
1589
+ return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
1590
+ }
1098
1591
  function detectUpstream(path) {
1099
- if (path.includes("/v1/messages")) return "https://api.anthropic.com";
1100
- if (path.includes("/v1/chat/completions") || path.includes("/v1/completions")) return "https://api.openai.com";
1101
- if (path.includes("/v1beta/") || path.includes("/v1/models")) return "https://generativelanguage.googleapis.com";
1102
- return "https://api.openai.com";
1592
+ if (path.startsWith("/v1/messages")) return anthropicUpstream();
1593
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
1594
+ return openaiUpstream();
1595
+ }
1596
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
1597
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
1598
+ return anthropicUpstream();
1103
1599
  }
1104
1600
  function detectProvider(path) {
1105
- if (path.includes("/v1/messages")) return "anthropic";
1106
- if (path.includes("/v1/chat/completions") || path.includes("/v1/completions")) return "openai";
1107
- if (path.includes("/v1beta/") || path.includes("/v1/models")) return "google";
1108
- return "openai";
1601
+ if (path.startsWith("/v1/messages")) return "anthropic";
1602
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
1603
+ return "openai";
1604
+ }
1605
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
1606
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
1607
+ return "anthropic";
1109
1608
  }
1110
1609
  function extractText(body) {
1111
1610
  const parts = [];
@@ -1184,6 +1683,24 @@ function createProxy(opts = {}) {
1184
1683
  const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
1185
1684
  const verbose = opts.verbose ?? false;
1186
1685
  const app = new Hono();
1686
+ app.use("*", requireApiKey);
1687
+ app.get(
1688
+ "/",
1689
+ (c) => c.json({
1690
+ name: "pretense",
1691
+ version: "0.3.0",
1692
+ status: "running",
1693
+ routes: {
1694
+ "GET /": "This welcome page",
1695
+ "GET /health": "Health check with uptime",
1696
+ "GET /stats": "Mutation statistics",
1697
+ "GET /audit": "Audit log (?limit=50)",
1698
+ "POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
1699
+ },
1700
+ languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
1701
+ docs: "https://pretense.ai/docs"
1702
+ })
1703
+ );
1187
1704
  app.get(
1188
1705
  "/health",
1189
1706
  (c) => c.json({
@@ -1200,7 +1717,8 @@ function createProxy(opts = {}) {
1200
1717
  "/stats",
1201
1718
  (c) => c.json({
1202
1719
  ...stats,
1203
- storeSize: store.size
1720
+ storeSize: store.size,
1721
+ activeSessions: sessionCount()
1204
1722
  })
1205
1723
  );
1206
1724
  app.get("/audit", (c) => {
@@ -1233,15 +1751,17 @@ function createProxy(opts = {}) {
1233
1751
  const requestId = nanoid(12);
1234
1752
  const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
1235
1753
  const provider = detectProvider(c.req.path);
1754
+ const sessHash = c.get("sessionHash");
1755
+ const session = getSession(sessHash);
1236
1756
  const tier = detectTier();
1237
- const { dailyMutations } = getTierLimits(tier);
1757
+ const { monthlyMutations } = getTierLimits(tier);
1238
1758
  const usage = loadUsage();
1239
- if (tier === "free" && usage.mutations >= dailyMutations) {
1759
+ if (tier === "free" && usage.mutations >= monthlyMutations) {
1240
1760
  return c.json(
1241
1761
  {
1242
1762
  error: {
1243
1763
  type: "pretense_rate_limit",
1244
- message: `Daily mutation limit reached (${dailyMutations}/day). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
1764
+ message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
1245
1765
  }
1246
1766
  },
1247
1767
  429
@@ -1274,7 +1794,11 @@ function createProxy(opts = {}) {
1274
1794
  const mutatedBlocks = [];
1275
1795
  for (const block of blocks) {
1276
1796
  const result = mutate(block.code, block.language);
1277
- for (const [k, v] of result.map.entries()) mutationMap.set(k, v);
1797
+ for (const [k, v] of result.map) {
1798
+ mutationMap.set(k, v);
1799
+ session.mutationMap.set(k, v);
1800
+ session.reverseMap.set(v, k);
1801
+ }
1278
1802
  tokensMutated += result.stats.tokensMutated;
1279
1803
  mutatedBlocks.push(result.mutatedCode);
1280
1804
  }
@@ -1282,8 +1806,9 @@ function createProxy(opts = {}) {
1282
1806
  });
1283
1807
  usage.mutations += tokensMutated;
1284
1808
  saveUsage(usage);
1285
- if (tier === "free" && usage.mutations >= 400 && usage.mutations - tokensMutated < 400) {
1286
- printUpgradeNudge(`${usage.mutations}/${dailyMutations} daily mutations used. Running low!`);
1809
+ const warningThreshold = Math.floor(monthlyMutations * 0.8);
1810
+ if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
1811
+ printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
1287
1812
  }
1288
1813
  if (tier === "free" && daysSinceFirstUse() >= 7) {
1289
1814
  printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
@@ -1326,14 +1851,59 @@ function createProxy(opts = {}) {
1326
1851
  return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
1327
1852
  }
1328
1853
  if (body["stream"] === true) {
1329
- return new Response(upstreamResp.body, {
1854
+ const streamReverseMap = new Map(mutationMap);
1855
+ const upstream2 = upstreamResp.body;
1856
+ if (!upstream2 || tokensMutated === 0) {
1857
+ return new Response(upstream2, {
1858
+ status: upstreamResp.status,
1859
+ headers: {
1860
+ "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
1861
+ "cache-control": "no-cache",
1862
+ "x-pretense-request-id": requestId,
1863
+ "x-pretense-protected": "true",
1864
+ "x-pretense-mutations": String(tokensMutated)
1865
+ }
1866
+ });
1867
+ }
1868
+ const decoder = new TextDecoder();
1869
+ const encoder = new TextEncoder();
1870
+ let buffer = "";
1871
+ const transform = new TransformStream({
1872
+ transform(chunk, controller) {
1873
+ buffer += decoder.decode(chunk, { stream: true });
1874
+ const lines = buffer.split("\n");
1875
+ buffer = lines.pop() ?? "";
1876
+ for (const line of lines) {
1877
+ if (line.startsWith("data: ") && line !== "data: [DONE]") {
1878
+ try {
1879
+ const json = JSON.parse(line.slice(6));
1880
+ const reversed = applyToBody(json, (text) => reverse(text, streamReverseMap));
1881
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(reversed)}
1882
+ `));
1883
+ } catch {
1884
+ controller.enqueue(encoder.encode(line + "\n"));
1885
+ }
1886
+ } else {
1887
+ controller.enqueue(encoder.encode(line + "\n"));
1888
+ }
1889
+ }
1890
+ },
1891
+ flush(controller) {
1892
+ if (buffer.length > 0) {
1893
+ controller.enqueue(encoder.encode(buffer));
1894
+ }
1895
+ }
1896
+ });
1897
+ const reversedStream = upstream2.pipeThrough(transform);
1898
+ return new Response(reversedStream, {
1330
1899
  status: upstreamResp.status,
1331
1900
  headers: {
1332
1901
  "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
1333
1902
  "cache-control": "no-cache",
1334
1903
  "x-pretense-request-id": requestId,
1335
1904
  "x-pretense-protected": "true",
1336
- "x-pretense-mutations": String(tokensMutated)
1905
+ "x-pretense-mutations": String(tokensMutated),
1906
+ "x-pretense-stream-reversal": "active"
1337
1907
  }
1338
1908
  });
1339
1909
  }
@@ -1363,31 +1933,435 @@ function createProxy(opts = {}) {
1363
1933
  });
1364
1934
  return app;
1365
1935
  }
1936
+ function isPortAvailable(port) {
1937
+ return new Promise((resolve3) => {
1938
+ const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
1939
+ tester.once("close", () => resolve3(true)).close();
1940
+ }).listen(port, "127.0.0.1");
1941
+ });
1942
+ }
1943
+ async function findAvailablePort(startPort, maxAttempts = 10) {
1944
+ for (let i = 0; i < maxAttempts; i++) {
1945
+ const candidate = startPort + i;
1946
+ if (candidate < 1 || candidate > 65535) break;
1947
+ if (await isPortAvailable(candidate)) return candidate;
1948
+ }
1949
+ throw new Error(
1950
+ `No available port found in range ${startPort}-${startPort + maxAttempts - 1}. Free a port or pass --port <number>.`
1951
+ );
1952
+ }
1366
1953
  function startProxy(opts = {}) {
1367
- const port = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
1954
+ const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
1368
1955
  const app = createProxy(opts);
1369
1956
  const tier = detectTier();
1370
- serve({ fetch: app.fetch, port }, () => {
1371
- process.stdout.write(`
1372
- \x1B[36m\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
1373
- \u2502 \u2502
1374
- \u2502 \x1B[1mPretense\x1B[0m\x1B[36m v0.3.0 [${tier.toUpperCase()}]${" ".repeat(20 - tier.length)}\u2502
1375
- \u2502 Your code hits AI naked. We fix that. \u2502
1376
- \u2502 \u2502
1377
- \u2502 Proxy: http://localhost:${port} \u2502
1378
- \u2502 Health: http://localhost:${port}/health \u2502
1379
- \u2502 Stats: http://localhost:${port}/stats \u2502
1380
- \u2502 \u2502
1381
- \u2502 ANTHROPIC_BASE_URL=http://localhost:${port} \u2502
1382
- \u2502 OPENAI_BASE_URL=http://localhost:${port}/v1 \u2502
1383
- \u2502 \u2502
1384
- \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\x1B[0m
1957
+ void (async () => {
1958
+ let port;
1959
+ try {
1960
+ port = await findAvailablePort(requestedPort, 10);
1961
+ } catch (err) {
1962
+ process.stderr.write(`\x1B[31m[PRETENSE] ${err.message}\x1B[0m
1385
1963
  `);
1386
- });
1964
+ process.exit(1);
1965
+ }
1966
+ if (port !== requestedPort) {
1967
+ process.stderr.write(
1968
+ `\x1B[33m[PRETENSE] Port ${requestedPort} in use, falling back to ${port}.\x1B[0m
1969
+ `
1970
+ );
1971
+ }
1972
+ serve({ fetch: app.fetch, port }, () => {
1973
+ const portStr = String(port);
1974
+ process.stdout.write(`
1975
+ \x1B[36mPretense v0.3.0 [${tier.toUpperCase()}]\x1B[0m
1976
+ Your code hits AI naked. We fix that.
1977
+
1978
+ Proxy: http://localhost:${portStr}
1979
+ Health: http://localhost:${portStr}/health
1980
+ Stats: http://localhost:${portStr}/stats
1981
+
1982
+ Drop-in env vars:
1983
+ ANTHROPIC_BASE_URL=http://localhost:${portStr}
1984
+ OPENAI_BASE_URL=http://localhost:${portStr}/v1
1985
+ GEMINI_BASE_URL=http://localhost:${portStr}/v1beta
1986
+ OLLAMA_HOST=http://localhost:${portStr}
1987
+
1988
+ Routes intercepted:
1989
+ /v1/messages -> Anthropic (Claude Code, SDK)
1990
+ /v1/chat/completions -> OpenAI (ChatGPT, Cursor)
1991
+ /v1/responses -> OpenAI (Codex CLI)
1992
+ /v1beta/... -> Google Gemini
1993
+ /api/chat, /api/generate -> Ollama (local)
1994
+ `);
1995
+ });
1996
+ })();
1997
+ }
1998
+
1999
+ // src/token-footer.ts
2000
+ import chalk from "chalk";
2001
+ var PLAN_LIMITS = {
2002
+ free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
2003
+ pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
2004
+ enterprise: {
2005
+ monthlyMutations: Number.POSITIVE_INFINITY,
2006
+ monthlyScans: Number.POSITIVE_INFINITY,
2007
+ seats: Number.POSITIVE_INFINITY,
2008
+ pricePerMonth: "$99/seat/month"
2009
+ }
2010
+ };
2011
+ function getPlanLimits(tier) {
2012
+ return PLAN_LIMITS[tier];
2013
+ }
2014
+ function fmt(n) {
2015
+ if (!Number.isFinite(n)) return "unlimited";
2016
+ return n.toLocaleString("en-US");
2017
+ }
2018
+ function printTokenFooter(filesScanned, mutationsMade) {
2019
+ const tier = detectTier();
2020
+ const limits = getPlanLimits(tier);
2021
+ const usage = loadUsage();
2022
+ if (tier === "enterprise") {
2023
+ process.stderr.write(
2024
+ chalk.gray(`
2025
+ ${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
2026
+
2027
+ `)
2028
+ );
2029
+ return;
2030
+ }
2031
+ const used = usage.mutations;
2032
+ const cap = limits.monthlyMutations;
2033
+ const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
2034
+ const usageStr = pct >= 80 ? chalk.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk.gray(`${fmt(used)} / ${fmt(cap)}`);
2035
+ process.stderr.write(
2036
+ `
2037
+ ${chalk.cyan(filesScanned + " files")}, ${chalk.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
2038
+ `
2039
+ );
2040
+ if (tier === "free") {
2041
+ if (pct >= 80) {
2042
+ process.stderr.write(
2043
+ chalk.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk.bold("pretense upgrade")}
2044
+
2045
+ `)
2046
+ );
2047
+ } else {
2048
+ process.stderr.write(
2049
+ chalk.gray(` Run ${chalk.bold("pretense status")} for details, ${chalk.bold("pretense upgrade")} to remove limits.
2050
+
2051
+ `)
2052
+ );
2053
+ }
2054
+ } else {
2055
+ process.stderr.write("\n");
2056
+ }
2057
+ }
2058
+
2059
+ // src/banner.ts
2060
+ import chalk2 from "chalk";
2061
+ var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
2062
+ \u2588\u2588 \u2588\u2588
2063
+ \u2588\u2588 \u2588\u2588 Stop your code from
2064
+ \u2588\u2588 \u2588\u2588 leaking to any LLM.
2065
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
2066
+ \u2588\u2588 pretense.ai
2067
+ \u2588\u2588
2068
+ \u2588\u2588
2069
+ \u2588\u2588
2070
+ \u2588\u2588`;
2071
+ var ICE_BLUE = "#7DD3FC";
2072
+ function shouldPrint() {
2073
+ if (process.env.PRETENSE_NO_BANNER === "1") return false;
2074
+ if (!process.stdout.isTTY) return false;
2075
+ if (process.env.CI) return false;
2076
+ return true;
2077
+ }
2078
+ function tint(line) {
2079
+ try {
2080
+ return chalk2.hex(ICE_BLUE)(line);
2081
+ } catch {
2082
+ return line;
2083
+ }
2084
+ }
2085
+ function printBanner() {
2086
+ if (!shouldPrint()) return;
2087
+ const lines = BANNER_RAW.split("\n");
2088
+ for (const line of lines) {
2089
+ process.stderr.write(tint(line) + "\n");
2090
+ }
2091
+ process.stderr.write("\n");
2092
+ }
2093
+
2094
+ // src/commands/status.ts
2095
+ import chalk3 from "chalk";
2096
+ var PLAN_LABEL = {
2097
+ free: "Free",
2098
+ pro: "Pro",
2099
+ enterprise: "Enterprise"
2100
+ };
2101
+ function fmt2(n) {
2102
+ if (!Number.isFinite(n)) return "unlimited";
2103
+ return n.toLocaleString("en-US");
2104
+ }
2105
+ function runStatus() {
2106
+ const tier = detectTier();
2107
+ const planLimits = getPlanLimits(tier);
2108
+ const tierLimits = getTierLimits(tier);
2109
+ const usage = loadUsage();
2110
+ const tierBadge = tier === "enterprise" ? chalk3.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk3.bgBlue.white.bold(" PRO ") : chalk3.bgGray.white.bold(" FREE ");
2111
+ const lines = [];
2112
+ lines.push("");
2113
+ lines.push(`${chalk3.cyan("Pretense")} ${chalk3.gray("v0.3.0")} ${tierBadge}`);
2114
+ lines.push("");
2115
+ lines.push(` Plan: ${chalk3.bold(PLAN_LABEL[tier])} ${chalk3.gray("(" + planLimits.pricePerMonth + ")")}`);
2116
+ lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
2117
+ lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
2118
+ lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
2119
+ lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
2120
+ lines.push("");
2121
+ if (tier === "free") {
2122
+ lines.push(` ${chalk3.gray("Upgrade:")} ${chalk3.bold("pretense upgrade")}`);
2123
+ lines.push(` ${chalk3.gray("Credits:")} ${chalk3.bold("pretense credits")}`);
2124
+ lines.push("");
2125
+ }
2126
+ process.stdout.write(lines.join("\n") + "\n");
2127
+ }
2128
+
2129
+ // src/commands/upgrade.ts
2130
+ import chalk4 from "chalk";
2131
+ var PRICING_URL = "https://pretense.ai/pricing";
2132
+ function runUpgrade() {
2133
+ const tier = detectTier();
2134
+ const lines = [];
2135
+ lines.push("");
2136
+ lines.push(chalk4.cyan.bold(" Pretense Plans"));
2137
+ lines.push("");
2138
+ lines.push(chalk4.gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2139
+ lines.push(chalk4.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
2140
+ lines.push(chalk4.gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
2141
+ lines.push(` \u2502 Price/seat \u2502 ${chalk4.bold("$0")} \u2502 ${chalk4.bold("$29")} \u2502 ${chalk4.bold("$99")} \u2502`);
2142
+ lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
2143
+ lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
2144
+ lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
2145
+ lines.push(" \u2502 Audit log \u2502 30 days \u2502 90 days \u2502 unlimited \u2502");
2146
+ lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
2147
+ lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
2148
+ lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
2149
+ lines.push(chalk4.gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2150
+ lines.push("");
2151
+ lines.push(` ${chalk4.gray("Current plan:")} ${chalk4.bold(tier.toUpperCase())}`);
2152
+ lines.push(` ${chalk4.gray("Upgrade at:")} ${chalk4.cyan.underline(PRICING_URL)}`);
2153
+ lines.push("");
2154
+ lines.push(chalk4.gray(" After purchase, set:"));
2155
+ lines.push(chalk4.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
2156
+ lines.push("");
2157
+ process.stdout.write(lines.join("\n") + "\n");
2158
+ }
2159
+
2160
+ // src/commands/credits.ts
2161
+ import chalk5 from "chalk";
2162
+ var PLAN_LABEL2 = {
2163
+ free: "Free",
2164
+ pro: "Pro",
2165
+ enterprise: "Enterprise"
2166
+ };
2167
+ function fmt3(n) {
2168
+ if (!Number.isFinite(n)) return "unlimited";
2169
+ return n.toLocaleString("en-US");
2170
+ }
2171
+ function progressBar(used, cap, width = 24) {
2172
+ if (!Number.isFinite(cap) || cap <= 0) return chalk5.green("[" + "=".repeat(width) + "]");
2173
+ const pct = Math.min(1, used / cap);
2174
+ const filled = Math.round(pct * width);
2175
+ const bar = "=".repeat(filled) + "-".repeat(width - filled);
2176
+ const colored = pct >= 0.8 ? chalk5.yellow(bar) : pct >= 1 ? chalk5.red(bar) : chalk5.green(bar);
2177
+ return "[" + colored + "]";
2178
+ }
2179
+ function runCredits() {
2180
+ const tier = detectTier();
2181
+ const limits = getPlanLimits(tier);
2182
+ const usage = loadUsage();
2183
+ const used = usage.mutations;
2184
+ const cap = limits.monthlyMutations;
2185
+ const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
2186
+ const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
2187
+ const lines = [];
2188
+ lines.push("");
2189
+ lines.push(chalk5.cyan.bold(" Pretense Credits"));
2190
+ lines.push("");
2191
+ lines.push(` Plan: ${chalk5.bold(PLAN_LABEL2[tier])}`);
2192
+ lines.push(` Month: ${usage.month}`);
2193
+ lines.push("");
2194
+ lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
2195
+ lines.push(` Used: ${pct}%`);
2196
+ lines.push(` Remaining: ${fmt3(remaining)}`);
2197
+ lines.push("");
2198
+ if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
2199
+ lines.push(chalk5.yellow(" \u26A0 Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
2200
+ lines.push("");
2201
+ } else if (tier === "free") {
2202
+ lines.push(chalk5.gray(" Tip: Run 'pretense upgrade' to compare plans."));
2203
+ lines.push("");
2204
+ }
2205
+ process.stdout.write(lines.join("\n") + "\n");
2206
+ }
2207
+
2208
+ // src/commands/login.ts
2209
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
2210
+ import { join as join4 } from "path";
2211
+ import { homedir as homedir2 } from "os";
2212
+ import chalk6 from "chalk";
2213
+ var CONFIG_DIR_NAME2 = ".pretense";
2214
+ var CONFIG_FILE2 = "config.json";
2215
+ function getUserConfigDir() {
2216
+ return join4(homedir2(), CONFIG_DIR_NAME2);
2217
+ }
2218
+ function isValidKeyShape(key) {
2219
+ const trimmed = key.trim();
2220
+ if (trimmed.length < 24) return false;
2221
+ if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
2222
+ return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_");
2223
+ }
2224
+ function readStdinIfPiped() {
2225
+ if (process.stdin.isTTY) return "";
2226
+ try {
2227
+ const chunks = [];
2228
+ const buf = Buffer.alloc(4096);
2229
+ let bytesRead = 0;
2230
+ do {
2231
+ try {
2232
+ bytesRead = readSync(0, buf, 0, buf.length, null);
2233
+ } catch {
2234
+ bytesRead = 0;
2235
+ }
2236
+ if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
2237
+ } while (bytesRead === buf.length);
2238
+ return Buffer.concat(chunks).toString("utf-8").trim();
2239
+ } catch {
2240
+ return "";
2241
+ }
2242
+ }
2243
+ function maskKey(key) {
2244
+ const trimmed = key.trim();
2245
+ if (trimmed.length <= 12) return trimmed;
2246
+ return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
2247
+ }
2248
+ function saveApiKey(key, configDir = getUserConfigDir()) {
2249
+ if (!existsSync5(configDir)) {
2250
+ mkdirSync5(configDir, { recursive: true });
2251
+ }
2252
+ const path = join4(configDir, CONFIG_FILE2);
2253
+ let existing = {};
2254
+ if (existsSync5(path)) {
2255
+ try {
2256
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
2257
+ } catch {
2258
+ existing = {};
2259
+ }
2260
+ }
2261
+ const next = {
2262
+ ...existing,
2263
+ api_key: key.trim(),
2264
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
2265
+ };
2266
+ writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
2267
+ try {
2268
+ chmodSync(path, 384);
2269
+ } catch {
2270
+ }
2271
+ return path;
2272
+ }
2273
+ function runLogin(opts = {}) {
2274
+ const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
2275
+ if (!candidate) {
2276
+ console.error(chalk6.red("Error: no API key provided."));
2277
+ console.error(
2278
+ chalk6.gray("Try: pretense login --key pk_live_xxxxx")
2279
+ );
2280
+ console.error(
2281
+ chalk6.gray(" or: echo $PRETENSE_API_KEY | pretense login")
2282
+ );
2283
+ return 1;
2284
+ }
2285
+ if (!isValidKeyShape(candidate)) {
2286
+ console.error(chalk6.red("Error: that does not look like a Pretense API key."));
2287
+ console.error(
2288
+ chalk6.gray("Keys start with pk_live_ and are at least 24 characters long.")
2289
+ );
2290
+ console.error(
2291
+ chalk6.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
2292
+ );
2293
+ return 1;
2294
+ }
2295
+ const path = saveApiKey(candidate, opts.configDir);
2296
+ console.log(chalk6.green("Saved API key to"), chalk6.bold(path));
2297
+ console.log(chalk6.gray(` ${maskKey(candidate)}`));
2298
+ console.log(chalk6.gray(" File mode 600 \u2014 readable only by you."));
2299
+ return 0;
1387
2300
  }
1388
2301
 
1389
2302
  // src/index.ts
2303
+ {
2304
+ const major = parseInt(process.versions.node.split(".")[0], 10);
2305
+ if (Number.isNaN(major) || major < 18) {
2306
+ process.stderr.write(
2307
+ `pretense requires Node.js 18 or newer (you have v${process.versions.node}).
2308
+ `
2309
+ );
2310
+ process.stderr.write(`Upgrade via nvm: nvm install 20 && nvm use 20
2311
+ `);
2312
+ process.exit(1);
2313
+ }
2314
+ }
1390
2315
  var VERSION = "0.3.0";
2316
+ var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
2317
+ var PROGRESS_INTERVAL_MS = 500;
2318
+ var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
2319
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".rs"]);
2320
+ var SUPPORTED_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
2321
+ ".json",
2322
+ ".yaml",
2323
+ ".yml",
2324
+ ".toml",
2325
+ ".env",
2326
+ ".cfg",
2327
+ ".ini",
2328
+ ".tf",
2329
+ ".tfstate",
2330
+ ".tfvars",
2331
+ ".hcl",
2332
+ ".properties",
2333
+ ".conf",
2334
+ ".xml",
2335
+ ".plist",
2336
+ ".pem",
2337
+ ".key",
2338
+ ".crt",
2339
+ ".pub",
2340
+ ".pfx",
2341
+ ".p12",
2342
+ ".htpasswd"
2343
+ ]);
2344
+ var SUPPORTED_FILENAMES = /* @__PURE__ */ new Set([
2345
+ ".env",
2346
+ ".env.local",
2347
+ ".env.production",
2348
+ ".env.development",
2349
+ ".env.staging",
2350
+ ".env.test",
2351
+ ".npmrc",
2352
+ ".netrc",
2353
+ ".pypirc",
2354
+ "Dockerfile",
2355
+ "docker-compose.yml",
2356
+ "docker-compose.yaml"
2357
+ ]);
2358
+ function isScannableFile(filePath) {
2359
+ const ext = extname(filePath).toLowerCase();
2360
+ return SUPPORTED_EXTENSIONS.has(ext) || SUPPORTED_CONFIG_EXTENSIONS.has(ext) || SUPPORTED_FILENAMES.has(basename(filePath));
2361
+ }
2362
+ function isSourceCodeFile(filePath) {
2363
+ return SUPPORTED_EXTENSIONS.has(extname(filePath).toLowerCase());
2364
+ }
1391
2365
  function langFromExt(filePath) {
1392
2366
  const ext = extname(filePath).toLowerCase();
1393
2367
  const map = {
@@ -1399,26 +2373,82 @@ function langFromExt(filePath) {
1399
2373
  ".cjs": "javascript",
1400
2374
  ".py": "python",
1401
2375
  ".go": "go",
1402
- ".java": "java"
2376
+ ".java": "java",
2377
+ ".cs": "csharp",
2378
+ ".rb": "ruby",
2379
+ ".rs": "rust"
1403
2380
  };
1404
2381
  return map[ext] ?? "typescript";
1405
2382
  }
2383
+ function validatePort(value) {
2384
+ const port = parseInt(value, 10);
2385
+ if (isNaN(port) || port.toString() !== value.trim()) return null;
2386
+ if (port < 1 || port > 65535) return null;
2387
+ return port;
2388
+ }
2389
+ function isBinaryFile(filePath) {
2390
+ let fd;
2391
+ try {
2392
+ fd = openSync(filePath, "r");
2393
+ const buf = Buffer.alloc(8192);
2394
+ const bytesRead = readSync2(fd, buf, 0, 8192, 0);
2395
+ for (let i = 0; i < bytesRead; i++) {
2396
+ if (buf[i] === 0) return true;
2397
+ }
2398
+ return false;
2399
+ } catch {
2400
+ return false;
2401
+ } finally {
2402
+ if (fd !== void 0) closeSync(fd);
2403
+ }
2404
+ }
2405
+ async function isBinaryFileAsync(filePath) {
2406
+ let handle;
2407
+ try {
2408
+ handle = await fsOpen(filePath, "r");
2409
+ const buf = Buffer.alloc(8192);
2410
+ const { bytesRead } = await handle.read(buf, 0, 8192, 0);
2411
+ for (let i = 0; i < bytesRead; i++) {
2412
+ if (buf[i] === 0) return true;
2413
+ }
2414
+ return false;
2415
+ } catch {
2416
+ return false;
2417
+ } finally {
2418
+ if (handle) await handle.close().catch(() => {
2419
+ });
2420
+ }
2421
+ }
2422
+ function validateLanguage(lang) {
2423
+ const normalized = lang.toLowerCase();
2424
+ const aliases = {
2425
+ ts: "typescript",
2426
+ tsx: "typescript",
2427
+ js: "javascript",
2428
+ jsx: "javascript",
2429
+ py: "python",
2430
+ cs: "csharp",
2431
+ rb: "ruby",
2432
+ rs: "rust"
2433
+ };
2434
+ const resolved = aliases[normalized] ?? normalized;
2435
+ return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
2436
+ }
1406
2437
  function collectFiles(target) {
1407
2438
  const abs = resolve2(target);
1408
- if (!existsSync4(abs)) {
2439
+ if (!existsSync6(abs)) {
1409
2440
  return [];
1410
2441
  }
1411
2442
  const stat = statSync(abs);
1412
2443
  if (stat.isFile()) return [abs];
1413
2444
  const files = [];
1414
- const supportedExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java"]);
1415
2445
  function walk(dir) {
1416
2446
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
1417
- const fullPath = join3(dir, entry.name);
2447
+ const fullPath = join5(dir, entry.name);
1418
2448
  if (entry.isDirectory()) {
1419
2449
  if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
1420
2450
  walk(fullPath);
1421
- } else if (entry.isFile() && supportedExts.has(extname(entry.name).toLowerCase())) {
2451
+ } else if (entry.isFile() && isScannableFile(entry.name)) {
1422
2452
  files.push(fullPath);
1423
2453
  }
1424
2454
  }
@@ -1427,10 +2457,12 @@ function collectFiles(target) {
1427
2457
  return files;
1428
2458
  }
1429
2459
  var program = new Command();
1430
- program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION);
2460
+ program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
2461
+ printBanner();
2462
+ });
1431
2463
  program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
1432
2464
  const dir = resolve2(opts.dir);
1433
- console.log(chalk.cyan("Initializing Pretense in"), chalk.bold(dir));
2465
+ console.log(chalk7.cyan("Initializing Pretense in"), chalk7.bold(dir));
1434
2466
  const configDir = initConfig(dir);
1435
2467
  const files = collectFiles(dir);
1436
2468
  const langCounts = {};
@@ -1438,83 +2470,246 @@ program.command("init").description("Initialize .pretense/ config in current dir
1438
2470
  const lang = langFromExt(f);
1439
2471
  langCounts[lang] = (langCounts[lang] ?? 0) + 1;
1440
2472
  }
1441
- console.log(chalk.green("\n .pretense/ created at"), chalk.bold(configDir));
1442
- console.log(chalk.gray(`
2473
+ console.log(chalk7.green("\n .pretense/ created at"), chalk7.bold(configDir));
2474
+ console.log(chalk7.gray(`
1443
2475
  Scanned ${files.length} source files:`));
1444
2476
  for (const [lang, count] of Object.entries(langCounts)) {
1445
- console.log(chalk.gray(` ${lang}: ${count} files`));
2477
+ console.log(chalk7.gray(` ${lang}: ${count} files`));
1446
2478
  }
1447
- console.log(chalk.cyan("\n Next: run"), chalk.bold("pretense start"), chalk.cyan("to launch the proxy"));
2479
+ console.log(chalk7.cyan("\n Next: run"), chalk7.bold("pretense start"), chalk7.cyan("to launch the proxy"));
1448
2480
  console.log();
1449
2481
  });
1450
2482
  program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action((opts) => {
1451
2483
  const config = loadConfig();
1452
- const port = parseInt(opts.port) || config.port;
2484
+ const port = validatePort(opts.port);
2485
+ if (port === null) {
2486
+ console.error(chalk7.red("Error: Invalid port number:"), chalk7.bold(opts.port));
2487
+ console.error(chalk7.gray("Port must be an integer between 1 and 65535."));
2488
+ process.exit(1);
2489
+ }
1453
2490
  startProxy({ config, port, verbose: opts.verbose });
1454
2491
  });
1455
- program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).action((target, opts) => {
1456
- const files = collectFiles(target);
1457
- if (files.length === 0) {
1458
- console.error(chalk.red("No source files found at"), chalk.bold(target));
1459
- process.exit(1);
2492
+ async function scanOneFile(file, opts) {
2493
+ let size = 0;
2494
+ try {
2495
+ const st = await fsStat(file);
2496
+ size = st.size;
2497
+ if (st.size > opts.maxFileSize) {
2498
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "too-large", sizeBytes: size };
2499
+ }
2500
+ } catch {
2501
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error" };
2502
+ }
2503
+ if (!isSourceCodeFile(file)) {
2504
+ try {
2505
+ if (await isBinaryFileAsync(file)) {
2506
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "binary", sizeBytes: size };
2507
+ }
2508
+ } catch {
2509
+ }
2510
+ }
2511
+ let code;
2512
+ try {
2513
+ code = await readFile(file, "utf-8");
2514
+ } catch {
2515
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error", sizeBytes: size };
1460
2516
  }
1461
- let totalIdentifiers = 0;
1462
- let totalSecrets = 0;
1463
- const allResults = [];
1464
- for (const file of files) {
1465
- const code = readFileSync4(file, "utf-8");
1466
- const lang = langFromExt(file);
1467
- let identifiers = 0;
1468
- if (!opts.secretsOnly) {
2517
+ const lang = langFromExt(file);
2518
+ let identifiers = 0;
2519
+ if (!opts.secretsOnly && isSourceCodeFile(file)) {
2520
+ try {
1469
2521
  const scanResult = scan(code, lang);
1470
2522
  identifiers = scanResult.tokens.length;
1471
- totalIdentifiers += identifiers;
2523
+ } catch {
1472
2524
  }
2525
+ }
2526
+ let blocked = [];
2527
+ try {
1473
2528
  const secretResult = scan2(code);
1474
- const blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
1475
- totalSecrets += blocked.length;
1476
- allResults.push({
1477
- file,
1478
- identifiers,
1479
- secrets: blocked.length,
1480
- secretTypes: blocked.map((m) => m.type)
1481
- });
2529
+ blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
2530
+ } catch {
2531
+ }
2532
+ return {
2533
+ file,
2534
+ identifiers,
2535
+ secrets: blocked.length,
2536
+ secretTypes: blocked.map((m) => m.type),
2537
+ sizeBytes: size
2538
+ };
2539
+ }
2540
+ async function scanFilesParallel(files, opts) {
2541
+ const results = [];
2542
+ let cursor = 0;
2543
+ let done = 0;
2544
+ let lastPrint = 0;
2545
+ let interrupted = false;
2546
+ const stopHandler = () => {
2547
+ interrupted = true;
2548
+ };
2549
+ process.on("SIGINT", stopHandler);
2550
+ process.on("SIGTERM", stopHandler);
2551
+ const isTTY = process.stderr.isTTY === true;
2552
+ const printProgress = (force = false) => {
2553
+ if (opts.quiet) return;
2554
+ const now = Date.now();
2555
+ if (!force && now - lastPrint < PROGRESS_INTERVAL_MS) return;
2556
+ lastPrint = now;
2557
+ const found = results.reduce((acc, r) => acc + r.secrets, 0);
2558
+ const msg = `Scanning ${done}/${files.length} files (${found} secrets so far)...`;
2559
+ if (isTTY) {
2560
+ process.stderr.write(`\r\x1B[K${msg}`);
2561
+ } else {
2562
+ process.stderr.write(`${msg}
2563
+ `);
2564
+ }
2565
+ };
2566
+ async function worker() {
2567
+ while (!interrupted) {
2568
+ const i = cursor++;
2569
+ if (i >= files.length) return;
2570
+ const file = files[i];
2571
+ const row = await scanOneFile(file, { secretsOnly: opts.secretsOnly, maxFileSize: opts.maxFileSize });
2572
+ if (row) results.push(row);
2573
+ done++;
2574
+ printProgress();
2575
+ }
2576
+ }
2577
+ printProgress(true);
2578
+ const workers = Array.from({ length: Math.min(opts.concurrency, files.length || 1) }, () => worker());
2579
+ await Promise.all(workers);
2580
+ process.removeListener("SIGINT", stopHandler);
2581
+ process.removeListener("SIGTERM", stopHandler);
2582
+ if (!opts.quiet && isTTY) {
2583
+ process.stderr.write(`\r\x1B[K`);
2584
+ }
2585
+ return { results, interrupted };
2586
+ }
2587
+ function parseSizeOption(value) {
2588
+ const trimmed = value.trim().toLowerCase();
2589
+ const m = /^(\d+(?:\.\d+)?)\s*(b|k|kb|m|mb|g|gb)?$/.exec(trimmed);
2590
+ if (!m) return null;
2591
+ const n = parseFloat(m[1]);
2592
+ if (!isFinite(n) || n <= 0) return null;
2593
+ const unit = m[2] ?? "b";
2594
+ const mult = unit.startsWith("g") ? 1024 ** 3 : unit.startsWith("m") ? 1024 ** 2 : unit.startsWith("k") ? 1024 : 1;
2595
+ return Math.floor(n * mult);
2596
+ }
2597
+ program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).option("--max-file-size <size>", "Skip files larger than this (e.g. 10mb, 500k)", "10mb").option("--concurrency <n>", "Max files scanned in parallel", String(SCAN_CONCURRENCY)).option("--quiet", "Suppress progress output", false).action(async (target, opts) => {
2598
+ const absTarget = resolve2(target);
2599
+ if (!existsSync6(absTarget)) {
2600
+ console.error(chalk7.red("Error: Path not found:"), chalk7.bold(absTarget));
2601
+ process.exit(1);
2602
+ }
2603
+ const maxFileSize = parseSizeOption(opts.maxFileSize);
2604
+ if (maxFileSize === null) {
2605
+ console.error(chalk7.red("Error: Invalid --max-file-size value:"), chalk7.bold(opts.maxFileSize));
2606
+ console.error(chalk7.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
2607
+ process.exit(1);
2608
+ }
2609
+ const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
2610
+ if (!opts.quiet && !opts.json) {
2611
+ process.stderr.write(chalk7.gray(`Discovering files in ${absTarget}...
2612
+ `));
2613
+ }
2614
+ const files = collectFiles(target);
2615
+ if (files.length === 0) {
2616
+ console.error(chalk7.red("Error: No source files found at"), chalk7.bold(absTarget));
2617
+ console.error(chalk7.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
2618
+ process.exit(1);
2619
+ }
2620
+ if (!opts.quiet && !opts.json) {
2621
+ process.stderr.write(chalk7.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
2622
+ `));
1482
2623
  }
2624
+ const startedAt = Date.now();
2625
+ const { results: allResults, interrupted } = await scanFilesParallel(files, {
2626
+ secretsOnly: opts.secretsOnly,
2627
+ maxFileSize,
2628
+ quiet: opts.quiet || opts.json,
2629
+ concurrency
2630
+ });
2631
+ const elapsedMs = Date.now() - startedAt;
2632
+ const totalIdentifiers = allResults.reduce((acc, r) => acc + r.identifiers, 0);
2633
+ const totalSecrets = allResults.reduce((acc, r) => acc + r.secrets, 0);
2634
+ const skippedLarge = allResults.filter((r) => r.skipped === "too-large").length;
2635
+ const skippedBinary = allResults.filter((r) => r.skipped === "binary").length;
1483
2636
  if (opts.json) {
1484
- console.log(JSON.stringify({ files: allResults, totalIdentifiers, totalSecrets }, null, 2));
2637
+ console.log(JSON.stringify({
2638
+ files: allResults,
2639
+ totalIdentifiers,
2640
+ totalSecrets,
2641
+ skippedLarge,
2642
+ skippedBinary,
2643
+ elapsedMs,
2644
+ interrupted
2645
+ }, null, 2));
1485
2646
  } else {
1486
- console.log(chalk.cyan("\nPretense Scan Results\n"));
2647
+ console.log(chalk7.cyan("\nPretense Scan Results\n"));
1487
2648
  for (const r of allResults) {
1488
- const secretBadge = r.secrets > 0 ? chalk.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
2649
+ if (r.skipped) {
2650
+ if (r.skipped === "too-large") {
2651
+ const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
2652
+ console.log(chalk7.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
2653
+ }
2654
+ continue;
2655
+ }
2656
+ const secretBadge = r.secrets > 0 ? chalk7.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
2657
+ const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
2658
+ if (!showRow) continue;
1489
2659
  console.log(
1490
- chalk.gray(" ") + chalk.white(r.file) + chalk.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
2660
+ chalk7.gray(" ") + chalk7.white(r.file) + chalk7.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
1491
2661
  );
1492
2662
  if (r.secrets > 0) {
1493
2663
  for (const t of r.secretTypes) {
1494
- console.log(chalk.red(` ! ${t}`));
2664
+ console.log(chalk7.red(` ! ${t}`));
1495
2665
  }
1496
2666
  }
1497
2667
  }
1498
- console.log(chalk.gray(`
1499
- Total: ${files.length} files, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk.red(`${totalSecrets} secrets found`) : chalk.green("0 secrets")));
2668
+ const skipNote = skippedLarge + skippedBinary > 0 ? chalk7.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
2669
+ console.log(
2670
+ chalk7.gray(`
2671
+ Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk7.red(`${totalSecrets} secrets found`) : chalk7.green("0 secrets")) + skipNote
2672
+ );
2673
+ if (interrupted) {
2674
+ console.log(chalk7.yellow("\n Interrupted \u2014 partial results shown above."));
2675
+ }
1500
2676
  console.log();
1501
2677
  }
2678
+ if (!opts.json) {
2679
+ printTokenFooter(allResults.length, totalIdentifiers);
2680
+ }
2681
+ if (interrupted) {
2682
+ process.exit(130);
2683
+ }
1502
2684
  if (totalSecrets > 0) {
1503
2685
  process.exit(2);
1504
2686
  }
1505
2687
  });
1506
- program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
2688
+ program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
1507
2689
  const absPath = resolve2(file);
1508
- if (!existsSync4(absPath)) {
1509
- console.error(chalk.red("File not found:"), chalk.bold(absPath));
2690
+ if (!existsSync6(absPath)) {
2691
+ console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
2692
+ process.exit(1);
2693
+ }
2694
+ if (opts.language) {
2695
+ const validLang = validateLanguage(opts.language);
2696
+ if (!validLang) {
2697
+ console.error(chalk7.red("Error: Unsupported language:"), chalk7.bold(opts.language));
2698
+ console.error(chalk7.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
2699
+ process.exit(1);
2700
+ }
2701
+ }
2702
+ if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
2703
+ console.error(chalk7.red("Error: Not a supported source file:"), chalk7.bold(absPath));
2704
+ console.error(chalk7.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
1510
2705
  process.exit(1);
1511
2706
  }
1512
- const code = readFileSync4(absPath, "utf-8");
1513
- const lang = langFromExt(absPath);
2707
+ const code = readFileSync6(absPath, "utf-8");
2708
+ const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
1514
2709
  const result = mutate(code, lang, opts.seed);
1515
2710
  if (opts.save) {
1516
2711
  const configDir = getConfigDir();
1517
- const store = new MutationStore(join3(configDir, "mutation-map.json"));
2712
+ const store = new MutationStore(join5(configDir, "mutation-map.json"));
1518
2713
  store.load();
1519
2714
  store.save({
1520
2715
  id: absPath,
@@ -1524,7 +2719,7 @@ program.command("mutate <file>").description("Mutate a file and print result to
1524
2719
  language: lang
1525
2720
  });
1526
2721
  store.persist();
1527
- process.stderr.write(chalk.gray(`Mutation map saved to ${configDir}/mutation-map.json
2722
+ process.stderr.write(chalk7.gray(`Mutation map saved to ${configDir}/mutation-map.json
1528
2723
  `));
1529
2724
  }
1530
2725
  if (opts.json) {
@@ -1535,35 +2730,44 @@ program.command("mutate <file>").description("Mutate a file and print result to
1535
2730
  }, null, 2));
1536
2731
  } else {
1537
2732
  process.stdout.write(result.mutatedCode);
1538
- process.stderr.write(chalk.gray(`
2733
+ process.stderr.write(chalk7.gray(`
1539
2734
  --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
1540
2735
  `));
2736
+ printTokenFooter(1, result.stats.tokensMutated);
1541
2737
  }
1542
2738
  });
1543
2739
  program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
1544
2740
  const absPath = resolve2(file);
1545
- if (!existsSync4(absPath)) {
1546
- console.error(chalk.red("File not found:"), chalk.bold(absPath));
2741
+ if (!existsSync6(absPath)) {
2742
+ console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
1547
2743
  process.exit(1);
1548
2744
  }
1549
- const mutatedCode = readFileSync4(absPath, "utf-8");
1550
- const mapPath = opts.map ? resolve2(opts.map) : join3(getConfigDir(), "mutation-map.json");
1551
- if (!existsSync4(mapPath)) {
1552
- console.error(chalk.red("Mutation map not found:"), chalk.bold(mapPath));
1553
- console.error(chalk.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
2745
+ const mutatedCode = readFileSync6(absPath, "utf-8");
2746
+ const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
2747
+ if (!existsSync6(mapPath)) {
2748
+ console.error(chalk7.red("Error: Mutation map not found:"), chalk7.bold(mapPath));
2749
+ console.error(chalk7.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
2750
+ process.exit(1);
2751
+ }
2752
+ let store;
2753
+ try {
2754
+ store = new MutationStore(mapPath);
2755
+ store.load();
2756
+ } catch {
2757
+ console.error(chalk7.red("Error: Failed to load mutation map:"), chalk7.bold(mapPath));
2758
+ console.error(chalk7.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
1554
2759
  process.exit(1);
1555
2760
  }
1556
- const store = new MutationStore(mapPath);
1557
- store.load();
1558
2761
  const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
1559
2762
  if (!entry) {
1560
- console.error(chalk.red("No mutation map found for this file."));
2763
+ console.error(chalk7.red("Error: No mutation map entries found."));
2764
+ console.error(chalk7.gray("Run 'pretense mutate --save <file>' first to generate a map."));
1561
2765
  process.exit(1);
1562
2766
  }
1563
2767
  const map = MutationStore.toMap(entry);
1564
2768
  const restored = reverse(mutatedCode, map);
1565
2769
  process.stdout.write(restored);
1566
- process.stderr.write(chalk.gray(`
2770
+ process.stderr.write(chalk7.gray(`
1567
2771
  --- Reversed ${map.size} mutations ---
1568
2772
  `));
1569
2773
  });
@@ -1577,5 +2781,35 @@ program.command("audit").description("View the audit log").option("--json", "Out
1577
2781
  program.command("version").description("Print Pretense CLI version").action(() => {
1578
2782
  console.log(`@pretense/cli v${VERSION}`);
1579
2783
  });
2784
+ program.command("status").description("Show current plan, monthly quota, seat usage").action(() => {
2785
+ runStatus();
2786
+ });
2787
+ program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(() => {
2788
+ runStatus();
2789
+ });
2790
+ program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(() => {
2791
+ runCredits();
2792
+ });
2793
+ program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
2794
+ runUpgrade();
2795
+ });
2796
+ program.command("credits").description("Show remaining mutation/scan budget for the current month").action(() => {
2797
+ runCredits();
2798
+ });
2799
+ program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
2800
+ const code = runLogin({ key: opts.key });
2801
+ if (code !== 0) process.exit(code);
2802
+ });
2803
+ function handleFatalError(err) {
2804
+ if (err instanceof Error && err.message) {
2805
+ console.error(chalk7.red("Error:"), err.message);
2806
+ } else {
2807
+ console.error(chalk7.red("An unexpected error occurred."));
2808
+ }
2809
+ process.exit(1);
2810
+ }
2811
+ process.on("uncaughtException", handleFatalError);
2812
+ process.on("unhandledRejection", handleFatalError);
2813
+ program.showSuggestionAfterError(true).showHelpAfterError("(use --help for available commands)");
1580
2814
  program.parse();
1581
2815
  //# sourceMappingURL=index.js.map