@pretense/cli 0.3.0 → 0.4.0

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,25 @@
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 chalk9 from "chalk";
6
+ import {
7
+ readFileSync as readFileSync7,
8
+ existsSync as existsSync7,
9
+ readdirSync,
10
+ statSync,
11
+ openSync,
12
+ readSync as readSync2,
13
+ closeSync
14
+ } from "fs";
15
+ import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
16
+ import { resolve as resolve2, extname, join as join5, basename } from "path";
17
+ import os from "os";
8
18
 
9
19
  // src/types.ts
10
20
  var DEFAULT_CONFIG = {
11
21
  port: 9339,
12
22
  targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
13
- languages: ["typescript", "javascript", "python", "go", "java"],
23
+ languages: ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"],
14
24
  mutationRules: [
15
25
  { tokenType: "Variable" /* Variable */, prefix: "_v" },
16
26
  { tokenType: "Function" /* Function */, prefix: "_fn" },
@@ -262,29 +272,200 @@ var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
262
272
  "permits",
263
273
  "yield"
264
274
  ]);
275
+ var CSHARP_KEYWORDS = /* @__PURE__ */ new Set([
276
+ "class",
277
+ "interface",
278
+ "namespace",
279
+ "public",
280
+ "private",
281
+ "protected",
282
+ "internal",
283
+ "static",
284
+ "void",
285
+ "return",
286
+ "new",
287
+ "using",
288
+ "override",
289
+ "sealed",
290
+ "abstract",
291
+ "virtual",
292
+ "readonly",
293
+ "const",
294
+ "var",
295
+ "int",
296
+ "string",
297
+ "bool",
298
+ "double",
299
+ "float",
300
+ "object",
301
+ "null",
302
+ "true",
303
+ "false",
304
+ "if",
305
+ "else",
306
+ "for",
307
+ "foreach",
308
+ "while",
309
+ "do",
310
+ "switch",
311
+ "case",
312
+ "break",
313
+ "continue",
314
+ "try",
315
+ "catch",
316
+ "finally",
317
+ "throw",
318
+ "async",
319
+ "await",
320
+ "get",
321
+ "set",
322
+ "partial",
323
+ "enum",
324
+ "struct",
325
+ "delegate",
326
+ "event"
327
+ ]);
328
+ var RUBY_KEYWORDS = /* @__PURE__ */ new Set([
329
+ "def",
330
+ "class",
331
+ "module",
332
+ "end",
333
+ "do",
334
+ "if",
335
+ "else",
336
+ "elsif",
337
+ "unless",
338
+ "while",
339
+ "until",
340
+ "for",
341
+ "begin",
342
+ "rescue",
343
+ "ensure",
344
+ "raise",
345
+ "return",
346
+ "yield",
347
+ "self",
348
+ "super",
349
+ "true",
350
+ "false",
351
+ "nil",
352
+ "and",
353
+ "or",
354
+ "not",
355
+ "in",
356
+ "then",
357
+ "case",
358
+ "when",
359
+ "puts",
360
+ "print",
361
+ "require",
362
+ "require_relative",
363
+ "include",
364
+ "extend",
365
+ "attr_accessor",
366
+ "attr_reader",
367
+ "attr_writer",
368
+ "initialize",
369
+ "new",
370
+ "public",
371
+ "private",
372
+ "protected"
373
+ ]);
374
+ var RUST_KEYWORDS = /* @__PURE__ */ new Set([
375
+ "fn",
376
+ "struct",
377
+ "enum",
378
+ "trait",
379
+ "impl",
380
+ "mod",
381
+ "pub",
382
+ "let",
383
+ "mut",
384
+ "const",
385
+ "type",
386
+ "use",
387
+ "crate",
388
+ "self",
389
+ "super",
390
+ "move",
391
+ "ref",
392
+ "match",
393
+ "if",
394
+ "else",
395
+ "loop",
396
+ "while",
397
+ "for",
398
+ "return",
399
+ "break",
400
+ "continue",
401
+ "where",
402
+ "as",
403
+ "in",
404
+ "true",
405
+ "false",
406
+ "unsafe",
407
+ "extern",
408
+ "dyn",
409
+ "box",
410
+ "static",
411
+ "async",
412
+ "await",
413
+ "Box",
414
+ "String",
415
+ "Vec",
416
+ "Option",
417
+ "Result",
418
+ "Ok",
419
+ "Err",
420
+ "Some",
421
+ "None"
422
+ ]);
265
423
  function detectLanguage(code) {
266
424
  if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
267
425
  if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
268
426
  if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
269
427
  if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
428
+ if (/\bnamespace\s+\w+/.test(code) || /\busing\s+System/.test(code)) return "csharp";
429
+ if (/\bfn\s+\w+\s*\(/.test(code) || /\blet\s+mut\s+\w+/.test(code)) return "rust";
430
+ if (/\bdef\s+\w+/.test(code) && /\bend\b/.test(code)) return "ruby";
270
431
  if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
271
432
  return "typescript";
272
433
  }
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++;
434
+ function buildLineIndex(code) {
435
+ const offsets = [];
436
+ for (let i = 0; i < code.length; i++) {
437
+ if (code.charCodeAt(i) === 10) offsets.push(i);
277
438
  }
278
- return line;
439
+ return offsets;
440
+ }
441
+ function lineFromIndex(lineOffsets, index) {
442
+ let lo = 0;
443
+ let hi = lineOffsets.length;
444
+ while (lo < hi) {
445
+ const mid = lo + hi >> 1;
446
+ if (lineOffsets[mid] < index) lo = mid + 1;
447
+ else hi = mid;
448
+ }
449
+ return lo + 1;
279
450
  }
280
451
  function scanTypeScript(code) {
281
452
  const tokens = [];
282
453
  if (!code.trim()) return tokens;
454
+ const lineOffsets = buildLineIndex(code);
283
455
  const patterns = [
284
456
  // Single-line comment
285
457
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
286
458
  // Multi-line comment
287
459
  { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
460
+ // Quoted HTTP header names (fetch/axios headers objects) — mutated, not vendor-identifying literals
461
+ { re: /"(?:x-[a-z0-9][a-z0-9-]*|authorization|api-key)"/gi, type: "HttpHeaderString" /* HttpHeaderString */ },
462
+ { re: /'(?:x-[a-z0-9][a-z0-9-]*|authorization|api-key)'/gi, type: "HttpHeaderString" /* HttpHeaderString */ },
463
+ // Unquoted `Authorization` key — value must start with a string/template (skips interface/type shapes)
464
+ {
465
+ re: /(?:\{|\,)\s*(Authorization)\s*:\s*(?=[`'"])/g,
466
+ type: "HttpHeaderString" /* HttpHeaderString */,
467
+ groupIndex: 1
468
+ },
288
469
  // Import source (the module path string)
289
470
  { re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
290
471
  // Class name
@@ -292,11 +473,18 @@ function scanTypeScript(code) {
292
473
  // Function/method name (function keyword)
293
474
  { re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
294
475
  // Arrow function assigned to const/let/var
295
- { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g, type: "Function" /* Function */, groupIndex: 1 },
476
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*[-.][A-Za-z0-9_$])\s*=\s*(?:async\s*)?\(/g, type: "Function" /* Function */, groupIndex: 1 },
296
477
  // Method: identifier followed by (
297
478
  { re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
298
- // Variable declarations
299
- { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
479
+ // Env-style binding names with hyphens or dots (e.g. AWS-SECRET-KEY, AWS.SECRET.KEY) — not valid JS identifiers but common in pasted config
480
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*(?:(?:-|\.)(?:[A-Za-z0-9_$]+))+)(?=\s*=)/g, type: "Variable" /* Variable */, groupIndex: 1 },
481
+ // Variable declarations (exclude names continued with -/. so AWS-SECRET-KEY is captured only by the rule above)
482
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*[-.][A-Za-z0-9_$])/g, type: "Variable" /* Variable */, groupIndex: 1 },
483
+ // Member receiver: matches in matches.push, row in row.patientId, def in def.name
484
+ { re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\.|\?\.)[^0-9]/g, type: "Variable" /* Variable */, groupIndex: 1 },
485
+ // Property after . or ?. when not immediately followed by ( — method calls use Method rule
486
+ { re: /\.([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
487
+ { re: /\?\.([A-Za-z_$][A-Za-z0-9_$]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
300
488
  // Template literals (preserve backtick wrapper)
301
489
  { re: /`[^`]*`/g, type: "String" /* String */ },
302
490
  // Double-quoted strings
@@ -314,23 +502,38 @@ function scanTypeScript(code) {
314
502
  if (!captureValue || captureValue.trim() === "") continue;
315
503
  if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
316
504
  if (TS_JS_KEYWORDS.has(captureValue)) continue;
317
- if (/^(console|process|Object|Array|Math|JSON|Promise|Error|Map|Set|Date|RegExp|Symbol|Proxy|Reflect|global|globalThis|window|document|module|exports|require|__dirname|__filename)$/.test(captureValue)) continue;
505
+ if (/^(console|process|Object|Array|Math|JSON|Promise|Error|Map|Set|Date|RegExp|Symbol|Proxy|Reflect|global|globalThis|window|document|module|exports|require|__dirname|__filename|Intl|WebAssembly)$/.test(captureValue)) continue;
318
506
  if (captureValue.length <= 1) continue;
319
507
  }
508
+ if (type === "Property" /* Property */ && captureValue === "meta" && code.slice(Math.max(0, captureStart - 7), captureStart) === "import.") {
509
+ continue;
510
+ }
320
511
  tokens.push({
321
512
  type,
322
513
  value: captureValue,
323
514
  start: captureStart,
324
515
  end: captureStart + captureValue.length,
325
- line: lineAt(code, captureStart)
516
+ line: lineFromIndex(lineOffsets, captureStart)
326
517
  });
327
518
  }
328
519
  }
329
- return deduplicate(tokens);
520
+ const commentSpans = tokens.filter((t) => t.type === "Comment" /* Comment */);
521
+ const stringLikeSpans = tokens.filter(
522
+ (t) => t.type === "String" /* String */ || t.type === "HttpHeaderString" /* HttpHeaderString */
523
+ );
524
+ const dropsSpanOverlap = (t) => {
525
+ if (t.type === "Variable" /* Variable */ || t.type === "Function" /* Function */ || t.type === "Class" /* Class */ || t.type === "Method" /* Method */ || t.type === "Property" /* Property */) {
526
+ if (commentSpans.some((c) => t.start < c.end && t.end > c.start)) return false;
527
+ if (stringLikeSpans.some((s) => t.start < s.end && t.end > s.start)) return false;
528
+ }
529
+ return true;
530
+ };
531
+ return deduplicate(tokens.filter(dropsSpanOverlap));
330
532
  }
331
533
  function scanPython(code) {
332
534
  const tokens = [];
333
535
  if (!code.trim()) return tokens;
536
+ const lineOffsets = buildLineIndex(code);
334
537
  const patterns = [
335
538
  // Triple-quoted docstrings
336
539
  { re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
@@ -343,6 +546,11 @@ function scanPython(code) {
343
546
  { re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
344
547
  // Function/method def
345
548
  { re: /\bdef\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Function" /* Function */, groupIndex: 1 },
549
+ // Call site: method name in obj.method(
550
+ { re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
551
+ // obj.attr receiver and attribute (matches.push-style)
552
+ { re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?=[a-zA-Z_])/g, type: "Variable" /* Variable */, groupIndex: 1 },
553
+ { re: /\.([A-Za-z_][A-Za-z0-9_]*)\b(?!\s*\()/g, type: "Property" /* Property */, groupIndex: 1 },
346
554
  // Variable assignment (simple name = ...)
347
555
  { re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
348
556
  // Double-quoted strings
@@ -358,7 +566,7 @@ function scanPython(code) {
358
566
  const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
359
567
  const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
360
568
  if (!captureValue || captureValue.trim() === "") continue;
361
- if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
569
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
362
570
  if (PYTHON_KEYWORDS.has(captureValue)) continue;
363
571
  if (captureValue.length <= 1) continue;
364
572
  if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
@@ -368,15 +576,25 @@ function scanPython(code) {
368
576
  value: captureValue,
369
577
  start: captureStart,
370
578
  end: captureStart + captureValue.length,
371
- line: lineAt(code, captureStart)
579
+ line: lineFromIndex(lineOffsets, captureStart)
372
580
  });
373
581
  }
374
582
  }
375
- return deduplicate(tokens);
583
+ const pyCommentSpans = tokens.filter((t) => t.type === "Comment" /* Comment */);
584
+ const pyStringSpans = tokens.filter((t) => t.type === "String" /* String */);
585
+ const dropsPyOverlap = (t) => {
586
+ if (t.type === "Variable" /* Variable */ || t.type === "Function" /* Function */ || t.type === "Class" /* Class */ || t.type === "Method" /* Method */ || t.type === "Property" /* Property */) {
587
+ if (pyCommentSpans.some((c) => t.start < c.end && t.end > c.start)) return false;
588
+ if (pyStringSpans.some((s) => t.start < s.end && t.end > s.start)) return false;
589
+ }
590
+ return true;
591
+ };
592
+ return deduplicate(tokens.filter(dropsPyOverlap));
376
593
  }
377
594
  function scanGo(code) {
378
595
  const tokens = [];
379
596
  if (!code.trim()) return tokens;
597
+ const lineOffsets = buildLineIndex(code);
380
598
  const patterns = [
381
599
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
382
600
  { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
@@ -413,7 +631,7 @@ function scanGo(code) {
413
631
  value: captureValue,
414
632
  start: captureStart,
415
633
  end: captureStart + captureValue.length,
416
- line: lineAt(code, captureStart)
634
+ line: lineFromIndex(lineOffsets, captureStart)
417
635
  });
418
636
  }
419
637
  }
@@ -422,6 +640,7 @@ function scanGo(code) {
422
640
  function scanJava(code) {
423
641
  const tokens = [];
424
642
  if (!code.trim()) return tokens;
643
+ const lineOffsets = buildLineIndex(code);
425
644
  const patterns = [
426
645
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
427
646
  { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
@@ -454,7 +673,137 @@ function scanJava(code) {
454
673
  value: captureValue,
455
674
  start: captureStart,
456
675
  end: captureStart + captureValue.length,
457
- line: lineAt(code, captureStart)
676
+ line: lineFromIndex(lineOffsets, captureStart)
677
+ });
678
+ }
679
+ }
680
+ return deduplicate(tokens);
681
+ }
682
+ function scanCSharp(code) {
683
+ const tokens = [];
684
+ if (!code.trim()) return tokens;
685
+ const lineOffsets = buildLineIndex(code);
686
+ const patterns = [
687
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
688
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
689
+ // Namespace/using import
690
+ { re: /\busing\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
691
+ // Class/interface/struct/enum names
692
+ { re: /\b(?:class|interface|struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
693
+ // Method/property declaration
694
+ { re: /\b(?:public|private|protected|internal|static|virtual|override|async)\s+(?:\w+\s+)+([a-zA-Z_]\w*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
695
+ // Local variable declarations
696
+ { re: /\b(?:var|int|string|bool|double|float|object)\s+([a-zA-Z_]\w*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
697
+ // Double-quoted strings
698
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
699
+ // Verbatim strings
700
+ { re: /@"(?:[^"]|"")*"/g, type: "String" /* String */ }
701
+ ];
702
+ for (const { re, type, groupIndex } of patterns) {
703
+ re.lastIndex = 0;
704
+ let m;
705
+ while ((m = re.exec(code)) !== null) {
706
+ const fullMatch = m[0];
707
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
708
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
709
+ if (!captureValue || captureValue.trim() === "") continue;
710
+ if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
711
+ if (CSHARP_KEYWORDS.has(captureValue)) continue;
712
+ if (captureValue.length <= 1) continue;
713
+ if (/^(String|Object|Integer|Boolean|Double|Float|List|Dictionary|Array|Exception|Task|Console|Math|Enum|Delegate)$/.test(captureValue)) continue;
714
+ }
715
+ tokens.push({
716
+ type,
717
+ value: captureValue,
718
+ start: captureStart,
719
+ end: captureStart + captureValue.length,
720
+ line: lineFromIndex(lineOffsets, captureStart)
721
+ });
722
+ }
723
+ }
724
+ return deduplicate(tokens);
725
+ }
726
+ function scanRuby(code) {
727
+ const tokens = [];
728
+ if (!code.trim()) return tokens;
729
+ const lineOffsets = buildLineIndex(code);
730
+ const patterns = [
731
+ // Single-line comment
732
+ { re: /#[^\n]*/g, type: "Comment" /* Comment */ },
733
+ // Method names
734
+ { re: /\bdef\s+([a-zA-Z_]\w*[!?]?)/g, type: "Function" /* Function */, groupIndex: 1 },
735
+ // Class and module names
736
+ { re: /\b(?:class|module)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
737
+ // Instance variables
738
+ { re: /@([a-zA-Z_]\w*)/g, type: "Property" /* Property */, groupIndex: 1 },
739
+ // All-caps constants (3+ chars to avoid single-letter constants)
740
+ { re: /\b([A-Z][A-Z0-9_]{2,})\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
741
+ // Double-quoted strings
742
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
743
+ // Single-quoted strings
744
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
745
+ ];
746
+ for (const { re, type, groupIndex } of patterns) {
747
+ re.lastIndex = 0;
748
+ let m;
749
+ while ((m = re.exec(code)) !== null) {
750
+ const fullMatch = m[0];
751
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
752
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
753
+ if (!captureValue || captureValue.trim() === "") continue;
754
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Property" /* Property */) {
755
+ if (RUBY_KEYWORDS.has(captureValue)) continue;
756
+ if (captureValue.length <= 1) continue;
757
+ }
758
+ tokens.push({
759
+ type,
760
+ value: captureValue,
761
+ start: captureStart,
762
+ end: captureStart + captureValue.length,
763
+ line: lineFromIndex(lineOffsets, captureStart)
764
+ });
765
+ }
766
+ }
767
+ return deduplicate(tokens);
768
+ }
769
+ function scanRust(code) {
770
+ const tokens = [];
771
+ if (!code.trim()) return tokens;
772
+ const lineOffsets = buildLineIndex(code);
773
+ const patterns = [
774
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
775
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
776
+ // Use declarations
777
+ { re: /\buse\s+([\w:]+)/g, type: "Import" /* Import */, groupIndex: 1 },
778
+ // Named type declarations (struct, enum, trait)
779
+ { re: /\b(?:struct|enum|trait)\s+([A-Z][a-zA-Z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
780
+ // Function names
781
+ { re: /\bfn\s+([a-zA-Z_]\w*)/g, type: "Function" /* Function */, groupIndex: 1 },
782
+ // Constants (ALL_CAPS)
783
+ { re: /\bconst\s+([A-Z_][A-Z0-9_]*)\s*:/g, type: "Variable" /* Variable */, groupIndex: 1 },
784
+ // Variable bindings
785
+ { re: /\blet\s+(?:mut\s+)?([a-zA-Z_]\w*)/g, type: "Variable" /* Variable */, groupIndex: 1 },
786
+ // Double-quoted strings
787
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
788
+ ];
789
+ for (const { re, type, groupIndex } of patterns) {
790
+ re.lastIndex = 0;
791
+ let m;
792
+ while ((m = re.exec(code)) !== null) {
793
+ const fullMatch = m[0];
794
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
795
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
796
+ if (!captureValue || captureValue.trim() === "") continue;
797
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
798
+ if (RUST_KEYWORDS.has(captureValue)) continue;
799
+ if (captureValue.length <= 1) continue;
800
+ }
801
+ tokens.push({
802
+ type,
803
+ value: captureValue,
804
+ start: captureStart,
805
+ end: captureStart + captureValue.length,
806
+ line: lineFromIndex(lineOffsets, captureStart)
458
807
  });
459
808
  }
460
809
  }
@@ -498,13 +847,62 @@ function scan(code, language) {
498
847
  case "java":
499
848
  tokens = scanJava(code);
500
849
  break;
850
+ case "csharp":
851
+ case "cs":
852
+ tokens = scanCSharp(code);
853
+ break;
854
+ case "ruby":
855
+ case "rb":
856
+ tokens = scanRuby(code);
857
+ break;
858
+ case "rust":
859
+ case "rs":
860
+ tokens = scanRust(code);
861
+ break;
501
862
  default:
502
863
  tokens = scanTypeScript(code);
503
864
  }
504
865
  return { tokens, language: lang };
505
866
  }
506
867
 
507
- // src/mutator.ts
868
+ // src/salt.ts
869
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
870
+ import { join } from "path";
871
+ import { homedir } from "os";
872
+ import { randomBytes } from "crypto";
873
+ var SALT_BYTES = 32;
874
+ function getSaltPath() {
875
+ const dir = join(homedir(), ".pretense");
876
+ return { dir, path: join(dir, "mutation.salt") };
877
+ }
878
+ var _cachedSalt = null;
879
+ function getMutationSalt() {
880
+ if (_cachedSalt) return _cachedSalt;
881
+ const { dir, path } = getSaltPath();
882
+ if (existsSync(path)) {
883
+ try {
884
+ const raw = readFileSync(path, "utf-8").trim();
885
+ if (/^[0-9a-f]{64}$/i.test(raw)) {
886
+ _cachedSalt = raw.toLowerCase();
887
+ return _cachedSalt;
888
+ }
889
+ } catch {
890
+ }
891
+ }
892
+ const salt = randomBytes(SALT_BYTES).toString("hex");
893
+ try {
894
+ mkdirSync(dir, { recursive: true });
895
+ writeFileSync(path, salt, { mode: 384 });
896
+ } catch {
897
+ }
898
+ _cachedSalt = salt;
899
+ return salt;
900
+ }
901
+ function buildSaltedSeed(baseSeed) {
902
+ return `${baseSeed}:${getMutationSalt()}`;
903
+ }
904
+
905
+ // src/deterministic-id.ts
508
906
  function hash32(str) {
509
907
  let h = 5381;
510
908
  for (let i = 0; i < str.length; i++) {
@@ -522,11 +920,13 @@ function toAlphaNum(n, len) {
522
920
  }
523
921
  return result;
524
922
  }
525
- function deterministicId(original, seed, prefix, len = 4) {
923
+ function deterministicSyntheticId(original, seed, prefix, len = 4) {
526
924
  const combined = `${seed}:${prefix}:${original}`;
527
925
  const h = hash32(combined);
528
926
  return prefix + toAlphaNum(h, len);
529
927
  }
928
+
929
+ // src/mutator.ts
530
930
  function prefixForType(type) {
531
931
  switch (type) {
532
932
  case "Class" /* Class */:
@@ -538,6 +938,8 @@ function prefixForType(type) {
538
938
  return "_v";
539
939
  case "Property" /* Property */:
540
940
  return "_prop";
941
+ case "HttpHeaderString" /* HttpHeaderString */:
942
+ return "_hk";
541
943
  default:
542
944
  return "_tok";
543
945
  }
@@ -553,6 +955,7 @@ function mutate(code, language, seed = "pretense") {
553
955
  stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
554
956
  };
555
957
  }
958
+ const effectiveSeed = seed === "pretense" ? buildSaltedSeed(seed) : seed;
556
959
  const { tokens } = scan(code, lang);
557
960
  const map = /* @__PURE__ */ new Map();
558
961
  for (const token of tokens) {
@@ -563,11 +966,24 @@ function mutate(code, language, seed = "pretense") {
563
966
  if (token.type === "String" /* String */) {
564
967
  continue;
565
968
  }
969
+ if (token.type === "HttpHeaderString" /* HttpHeaderString */) {
970
+ const qm = /^("|')(.+)\1$/.exec(token.value);
971
+ if (qm) {
972
+ const inner = qm[2];
973
+ const q = qm[1];
974
+ const syntheticInner = deterministicSyntheticId(inner, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
975
+ map.set(token.value, `${q}${syntheticInner}${q}`);
976
+ } else {
977
+ const syntheticInner = deterministicSyntheticId(token.value, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
978
+ map.set(token.value, syntheticInner);
979
+ }
980
+ continue;
981
+ }
566
982
  if (token.type === "Import" /* Import */) {
567
983
  continue;
568
984
  }
569
985
  const prefix = prefixForType(token.type);
570
- const synthetic = deterministicId(token.value, seed, prefix);
986
+ const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
571
987
  map.set(token.value, synthetic);
572
988
  }
573
989
  const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
@@ -595,22 +1011,31 @@ function mutate(code, language, seed = "pretense") {
595
1011
  }
596
1012
 
597
1013
  // src/reverser.ts
598
- function reverse(mutatedCode, map) {
599
- if (!mutatedCode || map.size === 0) return mutatedCode;
600
- const reverseMap = /* @__PURE__ */ new Map();
601
- for (const [original, synthetic] of map.entries()) {
602
- if (synthetic === "" || !synthetic) continue;
603
- reverseMap.set(synthetic, original);
604
- }
605
- if (reverseMap.size === 0) return mutatedCode;
606
- const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
1014
+ function reverse(mutatedCode, map, secretsMap) {
1015
+ if (!mutatedCode) return mutatedCode;
607
1016
  let result = mutatedCode;
608
- for (const [synthetic, original] of sortedEntries) {
609
- const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
610
- if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
611
- result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
612
- } else {
613
- result = result.split(synthetic).join(original);
1017
+ if (map.size > 0) {
1018
+ const reverseMap = /* @__PURE__ */ new Map();
1019
+ for (const [original, synthetic] of map.entries()) {
1020
+ if (synthetic === "" || !synthetic) continue;
1021
+ reverseMap.set(synthetic, original);
1022
+ }
1023
+ if (reverseMap.size > 0) {
1024
+ const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
1025
+ for (const [synthetic, original] of sortedEntries) {
1026
+ const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1027
+ if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
1028
+ result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
1029
+ } else {
1030
+ result = result.split(synthetic).join(original);
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ if (secretsMap && secretsMap.size > 0) {
1036
+ const sortedSecrets = [...secretsMap.entries()].sort((a, b) => b[0].length - a[0].length);
1037
+ for (const [placeholder, original] of sortedSecrets) {
1038
+ result = result.split(placeholder).join(original);
614
1039
  }
615
1040
  }
616
1041
  return result;
@@ -619,16 +1044,40 @@ function reverse(mutatedCode, map) {
619
1044
  // src/secrets.ts
620
1045
  var SECRET_PATTERNS = [
621
1046
  { name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
622
- { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-[A-Za-z0-9]{20,}/g },
1047
+ /** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with @pretense/scanner */
1048
+ { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
623
1049
  { name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
624
- { name: "aws-secret-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/g },
1050
+ /** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
1051
+ { name: "aws-secret-assignment", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bAWS_SECRET\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gd, valueGroup: 1 },
1052
+ /**
1053
+ * Env-style labels with `-` or `.` (e.g. `AWS-SECRET-KEY`, `AWS.SECRET.KEY`, long `ACCESS` forms).
1054
+ * Case-insensitive `AWS` / `SECRET` / `KEY`. Value: 40-char secret; quotes optional (matches aws-secret-key).
1055
+ */
1056
+ {
1057
+ name: "aws-secret-key-delimited",
1058
+ category: "secret",
1059
+ severity: "critical",
1060
+ defaultAction: "block",
1061
+ pattern: /\bAWS[-.]SECRET(?:[-.]ACCESS)?[-.]KEY\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gdi,
1062
+ valueGroup: 1
1063
+ },
1064
+ { name: "aws-secret-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gd, valueGroup: 1 },
625
1065
  { name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
626
1066
  { name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
627
1067
  { name: "stripe-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/g },
628
- { name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
629
- { name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
1068
+ { name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
1069
+ /** Three base64url JWT segments; payload segment is not required to start with `eyJ` (Supabase and many issuers use compact payloads). */
1070
+ { name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{4,}/g },
630
1071
  { name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
631
- { name: "generic-password", category: "credential", severity: "high", defaultAction: "redact", pattern: /(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"][^\s'"]{8,}['"]/gi },
1072
+ {
1073
+ name: "generic-password",
1074
+ category: "credential",
1075
+ severity: "high",
1076
+ defaultAction: "redact",
1077
+ // Do not match `token` / `secret` inside identifiers (e.g. GITHUB_TOKEN, AWS_SECRET): require no word/underscore before keyword.
1078
+ pattern: /(?<![A-Za-z0-9_])(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"]([^\s'"]{8,})['"]/gid,
1079
+ valueGroup: 1
1080
+ },
632
1081
  { name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
633
1082
  { name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
634
1083
  { name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
@@ -654,7 +1103,36 @@ var SECRET_PATTERNS = [
654
1103
  { name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
655
1104
  { name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
656
1105
  { 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 }
1106
+ { name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g },
1107
+ // ─── Extended patterns (v0.6 unit 8) ─────────────────────────────────────────
1108
+ { name: "google-oauth-refresh", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bGOCSPX-[A-Za-z0-9_-]{20,}\b/g },
1109
+ { 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 },
1110
+ { name: "sentry-dsn", category: "secret", severity: "high", defaultAction: "redact", pattern: /https:\/\/[a-f0-9]{32}@o\d+\.ingest\.sentry\.io\/\d+/g },
1111
+ { name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
1112
+ { 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 },
1113
+ { 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 },
1114
+ /** Stripe signing secrets are often 32+ chars; allow shorter synthetic/test tails so redaction still applies. */
1115
+ { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g },
1116
+ /** Stripe Price / product IDs (`price_…`) — identifiers, not card data, but leak billing context. */
1117
+ { name: "stripe-price-id", category: "secret", severity: "high", defaultAction: "block", pattern: /\bprice_[A-Za-z0-9_]{14,}\b/g },
1118
+ /** `FOO_PASSWORD = '…'` / `NEXT_PUBLIC_*_PASSWORD` — generic-password skips when `password` is preceded by `_`. */
1119
+ {
1120
+ name: "env-password-assignment",
1121
+ category: "credential",
1122
+ severity: "high",
1123
+ defaultAction: "block",
1124
+ pattern: /\b[A-Z][A-Z0-9_]*PASSWORD\s*=\s*['"]([^'"]{6,})['"]/gd,
1125
+ valueGroup: 1
1126
+ },
1127
+ /** Quoted common DB dialect names — stack hints when mutating env-style config. */
1128
+ {
1129
+ name: "quoted-db-dialect",
1130
+ category: "credential",
1131
+ severity: "medium",
1132
+ defaultAction: "block",
1133
+ pattern: /(['"])(postgres|mysql|redis)\1/gd,
1134
+ valueGroup: 2
1135
+ }
658
1136
  ];
659
1137
  var PII_PATTERNS = [
660
1138
  { name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
@@ -712,6 +1190,7 @@ function maskValue(value, type) {
712
1190
  return "***";
713
1191
  }
714
1192
  var matchCounter = 0;
1193
+ var ENTROPY_SCAN_MAX_BYTES = 512 * 1024;
715
1194
  function scan2(text, actionOverrides) {
716
1195
  const start = performance.now();
717
1196
  const matches = [];
@@ -720,7 +1199,17 @@ function scan2(text, actionOverrides) {
720
1199
  def.pattern.lastIndex = 0;
721
1200
  let m;
722
1201
  while ((m = def.pattern.exec(text)) !== null) {
723
- const value = m[0];
1202
+ let value = m[0];
1203
+ let start2 = m.index;
1204
+ let end = start2 + value.length;
1205
+ if (def.valueGroup !== void 0) {
1206
+ const span = m.indices?.[def.valueGroup];
1207
+ if (!span) continue;
1208
+ const [gs, ge] = span;
1209
+ value = text.slice(gs, ge);
1210
+ start2 = gs;
1211
+ end = ge;
1212
+ }
724
1213
  if (def.validate && !def.validate(value)) continue;
725
1214
  const action = actionOverrides?.[def.name] ?? def.defaultAction;
726
1215
  matchCounter++;
@@ -730,35 +1219,51 @@ function scan2(text, actionOverrides) {
730
1219
  type: def.name,
731
1220
  value,
732
1221
  masked: maskValue(value, def.name),
733
- start: m.index,
734
- end: m.index + value.length,
1222
+ start: start2,
1223
+ end,
735
1224
  severity: def.severity,
736
1225
  action
737
1226
  });
738
1227
  }
739
1228
  }
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
- });
1229
+ if (text.length <= ENTROPY_SCAN_MAX_BYTES) {
1230
+ const sortedByStart = [...matches].sort((a, b) => a.start - b.start);
1231
+ const coveredEnd = (idx) => {
1232
+ let lo = 0;
1233
+ let hi = sortedByStart.length - 1;
1234
+ let best = -1;
1235
+ while (lo <= hi) {
1236
+ const mid = lo + hi >> 1;
1237
+ if (sortedByStart[mid].start <= idx) {
1238
+ best = mid;
1239
+ lo = mid + 1;
1240
+ } else {
1241
+ hi = mid - 1;
1242
+ }
1243
+ }
1244
+ return best >= 0 ? sortedByStart[best].end : -1;
1245
+ };
1246
+ const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
1247
+ highEntropyPattern.lastIndex = 0;
1248
+ let hem;
1249
+ while ((hem = highEntropyPattern.exec(text)) !== null) {
1250
+ const value = hem[0];
1251
+ const end = hem.index + value.length;
1252
+ if (coveredEnd(hem.index) >= end) continue;
1253
+ if (isHighEntropy(value)) {
1254
+ matchCounter++;
1255
+ matches.push({
1256
+ id: `scan_${matchCounter}`,
1257
+ category: "secret",
1258
+ type: "high-entropy-string",
1259
+ value,
1260
+ masked: value.slice(0, 6) + "..." + value.slice(-4),
1261
+ start: hem.index,
1262
+ end,
1263
+ severity: "medium",
1264
+ action: actionOverrides?.["high-entropy-string"] ?? "warn"
1265
+ });
1266
+ }
762
1267
  }
763
1268
  }
764
1269
  const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
@@ -779,18 +1284,30 @@ function scan2(text, actionOverrides) {
779
1284
  };
780
1285
  }
781
1286
  function applyRedactions(text, matches) {
782
- const sorted = [...matches].sort((a, b) => b.start - a.start);
1287
+ return applyRedactionsTracked(text, matches).redactedCode;
1288
+ }
1289
+ var SECRET_LITERAL_PREFIX = "_sl";
1290
+ function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("pretense")) {
1291
+ const actionable = matches.filter((m) => m.action === "block" || m.action === "redact");
1292
+ const sorted = [...actionable].sort((a, b) => b.start - a.start);
1293
+ const secretsMap = /* @__PURE__ */ new Map();
1294
+ const placeholders = /* @__PURE__ */ new Map();
1295
+ const forward = [...actionable].sort((a, b) => a.start - b.start);
1296
+ for (const match of forward) {
1297
+ const tag = deterministicSyntheticId(`${match.id}:${match.start}`, mutationSeed, SECRET_LITERAL_PREFIX);
1298
+ placeholders.set(match.id, tag);
1299
+ secretsMap.set(tag, match.value);
1300
+ }
783
1301
  let result = text;
784
1302
  for (const match of sorted) {
785
- if (match.action === "block" || match.action === "redact") {
786
- result = result.slice(0, match.start) + `[PRETENSE:${match.type.toUpperCase()}]` + result.slice(match.end);
787
- }
1303
+ const tag = placeholders.get(match.id);
1304
+ result = result.slice(0, match.start) + tag + result.slice(match.end);
788
1305
  }
789
- return result;
1306
+ return { redactedCode: result, secretsMap };
790
1307
  }
791
1308
 
792
1309
  // src/store.ts
793
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1310
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
794
1311
  import { dirname } from "path";
795
1312
  var MutationStore = class {
796
1313
  entries = /* @__PURE__ */ new Map();
@@ -855,20 +1372,20 @@ var MutationStore = class {
855
1372
  */
856
1373
  persist() {
857
1374
  const dir = dirname(this.filePath);
858
- if (!existsSync(dir)) {
859
- mkdirSync(dir, { recursive: true });
1375
+ if (!existsSync2(dir)) {
1376
+ mkdirSync2(dir, { recursive: true });
860
1377
  }
861
1378
  const data = JSON.stringify([...this.entries.values()], null, 2);
862
- writeFileSync(this.filePath, data, "utf-8");
1379
+ writeFileSync2(this.filePath, data, "utf-8");
863
1380
  }
864
1381
  /**
865
1382
  * Load entries from the JSON file into memory.
866
1383
  * No-ops if the file doesn't exist.
867
1384
  */
868
1385
  load() {
869
- if (!existsSync(this.filePath)) return;
1386
+ if (!existsSync2(this.filePath)) return;
870
1387
  try {
871
- const raw = readFileSync(this.filePath, "utf-8");
1388
+ const raw = readFileSync2(this.filePath, "utf-8");
872
1389
  const parsed = JSON.parse(raw);
873
1390
  this.entries.clear();
874
1391
  for (const entry of parsed) {
@@ -901,8 +1418,9 @@ var MutationStore = class {
901
1418
  };
902
1419
 
903
1420
  // 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";
1421
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1422
+ import { join as join2, resolve } from "path";
1423
+ import { createHmac } from "crypto";
906
1424
  var CONFIG_DIR_NAME = ".pretense";
907
1425
  var CONFIG_FILE = "config.json";
908
1426
  var MUTATION_MAP_FILE = "mutation-map.json";
@@ -914,30 +1432,33 @@ function getConfigDir(baseDir) {
914
1432
  }
915
1433
  function initConfig(dir) {
916
1434
  const configDir = getConfigDir(dir);
917
- if (!existsSync2(configDir)) {
918
- mkdirSync2(configDir, { recursive: true });
1435
+ if (!existsSync3(configDir)) {
1436
+ mkdirSync3(configDir, { recursive: true });
919
1437
  }
920
- const configPath = join(configDir, CONFIG_FILE);
921
- if (!existsSync2(configPath)) {
1438
+ const configPath = join2(configDir, CONFIG_FILE);
1439
+ if (!existsSync3(configPath)) {
922
1440
  const config = {
923
1441
  ...DEFAULT_CONFIG,
924
1442
  storePath: configDir
925
1443
  };
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");
1444
+ writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
931
1445
  }
932
- const auditPath = join(configDir, AUDIT_LOG_FILE);
933
- if (!existsSync2(auditPath)) {
934
- writeFileSync2(auditPath, "", "utf-8");
1446
+ const mapPath = join2(configDir, MUTATION_MAP_FILE);
1447
+ if (!existsSync3(mapPath)) {
1448
+ writeFileSync3(mapPath, "[]", "utf-8");
935
1449
  }
936
- const usagePath = join(configDir, USAGE_FILE);
937
- if (!existsSync2(usagePath)) {
938
- writeFileSync2(
1450
+ const auditPath = join2(configDir, AUDIT_LOG_FILE);
1451
+ if (!existsSync3(auditPath)) {
1452
+ writeFileSync3(auditPath, "", "utf-8");
1453
+ }
1454
+ const usagePath = join2(configDir, USAGE_FILE);
1455
+ if (!existsSync3(usagePath)) {
1456
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1457
+ const month = today.slice(0, 7);
1458
+ const usageData = { month, mutations: 0, firstUseDate: today };
1459
+ writeFileSync3(
939
1460
  usagePath,
940
- JSON.stringify({ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), mutations: 0, firstUseDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }, null, 2),
1461
+ JSON.stringify({ ...usageData, date: today, checksum: computeUsageChecksum(usageData) }, null, 2),
941
1462
  "utf-8"
942
1463
  );
943
1464
  }
@@ -945,71 +1466,116 @@ function initConfig(dir) {
945
1466
  }
946
1467
  function loadConfig(dir) {
947
1468
  const configDir = getConfigDir(dir);
948
- const configPath = join(configDir, CONFIG_FILE);
949
- if (!existsSync2(configPath)) {
1469
+ const configPath = join2(configDir, CONFIG_FILE);
1470
+ if (!existsSync3(configPath)) {
950
1471
  return { ...DEFAULT_CONFIG };
951
1472
  }
952
1473
  try {
953
- const raw = readFileSync2(configPath, "utf-8");
1474
+ const raw = readFileSync3(configPath, "utf-8");
954
1475
  const parsed = JSON.parse(raw);
955
1476
  return { ...DEFAULT_CONFIG, ...parsed };
956
1477
  } catch {
957
1478
  return { ...DEFAULT_CONFIG };
958
1479
  }
959
1480
  }
1481
+ var USAGE_HMAC_SECRET = "pretense_usage_integrity_v1";
1482
+ function getUsageSecret() {
1483
+ return process.env["PRETENSE_USAGE_SECRET"] ?? USAGE_HMAC_SECRET;
1484
+ }
1485
+ function computeUsageChecksum(data) {
1486
+ const payload = JSON.stringify({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate });
1487
+ return createHmac("sha256", getUsageSecret()).update(payload).digest("hex");
1488
+ }
1489
+ function verifyUsageChecksum(data) {
1490
+ if (!data.checksum) return false;
1491
+ return data.checksum === computeUsageChecksum(data);
1492
+ }
960
1493
  function loadUsage(dir) {
961
1494
  const configDir = getConfigDir(dir);
962
- const usagePath = join(configDir, USAGE_FILE);
1495
+ const usagePath = join2(configDir, USAGE_FILE);
963
1496
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
964
- if (!existsSync2(usagePath)) {
965
- return { date: today, mutations: 0, firstUseDate: today };
1497
+ const currentMonth = today.slice(0, 7);
1498
+ if (!existsSync3(usagePath)) {
1499
+ return { month: currentMonth, mutations: 0, firstUseDate: today };
966
1500
  }
967
1501
  try {
968
- const raw = readFileSync2(usagePath, "utf-8");
1502
+ const raw = readFileSync3(usagePath, "utf-8");
969
1503
  const data = JSON.parse(raw);
970
- if (data.date !== today) {
971
- return { date: today, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1504
+ if (data.date && !data.month) {
1505
+ const legacyDate = data.date;
1506
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? legacyDate };
972
1507
  }
973
- return data;
1508
+ if (!verifyUsageChecksum({ month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate, checksum: data.checksum })) {
1509
+ process.stderr.write("[PRETENSE] Warning: usage.json integrity check failed. Resetting usage counter.\n");
1510
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1511
+ }
1512
+ if (data.month !== currentMonth) {
1513
+ return { month: currentMonth, mutations: 0, firstUseDate: data.firstUseDate ?? today };
1514
+ }
1515
+ return { month: data.month, mutations: data.mutations, firstUseDate: data.firstUseDate };
974
1516
  } catch {
975
- return { date: today, mutations: 0, firstUseDate: today };
1517
+ return { month: currentMonth, mutations: 0, firstUseDate: today };
976
1518
  }
977
1519
  }
978
1520
  function saveUsage(usage, dir) {
979
1521
  const configDir = getConfigDir(dir);
980
- if (!existsSync2(configDir)) {
981
- mkdirSync2(configDir, { recursive: true });
1522
+ if (!existsSync3(configDir)) {
1523
+ mkdirSync3(configDir, { recursive: true });
982
1524
  }
983
- const usagePath = join(configDir, USAGE_FILE);
984
- writeFileSync2(usagePath, JSON.stringify(usage, null, 2), "utf-8");
1525
+ const usagePath = join2(configDir, USAGE_FILE);
1526
+ const checksum = computeUsageChecksum(usage);
1527
+ writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
1528
+ }
1529
+ var MIN_LICENSE_KEY_LENGTH = 40;
1530
+ var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
1531
+ function isValidLicenseKey(key, prefix) {
1532
+ if (key.length < MIN_LICENSE_KEY_LENGTH) return false;
1533
+ const afterPrefix = key.slice(prefix.length);
1534
+ const hmacSecret = process.env["PRETENSE_LICENSE_SECRET"];
1535
+ if (hmacSecret) {
1536
+ const lastUnderscore = afterPrefix.lastIndexOf("_");
1537
+ if (lastUnderscore === -1) return false;
1538
+ const payload = afterPrefix.slice(0, lastUnderscore);
1539
+ const providedHmac = afterPrefix.slice(lastUnderscore + 1);
1540
+ if (!payload || !providedHmac) return false;
1541
+ if (!LICENSE_PAYLOAD_REGEX.test(payload)) return false;
1542
+ const expectedHmac = createHmac("sha256", hmacSecret).update(payload).digest("hex").slice(0, 16);
1543
+ return providedHmac === expectedHmac;
1544
+ }
1545
+ return LICENSE_PAYLOAD_REGEX.test(afterPrefix);
985
1546
  }
986
1547
  function detectTier() {
987
1548
  const key = process.env["PRETENSE_LICENSE_KEY"] ?? "";
988
- if (key.startsWith("pre_ent_")) return "enterprise";
989
- if (key.startsWith("pre_pro_")) return "pro";
1549
+ if (key.startsWith("pre_ent_") && isValidLicenseKey(key, "pre_ent_")) return "enterprise";
1550
+ if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
990
1551
  return "free";
991
1552
  }
1553
+ function tierFromDashboardPlan(plan, fallback) {
1554
+ if (plan === "ENTERPRISE") return "enterprise";
1555
+ if (plan === "PRO") return "pro";
1556
+ return fallback;
1557
+ }
992
1558
  function getTierLimits(tier) {
993
1559
  switch (tier) {
994
1560
  case "enterprise":
995
- return { dailyMutations: Infinity, auditRetentionDays: Infinity };
1561
+ return { monthlyMutations: Infinity, auditRetentionDays: Infinity };
996
1562
  case "pro":
997
- return { dailyMutations: Infinity, auditRetentionDays: 90 };
1563
+ return { monthlyMutations: Infinity, auditRetentionDays: 90 };
998
1564
  case "free":
999
1565
  default:
1000
- return { dailyMutations: 500, auditRetentionDays: 3 };
1566
+ return { monthlyMutations: 1e3, auditRetentionDays: 30 };
1001
1567
  }
1002
1568
  }
1003
1569
 
1004
1570
  // 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";
1571
+ import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync4 } from "fs";
1572
+ import { join as join3, dirname as dirname2 } from "path";
1007
1573
  function writeAuditEntry(entry, dir) {
1008
1574
  const configDir = getConfigDir(dir);
1009
- const auditPath = join2(configDir, "audit.log");
1575
+ const auditPath = join3(configDir, "audit.log");
1010
1576
  const logDir = dirname2(auditPath);
1011
- if (!existsSync3(logDir)) {
1012
- mkdirSync3(logDir, { recursive: true });
1577
+ if (!existsSync4(logDir)) {
1578
+ mkdirSync4(logDir, { recursive: true });
1013
1579
  }
1014
1580
  const line = JSON.stringify(entry) + "\n";
1015
1581
  appendFileSync(auditPath, line, "utf-8");
@@ -1028,11 +1594,11 @@ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, l
1028
1594
  function readAuditLog(options = {}) {
1029
1595
  const { limit, dir } = options;
1030
1596
  const configDir = getConfigDir(dir);
1031
- const auditPath = join2(configDir, "audit.log");
1032
- if (!existsSync3(auditPath)) {
1597
+ const auditPath = join3(configDir, "audit.log");
1598
+ if (!existsSync4(auditPath)) {
1033
1599
  return [];
1034
1600
  }
1035
- const raw = readFileSync3(auditPath, "utf-8");
1601
+ const raw = readFileSync4(auditPath, "utf-8");
1036
1602
  const lines = raw.trim().split("\n").filter(Boolean);
1037
1603
  let entries = [];
1038
1604
  for (const line of lines) {
@@ -1095,18 +1661,758 @@ function formatAudit(entries, format = "text") {
1095
1661
  import { Hono } from "hono";
1096
1662
  import { serve } from "@hono/node-server";
1097
1663
  import { nanoid } from "nanoid";
1098
- 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";
1103
- }
1104
- 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";
1109
- }
1664
+ import { createServer } from "net";
1665
+ import { existsSync as existsSync6, watch } from "fs";
1666
+
1667
+ // src/auth.ts
1668
+ import { createHash } from "crypto";
1669
+ var AUTH_HEADERS = ["authorization", "x-api-key", "x-goog-api-key"];
1670
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set([
1671
+ "/",
1672
+ "/health",
1673
+ "/stats",
1674
+ "/audit",
1675
+ "/tokens"
1676
+ ]);
1677
+ function extractApiKey(c) {
1678
+ for (const h of AUTH_HEADERS) {
1679
+ const v = c.req.header(h);
1680
+ if (v && v.length > 0) {
1681
+ return v.replace(/^Bearer\s+/i, "").trim();
1682
+ }
1683
+ }
1684
+ return null;
1685
+ }
1686
+ function sessionHash(apiKey) {
1687
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
1688
+ }
1689
+ var requireApiKey = async (c, next) => {
1690
+ if (PUBLIC_PATHS.has(c.req.path)) {
1691
+ return next();
1692
+ }
1693
+ const key = extractApiKey(c);
1694
+ if (!key) {
1695
+ return c.json(
1696
+ {
1697
+ error: {
1698
+ type: "missing_api_key",
1699
+ message: "Authorization header required (Authorization, x-api-key, or x-goog-api-key)"
1700
+ }
1701
+ },
1702
+ 401
1703
+ );
1704
+ }
1705
+ c.set("sessionHash", sessionHash(key));
1706
+ return next();
1707
+ };
1708
+
1709
+ // src/session-store.ts
1710
+ var sessions = /* @__PURE__ */ new Map();
1711
+ function getSession(hash) {
1712
+ let s = sessions.get(hash);
1713
+ if (!s) {
1714
+ s = {
1715
+ mutationMap: /* @__PURE__ */ new Map(),
1716
+ reverseMap: /* @__PURE__ */ new Map(),
1717
+ createdAt: Date.now(),
1718
+ lastUsed: Date.now()
1719
+ };
1720
+ sessions.set(hash, s);
1721
+ }
1722
+ s.lastUsed = Date.now();
1723
+ return s;
1724
+ }
1725
+ function sessionCount() {
1726
+ return sessions.size;
1727
+ }
1728
+
1729
+ // src/git-context.ts
1730
+ import { execFile } from "child_process";
1731
+ import { promisify } from "util";
1732
+ var execFileAsync = promisify(execFile);
1733
+ var GIT_TIMEOUT_MS = 2e3;
1734
+ async function runGit(args, cwd) {
1735
+ try {
1736
+ const { stdout } = await execFileAsync("git", args, {
1737
+ cwd,
1738
+ timeout: GIT_TIMEOUT_MS,
1739
+ windowsHide: true
1740
+ });
1741
+ const trimmed = stdout.trim();
1742
+ return trimmed.length > 0 ? trimmed : void 0;
1743
+ } catch {
1744
+ return void 0;
1745
+ }
1746
+ }
1747
+ function sanitizeRemote(raw) {
1748
+ if (/^[\w.-]+@[^:]+:/.test(raw) && !raw.includes("://")) {
1749
+ return raw;
1750
+ }
1751
+ try {
1752
+ const url = new URL(raw);
1753
+ url.username = "";
1754
+ url.password = "";
1755
+ return url.toString();
1756
+ } catch {
1757
+ return raw;
1758
+ }
1759
+ }
1760
+ async function collectGitContext(cwd = process.cwd()) {
1761
+ const insideRepo = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1762
+ if (insideRepo !== "true") return {};
1763
+ const [remote, branch, sha] = await Promise.all([
1764
+ runGit(["remote", "get-url", "origin"], cwd),
1765
+ runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
1766
+ runGit(["rev-parse", "HEAD"], cwd)
1767
+ ]);
1768
+ const ctx = {};
1769
+ if (remote) ctx.git_remote = sanitizeRemote(remote);
1770
+ if (branch && branch !== "HEAD") ctx.git_branch = branch;
1771
+ if (sha) ctx.git_commit_sha = sha;
1772
+ return ctx;
1773
+ }
1774
+ var CACHE_TTL_MS = 3e4;
1775
+ var cache = /* @__PURE__ */ new Map();
1776
+ async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
1777
+ const hit = cache.get(cwd);
1778
+ if (hit && hit.expiresAt > now) return hit.value;
1779
+ const value = await collectGitContext(cwd);
1780
+ cache.set(cwd, { value, expiresAt: now + CACHE_TTL_MS });
1781
+ return value;
1782
+ }
1783
+
1784
+ // src/commands/login.ts
1785
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
1786
+ import { join as join4 } from "path";
1787
+ import { homedir as homedir2 } from "os";
1788
+ import readline from "readline/promises";
1789
+ import chalk from "chalk";
1790
+ var CONFIG_DIR_NAME2 = ".pretense";
1791
+ var CONFIG_FILE2 = "config.json";
1792
+ function getUserDashboardConfigDir() {
1793
+ return join4(homedir2(), CONFIG_DIR_NAME2);
1794
+ }
1795
+ function getUserDashboardConfigPath() {
1796
+ return join4(getUserDashboardConfigDir(), CONFIG_FILE2);
1797
+ }
1798
+ function isValidKeyShape(key) {
1799
+ const trimmed = key.trim();
1800
+ if (trimmed.length < 24) return false;
1801
+ if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
1802
+ return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_") || trimmed.startsWith("prtns_live_");
1803
+ }
1804
+ function readStdinIfPiped() {
1805
+ if (process.stdin.isTTY) return "";
1806
+ try {
1807
+ const chunks = [];
1808
+ const buf = Buffer.alloc(4096);
1809
+ let bytesRead = 0;
1810
+ do {
1811
+ try {
1812
+ bytesRead = readSync(0, buf, 0, buf.length, null);
1813
+ } catch {
1814
+ bytesRead = 0;
1815
+ }
1816
+ if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
1817
+ } while (bytesRead === buf.length);
1818
+ return Buffer.concat(chunks).toString("utf-8").trim();
1819
+ } catch {
1820
+ return "";
1821
+ }
1822
+ }
1823
+ function maskKey(key) {
1824
+ const trimmed = key.trim();
1825
+ if (trimmed.length <= 12) return trimmed;
1826
+ return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
1827
+ }
1828
+ function logDashboardCredentialHint() {
1829
+ const env = process.env["PRETENSE_API_KEY"]?.trim() ?? "";
1830
+ const envOk = env.length > 0 && isValidKeyShape(env);
1831
+ const abs = getUserDashboardConfigPath();
1832
+ const fk = loadApiKey();
1833
+ if (envOk) {
1834
+ console.error(
1835
+ chalk.gray(
1836
+ `[pretense] Dashboard key source: PRETENSE_API_KEY in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
1837
+ )
1838
+ );
1839
+ } else if (fk && isValidKeyShape(fk)) {
1840
+ console.error(chalk.gray(`[pretense] Dashboard key source: ${abs} (${maskKey(fk)})`));
1841
+ } else {
1842
+ console.error(chalk.gray("[pretense] Dashboard key source: none configured."));
1843
+ }
1844
+ }
1845
+ function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
1846
+ if (!existsSync5(configDir)) {
1847
+ mkdirSync5(configDir, { recursive: true });
1848
+ }
1849
+ const path = join4(configDir, CONFIG_FILE2);
1850
+ let existing = {};
1851
+ if (existsSync5(path)) {
1852
+ try {
1853
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1854
+ } catch {
1855
+ existing = {};
1856
+ }
1857
+ }
1858
+ const next = {
1859
+ ...existing,
1860
+ api_key: key.trim(),
1861
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
1862
+ };
1863
+ writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
1864
+ try {
1865
+ chmodSync(path, 384);
1866
+ } catch {
1867
+ }
1868
+ return path;
1869
+ }
1870
+ function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
1871
+ const path = join4(configDir, CONFIG_FILE2);
1872
+ if (!existsSync5(path)) return { removed: false, path };
1873
+ let existing = {};
1874
+ try {
1875
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1876
+ } catch {
1877
+ return { removed: false, path };
1878
+ }
1879
+ const ak = existing.api_key;
1880
+ const hadKey = typeof ak === "string" && ak.trim().length > 0;
1881
+ if (!hadKey) return { removed: false, path };
1882
+ delete existing.api_key;
1883
+ delete existing.saved_at;
1884
+ writeFileSync4(path, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
1885
+ try {
1886
+ chmodSync(path, 384);
1887
+ } catch {
1888
+ }
1889
+ return { removed: true, path };
1890
+ }
1891
+ function loadApiKey(configDir = getUserDashboardConfigDir()) {
1892
+ const path = join4(configDir, CONFIG_FILE2);
1893
+ if (!existsSync5(path)) return null;
1894
+ try {
1895
+ const raw = readFileSync5(path, "utf-8");
1896
+ const parsed = JSON.parse(raw);
1897
+ if (typeof parsed.api_key === "string") {
1898
+ const t = parsed.api_key.trim();
1899
+ if (t.length > 0) return t;
1900
+ }
1901
+ } catch {
1902
+ }
1903
+ return null;
1904
+ }
1905
+ function resolveDashboardKeyCandidate() {
1906
+ const env = (process.env["PRETENSE_API_KEY"] ?? "").trim();
1907
+ if (env.length > 0) {
1908
+ if (!isValidKeyShape(env)) {
1909
+ return { key: null, fromFile: false, badEnv: true };
1910
+ }
1911
+ return { key: env, fromFile: false, badEnv: false };
1912
+ }
1913
+ const fileKey = loadApiKey();
1914
+ if (fileKey && isValidKeyShape(fileKey)) {
1915
+ return { key: fileKey.trim(), fromFile: true, badEnv: false };
1916
+ }
1917
+ return { key: null, fromFile: Boolean(fileKey), badEnv: false };
1918
+ }
1919
+ function installDashboardKeyForCurrentProcess(key) {
1920
+ const trimmed = key.trim();
1921
+ const path = saveApiKey(trimmed);
1922
+ process.env["PRETENSE_API_KEY"] = trimmed;
1923
+ return path;
1924
+ }
1925
+ async function promptDashboardApiKeyAfterFailure() {
1926
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
1927
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1928
+ try {
1929
+ const line = (await rl.question(chalk.bold("Pretense dashboard API key: "))).trim();
1930
+ return line.length > 0 ? line : null;
1931
+ } finally {
1932
+ rl.close();
1933
+ }
1934
+ }
1935
+ async function ensureDashboardKeyForStart() {
1936
+ const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
1937
+ if (badEnv) {
1938
+ console.error(chalk.red("Error: PRETENSE_API_KEY is set but is not a valid Pretense dashboard API key shape."));
1939
+ console.error(
1940
+ chalk.gray("Keys look like prtns_live_\u2026 or pk_live_\u2026. Fix the variable or unset it to use ~/.pretense/config.json.")
1941
+ );
1942
+ process.exit(1);
1943
+ }
1944
+ if (key) {
1945
+ return;
1946
+ }
1947
+ if (fromFile) {
1948
+ console.warn(
1949
+ chalk.yellow(
1950
+ "Ignoring invalid `api_key` in ~/.pretense/config.json (wrong shape). Enter a new key or run `pretense login --key ...`."
1951
+ )
1952
+ );
1953
+ }
1954
+ const stdinOk = process.stdin.isTTY;
1955
+ const stdoutOk = process.stdout.isTTY;
1956
+ if (!stdinOk || !stdoutOk) {
1957
+ console.error(chalk.red("Error: Pretense API key required to start the proxy."));
1958
+ console.error(
1959
+ chalk.gray(
1960
+ "Save one with `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
1961
+ )
1962
+ );
1963
+ process.exit(1);
1964
+ }
1965
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1966
+ try {
1967
+ console.log(chalk.cyan("No Pretense API key found in ~/.pretense/config.json or $PRETENSE_API_KEY."));
1968
+ const entered = (await rl.question(chalk.bold("Enter your Pretense API key: "))).trim();
1969
+ if (!entered) {
1970
+ console.error(chalk.red("No key entered; exiting."));
1971
+ process.exit(1);
1972
+ }
1973
+ if (!isValidKeyShape(entered)) {
1974
+ console.error(chalk.red("That does not look like a Pretense API key (expect prtns_live_\u2026 or pk_live_\u2026)."));
1975
+ console.error(chalk.gray("Get one from your Pretense dashboard, then run `pretense login --key ...`."));
1976
+ process.exit(1);
1977
+ }
1978
+ const path = installDashboardKeyForCurrentProcess(entered);
1979
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
1980
+ console.log(chalk.gray(` ${maskKey(entered)}`));
1981
+ } finally {
1982
+ rl.close();
1983
+ }
1984
+ const persisted = resolveDashboardKeyCandidate();
1985
+ if (!persisted.key || persisted.badEnv) {
1986
+ console.error(chalk.red("Error: Pretense dashboard API key could not be confirmed after login prompt."));
1987
+ process.exit(1);
1988
+ }
1989
+ }
1990
+ function runLogin(opts = {}) {
1991
+ const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
1992
+ if (!candidate) {
1993
+ console.error(chalk.red("Error: no API key provided."));
1994
+ console.error(
1995
+ chalk.gray("Try: pretense login --key pk_live_xxxxx")
1996
+ );
1997
+ console.error(
1998
+ chalk.gray(" or: echo $PRETENSE_API_KEY | pretense login")
1999
+ );
2000
+ return 1;
2001
+ }
2002
+ if (!isValidKeyShape(candidate)) {
2003
+ console.error(chalk.red("Error: that does not look like a Pretense API key."));
2004
+ console.error(
2005
+ chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
2006
+ );
2007
+ console.error(
2008
+ chalk.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
2009
+ );
2010
+ return 1;
2011
+ }
2012
+ const path = saveApiKey(candidate, opts.configDir);
2013
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
2014
+ console.log(chalk.gray(` ${maskKey(candidate)}`));
2015
+ console.log(chalk.gray(" File mode 600 \u2014 readable only by you."));
2016
+ return 0;
2017
+ }
2018
+
2019
+ // src/backend-client.ts
2020
+ function usageSummaryFromValidate(v) {
2021
+ return {
2022
+ plan: v.plan,
2023
+ monthlyMutationLimit: v.monthlyMutationLimit,
2024
+ mutationsUsed: v.mutationsUsed,
2025
+ mutationsRemaining: v.mutationsRemaining,
2026
+ requestsThisPeriod: 0,
2027
+ secretsBlockedThisPeriod: 0,
2028
+ periodResetsAt: v.periodResetsAt,
2029
+ keyExpiresAt: null,
2030
+ keyStatus: "active"
2031
+ };
2032
+ }
2033
+ var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
2034
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
2035
+ function getConfiguredApiRoot(rootOverride) {
2036
+ const raw = (rootOverride ?? process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
2037
+ if (!raw) {
2038
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2039
+ }
2040
+ let normalized = raw;
2041
+ if (!/^https?:\/\//i.test(normalized)) {
2042
+ normalized = `https://${normalized}`;
2043
+ }
2044
+ try {
2045
+ const u = new URL(normalized);
2046
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? "https://api.pretense.ai" : u.origin;
2047
+ const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
2048
+ return `${origin}${path}`;
2049
+ } catch {
2050
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2051
+ }
2052
+ }
2053
+ function getCliApiUrl(pathWithinCli, rootOverride) {
2054
+ const root = getConfiguredApiRoot(rootOverride);
2055
+ const tail = pathWithinCli.replace(/^\/+/, "");
2056
+ return `${root}/api/cli/${tail}`;
2057
+ }
2058
+ function getApiRequestTimeoutMs() {
2059
+ const raw = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
2060
+ if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
2061
+ const n = parseInt(raw, 10);
2062
+ return Math.min(Math.max(n, 3e3), 12e4);
2063
+ }
2064
+ function getApiKey() {
2065
+ const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
2066
+ if (fromEnv) return fromEnv;
2067
+ const fromFile = loadApiKey();
2068
+ if (!fromFile) return null;
2069
+ const t = fromFile.trim();
2070
+ return t.length > 0 ? t : null;
2071
+ }
2072
+ function getPretenseApiKey() {
2073
+ return getApiKey();
2074
+ }
2075
+ var cachedValidation = null;
2076
+ var lastValidationSuccessAt = 0;
2077
+ var VALIDATION_CACHE_FRESH_MS = 45e3;
2078
+ function getCachedValidation() {
2079
+ return cachedValidation;
2080
+ }
2081
+ function clearValidationCache() {
2082
+ cachedValidation = null;
2083
+ lastValidationSuccessAt = 0;
2084
+ }
2085
+ function bumpCachedUsageAfterLog(identifiersMutated) {
2086
+ if (!cachedValidation || identifiersMutated <= 0) return;
2087
+ cachedValidation.mutationsUsed += identifiersMutated;
2088
+ if (cachedValidation.mutationsRemaining !== -1) {
2089
+ cachedValidation.mutationsRemaining = Math.max(
2090
+ 0,
2091
+ cachedValidation.mutationsRemaining - identifiersMutated
2092
+ );
2093
+ }
2094
+ }
2095
+ function makeBackendReachabilityError(code, message) {
2096
+ const err = new Error(message);
2097
+ err.code = code;
2098
+ return err;
2099
+ }
2100
+ async function validateKey(opts = {}) {
2101
+ const enforceReachability = opts.enforceReachability === true;
2102
+ const now = Date.now();
2103
+ const cacheFresh = cachedValidation !== null && now - lastValidationSuccessAt < VALIDATION_CACHE_FRESH_MS;
2104
+ if (!opts.force && cacheFresh) {
2105
+ return cachedValidation;
2106
+ }
2107
+ const validateUrl = getCliApiUrl("auth/validate");
2108
+ const timeoutMs = getApiRequestTimeoutMs();
2109
+ const apiKey = getApiKey();
2110
+ if (!apiKey) {
2111
+ if (enforceReachability) {
2112
+ const err = new Error("Missing Pretense dashboard API key");
2113
+ err.code = "MISSING_API_KEY";
2114
+ cachedValidation = null;
2115
+ lastValidationSuccessAt = 0;
2116
+ throw err;
2117
+ }
2118
+ return null;
2119
+ }
2120
+ const controller = new AbortController();
2121
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2122
+ try {
2123
+ const resp = await fetch(validateUrl, {
2124
+ method: "POST",
2125
+ headers: {
2126
+ "content-type": "application/json",
2127
+ authorization: `Bearer ${apiKey}`
2128
+ },
2129
+ body: "{}",
2130
+ signal: controller.signal
2131
+ });
2132
+ if (resp.status === 401 || resp.status === 403) {
2133
+ const raw = await resp.json().catch(() => ({}));
2134
+ const msg = typeof raw.message === "string" ? raw.message : "API key rejected by server";
2135
+ const err = new Error(msg);
2136
+ err.code = typeof raw.code === "string" ? raw.code : "INVALID_API_KEY";
2137
+ cachedValidation = null;
2138
+ lastValidationSuccessAt = 0;
2139
+ throw err;
2140
+ }
2141
+ if (!resp.ok) {
2142
+ const msg = `Pretense API returned HTTP ${resp.status} from ${validateUrl}`;
2143
+ if (enforceReachability) {
2144
+ cachedValidation = null;
2145
+ lastValidationSuccessAt = 0;
2146
+ throw makeBackendReachabilityError("AUTH_BACKEND_REJECTED", msg);
2147
+ }
2148
+ return null;
2149
+ }
2150
+ let data;
2151
+ try {
2152
+ data = await resp.json();
2153
+ } catch {
2154
+ if (enforceReachability) {
2155
+ cachedValidation = null;
2156
+ lastValidationSuccessAt = 0;
2157
+ throw makeBackendReachabilityError(
2158
+ "AUTH_BACKEND_REJECTED",
2159
+ `Invalid JSON from ${validateUrl}`
2160
+ );
2161
+ }
2162
+ return null;
2163
+ }
2164
+ if (typeof data.valid === "boolean" && data.valid === false) {
2165
+ const err = new Error("API key is not valid");
2166
+ err.code = "INVALID_API_KEY";
2167
+ cachedValidation = null;
2168
+ lastValidationSuccessAt = 0;
2169
+ throw err;
2170
+ }
2171
+ cachedValidation = data;
2172
+ lastValidationSuccessAt = Date.now();
2173
+ return data;
2174
+ } catch (err) {
2175
+ if (err.code === "KEY_REVOKED" || err.code === "KEY_EXPIRED" || err.code === "INVALID_API_KEY" || err.code === "AUTH_ERROR" || err.code === "ORG_INACTIVE" || err.code === "MISSING_API_KEY" || err.code === "BACKEND_UNAVAILABLE" || err.code === "AUTH_BACKEND_REJECTED") {
2176
+ throw err;
2177
+ }
2178
+ if (enforceReachability) {
2179
+ const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS)` : `Cannot reach Pretense API at ${validateUrl}: ${err.message}`;
2180
+ cachedValidation = null;
2181
+ lastValidationSuccessAt = 0;
2182
+ throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
2183
+ }
2184
+ if (opts.verbose) {
2185
+ process.stderr.write(
2186
+ `[PRETENSE] Backend validation failed: ${err.message}
2187
+ `
2188
+ );
2189
+ }
2190
+ return null;
2191
+ } finally {
2192
+ clearTimeout(timer);
2193
+ }
2194
+ }
2195
+ async function fetchUsageSummary(opts = {}) {
2196
+ const summaryUrl = getCliApiUrl("usage/summary");
2197
+ const apiKey = getApiKey();
2198
+ if (!apiKey) return null;
2199
+ const controller = new AbortController();
2200
+ const timer = setTimeout(() => controller.abort(), getApiRequestTimeoutMs());
2201
+ try {
2202
+ const resp = await fetch(summaryUrl, {
2203
+ method: "GET",
2204
+ headers: { authorization: `Bearer ${apiKey}` },
2205
+ signal: controller.signal
2206
+ });
2207
+ if (!resp.ok) return null;
2208
+ return await resp.json();
2209
+ } catch (err) {
2210
+ if (opts.verbose) {
2211
+ process.stderr.write(
2212
+ `[PRETENSE] Usage summary fetch failed: ${err.message}
2213
+ `
2214
+ );
2215
+ }
2216
+ return null;
2217
+ } finally {
2218
+ clearTimeout(timer);
2219
+ }
2220
+ }
2221
+ async function isWithinLimits() {
2222
+ const v = getCachedValidation();
2223
+ if (!v) return { allowed: true };
2224
+ if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
2225
+ return {
2226
+ allowed: false,
2227
+ reason: `Monthly mutation limit reached (${v.mutationsUsed}/${v.monthlyMutationLimit}). Upgrade at https://pretense.ai/pricing`
2228
+ };
2229
+ }
2230
+ return { allowed: true };
2231
+ }
2232
+
2233
+ // src/log-uploader.ts
2234
+ async function uploadLog(event, options) {
2235
+ const apiRoot = options.apiUrl?.trim() || void 0;
2236
+ const endpoint = getCliApiUrl("log", apiRoot);
2237
+ const { apiKey, verbose = false, timeoutMs = getApiRequestTimeoutMs() } = options;
2238
+ if (!apiKey) return;
2239
+ const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
2240
+ const body = {
2241
+ ...event,
2242
+ ...gitCtx,
2243
+ repo_remote_url: gitCtx.git_remote,
2244
+ git_branch: gitCtx.git_branch,
2245
+ commit_sha: gitCtx.git_commit_sha
2246
+ };
2247
+ const controller = new AbortController();
2248
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2249
+ try {
2250
+ const resp = await fetch(endpoint, {
2251
+ method: "POST",
2252
+ headers: {
2253
+ "content-type": "application/json",
2254
+ authorization: `Bearer ${apiKey}`
2255
+ },
2256
+ body: JSON.stringify(body),
2257
+ signal: controller.signal
2258
+ });
2259
+ if (!resp.ok && verbose) {
2260
+ process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status}
2261
+ `);
2262
+ }
2263
+ } catch (err) {
2264
+ if (verbose) {
2265
+ const msg = err instanceof Error ? err.message : String(err);
2266
+ process.stderr.write(`[PRETENSE] log upload failed: ${msg}
2267
+ `);
2268
+ }
2269
+ } finally {
2270
+ clearTimeout(timer);
2271
+ }
2272
+ }
2273
+
2274
+ // src/start-client-instructions.ts
2275
+ import chalk2 from "chalk";
2276
+ function printStartClientInstructions(port) {
2277
+ const host = `http://localhost:${port}`;
2278
+ console.log();
2279
+ console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
2280
+ console.log(chalk2.gray("Point the CLI at the Pretense backend API (optional if using default):"));
2281
+ console.log(
2282
+ chalk2.white(`export PRETENSE_API_URL='https://api.pretense.ai'`)
2283
+ );
2284
+ console.log();
2285
+ console.log(chalk2.gray("Log in with your Pretense dashboard API key:"));
2286
+ console.log(chalk2.white("pretense login --key your-pretense-api-key"));
2287
+ console.log();
2288
+ console.log(
2289
+ chalk2.gray("Set the LLM base URL for your provider (proxy below):")
2290
+ );
2291
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL="${host}"`));
2292
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL=${host}`));
2293
+ console.log(chalk2.white(`export OPENAI_BASE_URL=${host}/v1`));
2294
+ console.log(chalk2.white(`export GEMINI_BASE_URL=${host}/v1beta`));
2295
+ console.log(chalk2.white(`export OLLAMA_HOST=${host}`));
2296
+ console.log();
2297
+ console.log(chalk2.gray("Set your real upstream LLM API key (unchanged):"));
2298
+ console.log(chalk2.white('export ANTHROPIC_API_KEY="sk-ant-API-KEY"'));
2299
+ console.log();
2300
+ console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
2301
+ console.log(
2302
+ chalk2.gray(
2303
+ "Stop the proxy: press Ctrl+C in the terminal where pretense start is running."
2304
+ )
2305
+ );
2306
+ console.log(
2307
+ chalk2.gray(
2308
+ "Remove the saved Pretense dashboard key from ~/.pretense/config.json:"
2309
+ )
2310
+ );
2311
+ console.log(chalk2.white("pretense logout"));
2312
+ console.log(
2313
+ chalk2.gray(
2314
+ "(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
2315
+ )
2316
+ );
2317
+ console.log(
2318
+ chalk2.gray(
2319
+ "PRETENSE_API_KEY set in that same terminal \u2014 then press Ctrl+C and run pretense start again."
2320
+ )
2321
+ );
2322
+ console.log();
2323
+ }
2324
+
2325
+ // src/version.ts
2326
+ import { readFileSync as readFileSync6 } from "fs";
2327
+ import { fileURLToPath } from "url";
2328
+ var cached = null;
2329
+ function getCliSemver() {
2330
+ if (cached !== null) return cached;
2331
+ try {
2332
+ const url = new URL("../package.json", import.meta.url);
2333
+ const raw = readFileSync6(fileURLToPath(url), "utf-8");
2334
+ cached = JSON.parse(raw).version ?? "0.0.0";
2335
+ } catch {
2336
+ cached = "0.0.0";
2337
+ }
2338
+ return cached;
2339
+ }
2340
+
2341
+ // src/proxy.ts
2342
+ var dashboardSavedCredentialsRevoked = false;
2343
+ function attachDashboardLogoutWatcher(enabled) {
2344
+ if (!enabled) return;
2345
+ const dir = getUserDashboardConfigDir();
2346
+ let debounceTimer;
2347
+ const applyFilesystemCredentialState = () => {
2348
+ const fk = loadApiKey();
2349
+ if (!fk) {
2350
+ if (dashboardSavedCredentialsRevoked) return;
2351
+ dashboardSavedCredentialsRevoked = true;
2352
+ delete process.env["PRETENSE_API_KEY"];
2353
+ clearValidationCache();
2354
+ try {
2355
+ process.stderr.write(
2356
+ `\x1B[33m[PRETENSE] Saved dashboard API key removed (~/.pretense/config.json, e.g. pretense logout). LLM proxy POST requests are disabled until you run pretense login and restart pretense start.\x1B[0m
2357
+ `
2358
+ );
2359
+ } catch {
2360
+ }
2361
+ return;
2362
+ }
2363
+ if (dashboardSavedCredentialsRevoked) {
2364
+ dashboardSavedCredentialsRevoked = false;
2365
+ clearValidationCache();
2366
+ }
2367
+ };
2368
+ const schedule = () => {
2369
+ if (debounceTimer) clearTimeout(debounceTimer);
2370
+ debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
2371
+ };
2372
+ try {
2373
+ if (!existsSync6(dir)) return;
2374
+ watch(dir, () => {
2375
+ schedule();
2376
+ });
2377
+ } catch {
2378
+ }
2379
+ }
2380
+ function anthropicUpstream() {
2381
+ return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
2382
+ }
2383
+ function openaiUpstream() {
2384
+ return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
2385
+ }
2386
+ function geminiUpstream() {
2387
+ return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
2388
+ }
2389
+ function ollamaUpstream() {
2390
+ return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
2391
+ }
2392
+ function detectUpstream(path) {
2393
+ if (path.startsWith("/v1/messages")) return anthropicUpstream();
2394
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
2395
+ return openaiUpstream();
2396
+ }
2397
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
2398
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
2399
+ return anthropicUpstream();
2400
+ }
2401
+ function detectProvider(path) {
2402
+ if (path.startsWith("/v1/messages")) return "anthropic";
2403
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
2404
+ return "openai";
2405
+ }
2406
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
2407
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
2408
+ return "anthropic";
2409
+ }
2410
+ function stripPretenseOnlyForwardingHeaders(headers) {
2411
+ const drop = /* @__PURE__ */ new Set(["x-pretense-api-key", "x-pretense-key"]);
2412
+ for (const k of [...Object.keys(headers)]) {
2413
+ if (drop.has(k.toLowerCase())) delete headers[k];
2414
+ }
2415
+ }
1110
2416
  function extractText(body) {
1111
2417
  const parts = [];
1112
2418
  if (typeof body["system"] === "string") parts.push(body["system"]);
@@ -1184,11 +2490,29 @@ function createProxy(opts = {}) {
1184
2490
  const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
1185
2491
  const verbose = opts.verbose ?? false;
1186
2492
  const app = new Hono();
2493
+ app.use("*", requireApiKey);
2494
+ app.get(
2495
+ "/",
2496
+ (c) => c.json({
2497
+ name: "pretense",
2498
+ version: getCliSemver(),
2499
+ status: "running",
2500
+ routes: {
2501
+ "GET /": "This welcome page",
2502
+ "GET /health": "Health check with uptime",
2503
+ "GET /stats": "Mutation statistics",
2504
+ "GET /audit": "Audit log (?limit=50)",
2505
+ "POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
2506
+ },
2507
+ languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
2508
+ docs: "https://pretense.ai/docs"
2509
+ })
2510
+ );
1187
2511
  app.get(
1188
2512
  "/health",
1189
2513
  (c) => c.json({
1190
2514
  status: "ok",
1191
- version: "0.3.0",
2515
+ version: getCliSemver(),
1192
2516
  uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
1193
2517
  stats: {
1194
2518
  requestsProcessed: stats.requestsProcessed,
@@ -1200,7 +2524,8 @@ function createProxy(opts = {}) {
1200
2524
  "/stats",
1201
2525
  (c) => c.json({
1202
2526
  ...stats,
1203
- storeSize: store.size
2527
+ storeSize: store.size,
2528
+ activeSessions: sessionCount()
1204
2529
  })
1205
2530
  );
1206
2531
  app.get("/audit", (c) => {
@@ -1217,6 +2542,7 @@ function createProxy(opts = {}) {
1217
2542
  });
1218
2543
  delete headers2["host"];
1219
2544
  headers2["host"] = new URL(upstream2).host;
2545
+ stripPretenseOnlyForwardingHeaders(headers2);
1220
2546
  const resp = await fetch(url, { method: c.req.method, headers: headers2 });
1221
2547
  return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
1222
2548
  }
@@ -1233,20 +2559,106 @@ function createProxy(opts = {}) {
1233
2559
  const requestId = nanoid(12);
1234
2560
  const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
1235
2561
  const provider = detectProvider(c.req.path);
1236
- const tier = detectTier();
1237
- const { dailyMutations } = getTierLimits(tier);
2562
+ if (dashboardSavedCredentialsRevoked) {
2563
+ return c.json(
2564
+ {
2565
+ error: {
2566
+ type: "pretense_session_logged_out",
2567
+ message: "Saved Pretense dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with PRETENSE_API_KEY in this shell, stop it (Ctrl+C) and start again after logout."
2568
+ }
2569
+ },
2570
+ 401
2571
+ );
2572
+ }
2573
+ const pretenseDashboardKey = getPretenseApiKey();
2574
+ if (!pretenseDashboardKey) {
2575
+ return c.json(
2576
+ {
2577
+ error: {
2578
+ type: "pretense_dashboard_key_required",
2579
+ message: "Configure your Pretense dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
2580
+ }
2581
+ },
2582
+ 401
2583
+ );
2584
+ }
2585
+ try {
2586
+ await validateKey({ verbose, force: false, enforceReachability: true });
2587
+ } catch (err) {
2588
+ const code = err.code ?? "";
2589
+ const msg = err.message;
2590
+ const knownAuthCodes = /* @__PURE__ */ new Set([
2591
+ "KEY_REVOKED",
2592
+ "KEY_EXPIRED",
2593
+ "INVALID_API_KEY",
2594
+ "MISSING_API_KEY"
2595
+ ]);
2596
+ if (knownAuthCodes.has(code)) {
2597
+ return c.json(
2598
+ { error: { type: "pretense_auth_error", code, message: msg } },
2599
+ 401
2600
+ );
2601
+ }
2602
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2603
+ return c.json(
2604
+ { error: { type: "pretense_auth_backend_error", code, message: msg } },
2605
+ 503
2606
+ );
2607
+ }
2608
+ if (code === "ORG_INACTIVE") {
2609
+ return c.json(
2610
+ { error: { type: "pretense_org_inactive", code, message: msg } },
2611
+ 403
2612
+ );
2613
+ }
2614
+ return c.json(
2615
+ { error: { type: "pretense_auth_error", message: msg } },
2616
+ 401
2617
+ );
2618
+ }
2619
+ const sessHash = c.get("sessionHash");
2620
+ const session = getSession(sessHash);
2621
+ const tier = tierFromDashboardPlan(
2622
+ getCachedValidation()?.plan,
2623
+ detectTier()
2624
+ );
2625
+ const { monthlyMutations } = getTierLimits(tier);
1238
2626
  const usage = loadUsage();
1239
- if (tier === "free" && usage.mutations >= dailyMutations) {
2627
+ if (tier === "free" && usage.mutations >= monthlyMutations) {
1240
2628
  return c.json(
1241
2629
  {
1242
2630
  error: {
1243
2631
  type: "pretense_rate_limit",
1244
- message: `Daily mutation limit reached (${dailyMutations}/day). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
2632
+ message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
1245
2633
  }
1246
2634
  },
1247
2635
  429
1248
2636
  );
1249
2637
  }
2638
+ const backendLimits = await isWithinLimits();
2639
+ if (!backendLimits.allowed) {
2640
+ return c.json(
2641
+ {
2642
+ error: {
2643
+ type: "pretense_rate_limit",
2644
+ message: backendLimits.reason ?? "Mutation limit exceeded."
2645
+ }
2646
+ },
2647
+ 429
2648
+ );
2649
+ }
2650
+ const cliAuth = getCachedValidation();
2651
+ if (cliAuth?.permission === "read_only") {
2652
+ return c.json(
2653
+ {
2654
+ error: {
2655
+ type: "pretense_forbidden",
2656
+ message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretense proxy."
2657
+ }
2658
+ },
2659
+ 403
2660
+ );
2661
+ }
1250
2662
  const fullText = extractText(body);
1251
2663
  const secretScan = scan2(fullText);
1252
2664
  const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
@@ -1270,11 +2682,25 @@ function createProxy(opts = {}) {
1270
2682
  let tokensMutated = 0;
1271
2683
  const mutatedBody = applyToBody(processedBody, (text) => {
1272
2684
  const blocks = extractCodeBlocks(text);
1273
- if (blocks.length === 0) return text;
2685
+ if (blocks.length === 0) {
2686
+ const lang = detectLanguage(text);
2687
+ const result = mutate(text, lang);
2688
+ for (const [k, v] of result.map) {
2689
+ mutationMap.set(k, v);
2690
+ session.mutationMap.set(k, v);
2691
+ session.reverseMap.set(v, k);
2692
+ }
2693
+ tokensMutated += result.stats.tokensMutated;
2694
+ return result.mutatedCode;
2695
+ }
1274
2696
  const mutatedBlocks = [];
1275
2697
  for (const block of blocks) {
1276
2698
  const result = mutate(block.code, block.language);
1277
- for (const [k, v] of result.map.entries()) mutationMap.set(k, v);
2699
+ for (const [k, v] of result.map) {
2700
+ mutationMap.set(k, v);
2701
+ session.mutationMap.set(k, v);
2702
+ session.reverseMap.set(v, k);
2703
+ }
1278
2704
  tokensMutated += result.stats.tokensMutated;
1279
2705
  mutatedBlocks.push(result.mutatedCode);
1280
2706
  }
@@ -1282,8 +2708,23 @@ function createProxy(opts = {}) {
1282
2708
  });
1283
2709
  usage.mutations += tokensMutated;
1284
2710
  saveUsage(usage);
1285
- if (tier === "free" && usage.mutations >= 400 && usage.mutations - tokensMutated < 400) {
1286
- printUpgradeNudge(`${usage.mutations}/${dailyMutations} daily mutations used. Running low!`);
2711
+ const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
2712
+ if (pretenseDashboardKey && !remoteLogDisabled) {
2713
+ void uploadLog(
2714
+ {
2715
+ file_count: 0,
2716
+ identifiers_mutated: tokensMutated,
2717
+ secrets_blocked: blockedSecrets.length,
2718
+ llm_provider: provider,
2719
+ cli_version: getCliSemver()
2720
+ },
2721
+ { apiKey: pretenseDashboardKey, verbose }
2722
+ );
2723
+ bumpCachedUsageAfterLog(tokensMutated);
2724
+ }
2725
+ const warningThreshold = Math.floor(monthlyMutations * 0.8);
2726
+ if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
2727
+ printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
1287
2728
  }
1288
2729
  if (tier === "free" && daysSinceFirstUse() >= 7) {
1289
2730
  printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
@@ -1314,6 +2755,7 @@ function createProxy(opts = {}) {
1314
2755
  delete headers["content-length"];
1315
2756
  headers["host"] = new URL(upstream).host;
1316
2757
  headers["x-pretense-request-id"] = requestId;
2758
+ stripPretenseOnlyForwardingHeaders(headers);
1317
2759
  const forwardBody = JSON.stringify(mutatedBody);
1318
2760
  let upstreamResp;
1319
2761
  try {
@@ -1326,14 +2768,59 @@ function createProxy(opts = {}) {
1326
2768
  return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
1327
2769
  }
1328
2770
  if (body["stream"] === true) {
1329
- return new Response(upstreamResp.body, {
2771
+ const streamReverseMap = new Map(mutationMap);
2772
+ const upstream2 = upstreamResp.body;
2773
+ if (!upstream2 || tokensMutated === 0) {
2774
+ return new Response(upstream2, {
2775
+ status: upstreamResp.status,
2776
+ headers: {
2777
+ "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
2778
+ "cache-control": "no-cache",
2779
+ "x-pretense-request-id": requestId,
2780
+ "x-pretense-protected": "true",
2781
+ "x-pretense-mutations": String(tokensMutated)
2782
+ }
2783
+ });
2784
+ }
2785
+ const decoder = new TextDecoder();
2786
+ const encoder = new TextEncoder();
2787
+ let buffer = "";
2788
+ const transform = new TransformStream({
2789
+ transform(chunk, controller) {
2790
+ buffer += decoder.decode(chunk, { stream: true });
2791
+ const lines = buffer.split("\n");
2792
+ buffer = lines.pop() ?? "";
2793
+ for (const line of lines) {
2794
+ if (line.startsWith("data: ") && line !== "data: [DONE]") {
2795
+ try {
2796
+ const json = JSON.parse(line.slice(6));
2797
+ const reversed = applyToBody(json, (text) => reverse(text, streamReverseMap));
2798
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(reversed)}
2799
+ `));
2800
+ } catch {
2801
+ controller.enqueue(encoder.encode(line + "\n"));
2802
+ }
2803
+ } else {
2804
+ controller.enqueue(encoder.encode(line + "\n"));
2805
+ }
2806
+ }
2807
+ },
2808
+ flush(controller) {
2809
+ if (buffer.length > 0) {
2810
+ controller.enqueue(encoder.encode(buffer));
2811
+ }
2812
+ }
2813
+ });
2814
+ const reversedStream = upstream2.pipeThrough(transform);
2815
+ return new Response(reversedStream, {
1330
2816
  status: upstreamResp.status,
1331
2817
  headers: {
1332
2818
  "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
1333
2819
  "cache-control": "no-cache",
1334
2820
  "x-pretense-request-id": requestId,
1335
2821
  "x-pretense-protected": "true",
1336
- "x-pretense-mutations": String(tokensMutated)
2822
+ "x-pretense-mutations": String(tokensMutated),
2823
+ "x-pretense-stream-reversal": "active"
1337
2824
  }
1338
2825
  });
1339
2826
  }
@@ -1363,31 +2850,559 @@ function createProxy(opts = {}) {
1363
2850
  });
1364
2851
  return app;
1365
2852
  }
2853
+ function isPortAvailable(port) {
2854
+ return new Promise((resolve3) => {
2855
+ const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
2856
+ tester.once("close", () => resolve3(true)).close();
2857
+ }).listen(port, "127.0.0.1");
2858
+ });
2859
+ }
2860
+ async function findAvailablePort(startPort, maxAttempts = 10) {
2861
+ for (let i = 0; i < maxAttempts; i++) {
2862
+ const candidate = startPort + i;
2863
+ if (candidate < 1 || candidate > 65535) break;
2864
+ if (await isPortAvailable(candidate)) return candidate;
2865
+ }
2866
+ throw new Error(
2867
+ `No available port found in range ${startPort}-${startPort + maxAttempts - 1}. Free a port or pass --port <number>.`
2868
+ );
2869
+ }
1366
2870
  function startProxy(opts = {}) {
1367
- const port = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
2871
+ const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
1368
2872
  const app = createProxy(opts);
1369
- 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
2873
+ void (async () => {
2874
+ const pretenseDashboardKeyFromEnvAtLaunch = !!process.env["PRETENSE_API_KEY"]?.trim();
2875
+ const dashboardKey = getPretenseApiKey();
2876
+ if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
2877
+ process.stderr.write(
2878
+ `\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
2879
+ \x1B[31m[PRETENSE] or set PRETENSE_API_KEY. Get a key at https://pretense.ai/dashboard/settings/api-keys\x1B[0m
2880
+ `
2881
+ );
2882
+ process.exit(1);
2883
+ }
2884
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2885
+ for (; ; ) {
2886
+ try {
2887
+ const validation = await validateKey({
2888
+ force: true,
2889
+ verbose: opts.verbose,
2890
+ enforceReachability: true
2891
+ });
2892
+ if (!validation) {
2893
+ process.stderr.write(
2894
+ `\x1B[31m[PRETENSE] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
2895
+ `
2896
+ );
2897
+ process.exit(1);
2898
+ }
2899
+ const plan = validation.plan ?? "FREE";
2900
+ const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
2901
+ process.stdout.write(
2902
+ `\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} mutations remaining\x1B[0m
2903
+ `
2904
+ );
2905
+ break;
2906
+ } catch (err) {
2907
+ const code = err.code;
2908
+ const msg = err.message;
2909
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2910
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
1385
2911
  `);
1386
- });
2912
+ process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL if using a custom API.\x1B[0m
2913
+ `);
2914
+ process.exit(1);
2915
+ }
2916
+ if (code === "ORG_INACTIVE") {
2917
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2918
+ `);
2919
+ process.stderr.write(`\x1B[31m[PRETENSE] Your organization is inactive; contact support.\x1B[0m
2920
+ `);
2921
+ process.exit(1);
2922
+ }
2923
+ const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
2924
+ if (authRetryable && interactive) {
2925
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2926
+ `);
2927
+ process.stderr.write(
2928
+ `\x1B[33m[PRETENSE] Enter your Pretense dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
2929
+ `
2930
+ );
2931
+ let entered = null;
2932
+ for (; ; ) {
2933
+ entered = await promptDashboardApiKeyAfterFailure();
2934
+ if (!entered) {
2935
+ process.stderr.write(`\x1B[31m[PRETENSE] No key entered; exiting.\x1B[0m
2936
+ `);
2937
+ process.exit(1);
2938
+ }
2939
+ if (isValidKeyShape(entered)) break;
2940
+ process.stderr.write(
2941
+ `\x1B[31m[PRETENSE] That does not look like a Pretense dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
2942
+ `
2943
+ );
2944
+ }
2945
+ const savedPath = installDashboardKeyForCurrentProcess(entered);
2946
+ process.stdout.write(`\x1B[32m[PRETENSE] Saved API key to ${savedPath}\x1B[0m
2947
+ `);
2948
+ clearValidationCache();
2949
+ continue;
2950
+ }
2951
+ if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
2952
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2953
+ `);
2954
+ process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' to fix.\x1B[0m
2955
+ `);
2956
+ process.exit(1);
2957
+ }
2958
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2959
+ `);
2960
+ process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' or check connectivity.\x1B[0m
2961
+ `);
2962
+ process.exit(1);
2963
+ }
2964
+ }
2965
+ attachDashboardLogoutWatcher(!pretenseDashboardKeyFromEnvAtLaunch);
2966
+ const bannerTier = tierFromDashboardPlan(
2967
+ getCachedValidation()?.plan,
2968
+ detectTier()
2969
+ );
2970
+ let port;
2971
+ try {
2972
+ port = await findAvailablePort(requestedPort, 10);
2973
+ } catch (err) {
2974
+ process.stderr.write(`\x1B[31m[PRETENSE] ${err.message}\x1B[0m
2975
+ `);
2976
+ process.exit(1);
2977
+ }
2978
+ if (port !== requestedPort) {
2979
+ process.stderr.write(
2980
+ `\x1B[33m[PRETENSE] Port ${requestedPort} in use, falling back to ${port}.\x1B[0m
2981
+ `
2982
+ );
2983
+ }
2984
+ printStartClientInstructions(port);
2985
+ serve({ fetch: app.fetch, port }, () => {
2986
+ const portStr = String(port);
2987
+ process.stdout.write(`
2988
+ \x1B[36mPretense v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
2989
+ Your code hits AI naked. We fix that.
2990
+
2991
+ Proxy: http://localhost:${portStr}
2992
+ Health: http://localhost:${portStr}/health
2993
+ Stats: http://localhost:${portStr}/stats
2994
+
2995
+ Drop-in env vars:
2996
+ ANTHROPIC_BASE_URL=http://localhost:${portStr}
2997
+ OPENAI_BASE_URL=http://localhost:${portStr}/v1
2998
+ GEMINI_BASE_URL=http://localhost:${portStr}/v1beta
2999
+ OLLAMA_HOST=http://localhost:${portStr}
3000
+
3001
+ Routes intercepted:
3002
+ /v1/messages -> Anthropic (Claude Code, SDK)
3003
+ /v1/chat/completions -> OpenAI (ChatGPT, Cursor)
3004
+ /v1/responses -> OpenAI (Codex CLI)
3005
+ /v1beta/... -> Google Gemini
3006
+ /api/chat, /api/generate -> Ollama (local)
3007
+ `);
3008
+ setInterval(() => {
3009
+ if (dashboardSavedCredentialsRevoked) return;
3010
+ if (!getPretenseApiKey()) return;
3011
+ void validateKey({
3012
+ force: true,
3013
+ verbose: opts.verbose,
3014
+ enforceReachability: true
3015
+ }).catch(() => {
3016
+ });
3017
+ }, 6e4);
3018
+ });
3019
+ })();
3020
+ }
3021
+
3022
+ // src/token-footer.ts
3023
+ import chalk3 from "chalk";
3024
+ var PLAN_LIMITS = {
3025
+ free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
3026
+ pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
3027
+ enterprise: {
3028
+ monthlyMutations: Number.POSITIVE_INFINITY,
3029
+ monthlyScans: Number.POSITIVE_INFINITY,
3030
+ seats: Number.POSITIVE_INFINITY,
3031
+ pricePerMonth: "$99/seat/month"
3032
+ }
3033
+ };
3034
+ function getPlanLimits(tier) {
3035
+ return PLAN_LIMITS[tier];
3036
+ }
3037
+ function fmt(n) {
3038
+ if (!Number.isFinite(n)) return "unlimited";
3039
+ return n.toLocaleString("en-US");
3040
+ }
3041
+ function printTokenFooter(filesScanned, mutationsMade) {
3042
+ const tier = detectTier();
3043
+ const limits = getPlanLimits(tier);
3044
+ const usage = loadUsage();
3045
+ if (tier === "enterprise") {
3046
+ process.stderr.write(
3047
+ chalk3.gray(`
3048
+ ${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
3049
+
3050
+ `)
3051
+ );
3052
+ return;
3053
+ }
3054
+ const used = usage.mutations;
3055
+ const cap = limits.monthlyMutations;
3056
+ const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
3057
+ const usageStr = pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
3058
+ process.stderr.write(
3059
+ `
3060
+ ${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk3.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
3061
+ `
3062
+ );
3063
+ if (tier === "free") {
3064
+ if (pct >= 80) {
3065
+ process.stderr.write(
3066
+ chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretense upgrade")}
3067
+
3068
+ `)
3069
+ );
3070
+ } else {
3071
+ process.stderr.write(
3072
+ chalk3.gray(` Run ${chalk3.bold("pretense status")} for details, ${chalk3.bold("pretense upgrade")} to remove limits.
3073
+
3074
+ `)
3075
+ );
3076
+ }
3077
+ } else {
3078
+ process.stderr.write("\n");
3079
+ }
3080
+ }
3081
+
3082
+ // src/banner.ts
3083
+ import chalk4 from "chalk";
3084
+ var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
3085
+ \u2588\u2588 \u2588\u2588
3086
+ \u2588\u2588 \u2588\u2588 Stop your code from
3087
+ \u2588\u2588 \u2588\u2588 leaking to any LLM.
3088
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
3089
+ \u2588\u2588 pretense.ai
3090
+ \u2588\u2588
3091
+ \u2588\u2588
3092
+ \u2588\u2588
3093
+ \u2588\u2588`;
3094
+ var ICE_BLUE = "#7DD3FC";
3095
+ function shouldPrint() {
3096
+ if (process.env.PRETENSE_NO_BANNER === "1") return false;
3097
+ if (!process.stdout.isTTY) return false;
3098
+ if (process.env.CI) return false;
3099
+ return true;
3100
+ }
3101
+ function tint(line) {
3102
+ try {
3103
+ return chalk4.hex(ICE_BLUE)(line);
3104
+ } catch {
3105
+ return line;
3106
+ }
3107
+ }
3108
+ function printBanner() {
3109
+ if (!shouldPrint()) return;
3110
+ const lines = BANNER_RAW.split("\n");
3111
+ for (const line of lines) {
3112
+ process.stderr.write(tint(line) + "\n");
3113
+ }
3114
+ process.stderr.write("\n");
3115
+ }
3116
+
3117
+ // src/commands/status.ts
3118
+ import chalk5 from "chalk";
3119
+ var PLAN_LABEL = {
3120
+ free: "Free",
3121
+ pro: "Pro",
3122
+ enterprise: "Enterprise"
3123
+ };
3124
+ function fmt2(n) {
3125
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
3126
+ return n.toLocaleString("en-US");
3127
+ }
3128
+ async function runStatus() {
3129
+ let remote = await fetchUsageSummary();
3130
+ if (!remote) {
3131
+ const v = await validateKey({ force: true });
3132
+ if (v?.valid) {
3133
+ remote = usageSummaryFromValidate(v);
3134
+ }
3135
+ }
3136
+ if (remote) {
3137
+ const plan = remote.plan;
3138
+ const tierBadge2 = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3139
+ const lines2 = [];
3140
+ lines2.push("");
3141
+ lines2.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge2} ${chalk5.green("(synced)")}`);
3142
+ lines2.push("");
3143
+ lines2.push(` Plan: ${chalk5.bold(plan)}`);
3144
+ lines2.push(` Mutations: ${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
3145
+ lines2.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
3146
+ lines2.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
3147
+ lines2.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
3148
+ lines2.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
3149
+ lines2.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3150
+ if (remote.keyExpiresAt) {
3151
+ lines2.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
3152
+ }
3153
+ lines2.push("");
3154
+ if (plan === "FREE") {
3155
+ lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3156
+ lines2.push("");
3157
+ }
3158
+ process.stdout.write(lines2.join("\n") + "\n");
3159
+ return;
3160
+ }
3161
+ const tier = detectTier();
3162
+ const planLimits = getPlanLimits(tier);
3163
+ const tierLimits = getTierLimits(tier);
3164
+ const usage = loadUsage();
3165
+ const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3166
+ const lines = [];
3167
+ lines.push("");
3168
+ lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
3169
+ lines.push("");
3170
+ lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
3171
+ lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
3172
+ lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
3173
+ lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
3174
+ lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
3175
+ lines.push("");
3176
+ if (tier === "free") {
3177
+ lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3178
+ lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretense credits")}`);
3179
+ lines.push("");
3180
+ }
3181
+ process.stdout.write(lines.join("\n") + "\n");
3182
+ }
3183
+
3184
+ // src/commands/upgrade.ts
3185
+ import chalk6 from "chalk";
3186
+ var PRICING_URL = "https://pretense.ai/pricing";
3187
+ function runUpgrade() {
3188
+ const tier = detectTier();
3189
+ const lines = [];
3190
+ lines.push("");
3191
+ lines.push(chalk6.cyan.bold(" Pretense Plans"));
3192
+ lines.push("");
3193
+ lines.push(chalk6.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"));
3194
+ lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
3195
+ lines.push(chalk6.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"));
3196
+ lines.push(` \u2502 Price/seat \u2502 ${chalk6.bold("$0")} \u2502 ${chalk6.bold("$29")} \u2502 ${chalk6.bold("$99")} \u2502`);
3197
+ lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
3198
+ lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
3199
+ lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
3200
+ lines.push(" \u2502 Audit log \u2502 30 days \u2502 90 days \u2502 unlimited \u2502");
3201
+ lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
3202
+ lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
3203
+ lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
3204
+ lines.push(chalk6.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"));
3205
+ lines.push("");
3206
+ lines.push(` ${chalk6.gray("Current plan:")} ${chalk6.bold(tier.toUpperCase())}`);
3207
+ lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
3208
+ lines.push("");
3209
+ lines.push(chalk6.gray(" After purchase, set:"));
3210
+ lines.push(chalk6.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
3211
+ lines.push("");
3212
+ process.stdout.write(lines.join("\n") + "\n");
3213
+ }
3214
+
3215
+ // src/commands/credits.ts
3216
+ import chalk7 from "chalk";
3217
+ var PLAN_LABEL2 = {
3218
+ free: "Free",
3219
+ pro: "Pro",
3220
+ enterprise: "Enterprise"
3221
+ };
3222
+ function fmt3(n) {
3223
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
3224
+ return n.toLocaleString("en-US");
3225
+ }
3226
+ function progressBar(used, cap, width = 24) {
3227
+ if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
3228
+ const pct = Math.min(1, used / cap);
3229
+ const filled = Math.round(pct * width);
3230
+ const bar = "=".repeat(filled) + "-".repeat(width - filled);
3231
+ const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
3232
+ return "[" + colored + "]";
3233
+ }
3234
+ async function runCredits() {
3235
+ let remote = await fetchUsageSummary();
3236
+ if (!remote) {
3237
+ const v = await validateKey({ force: true });
3238
+ if (v?.valid) {
3239
+ remote = usageSummaryFromValidate(v);
3240
+ }
3241
+ }
3242
+ if (remote) {
3243
+ const used2 = remote.mutationsUsed;
3244
+ const cap2 = remote.monthlyMutationLimit;
3245
+ const remaining2 = remote.mutationsRemaining;
3246
+ const pct2 = cap2 > 0 && cap2 !== -1 ? Math.round(used2 / cap2 * 100) : 0;
3247
+ const lines2 = [];
3248
+ lines2.push("");
3249
+ lines2.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
3250
+ lines2.push("");
3251
+ lines2.push(` Plan: ${chalk7.bold(remote.plan)}`);
3252
+ lines2.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3253
+ lines2.push("");
3254
+ lines2.push(` Mutations: ${progressBar(used2, cap2)} ${fmt3(used2)} / ${fmt3(cap2)}`);
3255
+ lines2.push(` Used: ${pct2}%`);
3256
+ lines2.push(` Remaining: ${fmt3(remaining2)}`);
3257
+ lines2.push("");
3258
+ if (remote.plan === "FREE" && cap2 !== -1 && used2 >= cap2 * 0.8) {
3259
+ lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3260
+ lines2.push("");
3261
+ } else if (remote.plan === "FREE") {
3262
+ lines2.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3263
+ lines2.push("");
3264
+ }
3265
+ process.stdout.write(lines2.join("\n") + "\n");
3266
+ return;
3267
+ }
3268
+ const tier = detectTier();
3269
+ const limits = getPlanLimits(tier);
3270
+ const usage = loadUsage();
3271
+ const used = usage.mutations;
3272
+ const cap = limits.monthlyMutations;
3273
+ const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
3274
+ const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
3275
+ const lines = [];
3276
+ lines.push("");
3277
+ lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.yellow("(offline)"));
3278
+ lines.push("");
3279
+ lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
3280
+ lines.push(` Month: ${usage.month}`);
3281
+ lines.push("");
3282
+ lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
3283
+ lines.push(` Used: ${pct}%`);
3284
+ lines.push(` Remaining: ${fmt3(remaining)}`);
3285
+ lines.push("");
3286
+ if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
3287
+ lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3288
+ lines.push("");
3289
+ } else if (tier === "free") {
3290
+ lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3291
+ lines.push("");
3292
+ }
3293
+ process.stdout.write(lines.join("\n") + "\n");
3294
+ }
3295
+
3296
+ // src/commands/logout.ts
3297
+ import readline2 from "readline/promises";
3298
+ import chalk8 from "chalk";
3299
+ async function runLogout(opts = {}) {
3300
+ if (!loadApiKey()) {
3301
+ console.log(chalk8.gray("No saved Pretense dashboard API key in ~/.pretense/config.json."));
3302
+ console.log(chalk8.gray("If you use PRETENSE_API_KEY in your shell, run unset PRETENSE_API_KEY."));
3303
+ return 0;
3304
+ }
3305
+ if (!opts.assumeYes) {
3306
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3307
+ console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
3308
+ console.error(chalk8.gray("Run `pretense logout --yes` to skip the prompt."));
3309
+ return 1;
3310
+ }
3311
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3312
+ try {
3313
+ const raw = await rl.question(
3314
+ chalk8.bold("Log out and remove your saved API key? Type y for yes, n for no: ")
3315
+ );
3316
+ const answer = raw.trim().toLowerCase();
3317
+ if (answer !== "y" && answer !== "yes") {
3318
+ console.log(chalk8.yellow("Logout cancelled."));
3319
+ return 0;
3320
+ }
3321
+ } finally {
3322
+ rl.close();
3323
+ }
3324
+ }
3325
+ const hadEnvDashboardKey = !!process.env["PRETENSE_API_KEY"]?.trim();
3326
+ const { removed, path } = removeSavedDashboardCredentials();
3327
+ clearValidationCache();
3328
+ delete process.env["PRETENSE_API_KEY"];
3329
+ if (removed) {
3330
+ console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
3331
+ console.log(chalk8.gray("Other terminals: run unset PRETENSE_API_KEY if that variable is still exported (e.g. in ~/.zshrc)."));
3332
+ if (hadEnvDashboardKey) {
3333
+ console.log(
3334
+ chalk8.yellow(
3335
+ "This shell had PRETENSE_API_KEY set; it was cleared for this process only. Restart any running pretense proxy (Ctrl+C, then pretense start)."
3336
+ )
3337
+ );
3338
+ }
3339
+ }
3340
+ return 0;
1387
3341
  }
1388
3342
 
1389
3343
  // src/index.ts
1390
- var VERSION = "0.3.0";
3344
+ {
3345
+ const major = parseInt(process.versions.node.split(".")[0], 10);
3346
+ if (Number.isNaN(major) || major < 18) {
3347
+ process.stderr.write(
3348
+ `pretense requires Node.js 18 or newer (you have v${process.versions.node}).
3349
+ `
3350
+ );
3351
+ process.stderr.write(`Upgrade via nvm: nvm install 20 && nvm use 20
3352
+ `);
3353
+ process.exit(1);
3354
+ }
3355
+ }
3356
+ var VERSION = getCliSemver();
3357
+ var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
3358
+ var PROGRESS_INTERVAL_MS = 500;
3359
+ var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
3360
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".cs", ".rb", ".rs"]);
3361
+ var SUPPORTED_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
3362
+ ".json",
3363
+ ".yaml",
3364
+ ".yml",
3365
+ ".toml",
3366
+ ".env",
3367
+ ".cfg",
3368
+ ".ini",
3369
+ ".tf",
3370
+ ".tfstate",
3371
+ ".tfvars",
3372
+ ".hcl",
3373
+ ".properties",
3374
+ ".conf",
3375
+ ".xml",
3376
+ ".plist",
3377
+ ".pem",
3378
+ ".key",
3379
+ ".crt",
3380
+ ".pub",
3381
+ ".pfx",
3382
+ ".p12",
3383
+ ".htpasswd"
3384
+ ]);
3385
+ var SUPPORTED_FILENAMES = /* @__PURE__ */ new Set([
3386
+ ".env",
3387
+ ".env.local",
3388
+ ".env.production",
3389
+ ".env.development",
3390
+ ".env.staging",
3391
+ ".env.test",
3392
+ ".npmrc",
3393
+ ".netrc",
3394
+ ".pypirc",
3395
+ "Dockerfile",
3396
+ "docker-compose.yml",
3397
+ "docker-compose.yaml"
3398
+ ]);
3399
+ function isScannableFile(filePath) {
3400
+ const ext = extname(filePath).toLowerCase();
3401
+ return SUPPORTED_EXTENSIONS.has(ext) || SUPPORTED_CONFIG_EXTENSIONS.has(ext) || SUPPORTED_FILENAMES.has(basename(filePath));
3402
+ }
3403
+ function isSourceCodeFile(filePath) {
3404
+ return SUPPORTED_EXTENSIONS.has(extname(filePath).toLowerCase());
3405
+ }
1391
3406
  function langFromExt(filePath) {
1392
3407
  const ext = extname(filePath).toLowerCase();
1393
3408
  const map = {
@@ -1399,26 +3414,98 @@ function langFromExt(filePath) {
1399
3414
  ".cjs": "javascript",
1400
3415
  ".py": "python",
1401
3416
  ".go": "go",
1402
- ".java": "java"
3417
+ ".java": "java",
3418
+ ".cs": "csharp",
3419
+ ".rb": "ruby",
3420
+ ".rs": "rust"
1403
3421
  };
1404
3422
  return map[ext] ?? "typescript";
1405
3423
  }
3424
+ function validatePort(value) {
3425
+ const port = parseInt(value, 10);
3426
+ if (isNaN(port) || port.toString() !== value.trim()) return null;
3427
+ if (port < 1 || port > 65535) return null;
3428
+ return port;
3429
+ }
3430
+ function isBinaryFile(filePath) {
3431
+ let fd;
3432
+ try {
3433
+ fd = openSync(filePath, "r");
3434
+ const buf = Buffer.alloc(8192);
3435
+ const bytesRead = readSync2(fd, buf, 0, 8192, 0);
3436
+ for (let i = 0; i < bytesRead; i++) {
3437
+ if (buf[i] === 0) return true;
3438
+ }
3439
+ return false;
3440
+ } catch {
3441
+ return false;
3442
+ } finally {
3443
+ if (fd !== void 0) closeSync(fd);
3444
+ }
3445
+ }
3446
+ async function isBinaryFileAsync(filePath) {
3447
+ let handle;
3448
+ try {
3449
+ handle = await fsOpen(filePath, "r");
3450
+ const buf = Buffer.alloc(8192);
3451
+ const { bytesRead } = await handle.read(buf, 0, 8192, 0);
3452
+ for (let i = 0; i < bytesRead; i++) {
3453
+ if (buf[i] === 0) return true;
3454
+ }
3455
+ return false;
3456
+ } catch {
3457
+ return false;
3458
+ } finally {
3459
+ if (handle) await handle.close().catch(() => {
3460
+ });
3461
+ }
3462
+ }
3463
+ function validateLanguage(lang) {
3464
+ const normalized = lang.toLowerCase();
3465
+ const aliases = {
3466
+ ts: "typescript",
3467
+ tsx: "typescript",
3468
+ js: "javascript",
3469
+ jsx: "javascript",
3470
+ py: "python",
3471
+ cs: "csharp",
3472
+ rb: "ruby",
3473
+ rs: "rust"
3474
+ };
3475
+ const resolved = aliases[normalized] ?? normalized;
3476
+ return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
3477
+ }
1406
3478
  function collectFiles(target) {
1407
3479
  const abs = resolve2(target);
1408
- if (!existsSync4(abs)) {
3480
+ if (!existsSync7(abs)) {
1409
3481
  return [];
1410
3482
  }
1411
3483
  const stat = statSync(abs);
1412
3484
  if (stat.isFile()) return [abs];
1413
3485
  const files = [];
1414
- const supportedExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java"]);
1415
3486
  function walk(dir) {
1416
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1417
- const fullPath = join3(dir, entry.name);
3487
+ let entries;
3488
+ try {
3489
+ entries = readdirSync(dir, { withFileTypes: true });
3490
+ } catch {
3491
+ return;
3492
+ }
3493
+ const skipDirs = /* @__PURE__ */ new Set([
3494
+ "node_modules",
3495
+ ".git",
3496
+ "dist",
3497
+ "build",
3498
+ ".pretense",
3499
+ ".next",
3500
+ "__pycache__",
3501
+ ".Trash"
3502
+ ]);
3503
+ for (const entry of entries) {
3504
+ const fullPath = join5(dir, entry.name);
1418
3505
  if (entry.isDirectory()) {
1419
- if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
3506
+ if (skipDirs.has(entry.name)) continue;
1420
3507
  walk(fullPath);
1421
- } else if (entry.isFile() && supportedExts.has(extname(entry.name).toLowerCase())) {
3508
+ } else if (entry.isFile() && isScannableFile(entry.name)) {
1422
3509
  files.push(fullPath);
1423
3510
  }
1424
3511
  }
@@ -1427,10 +3514,12 @@ function collectFiles(target) {
1427
3514
  return files;
1428
3515
  }
1429
3516
  var program = new Command();
1430
- program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION);
3517
+ program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
3518
+ printBanner();
3519
+ });
1431
3520
  program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
1432
3521
  const dir = resolve2(opts.dir);
1433
- console.log(chalk.cyan("Initializing Pretense in"), chalk.bold(dir));
3522
+ console.log(chalk9.cyan("Initializing Pretense in"), chalk9.bold(dir));
1434
3523
  const configDir = initConfig(dir);
1435
3524
  const files = collectFiles(dir);
1436
3525
  const langCounts = {};
@@ -1438,133 +3527,323 @@ program.command("init").description("Initialize .pretense/ config in current dir
1438
3527
  const lang = langFromExt(f);
1439
3528
  langCounts[lang] = (langCounts[lang] ?? 0) + 1;
1440
3529
  }
1441
- console.log(chalk.green("\n .pretense/ created at"), chalk.bold(configDir));
1442
- console.log(chalk.gray(`
3530
+ console.log(chalk9.green("\n .pretense/ created at"), chalk9.bold(configDir));
3531
+ console.log(chalk9.gray(`
1443
3532
  Scanned ${files.length} source files:`));
1444
3533
  for (const [lang, count] of Object.entries(langCounts)) {
1445
- console.log(chalk.gray(` ${lang}: ${count} files`));
3534
+ console.log(chalk9.gray(` ${lang}: ${count} files`));
1446
3535
  }
1447
- console.log(chalk.cyan("\n Next: run"), chalk.bold("pretense start"), chalk.cyan("to launch the proxy"));
3536
+ console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretense start"), chalk9.cyan("to launch the proxy"));
1448
3537
  console.log();
1449
3538
  });
1450
- 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) => {
3539
+ program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action(async (opts) => {
1451
3540
  const config = loadConfig();
1452
- const port = parseInt(opts.port) || config.port;
3541
+ const port = validatePort(opts.port);
3542
+ if (port === null) {
3543
+ console.error(chalk9.red("Error: Invalid port number:"), chalk9.bold(opts.port));
3544
+ console.error(chalk9.gray("Port must be an integer between 1 and 65535."));
3545
+ process.exit(1);
3546
+ }
3547
+ await ensureDashboardKeyForStart();
3548
+ logDashboardCredentialHint();
1453
3549
  startProxy({ config, port, verbose: opts.verbose });
1454
3550
  });
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);
3551
+ async function scanOneFile(file, opts) {
3552
+ let size = 0;
3553
+ try {
3554
+ const st = await fsStat(file);
3555
+ size = st.size;
3556
+ if (st.size > opts.maxFileSize) {
3557
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "too-large", sizeBytes: size };
3558
+ }
3559
+ } catch {
3560
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error" };
1460
3561
  }
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) {
3562
+ if (!isSourceCodeFile(file)) {
3563
+ try {
3564
+ if (await isBinaryFileAsync(file)) {
3565
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "binary", sizeBytes: size };
3566
+ }
3567
+ } catch {
3568
+ }
3569
+ }
3570
+ let code;
3571
+ try {
3572
+ code = await readFile(file, "utf-8");
3573
+ } catch {
3574
+ return { file, identifiers: 0, secrets: 0, secretTypes: [], skipped: "read-error", sizeBytes: size };
3575
+ }
3576
+ const lang = langFromExt(file);
3577
+ let identifiers = 0;
3578
+ if (!opts.secretsOnly && isSourceCodeFile(file)) {
3579
+ try {
1469
3580
  const scanResult = scan(code, lang);
1470
3581
  identifiers = scanResult.tokens.length;
1471
- totalIdentifiers += identifiers;
3582
+ } catch {
1472
3583
  }
3584
+ }
3585
+ let blocked = [];
3586
+ try {
1473
3587
  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
- });
3588
+ blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
3589
+ } catch {
3590
+ }
3591
+ return {
3592
+ file,
3593
+ identifiers,
3594
+ secrets: blocked.length,
3595
+ secretTypes: blocked.map((m) => m.type),
3596
+ sizeBytes: size
3597
+ };
3598
+ }
3599
+ async function scanFilesParallel(files, opts) {
3600
+ const results = [];
3601
+ let cursor = 0;
3602
+ let done = 0;
3603
+ let lastPrint = 0;
3604
+ let interrupted = false;
3605
+ const stopHandler = () => {
3606
+ interrupted = true;
3607
+ };
3608
+ process.on("SIGINT", stopHandler);
3609
+ process.on("SIGTERM", stopHandler);
3610
+ const isTTY = process.stderr.isTTY === true;
3611
+ const printProgress = (force = false) => {
3612
+ if (opts.quiet) return;
3613
+ const now = Date.now();
3614
+ if (!force && now - lastPrint < PROGRESS_INTERVAL_MS) return;
3615
+ lastPrint = now;
3616
+ const found = results.reduce((acc, r) => acc + r.secrets, 0);
3617
+ const msg = `Scanning ${done}/${files.length} files (${found} secrets so far)...`;
3618
+ if (isTTY) {
3619
+ process.stderr.write(`\r\x1B[K${msg}`);
3620
+ } else {
3621
+ process.stderr.write(`${msg}
3622
+ `);
3623
+ }
3624
+ };
3625
+ async function worker() {
3626
+ while (!interrupted) {
3627
+ const i = cursor++;
3628
+ if (i >= files.length) return;
3629
+ const file = files[i];
3630
+ const row = await scanOneFile(file, { secretsOnly: opts.secretsOnly, maxFileSize: opts.maxFileSize });
3631
+ if (row) results.push(row);
3632
+ done++;
3633
+ printProgress();
3634
+ }
3635
+ }
3636
+ printProgress(true);
3637
+ const workers = Array.from({ length: Math.min(opts.concurrency, files.length || 1) }, () => worker());
3638
+ await Promise.all(workers);
3639
+ process.removeListener("SIGINT", stopHandler);
3640
+ process.removeListener("SIGTERM", stopHandler);
3641
+ if (!opts.quiet && isTTY) {
3642
+ process.stderr.write(`\r\x1B[K`);
3643
+ }
3644
+ return { results, interrupted };
3645
+ }
3646
+ function parseSizeOption(value) {
3647
+ const trimmed = value.trim().toLowerCase();
3648
+ const m = /^(\d+(?:\.\d+)?)\s*(b|k|kb|m|mb|g|gb)?$/.exec(trimmed);
3649
+ if (!m) return null;
3650
+ const n = parseFloat(m[1]);
3651
+ if (!isFinite(n) || n <= 0) return null;
3652
+ const unit = m[2] ?? "b";
3653
+ const mult = unit.startsWith("g") ? 1024 ** 3 : unit.startsWith("m") ? 1024 ** 2 : unit.startsWith("k") ? 1024 : 1;
3654
+ return Math.floor(n * mult);
3655
+ }
3656
+ 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) => {
3657
+ const absTarget = resolve2(target);
3658
+ if (!existsSync7(absTarget)) {
3659
+ console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
3660
+ process.exit(1);
3661
+ }
3662
+ const maxFileSize = parseSizeOption(opts.maxFileSize);
3663
+ if (maxFileSize === null) {
3664
+ console.error(chalk9.red("Error: Invalid --max-file-size value:"), chalk9.bold(opts.maxFileSize));
3665
+ console.error(chalk9.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
3666
+ process.exit(1);
3667
+ }
3668
+ const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
3669
+ if (!opts.quiet && !opts.json) {
3670
+ process.stderr.write(chalk9.gray(`Discovering files in ${absTarget}...
3671
+ `));
3672
+ }
3673
+ const files = collectFiles(target);
3674
+ if (files.length === 0) {
3675
+ console.error(chalk9.red("Error: No source files found at"), chalk9.bold(absTarget));
3676
+ console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3677
+ process.exit(1);
3678
+ }
3679
+ if (!opts.quiet && !opts.json) {
3680
+ process.stderr.write(chalk9.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
3681
+ `));
1482
3682
  }
3683
+ const startedAt = Date.now();
3684
+ const { results: allResults, interrupted } = await scanFilesParallel(files, {
3685
+ secretsOnly: opts.secretsOnly,
3686
+ maxFileSize,
3687
+ quiet: opts.quiet || opts.json,
3688
+ concurrency
3689
+ });
3690
+ const elapsedMs = Date.now() - startedAt;
3691
+ const totalIdentifiers = allResults.reduce((acc, r) => acc + r.identifiers, 0);
3692
+ const totalSecrets = allResults.reduce((acc, r) => acc + r.secrets, 0);
3693
+ const skippedLarge = allResults.filter((r) => r.skipped === "too-large").length;
3694
+ const skippedBinary = allResults.filter((r) => r.skipped === "binary").length;
1483
3695
  if (opts.json) {
1484
- console.log(JSON.stringify({ files: allResults, totalIdentifiers, totalSecrets }, null, 2));
3696
+ console.log(JSON.stringify({
3697
+ files: allResults,
3698
+ totalIdentifiers,
3699
+ totalSecrets,
3700
+ skippedLarge,
3701
+ skippedBinary,
3702
+ elapsedMs,
3703
+ interrupted
3704
+ }, null, 2));
1485
3705
  } else {
1486
- console.log(chalk.cyan("\nPretense Scan Results\n"));
3706
+ console.log(chalk9.cyan("\nPretense Scan Results\n"));
1487
3707
  for (const r of allResults) {
1488
- const secretBadge = r.secrets > 0 ? chalk.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3708
+ if (r.skipped) {
3709
+ if (r.skipped === "too-large") {
3710
+ const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
3711
+ console.log(chalk9.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
3712
+ }
3713
+ continue;
3714
+ }
3715
+ const secretBadge = r.secrets > 0 ? chalk9.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3716
+ const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
3717
+ if (!showRow) continue;
1489
3718
  console.log(
1490
- chalk.gray(" ") + chalk.white(r.file) + chalk.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
3719
+ chalk9.gray(" ") + chalk9.white(r.file) + chalk9.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
1491
3720
  );
1492
3721
  if (r.secrets > 0) {
1493
3722
  for (const t of r.secretTypes) {
1494
- console.log(chalk.red(` ! ${t}`));
3723
+ console.log(chalk9.red(` ! ${t}`));
1495
3724
  }
1496
3725
  }
1497
3726
  }
1498
- console.log(chalk.gray(`
1499
- Total: ${files.length} files, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk.red(`${totalSecrets} secrets found`) : chalk.green("0 secrets")));
3727
+ const skipNote = skippedLarge + skippedBinary > 0 ? chalk9.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
3728
+ console.log(
3729
+ chalk9.gray(`
3730
+ Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk9.red(`${totalSecrets} secrets found`) : chalk9.green("0 secrets")) + skipNote
3731
+ );
3732
+ if (interrupted) {
3733
+ console.log(chalk9.yellow("\n Interrupted \u2014 partial results shown above."));
3734
+ }
1500
3735
  console.log();
1501
3736
  }
3737
+ if (!opts.json) {
3738
+ printTokenFooter(allResults.length, totalIdentifiers);
3739
+ }
3740
+ if (interrupted) {
3741
+ process.exit(130);
3742
+ }
1502
3743
  if (totalSecrets > 0) {
1503
3744
  process.exit(2);
1504
3745
  }
1505
3746
  });
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) => {
3747
+ program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").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
3748
  const absPath = resolve2(file);
1508
- if (!existsSync4(absPath)) {
1509
- console.error(chalk.red("File not found:"), chalk.bold(absPath));
3749
+ if (!existsSync7(absPath)) {
3750
+ console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
1510
3751
  process.exit(1);
1511
3752
  }
1512
- const code = readFileSync4(absPath, "utf-8");
1513
- const lang = langFromExt(absPath);
1514
- const result = mutate(code, lang, opts.seed);
3753
+ if (opts.language) {
3754
+ const validLang = validateLanguage(opts.language);
3755
+ if (!validLang) {
3756
+ console.error(chalk9.red("Error: Unsupported language:"), chalk9.bold(opts.language));
3757
+ console.error(chalk9.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
3758
+ process.exit(1);
3759
+ }
3760
+ }
3761
+ if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
3762
+ console.error(chalk9.red("Error: Not a supported source file:"), chalk9.bold(absPath));
3763
+ console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3764
+ process.exit(1);
3765
+ }
3766
+ const code = readFileSync7(absPath, "utf-8");
3767
+ const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
3768
+ const secretScan = scan2(code);
3769
+ const effectiveSeed = opts.seed === "pretense" ? buildSaltedSeed(opts.seed) : opts.seed;
3770
+ const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
3771
+ const secretsRedacted = secretsMap.size;
3772
+ const result = mutate(redacted, lang, opts.seed);
1515
3773
  if (opts.save) {
1516
3774
  const configDir = getConfigDir();
1517
- const store = new MutationStore(join3(configDir, "mutation-map.json"));
3775
+ const store = new MutationStore(join5(configDir, "mutation-map.json"));
1518
3776
  store.load();
1519
3777
  store.save({
1520
3778
  id: absPath,
1521
3779
  originalHash: absPath,
1522
3780
  mapEntries: MutationStore.fromMap(result.map),
3781
+ secretsMapEntries: [...secretsMap.entries()],
1523
3782
  timestamp: Date.now(),
1524
3783
  language: lang
1525
3784
  });
1526
3785
  store.persist();
1527
- process.stderr.write(chalk.gray(`Mutation map saved to ${configDir}/mutation-map.json
3786
+ process.stderr.write(chalk9.gray(`Mutation map saved to ${configDir}/mutation-map.json
1528
3787
  `));
1529
3788
  }
1530
3789
  if (opts.json) {
1531
3790
  console.log(JSON.stringify({
1532
3791
  mutatedCode: result.mutatedCode,
1533
3792
  map: Object.fromEntries(result.map),
1534
- stats: result.stats
3793
+ stats: result.stats,
3794
+ secretsRedacted,
3795
+ secretsMap: Object.fromEntries(secretsMap)
1535
3796
  }, null, 2));
1536
3797
  } else {
1537
3798
  process.stdout.write(result.mutatedCode);
1538
- process.stderr.write(chalk.gray(`
3799
+ if (secretsRedacted > 0) {
3800
+ process.stderr.write(
3801
+ chalk9.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before identifier mutation ---
3802
+ `)
3803
+ );
3804
+ }
3805
+ process.stderr.write(chalk9.gray(`
1539
3806
  --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
1540
3807
  `));
3808
+ printTokenFooter(1, result.stats.tokensMutated);
1541
3809
  }
1542
3810
  });
1543
3811
  program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
1544
3812
  const absPath = resolve2(file);
1545
- if (!existsSync4(absPath)) {
1546
- console.error(chalk.red("File not found:"), chalk.bold(absPath));
3813
+ if (!existsSync7(absPath)) {
3814
+ console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
1547
3815
  process.exit(1);
1548
3816
  }
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>"));
3817
+ const mutatedCode = readFileSync7(absPath, "utf-8");
3818
+ const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
3819
+ if (!existsSync7(mapPath)) {
3820
+ console.error(chalk9.red("Error: Mutation map not found:"), chalk9.bold(mapPath));
3821
+ console.error(chalk9.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
3822
+ process.exit(1);
3823
+ }
3824
+ let store;
3825
+ try {
3826
+ store = new MutationStore(mapPath);
3827
+ store.load();
3828
+ } catch {
3829
+ console.error(chalk9.red("Error: Failed to load mutation map:"), chalk9.bold(mapPath));
3830
+ console.error(chalk9.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
1554
3831
  process.exit(1);
1555
3832
  }
1556
- const store = new MutationStore(mapPath);
1557
- store.load();
1558
3833
  const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
1559
3834
  if (!entry) {
1560
- console.error(chalk.red("No mutation map found for this file."));
3835
+ console.error(chalk9.red("Error: No mutation map entries found."));
3836
+ console.error(chalk9.gray("Run 'pretense mutate --save <file>' first to generate a map."));
1561
3837
  process.exit(1);
1562
3838
  }
1563
3839
  const map = MutationStore.toMap(entry);
1564
- const restored = reverse(mutatedCode, map);
3840
+ const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
3841
+ const restored = reverse(mutatedCode, map, secretsMap);
1565
3842
  process.stdout.write(restored);
1566
- process.stderr.write(chalk.gray(`
1567
- --- Reversed ${map.size} mutations ---
3843
+ const secretsCount = secretsMap?.size ?? 0;
3844
+ const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
3845
+ process.stderr.write(chalk9.gray(`
3846
+ --- Reversed ${map.size} mutations${secretsSuffix} ---
1568
3847
  `));
1569
3848
  });
1570
3849
  program.command("audit").description("View the audit log").option("--json", "Output as JSON", false).option("--csv", "Output as CSV", false).option("-l, --limit <number>", "Limit entries", "50").action((opts) => {
@@ -1577,5 +3856,39 @@ program.command("audit").description("View the audit log").option("--json", "Out
1577
3856
  program.command("version").description("Print Pretense CLI version").action(() => {
1578
3857
  console.log(`@pretense/cli v${VERSION}`);
1579
3858
  });
3859
+ program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
3860
+ await runStatus();
3861
+ });
3862
+ program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
3863
+ await runStatus();
3864
+ });
3865
+ program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(async () => {
3866
+ await runCredits();
3867
+ });
3868
+ program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
3869
+ runUpgrade();
3870
+ });
3871
+ program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
3872
+ await runCredits();
3873
+ });
3874
+ program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
3875
+ const code = runLogin({ key: opts.key });
3876
+ if (code !== 0) process.exit(code);
3877
+ });
3878
+ program.command("logout").description("Remove the saved Pretense dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
3879
+ const code = await runLogout({ assumeYes: opts.yes === true });
3880
+ if (code !== 0) process.exit(code);
3881
+ });
3882
+ function handleFatalError(err) {
3883
+ if (err instanceof Error && err.message) {
3884
+ console.error(chalk9.red("Error:"), err.message);
3885
+ } else {
3886
+ console.error(chalk9.red("An unexpected error occurred."));
3887
+ }
3888
+ process.exit(1);
3889
+ }
3890
+ process.on("uncaughtException", handleFatalError);
3891
+ process.on("unhandledRejection", handleFatalError);
3892
+ program.showSuggestionAfterError(true).showHelpAfterError("(use --help for available commands)");
1580
3893
  program.parse();
1581
3894
  //# sourceMappingURL=index.js.map