@pretense/cli 0.3.1 → 0.5.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
@@ -1,11 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
5
- import chalk7 from "chalk";
6
- import { readFileSync as readFileSync6, existsSync as existsSync6, readdirSync, statSync, openSync, readSync as readSync2, closeSync } from "fs";
4
+ import { Command as Command2 } from "commander";
5
+ import chalk10 from "chalk";
6
+ import {
7
+ readFileSync as readFileSync8,
8
+ existsSync as existsSync8,
9
+ readdirSync,
10
+ statSync,
11
+ openSync,
12
+ readSync as readSync2,
13
+ closeSync
14
+ } from "fs";
7
15
  import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
8
- import { resolve as resolve2, extname, join as join5, basename } from "path";
16
+ import { resolve as resolve3, extname, join as join6, basename } from "path";
9
17
  import os from "os";
10
18
 
11
19
  // src/types.ts
@@ -449,6 +457,15 @@ function scanTypeScript(code) {
449
457
  { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
450
458
  // Multi-line comment
451
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
+ },
452
469
  // Import source (the module path string)
453
470
  { re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
454
471
  // Class name
@@ -456,11 +473,18 @@ function scanTypeScript(code) {
456
473
  // Function/method name (function keyword)
457
474
  { re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
458
475
  // Arrow function assigned to const/let/var
459
- { 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 },
460
477
  // Method: identifier followed by (
461
478
  { re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
462
- // Variable declarations
463
- { 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 },
464
488
  // Template literals (preserve backtick wrapper)
465
489
  { re: /`[^`]*`/g, type: "String" /* String */ },
466
490
  // Double-quoted strings
@@ -478,9 +502,12 @@ function scanTypeScript(code) {
478
502
  if (!captureValue || captureValue.trim() === "") continue;
479
503
  if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
480
504
  if (TS_JS_KEYWORDS.has(captureValue)) continue;
481
- 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;
482
506
  if (captureValue.length <= 1) continue;
483
507
  }
508
+ if (type === "Property" /* Property */ && captureValue === "meta" && code.slice(Math.max(0, captureStart - 7), captureStart) === "import.") {
509
+ continue;
510
+ }
484
511
  tokens.push({
485
512
  type,
486
513
  value: captureValue,
@@ -490,7 +517,18 @@ function scanTypeScript(code) {
490
517
  });
491
518
  }
492
519
  }
493
- 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));
494
532
  }
495
533
  function scanPython(code) {
496
534
  const tokens = [];
@@ -508,6 +546,11 @@ function scanPython(code) {
508
546
  { re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
509
547
  // Function/method def
510
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 },
511
554
  // Variable assignment (simple name = ...)
512
555
  { re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
513
556
  // Double-quoted strings
@@ -523,7 +566,7 @@ function scanPython(code) {
523
566
  const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
524
567
  const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
525
568
  if (!captureValue || captureValue.trim() === "") continue;
526
- 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 */) {
527
570
  if (PYTHON_KEYWORDS.has(captureValue)) continue;
528
571
  if (captureValue.length <= 1) continue;
529
572
  if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
@@ -537,7 +580,16 @@ function scanPython(code) {
537
580
  });
538
581
  }
539
582
  }
540
- 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));
541
593
  }
542
594
  function scanGo(code) {
543
595
  const tokens = [];
@@ -849,8 +901,14 @@ function getMutationSalt() {
849
901
  function buildSaltedSeed(baseSeed) {
850
902
  return `${baseSeed}:${getMutationSalt()}`;
851
903
  }
904
+ function effectiveSeedForMutation(userSeed) {
905
+ if (userSeed === "pretense" || userSeed === "pretest") {
906
+ return buildSaltedSeed("pretense");
907
+ }
908
+ return userSeed;
909
+ }
852
910
 
853
- // src/mutator.ts
911
+ // src/deterministic-id.ts
854
912
  function hash32(str) {
855
913
  let h = 5381;
856
914
  for (let i = 0; i < str.length; i++) {
@@ -868,11 +926,13 @@ function toAlphaNum(n, len) {
868
926
  }
869
927
  return result;
870
928
  }
871
- function deterministicId(original, seed, prefix, len = 4) {
929
+ function deterministicSyntheticId(original, seed, prefix, len = 4) {
872
930
  const combined = `${seed}:${prefix}:${original}`;
873
931
  const h = hash32(combined);
874
932
  return prefix + toAlphaNum(h, len);
875
933
  }
934
+
935
+ // src/mutator.ts
876
936
  function prefixForType(type) {
877
937
  switch (type) {
878
938
  case "Class" /* Class */:
@@ -884,6 +944,8 @@ function prefixForType(type) {
884
944
  return "_v";
885
945
  case "Property" /* Property */:
886
946
  return "_prop";
947
+ case "HttpHeaderString" /* HttpHeaderString */:
948
+ return "_hk";
887
949
  default:
888
950
  return "_tok";
889
951
  }
@@ -910,11 +972,24 @@ function mutate(code, language, seed = "pretense") {
910
972
  if (token.type === "String" /* String */) {
911
973
  continue;
912
974
  }
975
+ if (token.type === "HttpHeaderString" /* HttpHeaderString */) {
976
+ const qm = /^("|')(.+)\1$/.exec(token.value);
977
+ if (qm) {
978
+ const inner = qm[2];
979
+ const q = qm[1];
980
+ const syntheticInner = deterministicSyntheticId(inner, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
981
+ map.set(token.value, `${q}${syntheticInner}${q}`);
982
+ } else {
983
+ const syntheticInner = deterministicSyntheticId(token.value, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
984
+ map.set(token.value, syntheticInner);
985
+ }
986
+ continue;
987
+ }
913
988
  if (token.type === "Import" /* Import */) {
914
989
  continue;
915
990
  }
916
991
  const prefix = prefixForType(token.type);
917
- const synthetic = deterministicId(token.value, effectiveSeed, prefix);
992
+ const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
918
993
  map.set(token.value, synthetic);
919
994
  }
920
995
  const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
@@ -942,22 +1017,31 @@ function mutate(code, language, seed = "pretense") {
942
1017
  }
943
1018
 
944
1019
  // src/reverser.ts
945
- function reverse(mutatedCode, map) {
946
- if (!mutatedCode || map.size === 0) return mutatedCode;
947
- const reverseMap = /* @__PURE__ */ new Map();
948
- for (const [original, synthetic] of map.entries()) {
949
- if (synthetic === "" || !synthetic) continue;
950
- reverseMap.set(synthetic, original);
951
- }
952
- if (reverseMap.size === 0) return mutatedCode;
953
- const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
1020
+ function reverse(mutatedCode, map, secretsMap) {
1021
+ if (!mutatedCode) return mutatedCode;
954
1022
  let result = mutatedCode;
955
- for (const [synthetic, original] of sortedEntries) {
956
- const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
957
- if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
958
- result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
959
- } else {
960
- result = result.split(synthetic).join(original);
1023
+ if (map.size > 0) {
1024
+ const reverseMap = /* @__PURE__ */ new Map();
1025
+ for (const [original, synthetic] of map.entries()) {
1026
+ if (synthetic === "" || !synthetic) continue;
1027
+ reverseMap.set(synthetic, original);
1028
+ }
1029
+ if (reverseMap.size > 0) {
1030
+ const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
1031
+ for (const [synthetic, original] of sortedEntries) {
1032
+ const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1033
+ if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
1034
+ result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
1035
+ } else {
1036
+ result = result.split(synthetic).join(original);
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ if (secretsMap && secretsMap.size > 0) {
1042
+ const sortedSecrets = [...secretsMap.entries()].sort((a, b) => b[0].length - a[0].length);
1043
+ for (const [placeholder, original] of sortedSecrets) {
1044
+ result = result.split(placeholder).join(original);
961
1045
  }
962
1046
  }
963
1047
  return result;
@@ -966,16 +1050,40 @@ function reverse(mutatedCode, map) {
966
1050
  // src/secrets.ts
967
1051
  var SECRET_PATTERNS = [
968
1052
  { name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
969
- { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-[A-Za-z0-9]{20,}/g },
1053
+ /** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with @pretense/scanner */
1054
+ { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
970
1055
  { name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
971
- { 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 },
1056
+ /** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
1057
+ { name: "aws-secret-assignment", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bAWS_SECRET\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gd, valueGroup: 1 },
1058
+ /**
1059
+ * Env-style labels with `-` or `.` (e.g. `AWS-SECRET-KEY`, `AWS.SECRET.KEY`, long `ACCESS` forms).
1060
+ * Case-insensitive `AWS` / `SECRET` / `KEY`. Value: 40-char secret; quotes optional (matches aws-secret-key).
1061
+ */
1062
+ {
1063
+ name: "aws-secret-key-delimited",
1064
+ category: "secret",
1065
+ severity: "critical",
1066
+ defaultAction: "block",
1067
+ pattern: /\bAWS[-.]SECRET(?:[-.]ACCESS)?[-.]KEY\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gdi,
1068
+ valueGroup: 1
1069
+ },
1070
+ { 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 },
972
1071
  { name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
973
1072
  { name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
974
1073
  { name: "stripe-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/g },
975
- { name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
976
- { 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 },
1074
+ { 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 },
1075
+ /** Three base64url JWT segments; payload segment is not required to start with `eyJ` (Supabase and many issuers use compact payloads). */
1076
+ { 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 },
977
1077
  { name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
978
- { 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 },
1078
+ {
1079
+ name: "generic-password",
1080
+ category: "credential",
1081
+ severity: "high",
1082
+ defaultAction: "redact",
1083
+ // Do not match `token` / `secret` inside identifiers (e.g. GITHUB_TOKEN, AWS_SECRET): require no word/underscore before keyword.
1084
+ pattern: /(?<![A-Za-z0-9_])(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"]([^\s'"]{8,})['"]/gid,
1085
+ valueGroup: 1
1086
+ },
979
1087
  { name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
980
1088
  { name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
981
1089
  { name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
@@ -1009,7 +1117,28 @@ var SECRET_PATTERNS = [
1009
1117
  { name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
1010
1118
  { name: "launchdarkly-sdk-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bsdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
1011
1119
  { name: "launchdarkly-mobile-key", category: "secret", severity: "high", defaultAction: "block", pattern: /\bmob-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
1012
- { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{32,}\b/g }
1120
+ /** Stripe signing secrets are often 32+ chars; allow shorter synthetic/test tails so redaction still applies. */
1121
+ { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g },
1122
+ /** Stripe Price / product IDs (`price_…`) — identifiers, not card data, but leak billing context. */
1123
+ { name: "stripe-price-id", category: "secret", severity: "high", defaultAction: "block", pattern: /\bprice_[A-Za-z0-9_]{14,}\b/g },
1124
+ /** `FOO_PASSWORD = '…'` / `NEXT_PUBLIC_*_PASSWORD` — generic-password skips when `password` is preceded by `_`. */
1125
+ {
1126
+ name: "env-password-assignment",
1127
+ category: "credential",
1128
+ severity: "high",
1129
+ defaultAction: "block",
1130
+ pattern: /\b[A-Z][A-Z0-9_]*PASSWORD\s*=\s*['"]([^'"]{6,})['"]/gd,
1131
+ valueGroup: 1
1132
+ },
1133
+ /** Quoted common DB dialect names — stack hints when mutating env-style config. */
1134
+ {
1135
+ name: "quoted-db-dialect",
1136
+ category: "credential",
1137
+ severity: "medium",
1138
+ defaultAction: "block",
1139
+ pattern: /(['"])(postgres|mysql|redis)\1/gd,
1140
+ valueGroup: 2
1141
+ }
1013
1142
  ];
1014
1143
  var PII_PATTERNS = [
1015
1144
  { name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
@@ -1076,7 +1205,17 @@ function scan2(text, actionOverrides) {
1076
1205
  def.pattern.lastIndex = 0;
1077
1206
  let m;
1078
1207
  while ((m = def.pattern.exec(text)) !== null) {
1079
- const value = m[0];
1208
+ let value = m[0];
1209
+ let start2 = m.index;
1210
+ let end = start2 + value.length;
1211
+ if (def.valueGroup !== void 0) {
1212
+ const span = m.indices?.[def.valueGroup];
1213
+ if (!span) continue;
1214
+ const [gs, ge] = span;
1215
+ value = text.slice(gs, ge);
1216
+ start2 = gs;
1217
+ end = ge;
1218
+ }
1080
1219
  if (def.validate && !def.validate(value)) continue;
1081
1220
  const action = actionOverrides?.[def.name] ?? def.defaultAction;
1082
1221
  matchCounter++;
@@ -1086,8 +1225,8 @@ function scan2(text, actionOverrides) {
1086
1225
  type: def.name,
1087
1226
  value,
1088
1227
  masked: maskValue(value, def.name),
1089
- start: m.index,
1090
- end: m.index + value.length,
1228
+ start: start2,
1229
+ end,
1091
1230
  severity: def.severity,
1092
1231
  action
1093
1232
  });
@@ -1151,14 +1290,26 @@ function scan2(text, actionOverrides) {
1151
1290
  };
1152
1291
  }
1153
1292
  function applyRedactions(text, matches) {
1154
- const sorted = [...matches].sort((a, b) => b.start - a.start);
1293
+ return applyRedactionsTracked(text, matches).redactedCode;
1294
+ }
1295
+ var SECRET_LITERAL_PREFIX = "_sl";
1296
+ function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("pretense")) {
1297
+ const actionable = matches.filter((m) => m.action === "block" || m.action === "redact");
1298
+ const sorted = [...actionable].sort((a, b) => b.start - a.start);
1299
+ const secretsMap = /* @__PURE__ */ new Map();
1300
+ const placeholders = /* @__PURE__ */ new Map();
1301
+ const forward = [...actionable].sort((a, b) => a.start - b.start);
1302
+ for (const match of forward) {
1303
+ const tag = deterministicSyntheticId(`${match.id}:${match.start}`, mutationSeed, SECRET_LITERAL_PREFIX);
1304
+ placeholders.set(match.id, tag);
1305
+ secretsMap.set(tag, match.value);
1306
+ }
1155
1307
  let result = text;
1156
1308
  for (const match of sorted) {
1157
- if (match.action === "block" || match.action === "redact") {
1158
- result = result.slice(0, match.start) + `[PRETENSE:${match.type.toUpperCase()}]` + result.slice(match.end);
1159
- }
1309
+ const tag = placeholders.get(match.id);
1310
+ result = result.slice(0, match.start) + tag + result.slice(match.end);
1160
1311
  }
1161
- return result;
1312
+ return { redactedCode: result, secretsMap };
1162
1313
  }
1163
1314
 
1164
1315
  // src/store.ts
@@ -1372,15 +1523,6 @@ function loadUsage(dir) {
1372
1523
  return { month: currentMonth, mutations: 0, firstUseDate: today };
1373
1524
  }
1374
1525
  }
1375
- function saveUsage(usage, dir) {
1376
- const configDir = getConfigDir(dir);
1377
- if (!existsSync3(configDir)) {
1378
- mkdirSync3(configDir, { recursive: true });
1379
- }
1380
- const usagePath = join2(configDir, USAGE_FILE);
1381
- const checksum = computeUsageChecksum(usage);
1382
- writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
1383
- }
1384
1526
  var MIN_LICENSE_KEY_LENGTH = 40;
1385
1527
  var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
1386
1528
  function isValidLicenseKey(key, prefix) {
@@ -1405,6 +1547,11 @@ function detectTier() {
1405
1547
  if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
1406
1548
  return "free";
1407
1549
  }
1550
+ function tierFromDashboardPlan(plan, fallback) {
1551
+ if (plan === "ENTERPRISE") return "enterprise";
1552
+ if (plan === "PRO") return "pro";
1553
+ return fallback;
1554
+ }
1408
1555
  function getTierLimits(tier) {
1409
1556
  switch (tier) {
1410
1557
  case "enterprise":
@@ -1491,7 +1638,7 @@ function formatText(entries) {
1491
1638
  if (entries.length === 0) return "No audit entries found.";
1492
1639
  const lines = entries.map((e) => {
1493
1640
  const date = new Date(e.timestamp).toLocaleString();
1494
- return `[${date}] ${e.file} | ${e.identifiersMutated} mutated | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
1641
+ return `[${date}] ${e.file} | ${e.identifiersMutated} protected | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
1495
1642
  });
1496
1643
  return lines.join("\n");
1497
1644
  }
@@ -1512,6 +1659,7 @@ import { Hono } from "hono";
1512
1659
  import { serve } from "@hono/node-server";
1513
1660
  import { nanoid } from "nanoid";
1514
1661
  import { createServer } from "net";
1662
+ import { existsSync as existsSync6, watch } from "fs";
1515
1663
 
1516
1664
  // src/auth.ts
1517
1665
  import { createHash } from "crypto";
@@ -1575,111 +1723,892 @@ function sessionCount() {
1575
1723
  return sessions.size;
1576
1724
  }
1577
1725
 
1578
- // src/proxy.ts
1579
- function anthropicUpstream() {
1580
- return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
1726
+ // src/git-context.ts
1727
+ import { execFile } from "child_process";
1728
+ import { promisify } from "util";
1729
+ var execFileAsync = promisify(execFile);
1730
+ var GIT_TIMEOUT_MS = 2e3;
1731
+ async function runGit(args, cwd) {
1732
+ try {
1733
+ const { stdout } = await execFileAsync("git", args, {
1734
+ cwd,
1735
+ timeout: GIT_TIMEOUT_MS,
1736
+ windowsHide: true
1737
+ });
1738
+ const trimmed = stdout.trim();
1739
+ return trimmed.length > 0 ? trimmed : void 0;
1740
+ } catch {
1741
+ return void 0;
1742
+ }
1581
1743
  }
1582
- function openaiUpstream() {
1583
- return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
1744
+ function sanitizeRemote(raw) {
1745
+ if (/^[\w.-]+@[^:]+:/.test(raw) && !raw.includes("://")) {
1746
+ return raw;
1747
+ }
1748
+ try {
1749
+ const url = new URL(raw);
1750
+ url.username = "";
1751
+ url.password = "";
1752
+ return url.toString();
1753
+ } catch {
1754
+ return raw;
1755
+ }
1756
+ }
1757
+ async function collectGitContext(cwd = process.cwd()) {
1758
+ const insideRepo = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1759
+ if (insideRepo !== "true") return {};
1760
+ const [remote, branch, sha] = await Promise.all([
1761
+ runGit(["remote", "get-url", "origin"], cwd),
1762
+ runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
1763
+ runGit(["rev-parse", "HEAD"], cwd)
1764
+ ]);
1765
+ const ctx = {};
1766
+ if (remote) ctx.git_remote = sanitizeRemote(remote);
1767
+ if (branch && branch !== "HEAD") ctx.git_branch = branch;
1768
+ if (sha) ctx.git_commit_sha = sha;
1769
+ return ctx;
1770
+ }
1771
+ var CACHE_TTL_MS = 3e4;
1772
+ var cache = /* @__PURE__ */ new Map();
1773
+ async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
1774
+ const hit = cache.get(cwd);
1775
+ if (hit && hit.expiresAt > now) return hit.value;
1776
+ const value = await collectGitContext(cwd);
1777
+ cache.set(cwd, { value, expiresAt: now + CACHE_TTL_MS });
1778
+ return value;
1584
1779
  }
1585
- function geminiUpstream() {
1586
- return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
1780
+
1781
+ // src/commands/login.ts
1782
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
1783
+ import { join as join4 } from "path";
1784
+ import { homedir as homedir2 } from "os";
1785
+ import readline from "readline/promises";
1786
+ import chalk from "chalk";
1787
+ var CONFIG_DIR_NAME2 = ".pretense";
1788
+ var CONFIG_FILE2 = "config.json";
1789
+ function getUserDashboardConfigDir() {
1790
+ return join4(homedir2(), CONFIG_DIR_NAME2);
1587
1791
  }
1588
- function ollamaUpstream() {
1589
- return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
1792
+ function getUserDashboardConfigPath() {
1793
+ return join4(getUserDashboardConfigDir(), CONFIG_FILE2);
1590
1794
  }
1591
- function detectUpstream(path) {
1592
- if (path.startsWith("/v1/messages")) return anthropicUpstream();
1593
- if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
1594
- return openaiUpstream();
1795
+ function isValidKeyShape(key) {
1796
+ const trimmed = key.trim();
1797
+ if (trimmed.length < 24) return false;
1798
+ if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
1799
+ return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_") || trimmed.startsWith("prtns_live_");
1800
+ }
1801
+ function readStdinIfPiped() {
1802
+ if (process.stdin.isTTY) return "";
1803
+ try {
1804
+ const chunks = [];
1805
+ const buf = Buffer.alloc(4096);
1806
+ let bytesRead = 0;
1807
+ do {
1808
+ try {
1809
+ bytesRead = readSync(0, buf, 0, buf.length, null);
1810
+ } catch {
1811
+ bytesRead = 0;
1812
+ }
1813
+ if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
1814
+ } while (bytesRead === buf.length);
1815
+ return Buffer.concat(chunks).toString("utf-8").trim();
1816
+ } catch {
1817
+ return "";
1595
1818
  }
1596
- if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
1597
- if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
1598
- return anthropicUpstream();
1599
1819
  }
1600
- function detectProvider(path) {
1601
- if (path.startsWith("/v1/messages")) return "anthropic";
1602
- if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
1603
- return "openai";
1820
+ function maskKey(key) {
1821
+ const trimmed = key.trim();
1822
+ if (trimmed.length <= 12) return trimmed;
1823
+ return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
1824
+ }
1825
+ function logDashboardCredentialHint() {
1826
+ const env = process.env["PRETENSE_API_KEY"]?.trim() ?? "";
1827
+ const envOk = env.length > 0 && isValidKeyShape(env);
1828
+ const abs = getUserDashboardConfigPath();
1829
+ const fk = loadApiKey();
1830
+ if (envOk) {
1831
+ console.error(
1832
+ chalk.gray(
1833
+ `[pretense] Dashboard key source: PRETENSE_API_KEY in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
1834
+ )
1835
+ );
1836
+ } else if (fk && isValidKeyShape(fk)) {
1837
+ console.error(chalk.gray(`[pretense] Dashboard key source: ${abs} (${maskKey(fk)})`));
1838
+ } else {
1839
+ console.error(chalk.gray("[pretense] Dashboard key source: none configured."));
1604
1840
  }
1605
- if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
1606
- if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
1607
- return "anthropic";
1608
1841
  }
1609
- function extractText(body) {
1610
- const parts = [];
1611
- if (typeof body["system"] === "string") parts.push(body["system"]);
1612
- if (Array.isArray(body["messages"])) {
1613
- for (const msg of body["messages"]) {
1614
- if (typeof msg["content"] === "string") parts.push(msg["content"]);
1615
- else if (Array.isArray(msg["content"])) {
1616
- for (const block of msg["content"]) {
1617
- if (block["type"] === "text" && typeof block["text"] === "string") parts.push(block["text"]);
1618
- }
1619
- }
1842
+ function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
1843
+ if (!existsSync5(configDir)) {
1844
+ mkdirSync5(configDir, { recursive: true });
1845
+ }
1846
+ const path = join4(configDir, CONFIG_FILE2);
1847
+ let existing = {};
1848
+ if (existsSync5(path)) {
1849
+ try {
1850
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1851
+ } catch {
1852
+ existing = {};
1620
1853
  }
1621
1854
  }
1622
- return parts.join("\n");
1855
+ const next = {
1856
+ ...existing,
1857
+ api_key: key.trim(),
1858
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
1859
+ };
1860
+ writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
1861
+ try {
1862
+ chmodSync(path, 384);
1863
+ } catch {
1864
+ }
1865
+ return path;
1623
1866
  }
1624
- function extractCodeBlocks(text) {
1625
- const blocks = [];
1626
- const re = /```(\w*)\n([\s\S]*?)```/g;
1627
- let m;
1628
- while ((m = re.exec(text)) !== null) {
1629
- blocks.push({
1630
- code: m[2] ?? "",
1631
- language: m[1] ?? "typescript",
1632
- start: m.index,
1633
- end: m.index + m[0].length
1634
- });
1867
+ function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
1868
+ const path = join4(configDir, CONFIG_FILE2);
1869
+ if (!existsSync5(path)) return { removed: false, path };
1870
+ let existing = {};
1871
+ try {
1872
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1873
+ } catch {
1874
+ return { removed: false, path };
1875
+ }
1876
+ const ak = existing.api_key;
1877
+ const hadKey = typeof ak === "string" && ak.trim().length > 0;
1878
+ if (!hadKey) return { removed: false, path };
1879
+ delete existing.api_key;
1880
+ delete existing.saved_at;
1881
+ writeFileSync4(path, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
1882
+ try {
1883
+ chmodSync(path, 384);
1884
+ } catch {
1635
1885
  }
1636
- return blocks;
1886
+ return { removed: true, path };
1637
1887
  }
1638
- function replaceCodeBlocks(text, blocks, replacements) {
1639
- let result = text;
1640
- for (let i = blocks.length - 1; i >= 0; i--) {
1641
- const block = blocks[i];
1642
- const replacement = replacements[i] ?? block.code;
1643
- const lang = block.language ?? "typescript";
1644
- result = result.slice(0, block.start) + "```" + lang + "\n" + replacement + "```" + result.slice(block.end);
1888
+ function loadApiKey(configDir = getUserDashboardConfigDir()) {
1889
+ const path = join4(configDir, CONFIG_FILE2);
1890
+ if (!existsSync5(path)) return null;
1891
+ try {
1892
+ const raw = readFileSync5(path, "utf-8");
1893
+ const parsed = JSON.parse(raw);
1894
+ if (typeof parsed.api_key === "string") {
1895
+ const t = parsed.api_key.trim();
1896
+ if (t.length > 0) return t;
1897
+ }
1898
+ } catch {
1645
1899
  }
1646
- return result;
1900
+ return null;
1647
1901
  }
1648
- function applyToBody(body, transform) {
1649
- const mutateField = (val) => {
1650
- if (typeof val === "string") return transform(val);
1651
- if (Array.isArray(val)) return val.map(mutateField);
1652
- if (val && typeof val === "object") {
1653
- const obj = val;
1654
- return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
1902
+ function resolveDashboardKeyCandidate() {
1903
+ const env = (process.env["PRETENSE_API_KEY"] ?? "").trim();
1904
+ if (env.length > 0) {
1905
+ if (!isValidKeyShape(env)) {
1906
+ return { key: null, fromFile: false, badEnv: true };
1655
1907
  }
1656
- return val;
1657
- };
1658
- return mutateField(body);
1908
+ return { key: env, fromFile: false, badEnv: false };
1909
+ }
1910
+ const fileKey = loadApiKey();
1911
+ if (fileKey && isValidKeyShape(fileKey)) {
1912
+ return { key: fileKey.trim(), fromFile: true, badEnv: false };
1913
+ }
1914
+ return { key: null, fromFile: Boolean(fileKey), badEnv: false };
1659
1915
  }
1660
- var stats = {
1661
- requestsProcessed: 0,
1662
- totalTokensMutated: 0,
1663
- totalSecretsBlocked: 0,
1664
- startedAt: Date.now()
1665
- };
1666
- function daysSinceFirstUse() {
1667
- const usage = loadUsage();
1668
- if (!usage.firstUseDate) return 0;
1669
- const first = new Date(usage.firstUseDate).getTime();
1670
- return Math.floor((Date.now() - first) / (24 * 60 * 60 * 1e3));
1916
+ function installDashboardKeyForCurrentProcess(key) {
1917
+ const trimmed = key.trim();
1918
+ const path = saveApiKey(trimmed);
1919
+ process.env["PRETENSE_API_KEY"] = trimmed;
1920
+ return path;
1921
+ }
1922
+ async function promptDashboardApiKeyAfterFailure() {
1923
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
1924
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1925
+ try {
1926
+ const line = (await rl.question(chalk.bold("Pretense dashboard API key: "))).trim();
1927
+ return line.length > 0 ? line : null;
1928
+ } finally {
1929
+ rl.close();
1930
+ }
1931
+ }
1932
+ async function ensureDashboardKeyForStart() {
1933
+ const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
1934
+ if (badEnv) {
1935
+ console.error(chalk.red("Error: PRETENSE_API_KEY is set but is not a valid Pretense dashboard API key shape."));
1936
+ console.error(
1937
+ chalk.gray("Keys look like prtns_live_\u2026 or pk_live_\u2026. Fix the variable or unset it to use ~/.pretense/config.json.")
1938
+ );
1939
+ process.exit(1);
1940
+ }
1941
+ if (key) {
1942
+ return;
1943
+ }
1944
+ if (fromFile) {
1945
+ console.warn(
1946
+ chalk.yellow(
1947
+ "Ignoring invalid `api_key` in ~/.pretense/config.json (wrong shape). Enter a new key or run `pretense login --key ...`."
1948
+ )
1949
+ );
1950
+ }
1951
+ const stdinOk = process.stdin.isTTY;
1952
+ const stdoutOk = process.stdout.isTTY;
1953
+ if (!stdinOk || !stdoutOk) {
1954
+ console.error(chalk.red("Error: Pretense API key required to start the proxy."));
1955
+ console.error(
1956
+ chalk.gray(
1957
+ "Save one with `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
1958
+ )
1959
+ );
1960
+ process.exit(1);
1961
+ }
1962
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1963
+ try {
1964
+ console.log(chalk.cyan("No Pretense API key found in ~/.pretense/config.json or $PRETENSE_API_KEY."));
1965
+ const entered = (await rl.question(chalk.bold("Enter your Pretense API key: "))).trim();
1966
+ if (!entered) {
1967
+ console.error(chalk.red("No key entered; exiting."));
1968
+ process.exit(1);
1969
+ }
1970
+ if (!isValidKeyShape(entered)) {
1971
+ console.error(chalk.red("That does not look like a Pretense API key (expect prtns_live_\u2026 or pk_live_\u2026)."));
1972
+ console.error(chalk.gray("Get one from your Pretense dashboard, then run `pretense login --key ...`."));
1973
+ process.exit(1);
1974
+ }
1975
+ const path = installDashboardKeyForCurrentProcess(entered);
1976
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
1977
+ console.log(chalk.gray(` ${maskKey(entered)}`));
1978
+ } finally {
1979
+ rl.close();
1980
+ }
1981
+ const persisted = resolveDashboardKeyCandidate();
1982
+ if (!persisted.key || persisted.badEnv) {
1983
+ console.error(chalk.red("Error: Pretense dashboard API key could not be confirmed after login prompt."));
1984
+ process.exit(1);
1985
+ }
1986
+ }
1987
+ function runLogin(opts = {}) {
1988
+ const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
1989
+ if (!candidate) {
1990
+ console.error(chalk.red("Error: no API key provided."));
1991
+ console.error(
1992
+ chalk.gray("Try: pretense login --key pk_live_xxxxx")
1993
+ );
1994
+ console.error(
1995
+ chalk.gray(" or: echo $PRETENSE_API_KEY | pretense login")
1996
+ );
1997
+ return 1;
1998
+ }
1999
+ if (!isValidKeyShape(candidate)) {
2000
+ console.error(chalk.red("Error: that does not look like a Pretense API key."));
2001
+ console.error(
2002
+ chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
2003
+ );
2004
+ console.error(
2005
+ chalk.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
2006
+ );
2007
+ return 1;
2008
+ }
2009
+ const path = saveApiKey(candidate, opts.configDir);
2010
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
2011
+ console.log(chalk.gray(` ${maskKey(candidate)}`));
2012
+ console.log(chalk.gray(" File mode 600 \u2014 readable only by you."));
2013
+ return 0;
1671
2014
  }
1672
- function printUpgradeNudge(reason) {
1673
- process.stdout.write(
1674
- `
1675
- \x1B[33m[PRETENSE] ${reason}\x1B[0m
1676
- \x1B[33m[PRETENSE] Upgrade to Pro: https://pretense.ai/pricing\x1B[0m
1677
2015
 
1678
- `
1679
- );
2016
+ // src/dashboard-env.ts
2017
+ var DASHBOARD_API_KEY_ENVS = ["PRETENSE_API_KEY", "PRETEST_API_KEY"];
2018
+ var DASHBOARD_API_URL_ENVS = ["PRETENSE_API_URL", "PRETEST_API_URL"];
2019
+ function firstNonEmptyEnv(names) {
2020
+ for (const name of names) {
2021
+ const v = process.env[name]?.trim();
2022
+ if (v) return v;
2023
+ }
2024
+ return "";
1680
2025
  }
1681
- function createProxy(opts = {}) {
1682
- const config = { ...DEFAULT_CONFIG, ...opts.config };
2026
+ function clearDashboardApiKeyFromProcess() {
2027
+ delete process.env["PRETEST_API_KEY"];
2028
+ delete process.env["PRETENSE_API_KEY"];
2029
+ }
2030
+ function hasDashboardApiKeyInEnv() {
2031
+ return DASHBOARD_API_KEY_ENVS.some((n) => !!process.env[n]?.trim());
2032
+ }
2033
+
2034
+ // src/publish-info.ts
2035
+ var PUBLISH_PACKAGE_URL = "https://www.npmjs.com/package/@pretense/cli";
2036
+
2037
+ // src/backend-client.ts
2038
+ function usageSummaryFromValidate(v) {
2039
+ return {
2040
+ plan: v.plan,
2041
+ monthlyMutationLimit: v.monthlyMutationLimit,
2042
+ mutationsUsed: v.mutationsUsed,
2043
+ mutationsRemaining: v.mutationsRemaining,
2044
+ requestsThisPeriod: 0,
2045
+ secretsBlockedThisPeriod: 0,
2046
+ periodResetsAt: v.periodResetsAt,
2047
+ keyExpiresAt: null,
2048
+ keyStatus: "active"
2049
+ };
2050
+ }
2051
+ var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
2052
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
2053
+ function getConfiguredApiRoot(rootOverride) {
2054
+ const raw = (rootOverride?.trim() || firstNonEmptyEnv(DASHBOARD_API_URL_ENVS) || DEFAULT_PRETENSE_API_URL).trim();
2055
+ if (!raw) {
2056
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2057
+ }
2058
+ let normalized = raw;
2059
+ if (!/^https?:\/\//i.test(normalized)) {
2060
+ normalized = `https://${normalized}`;
2061
+ }
2062
+ try {
2063
+ const u = new URL(normalized);
2064
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL.replace(/\/$/, "") : u.origin;
2065
+ const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
2066
+ return `${origin}${path}`;
2067
+ } catch {
2068
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2069
+ }
2070
+ }
2071
+ function getCliApiUrl(pathWithinCli, rootOverride) {
2072
+ const root = getConfiguredApiRoot(rootOverride);
2073
+ const tail = pathWithinCli.replace(/^\/+/, "");
2074
+ return `${root}/api/cli/${tail}`;
2075
+ }
2076
+ function getApiRequestTimeoutMs() {
2077
+ const raw = process.env["PRETEST_API_TIMEOUT_MS"]?.trim() || process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
2078
+ if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
2079
+ const n = parseInt(raw, 10);
2080
+ return Math.min(Math.max(n, 3e3), 12e4);
2081
+ }
2082
+ function getApiKey() {
2083
+ const fromEnv = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
2084
+ if (fromEnv) return fromEnv;
2085
+ const fromFile = loadApiKey();
2086
+ if (!fromFile) return null;
2087
+ const t = fromFile.trim();
2088
+ return t.length > 0 ? t : null;
2089
+ }
2090
+ function getDashboardApiKey() {
2091
+ return getApiKey();
2092
+ }
2093
+ var cachedValidation = null;
2094
+ var lastValidationSuccessAt = 0;
2095
+ var VALIDATION_CACHE_FRESH_MS = 45e3;
2096
+ function getCachedValidation() {
2097
+ return cachedValidation;
2098
+ }
2099
+ function clearValidationCache() {
2100
+ cachedValidation = null;
2101
+ lastValidationSuccessAt = 0;
2102
+ }
2103
+ function bumpCachedUsageAfterLog(identifiersMutated) {
2104
+ if (!cachedValidation || identifiersMutated <= 0) return;
2105
+ cachedValidation.mutationsUsed += identifiersMutated;
2106
+ if (cachedValidation.mutationsRemaining !== -1) {
2107
+ cachedValidation.mutationsRemaining = Math.max(
2108
+ 0,
2109
+ cachedValidation.mutationsRemaining - identifiersMutated
2110
+ );
2111
+ }
2112
+ }
2113
+ function syncCachedUsageFromServer(codeProtectionsUsed, limit) {
2114
+ if (!cachedValidation) return;
2115
+ if (codeProtectionsUsed < cachedValidation.mutationsUsed) return;
2116
+ cachedValidation.mutationsUsed = codeProtectionsUsed;
2117
+ cachedValidation.monthlyMutationLimit = limit;
2118
+ if (limit !== -1) {
2119
+ cachedValidation.mutationsRemaining = Math.max(0, limit - codeProtectionsUsed);
2120
+ }
2121
+ }
2122
+ function makeBackendReachabilityError(code, message) {
2123
+ const err = new Error(message);
2124
+ err.code = code;
2125
+ return err;
2126
+ }
2127
+ async function validateKey(opts = {}) {
2128
+ const enforceReachability = opts.enforceReachability === true;
2129
+ const now = Date.now();
2130
+ const cacheFresh = cachedValidation !== null && now - lastValidationSuccessAt < VALIDATION_CACHE_FRESH_MS;
2131
+ if (!opts.force && cacheFresh) {
2132
+ return cachedValidation;
2133
+ }
2134
+ const validateUrl = getCliApiUrl("auth/validate");
2135
+ const timeoutMs = getApiRequestTimeoutMs();
2136
+ const apiKey = getApiKey();
2137
+ if (!apiKey) {
2138
+ if (enforceReachability) {
2139
+ const err = new Error("Missing dashboard API key");
2140
+ err.code = "MISSING_API_KEY";
2141
+ cachedValidation = null;
2142
+ lastValidationSuccessAt = 0;
2143
+ throw err;
2144
+ }
2145
+ return null;
2146
+ }
2147
+ const controller = new AbortController();
2148
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2149
+ try {
2150
+ const resp = await fetch(validateUrl, {
2151
+ method: "POST",
2152
+ headers: {
2153
+ "content-type": "application/json",
2154
+ authorization: `Bearer ${apiKey}`
2155
+ },
2156
+ body: "{}",
2157
+ signal: controller.signal
2158
+ });
2159
+ if (resp.status === 401 || resp.status === 403) {
2160
+ const raw2 = await resp.json().catch(() => ({}));
2161
+ const msg = typeof raw2.message === "string" ? raw2.message : "API key rejected by server";
2162
+ const err = new Error(msg);
2163
+ err.code = typeof raw2.code === "string" ? raw2.code : "INVALID_API_KEY";
2164
+ cachedValidation = null;
2165
+ lastValidationSuccessAt = 0;
2166
+ throw err;
2167
+ }
2168
+ if (!resp.ok) {
2169
+ const msg = `Dashboard API returned HTTP ${resp.status} from ${validateUrl}`;
2170
+ if (enforceReachability) {
2171
+ cachedValidation = null;
2172
+ lastValidationSuccessAt = 0;
2173
+ throw makeBackendReachabilityError("AUTH_BACKEND_REJECTED", msg);
2174
+ }
2175
+ return null;
2176
+ }
2177
+ let raw;
2178
+ try {
2179
+ raw = await resp.json();
2180
+ } catch {
2181
+ if (enforceReachability) {
2182
+ cachedValidation = null;
2183
+ lastValidationSuccessAt = 0;
2184
+ throw makeBackendReachabilityError(
2185
+ "AUTH_BACKEND_REJECTED",
2186
+ `Invalid JSON from ${validateUrl}`
2187
+ );
2188
+ }
2189
+ return null;
2190
+ }
2191
+ const data = {
2192
+ ...raw,
2193
+ mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
2194
+ mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
2195
+ monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
2196
+ };
2197
+ if (typeof data.valid === "boolean" && data.valid === false) {
2198
+ const err = new Error("API key is not valid");
2199
+ err.code = "INVALID_API_KEY";
2200
+ cachedValidation = null;
2201
+ lastValidationSuccessAt = 0;
2202
+ throw err;
2203
+ }
2204
+ cachedValidation = data;
2205
+ lastValidationSuccessAt = Date.now();
2206
+ return data;
2207
+ } catch (err) {
2208
+ 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") {
2209
+ throw err;
2210
+ }
2211
+ if (enforceReachability) {
2212
+ const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS or PRETEST_API_TIMEOUT_MS)` : `Cannot reach dashboard API at ${validateUrl}: ${err.message}`;
2213
+ cachedValidation = null;
2214
+ lastValidationSuccessAt = 0;
2215
+ throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
2216
+ }
2217
+ if (opts.verbose) {
2218
+ process.stderr.write(
2219
+ `[PRETENSE] Backend validation failed: ${err.message}
2220
+ `
2221
+ );
2222
+ }
2223
+ return null;
2224
+ } finally {
2225
+ clearTimeout(timer);
2226
+ }
2227
+ }
2228
+ async function fetchUsageSummary(opts = {}) {
2229
+ const summaryUrl = getCliApiUrl("usage/summary");
2230
+ const apiKey = getApiKey();
2231
+ if (!apiKey) return null;
2232
+ const controller = new AbortController();
2233
+ const timer = setTimeout(() => controller.abort(), getApiRequestTimeoutMs());
2234
+ try {
2235
+ const resp = await fetch(summaryUrl, {
2236
+ method: "GET",
2237
+ headers: { authorization: `Bearer ${apiKey}` },
2238
+ signal: controller.signal
2239
+ });
2240
+ if (!resp.ok) return null;
2241
+ const raw = await resp.json();
2242
+ return {
2243
+ ...raw,
2244
+ mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
2245
+ mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
2246
+ monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
2247
+ };
2248
+ } catch (err) {
2249
+ if (opts.verbose) {
2250
+ process.stderr.write(
2251
+ `[PRETENSE] Usage summary fetch failed: ${err.message}
2252
+ `
2253
+ );
2254
+ }
2255
+ return null;
2256
+ } finally {
2257
+ clearTimeout(timer);
2258
+ }
2259
+ }
2260
+ async function isWithinLimits() {
2261
+ const v = getCachedValidation();
2262
+ if (!v) return { allowed: true };
2263
+ if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
2264
+ const fresh = await validateKey({ force: true }).catch(() => null);
2265
+ const check = fresh ?? v;
2266
+ if (check.mutationsRemaining !== -1 && check.mutationsRemaining <= 0) {
2267
+ return {
2268
+ allowed: false,
2269
+ reason: `Monthly security token limit reached (${check.mutationsUsed}/${check.monthlyMutationLimit}). See ${PUBLISH_PACKAGE_URL}`
2270
+ };
2271
+ }
2272
+ return { allowed: true };
2273
+ }
2274
+ return { allowed: true };
2275
+ }
2276
+
2277
+ // src/log-uploader.ts
2278
+ var FLUSH_INTERVAL_MS = 5e3;
2279
+ var buffer = [];
2280
+ var flushTimer = null;
2281
+ function startFlushTimer(verbose) {
2282
+ if (flushTimer !== null) return;
2283
+ flushTimer = setInterval(() => {
2284
+ void flushBuffer(verbose);
2285
+ }, FLUSH_INTERVAL_MS);
2286
+ if (typeof flushTimer.unref === "function") flushTimer.unref();
2287
+ }
2288
+ function attachExitFlush() {
2289
+ const flush = () => {
2290
+ void flushBuffer(false);
2291
+ };
2292
+ process.once("exit", flush);
2293
+ process.once("SIGINT", () => {
2294
+ flush();
2295
+ process.exit(0);
2296
+ });
2297
+ process.once("SIGTERM", () => {
2298
+ flush();
2299
+ process.exit(0);
2300
+ });
2301
+ }
2302
+ var exitFlushAttached = false;
2303
+ async function flushBuffer(verbose) {
2304
+ if (buffer.length === 0) return;
2305
+ const snapshot = buffer;
2306
+ buffer = [];
2307
+ const last = snapshot[snapshot.length - 1];
2308
+ const { options } = last;
2309
+ let file_count = 0;
2310
+ let identifiers_mutated = 0;
2311
+ let secrets_blocked = 0;
2312
+ let scan_tokens = 0;
2313
+ let mutation_tokens = 0;
2314
+ let read_tokens = 0;
2315
+ let llm_provider = "unknown";
2316
+ let cli_version;
2317
+ for (const { event } of snapshot) {
2318
+ file_count += event.file_count ?? 0;
2319
+ identifiers_mutated += event.identifiers_mutated ?? 0;
2320
+ secrets_blocked += event.secrets_blocked ?? 0;
2321
+ scan_tokens += event.scan_tokens ?? 0;
2322
+ mutation_tokens += event.mutation_tokens ?? 0;
2323
+ read_tokens += event.read_tokens ?? 0;
2324
+ if (event.llm_provider) llm_provider = event.llm_provider;
2325
+ if (event.cli_version) cli_version = event.cli_version;
2326
+ }
2327
+ const apiRoot = options.apiUrl?.trim() || void 0;
2328
+ const endpoint = getCliApiUrl("log", apiRoot);
2329
+ const { apiKey, timeoutMs = getApiRequestTimeoutMs(), onResponse } = options;
2330
+ if (!apiKey) return;
2331
+ const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
2332
+ const idempotency_key = crypto.randomUUID();
2333
+ const body = {
2334
+ file_count,
2335
+ identifiers_mutated,
2336
+ identifiers_code_protected: identifiers_mutated,
2337
+ secrets_blocked,
2338
+ llm_provider,
2339
+ scan_tokens,
2340
+ code_protection_tokens: mutation_tokens,
2341
+ read_tokens,
2342
+ idempotency_key,
2343
+ ...cli_version ? { cli_version } : {},
2344
+ git_remote: gitCtx.git_remote,
2345
+ repo_remote_url: gitCtx.git_remote,
2346
+ git_branch: gitCtx.git_branch,
2347
+ commit_sha: gitCtx.git_commit_sha,
2348
+ git_commit_sha: gitCtx.git_commit_sha
2349
+ };
2350
+ const controller = new AbortController();
2351
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2352
+ try {
2353
+ const resp = await fetch(endpoint, {
2354
+ method: "POST",
2355
+ headers: {
2356
+ "content-type": "application/json",
2357
+ authorization: `Bearer ${apiKey}`
2358
+ },
2359
+ body: JSON.stringify(body),
2360
+ signal: controller.signal
2361
+ });
2362
+ if (resp.ok) {
2363
+ const data = await resp.json().catch(() => null);
2364
+ if (data && typeof data.codeProtectionsUsed === "number" && typeof data.limit === "number") {
2365
+ onResponse?.(data.codeProtectionsUsed, data.limit);
2366
+ }
2367
+ } else if (verbose) {
2368
+ const detail = await resp.text().catch(() => "");
2369
+ process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status} (batch of ${snapshot.length}): ${detail}
2370
+ `);
2371
+ }
2372
+ } catch (err) {
2373
+ if (verbose) {
2374
+ const msg = err instanceof Error ? err.message : String(err);
2375
+ process.stderr.write(`[PRETENSE] log upload failed: ${msg}
2376
+ `);
2377
+ }
2378
+ } finally {
2379
+ clearTimeout(timer);
2380
+ }
2381
+ }
2382
+ async function uploadLog(event, options) {
2383
+ if (!options.apiKey) return;
2384
+ buffer.push({ event, options });
2385
+ startFlushTimer(options.verbose ?? false);
2386
+ if (!exitFlushAttached) {
2387
+ exitFlushAttached = true;
2388
+ attachExitFlush();
2389
+ }
2390
+ }
2391
+
2392
+ // src/start-client-instructions.ts
2393
+ import chalk2 from "chalk";
2394
+ function printStartClientInstructions(port) {
2395
+ const host = `http://localhost:${port}`;
2396
+ console.log();
2397
+ console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
2398
+ console.log(chalk2.gray("Point the CLI at the Pretense backend API (optional if using default):"));
2399
+ console.log(
2400
+ chalk2.white(`export PRETENSE_API_URL='https://api.pretense.ai'`)
2401
+ );
2402
+ console.log();
2403
+ console.log(chalk2.gray("Log in with your Pretense dashboard API key:"));
2404
+ console.log(chalk2.white("pretense login --key your-pretense-api-key"));
2405
+ console.log();
2406
+ console.log(
2407
+ chalk2.gray("Set the LLM base URL for your provider (proxy below):")
2408
+ );
2409
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL="${host}"`));
2410
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL=${host}`));
2411
+ console.log(chalk2.white(`export OPENAI_BASE_URL=${host}/v1`));
2412
+ console.log(chalk2.white(`export GEMINI_BASE_URL=${host}/v1beta`));
2413
+ console.log(chalk2.white(`export OLLAMA_HOST=${host}`));
2414
+ console.log();
2415
+ console.log(chalk2.gray("Set your real upstream LLM API key (unchanged):"));
2416
+ console.log(chalk2.white('export ANTHROPIC_API_KEY="sk-ant-API-KEY"'));
2417
+ console.log();
2418
+ console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
2419
+ console.log(
2420
+ chalk2.gray(
2421
+ "Stop the proxy: press Ctrl+C in the terminal where pretense start is running."
2422
+ )
2423
+ );
2424
+ console.log(
2425
+ chalk2.gray(
2426
+ "Remove the saved Pretense dashboard key from ~/.pretense/config.json:"
2427
+ )
2428
+ );
2429
+ console.log(chalk2.white("pretense logout"));
2430
+ console.log(
2431
+ chalk2.gray(
2432
+ "(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
2433
+ )
2434
+ );
2435
+ console.log(
2436
+ chalk2.gray(
2437
+ "PRETENSE_API_KEY set in that same terminal \u2014 then press Ctrl+C and run pretense start again."
2438
+ )
2439
+ );
2440
+ console.log();
2441
+ }
2442
+
2443
+ // src/version.ts
2444
+ import { readFileSync as readFileSync6 } from "fs";
2445
+ import { fileURLToPath } from "url";
2446
+ var cached = null;
2447
+ function getCliSemver() {
2448
+ if (cached !== null) return cached;
2449
+ if (typeof __BINARY_VERSION__ !== "undefined" && __BINARY_VERSION__) {
2450
+ cached = __BINARY_VERSION__;
2451
+ return cached;
2452
+ }
2453
+ try {
2454
+ const url = new URL("../package.json", import.meta.url);
2455
+ const raw = readFileSync6(fileURLToPath(url), "utf-8");
2456
+ cached = JSON.parse(raw).version ?? "0.0.0";
2457
+ } catch {
2458
+ cached = "0.0.0";
2459
+ }
2460
+ return cached;
2461
+ }
2462
+
2463
+ // src/proxy.ts
2464
+ var dashboardSavedCredentialsRevoked = false;
2465
+ function attachDashboardLogoutWatcher(enabled) {
2466
+ if (!enabled) return;
2467
+ const dir = getUserDashboardConfigDir();
2468
+ let debounceTimer;
2469
+ const applyFilesystemCredentialState = () => {
2470
+ const fk = loadApiKey();
2471
+ if (!fk) {
2472
+ if (dashboardSavedCredentialsRevoked) return;
2473
+ dashboardSavedCredentialsRevoked = true;
2474
+ clearDashboardApiKeyFromProcess();
2475
+ clearValidationCache();
2476
+ try {
2477
+ process.stderr.write(
2478
+ `\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
2479
+ `
2480
+ );
2481
+ } catch {
2482
+ }
2483
+ return;
2484
+ }
2485
+ if (dashboardSavedCredentialsRevoked) {
2486
+ dashboardSavedCredentialsRevoked = false;
2487
+ clearValidationCache();
2488
+ }
2489
+ };
2490
+ const schedule = () => {
2491
+ if (debounceTimer) clearTimeout(debounceTimer);
2492
+ debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
2493
+ };
2494
+ try {
2495
+ if (!existsSync6(dir)) return;
2496
+ watch(dir, () => {
2497
+ schedule();
2498
+ });
2499
+ } catch {
2500
+ }
2501
+ }
2502
+ function anthropicUpstream() {
2503
+ return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
2504
+ }
2505
+ function openaiUpstream() {
2506
+ return process.env["OPENAI_UPSTREAM_URL"] ?? "https://api.openai.com";
2507
+ }
2508
+ function geminiUpstream() {
2509
+ return process.env["GEMINI_UPSTREAM_URL"] ?? "https://generativelanguage.googleapis.com";
2510
+ }
2511
+ function ollamaUpstream() {
2512
+ return process.env["OLLAMA_UPSTREAM_URL"] ?? "http://localhost:11434";
2513
+ }
2514
+ function detectUpstream(path) {
2515
+ if (path.startsWith("/v1/messages")) return anthropicUpstream();
2516
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
2517
+ return openaiUpstream();
2518
+ }
2519
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return geminiUpstream();
2520
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return ollamaUpstream();
2521
+ return anthropicUpstream();
2522
+ }
2523
+ function detectProvider(path) {
2524
+ if (path.startsWith("/v1/messages")) return "anthropic";
2525
+ if (path.startsWith("/v1/chat/completions") || path.startsWith("/v1/completions") || path.startsWith("/v1/responses")) {
2526
+ return "openai";
2527
+ }
2528
+ if (path.startsWith("/v1beta/") || path.startsWith("/v1/models")) return "google";
2529
+ if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
2530
+ return "anthropic";
2531
+ }
2532
+ function stripPretenseOnlyForwardingHeaders(headers) {
2533
+ const drop = /* @__PURE__ */ new Set(["x-pretense-api-key", "x-pretense-key"]);
2534
+ for (const k of [...Object.keys(headers)]) {
2535
+ if (drop.has(k.toLowerCase())) delete headers[k];
2536
+ }
2537
+ }
2538
+ function extractText(body) {
2539
+ const parts = [];
2540
+ if (typeof body["system"] === "string") parts.push(body["system"]);
2541
+ if (Array.isArray(body["messages"])) {
2542
+ for (const msg of body["messages"]) {
2543
+ if (typeof msg["content"] === "string") parts.push(msg["content"]);
2544
+ else if (Array.isArray(msg["content"])) {
2545
+ for (const block of msg["content"]) {
2546
+ if (block["type"] === "text" && typeof block["text"] === "string") parts.push(block["text"]);
2547
+ }
2548
+ }
2549
+ }
2550
+ }
2551
+ return parts.join("\n");
2552
+ }
2553
+ function extractCodeBlocks(text) {
2554
+ const blocks = [];
2555
+ const re = /```(\w*)\n([\s\S]*?)```/g;
2556
+ let m;
2557
+ while ((m = re.exec(text)) !== null) {
2558
+ blocks.push({
2559
+ code: m[2] ?? "",
2560
+ language: m[1] ?? "typescript",
2561
+ start: m.index,
2562
+ end: m.index + m[0].length
2563
+ });
2564
+ }
2565
+ return blocks;
2566
+ }
2567
+ function replaceCodeBlocks(text, blocks, replacements) {
2568
+ let result = text;
2569
+ for (let i = blocks.length - 1; i >= 0; i--) {
2570
+ const block = blocks[i];
2571
+ const replacement = replacements[i] ?? block.code;
2572
+ const lang = block.language ?? "typescript";
2573
+ result = result.slice(0, block.start) + "```" + lang + "\n" + replacement + "```" + result.slice(block.end);
2574
+ }
2575
+ return result;
2576
+ }
2577
+ function applyToBody(body, transform) {
2578
+ const mutateField = (val) => {
2579
+ if (typeof val === "string") return transform(val);
2580
+ if (Array.isArray(val)) return val.map(mutateField);
2581
+ if (val && typeof val === "object") {
2582
+ const obj = val;
2583
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
2584
+ }
2585
+ return val;
2586
+ };
2587
+ return mutateField(body);
2588
+ }
2589
+ var stats = {
2590
+ requestsProcessed: 0,
2591
+ totalTokensMutated: 0,
2592
+ totalSecretsBlocked: 0,
2593
+ startedAt: Date.now()
2594
+ };
2595
+ function daysSinceFirstUse() {
2596
+ const usage = loadUsage();
2597
+ if (!usage.firstUseDate) return 0;
2598
+ const first = new Date(usage.firstUseDate).getTime();
2599
+ return Math.floor((Date.now() - first) / (24 * 60 * 60 * 1e3));
2600
+ }
2601
+ function printUpgradeNudge(reason) {
2602
+ process.stdout.write(
2603
+ `
2604
+ \x1B[33m[PRETENSE] ${reason}\x1B[0m
2605
+ \x1B[33m[PRETENSE] Upgrade to Pro: ${PUBLISH_PACKAGE_URL}\x1B[0m
2606
+
2607
+ `
2608
+ );
2609
+ }
2610
+ function createProxy(opts = {}) {
2611
+ const config = { ...DEFAULT_CONFIG, ...opts.config };
1683
2612
  const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
1684
2613
  const verbose = opts.verbose ?? false;
1685
2614
  const app = new Hono();
@@ -1688,7 +2617,7 @@ function createProxy(opts = {}) {
1688
2617
  "/",
1689
2618
  (c) => c.json({
1690
2619
  name: "pretense",
1691
- version: "0.3.0",
2620
+ version: getCliSemver(),
1692
2621
  status: "running",
1693
2622
  routes: {
1694
2623
  "GET /": "This welcome page",
@@ -1698,14 +2627,14 @@ function createProxy(opts = {}) {
1698
2627
  "POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
1699
2628
  },
1700
2629
  languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
1701
- docs: "https://pretense.ai/docs"
2630
+ docs: PUBLISH_PACKAGE_URL
1702
2631
  })
1703
2632
  );
1704
2633
  app.get(
1705
2634
  "/health",
1706
2635
  (c) => c.json({
1707
2636
  status: "ok",
1708
- version: "0.3.0",
2637
+ version: getCliSemver(),
1709
2638
  uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
1710
2639
  stats: {
1711
2640
  requestsProcessed: stats.requestsProcessed,
@@ -1735,6 +2664,7 @@ function createProxy(opts = {}) {
1735
2664
  });
1736
2665
  delete headers2["host"];
1737
2666
  headers2["host"] = new URL(upstream2).host;
2667
+ stripPretenseOnlyForwardingHeaders(headers2);
1738
2668
  const resp = await fetch(url, { method: c.req.method, headers: headers2 });
1739
2669
  return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
1740
2670
  }
@@ -1751,22 +2681,93 @@ function createProxy(opts = {}) {
1751
2681
  const requestId = nanoid(12);
1752
2682
  const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
1753
2683
  const provider = detectProvider(c.req.path);
2684
+ if (dashboardSavedCredentialsRevoked) {
2685
+ return c.json(
2686
+ {
2687
+ error: {
2688
+ type: "pretense_session_logged_out",
2689
+ message: "Saved dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with a dashboard API key in this shell, stop it (Ctrl+C) and start again after logout."
2690
+ }
2691
+ },
2692
+ 401
2693
+ );
2694
+ }
2695
+ const dashboardApiKey = getDashboardApiKey();
2696
+ if (!dashboardApiKey) {
2697
+ return c.json(
2698
+ {
2699
+ error: {
2700
+ type: "pretense_dashboard_key_required",
2701
+ message: "Configure your dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
2702
+ }
2703
+ },
2704
+ 401
2705
+ );
2706
+ }
2707
+ try {
2708
+ await validateKey({ verbose, force: false, enforceReachability: true });
2709
+ } catch (err) {
2710
+ const code = err.code ?? "";
2711
+ const msg = err.message;
2712
+ const knownAuthCodes = /* @__PURE__ */ new Set([
2713
+ "KEY_REVOKED",
2714
+ "KEY_EXPIRED",
2715
+ "INVALID_API_KEY",
2716
+ "MISSING_API_KEY"
2717
+ ]);
2718
+ if (knownAuthCodes.has(code)) {
2719
+ return c.json(
2720
+ { error: { type: "pretense_auth_error", code, message: msg } },
2721
+ 401
2722
+ );
2723
+ }
2724
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2725
+ return c.json(
2726
+ { error: { type: "pretense_auth_backend_error", code, message: msg } },
2727
+ 503
2728
+ );
2729
+ }
2730
+ if (code === "ORG_INACTIVE") {
2731
+ return c.json(
2732
+ { error: { type: "pretense_org_inactive", code, message: msg } },
2733
+ 403
2734
+ );
2735
+ }
2736
+ return c.json(
2737
+ { error: { type: "pretense_auth_error", message: msg } },
2738
+ 401
2739
+ );
2740
+ }
1754
2741
  const sessHash = c.get("sessionHash");
1755
2742
  const session = getSession(sessHash);
1756
- const tier = detectTier();
1757
- const { monthlyMutations } = getTierLimits(tier);
1758
- const usage = loadUsage();
1759
- if (tier === "free" && usage.mutations >= monthlyMutations) {
2743
+ const tier = tierFromDashboardPlan(
2744
+ getCachedValidation()?.plan,
2745
+ detectTier()
2746
+ );
2747
+ const backendLimits = await isWithinLimits();
2748
+ if (!backendLimits.allowed) {
1760
2749
  return c.json(
1761
2750
  {
1762
2751
  error: {
1763
2752
  type: "pretense_rate_limit",
1764
- message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
2753
+ message: backendLimits.reason ?? "Security token limit exceeded."
1765
2754
  }
1766
2755
  },
1767
2756
  429
1768
2757
  );
1769
2758
  }
2759
+ const cliAuth = getCachedValidation();
2760
+ if (cliAuth?.permission === "read_only") {
2761
+ return c.json(
2762
+ {
2763
+ error: {
2764
+ type: "pretense_forbidden",
2765
+ message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretense proxy."
2766
+ }
2767
+ },
2768
+ 403
2769
+ );
2770
+ }
1770
2771
  const fullText = extractText(body);
1771
2772
  const secretScan = scan2(fullText);
1772
2773
  const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
@@ -1790,7 +2791,17 @@ function createProxy(opts = {}) {
1790
2791
  let tokensMutated = 0;
1791
2792
  const mutatedBody = applyToBody(processedBody, (text) => {
1792
2793
  const blocks = extractCodeBlocks(text);
1793
- if (blocks.length === 0) return text;
2794
+ if (blocks.length === 0) {
2795
+ const lang = detectLanguage(text);
2796
+ const result = mutate(text, lang);
2797
+ for (const [k, v] of result.map) {
2798
+ mutationMap.set(k, v);
2799
+ session.mutationMap.set(k, v);
2800
+ session.reverseMap.set(v, k);
2801
+ }
2802
+ tokensMutated += result.stats.tokensMutated;
2803
+ return result.mutatedCode;
2804
+ }
1794
2805
  const mutatedBlocks = [];
1795
2806
  for (const block of blocks) {
1796
2807
  const result = mutate(block.code, block.language);
@@ -1804,14 +2815,37 @@ function createProxy(opts = {}) {
1804
2815
  }
1805
2816
  return replaceCodeBlocks(text, blocks, mutatedBlocks);
1806
2817
  });
1807
- usage.mutations += tokensMutated;
1808
- saveUsage(usage);
1809
- const warningThreshold = Math.floor(monthlyMutations * 0.8);
1810
- if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
1811
- printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
2818
+ const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
2819
+ if (dashboardApiKey && !remoteLogDisabled) {
2820
+ bumpCachedUsageAfterLog(tokensMutated);
2821
+ void uploadLog(
2822
+ {
2823
+ file_count: 0,
2824
+ identifiers_mutated: tokensMutated,
2825
+ secrets_blocked: blockedSecrets.length,
2826
+ llm_provider: provider,
2827
+ cli_version: getCliSemver()
2828
+ },
2829
+ {
2830
+ apiKey: dashboardApiKey,
2831
+ verbose,
2832
+ onResponse: (codeProtectionsUsed, limit) => {
2833
+ syncCachedUsageFromServer(codeProtectionsUsed, limit);
2834
+ }
2835
+ }
2836
+ );
2837
+ }
2838
+ const warnCache = getCachedValidation();
2839
+ if (warnCache && warnCache.monthlyMutationLimit !== -1 && tier === "free") {
2840
+ const limit = warnCache.monthlyMutationLimit;
2841
+ const used = warnCache.mutationsUsed;
2842
+ const warningThreshold = Math.floor(limit * 0.8);
2843
+ if (used >= warningThreshold && used - tokensMutated < warningThreshold) {
2844
+ printUpgradeNudge(`${used}/${limit} monthly security tokens used. Running low!`);
2845
+ }
1812
2846
  }
1813
2847
  if (tier === "free" && daysSinceFirstUse() >= 7) {
1814
- printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
2848
+ printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited security tokens, 90-day audit, Slack alerts.");
1815
2849
  }
1816
2850
  const entry = {
1817
2851
  id: requestId,
@@ -1828,7 +2862,7 @@ function createProxy(opts = {}) {
1828
2862
  createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
1829
2863
  );
1830
2864
  if (verbose && tokensMutated > 0) {
1831
- process.stdout.write(`[PRETENSE] ${tokensMutated} tokens mutated for request ${requestId}
2865
+ process.stdout.write(`[PRETENSE] ${tokensMutated} security tokens protected for request ${requestId}
1832
2866
  `);
1833
2867
  }
1834
2868
  const headers = {};
@@ -1839,6 +2873,7 @@ function createProxy(opts = {}) {
1839
2873
  delete headers["content-length"];
1840
2874
  headers["host"] = new URL(upstream).host;
1841
2875
  headers["x-pretense-request-id"] = requestId;
2876
+ stripPretenseOnlyForwardingHeaders(headers);
1842
2877
  const forwardBody = JSON.stringify(mutatedBody);
1843
2878
  let upstreamResp;
1844
2879
  try {
@@ -1867,12 +2902,12 @@ function createProxy(opts = {}) {
1867
2902
  }
1868
2903
  const decoder = new TextDecoder();
1869
2904
  const encoder = new TextEncoder();
1870
- let buffer = "";
2905
+ let buffer2 = "";
1871
2906
  const transform = new TransformStream({
1872
2907
  transform(chunk, controller) {
1873
- buffer += decoder.decode(chunk, { stream: true });
1874
- const lines = buffer.split("\n");
1875
- buffer = lines.pop() ?? "";
2908
+ buffer2 += decoder.decode(chunk, { stream: true });
2909
+ const lines = buffer2.split("\n");
2910
+ buffer2 = lines.pop() ?? "";
1876
2911
  for (const line of lines) {
1877
2912
  if (line.startsWith("data: ") && line !== "data: [DONE]") {
1878
2913
  try {
@@ -1889,8 +2924,8 @@ function createProxy(opts = {}) {
1889
2924
  }
1890
2925
  },
1891
2926
  flush(controller) {
1892
- if (buffer.length > 0) {
1893
- controller.enqueue(encoder.encode(buffer));
2927
+ if (buffer2.length > 0) {
2928
+ controller.enqueue(encoder.encode(buffer2));
1894
2929
  }
1895
2930
  }
1896
2931
  });
@@ -1934,9 +2969,9 @@ function createProxy(opts = {}) {
1934
2969
  return app;
1935
2970
  }
1936
2971
  function isPortAvailable(port) {
1937
- return new Promise((resolve3) => {
1938
- const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
1939
- tester.once("close", () => resolve3(true)).close();
2972
+ return new Promise((resolve4) => {
2973
+ const tester = createServer().once("error", () => resolve4(false)).once("listening", () => {
2974
+ tester.once("close", () => resolve4(true)).close();
1940
2975
  }).listen(port, "127.0.0.1");
1941
2976
  });
1942
2977
  }
@@ -1953,8 +2988,103 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
1953
2988
  function startProxy(opts = {}) {
1954
2989
  const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
1955
2990
  const app = createProxy(opts);
1956
- const tier = detectTier();
1957
2991
  void (async () => {
2992
+ const dashboardKeyFromEnvAtLaunch = hasDashboardApiKeyInEnv();
2993
+ const dashboardKey = getDashboardApiKey();
2994
+ if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
2995
+ process.stderr.write(
2996
+ `\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
2997
+ \x1B[31m[PRETENSE] or set PRETENSE_API_KEY. See ${PUBLISH_PACKAGE_URL}\x1B[0m
2998
+ `
2999
+ );
3000
+ process.exit(1);
3001
+ }
3002
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
3003
+ for (; ; ) {
3004
+ try {
3005
+ const validation = await validateKey({
3006
+ force: true,
3007
+ verbose: opts.verbose,
3008
+ enforceReachability: true
3009
+ });
3010
+ if (!validation) {
3011
+ process.stderr.write(
3012
+ `\x1B[31m[PRETENSE] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
3013
+ `
3014
+ );
3015
+ process.exit(1);
3016
+ }
3017
+ const plan = validation.plan ?? "FREE";
3018
+ const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
3019
+ process.stdout.write(
3020
+ `\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} security tokens remaining\x1B[0m
3021
+ `
3022
+ );
3023
+ break;
3024
+ } catch (err) {
3025
+ const code = err.code;
3026
+ const msg = err.message;
3027
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
3028
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
3029
+ `);
3030
+ process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL (or PRETEST_API_URL) if using a custom API.\x1B[0m
3031
+ `);
3032
+ process.exit(1);
3033
+ }
3034
+ if (code === "ORG_INACTIVE") {
3035
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
3036
+ `);
3037
+ process.stderr.write(`\x1B[31m[PRETENSE] Your organization is inactive; contact support.\x1B[0m
3038
+ `);
3039
+ process.exit(1);
3040
+ }
3041
+ const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
3042
+ if (authRetryable && interactive) {
3043
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
3044
+ `);
3045
+ process.stderr.write(
3046
+ `\x1B[33m[PRETENSE] Enter your dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
3047
+ `
3048
+ );
3049
+ let entered = null;
3050
+ for (; ; ) {
3051
+ entered = await promptDashboardApiKeyAfterFailure();
3052
+ if (!entered) {
3053
+ process.stderr.write(`\x1B[31m[PRETENSE] No key entered; exiting.\x1B[0m
3054
+ `);
3055
+ process.exit(1);
3056
+ }
3057
+ if (isValidKeyShape(entered)) break;
3058
+ process.stderr.write(
3059
+ `\x1B[31m[PRETENSE] That does not look like a dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
3060
+ `
3061
+ );
3062
+ }
3063
+ const savedPath = installDashboardKeyForCurrentProcess(entered);
3064
+ process.stdout.write(`\x1B[32m[PRETENSE] Saved API key to ${savedPath}\x1B[0m
3065
+ `);
3066
+ clearValidationCache();
3067
+ continue;
3068
+ }
3069
+ if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
3070
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
3071
+ `);
3072
+ process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' to fix.\x1B[0m
3073
+ `);
3074
+ process.exit(1);
3075
+ }
3076
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
3077
+ `);
3078
+ process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' or check connectivity.\x1B[0m
3079
+ `);
3080
+ process.exit(1);
3081
+ }
3082
+ }
3083
+ attachDashboardLogoutWatcher(!dashboardKeyFromEnvAtLaunch);
3084
+ const bannerTier = tierFromDashboardPlan(
3085
+ getCachedValidation()?.plan,
3086
+ detectTier()
3087
+ );
1958
3088
  let port;
1959
3089
  try {
1960
3090
  port = await findAvailablePort(requestedPort, 10);
@@ -1969,10 +3099,11 @@ function startProxy(opts = {}) {
1969
3099
  `
1970
3100
  );
1971
3101
  }
3102
+ printStartClientInstructions(port);
1972
3103
  serve({ fetch: app.fetch, port }, () => {
1973
3104
  const portStr = String(port);
1974
3105
  process.stdout.write(`
1975
- \x1B[36mPretense v0.3.0 [${tier.toUpperCase()}]\x1B[0m
3106
+ \x1B[36mPretense v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
1976
3107
  Your code hits AI naked. We fix that.
1977
3108
 
1978
3109
  Proxy: http://localhost:${portStr}
@@ -1992,12 +3123,22 @@ function startProxy(opts = {}) {
1992
3123
  /v1beta/... -> Google Gemini
1993
3124
  /api/chat, /api/generate -> Ollama (local)
1994
3125
  `);
3126
+ setInterval(() => {
3127
+ if (dashboardSavedCredentialsRevoked) return;
3128
+ if (!getDashboardApiKey()) return;
3129
+ void validateKey({
3130
+ force: true,
3131
+ verbose: opts.verbose,
3132
+ enforceReachability: true
3133
+ }).catch(() => {
3134
+ });
3135
+ }, 6e4);
1995
3136
  });
1996
3137
  })();
1997
3138
  }
1998
3139
 
1999
3140
  // src/token-footer.ts
2000
- import chalk from "chalk";
3141
+ import chalk3 from "chalk";
2001
3142
  var PLAN_LIMITS = {
2002
3143
  free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
2003
3144
  pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
@@ -2008,45 +3149,49 @@ var PLAN_LIMITS = {
2008
3149
  pricePerMonth: "$99/seat/month"
2009
3150
  }
2010
3151
  };
2011
- function getPlanLimits(tier) {
2012
- return PLAN_LIMITS[tier];
2013
- }
2014
3152
  function fmt(n) {
2015
- if (!Number.isFinite(n)) return "unlimited";
3153
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
2016
3154
  return n.toLocaleString("en-US");
2017
3155
  }
2018
3156
  function printTokenFooter(filesScanned, mutationsMade) {
2019
- const tier = detectTier();
2020
- const limits = getPlanLimits(tier);
2021
- const usage = loadUsage();
2022
- if (tier === "enterprise") {
3157
+ const cached2 = getCachedValidation();
3158
+ if (!cached2) {
3159
+ process.stderr.write(chalk3.gray(`
3160
+ ${filesScanned} files, ${mutationsMade} security tokens
3161
+
3162
+ `));
3163
+ return;
3164
+ }
3165
+ const plan = cached2.plan;
3166
+ if (plan === "ENTERPRISE") {
2023
3167
  process.stderr.write(
2024
- chalk.gray(`
2025
- ${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
3168
+ chalk3.gray(`
3169
+ ${filesScanned} files, ${mutationsMade} security tokens \xB7 Plan: Enterprise (unlimited)
2026
3170
 
2027
3171
  `)
2028
3172
  );
2029
3173
  return;
2030
3174
  }
2031
- const used = usage.mutations;
2032
- const cap = limits.monthlyMutations;
2033
- const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
2034
- const usageStr = pct >= 80 ? chalk.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk.gray(`${fmt(used)} / ${fmt(cap)}`);
3175
+ const used = cached2.mutationsUsed;
3176
+ const cap = cached2.monthlyMutationLimit;
3177
+ const unlimited = cap === -1 || !Number.isFinite(cap);
3178
+ const pct = !unlimited && cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
3179
+ const usageStr = unlimited ? chalk3.gray(`${fmt(used)} / unlimited`) : pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
2035
3180
  process.stderr.write(
2036
3181
  `
2037
- ${chalk.cyan(filesScanned + " files")}, ${chalk.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
3182
+ ${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " security tokens")} \xB7 Plan: ${chalk3.bold(plan)} \xB7 Security tokens this month: ${usageStr}${!unlimited ? ` (${pct}%)` : ""}
2038
3183
  `
2039
3184
  );
2040
- if (tier === "free") {
3185
+ if (plan === "FREE") {
2041
3186
  if (pct >= 80) {
2042
3187
  process.stderr.write(
2043
- chalk.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk.bold("pretense upgrade")}
3188
+ chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold(PUBLISH_PACKAGE_URL)}
2044
3189
 
2045
3190
  `)
2046
3191
  );
2047
3192
  } else {
2048
3193
  process.stderr.write(
2049
- chalk.gray(` Run ${chalk.bold("pretense status")} for details, ${chalk.bold("pretense upgrade")} to remove limits.
3194
+ chalk3.gray(` Run ${chalk3.bold("pretense status")} for details, ${chalk3.bold("pretense upgrade")} to remove limits.
2050
3195
 
2051
3196
  `)
2052
3197
  );
@@ -2057,7 +3202,7 @@ function printTokenFooter(filesScanned, mutationsMade) {
2057
3202
  }
2058
3203
 
2059
3204
  // src/banner.ts
2060
- import chalk2 from "chalk";
3205
+ import chalk4 from "chalk";
2061
3206
  var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
2062
3207
  \u2588\u2588 \u2588\u2588
2063
3208
  \u2588\u2588 \u2588\u2588 Stop your code from
@@ -2077,7 +3222,7 @@ function shouldPrint() {
2077
3222
  }
2078
3223
  function tint(line) {
2079
3224
  try {
2080
- return chalk2.hex(ICE_BLUE)(line);
3225
+ return chalk4.hex(ICE_BLUE)(line);
2081
3226
  } catch {
2082
3227
  return line;
2083
3228
  }
@@ -2092,53 +3237,66 @@ function printBanner() {
2092
3237
  }
2093
3238
 
2094
3239
  // src/commands/status.ts
2095
- import chalk3 from "chalk";
2096
- var PLAN_LABEL = {
2097
- free: "Free",
2098
- pro: "Pro",
2099
- enterprise: "Enterprise"
2100
- };
3240
+ import chalk5 from "chalk";
2101
3241
  function fmt2(n) {
2102
- if (!Number.isFinite(n)) return "unlimited";
3242
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
2103
3243
  return n.toLocaleString("en-US");
2104
3244
  }
2105
- function runStatus() {
2106
- const tier = detectTier();
2107
- const planLimits = getPlanLimits(tier);
2108
- const tierLimits = getTierLimits(tier);
2109
- const usage = loadUsage();
2110
- const tierBadge = tier === "enterprise" ? chalk3.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk3.bgBlue.white.bold(" PRO ") : chalk3.bgGray.white.bold(" FREE ");
2111
- const lines = [];
2112
- lines.push("");
2113
- lines.push(`${chalk3.cyan("Pretense")} ${chalk3.gray("v0.3.0")} ${tierBadge}`);
2114
- lines.push("");
2115
- lines.push(` Plan: ${chalk3.bold(PLAN_LABEL[tier])} ${chalk3.gray("(" + planLimits.pricePerMonth + ")")}`);
2116
- lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
2117
- lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
2118
- lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
2119
- lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
2120
- lines.push("");
2121
- if (tier === "free") {
2122
- lines.push(` ${chalk3.gray("Upgrade:")} ${chalk3.bold("pretense upgrade")}`);
2123
- lines.push(` ${chalk3.gray("Credits:")} ${chalk3.bold("pretense credits")}`);
3245
+ async function runStatus() {
3246
+ let remote = await fetchUsageSummary();
3247
+ if (!remote) {
3248
+ const v = await validateKey({ force: true });
3249
+ if (v?.valid) {
3250
+ remote = usageSummaryFromValidate(v);
3251
+ }
3252
+ }
3253
+ if (remote) {
3254
+ const plan = remote.plan;
3255
+ const tierBadge = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3256
+ const lines = [];
3257
+ lines.push("");
3258
+ lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.green("(synced)")}`);
2124
3259
  lines.push("");
3260
+ lines.push(` Plan: ${chalk5.bold(plan)}`);
3261
+ lines.push(` Security tokens:${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
3262
+ lines.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
3263
+ lines.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
3264
+ lines.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
3265
+ lines.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
3266
+ lines.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3267
+ if (remote.keyExpiresAt) {
3268
+ lines.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
3269
+ }
3270
+ lines.push("");
3271
+ if (plan === "FREE") {
3272
+ lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3273
+ lines.push("");
3274
+ }
3275
+ process.stdout.write(lines.join("\n") + "\n");
3276
+ return;
2125
3277
  }
2126
- process.stdout.write(lines.join("\n") + "\n");
3278
+ process.stderr.write(
3279
+ chalk5.yellow(`
3280
+ [pretense] Could not reach server \u2014 usage data unavailable.
3281
+ `) + chalk5.gray(` Check your connection or run \`pretense login\` if your key has changed.
3282
+
3283
+ `)
3284
+ );
2127
3285
  }
2128
3286
 
2129
3287
  // src/commands/upgrade.ts
2130
- import chalk4 from "chalk";
3288
+ import chalk6 from "chalk";
2131
3289
  var PRICING_URL = "https://pretense.ai/pricing";
2132
3290
  function runUpgrade() {
2133
3291
  const tier = detectTier();
2134
3292
  const lines = [];
2135
3293
  lines.push("");
2136
- lines.push(chalk4.cyan.bold(" Pretense Plans"));
3294
+ lines.push(chalk6.cyan.bold(" Pretense Plans"));
2137
3295
  lines.push("");
2138
- lines.push(chalk4.gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2139
- lines.push(chalk4.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
2140
- lines.push(chalk4.gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
2141
- lines.push(` \u2502 Price/seat \u2502 ${chalk4.bold("$0")} \u2502 ${chalk4.bold("$29")} \u2502 ${chalk4.bold("$99")} \u2502`);
3296
+ 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"));
3297
+ lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
3298
+ 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"));
3299
+ lines.push(` \u2502 Price/seat \u2502 ${chalk6.bold("$0")} \u2502 ${chalk6.bold("$29")} \u2502 ${chalk6.bold("$99")} \u2502`);
2142
3300
  lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
2143
3301
  lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
2144
3302
  lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
@@ -2146,159 +3304,215 @@ function runUpgrade() {
2146
3304
  lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
2147
3305
  lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
2148
3306
  lines.push(" \u2502 SLA \u2502 - \u2502 - \u2502 24/7 \u2502");
2149
- lines.push(chalk4.gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
3307
+ 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"));
2150
3308
  lines.push("");
2151
- lines.push(` ${chalk4.gray("Current plan:")} ${chalk4.bold(tier.toUpperCase())}`);
2152
- lines.push(` ${chalk4.gray("Upgrade at:")} ${chalk4.cyan.underline(PRICING_URL)}`);
3309
+ lines.push(` ${chalk6.gray("Current plan:")} ${chalk6.bold(tier.toUpperCase())}`);
3310
+ lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
2153
3311
  lines.push("");
2154
- lines.push(chalk4.gray(" After purchase, set:"));
2155
- lines.push(chalk4.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
3312
+ lines.push(chalk6.gray(" After purchase, set:"));
3313
+ lines.push(chalk6.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
2156
3314
  lines.push("");
2157
3315
  process.stdout.write(lines.join("\n") + "\n");
2158
3316
  }
2159
3317
 
2160
3318
  // src/commands/credits.ts
2161
- import chalk5 from "chalk";
2162
- var PLAN_LABEL2 = {
2163
- free: "Free",
2164
- pro: "Pro",
2165
- enterprise: "Enterprise"
2166
- };
3319
+ import chalk7 from "chalk";
2167
3320
  function fmt3(n) {
2168
- if (!Number.isFinite(n)) return "unlimited";
3321
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
2169
3322
  return n.toLocaleString("en-US");
2170
3323
  }
2171
3324
  function progressBar(used, cap, width = 24) {
2172
- if (!Number.isFinite(cap) || cap <= 0) return chalk5.green("[" + "=".repeat(width) + "]");
3325
+ if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
2173
3326
  const pct = Math.min(1, used / cap);
2174
3327
  const filled = Math.round(pct * width);
2175
3328
  const bar = "=".repeat(filled) + "-".repeat(width - filled);
2176
- const colored = pct >= 0.8 ? chalk5.yellow(bar) : pct >= 1 ? chalk5.red(bar) : chalk5.green(bar);
3329
+ const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
2177
3330
  return "[" + colored + "]";
2178
3331
  }
2179
- function runCredits() {
2180
- const tier = detectTier();
2181
- const limits = getPlanLimits(tier);
2182
- const usage = loadUsage();
2183
- const used = usage.mutations;
2184
- const cap = limits.monthlyMutations;
2185
- const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
2186
- const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
2187
- const lines = [];
2188
- lines.push("");
2189
- lines.push(chalk5.cyan.bold(" Pretense Credits"));
2190
- lines.push("");
2191
- lines.push(` Plan: ${chalk5.bold(PLAN_LABEL2[tier])}`);
2192
- lines.push(` Month: ${usage.month}`);
2193
- lines.push("");
2194
- lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
2195
- lines.push(` Used: ${pct}%`);
2196
- lines.push(` Remaining: ${fmt3(remaining)}`);
2197
- lines.push("");
2198
- if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
2199
- lines.push(chalk5.yellow(" \u26A0 Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3332
+ async function runCredits() {
3333
+ let remote = await fetchUsageSummary();
3334
+ if (!remote) {
3335
+ const v = await validateKey({ force: true });
3336
+ if (v?.valid) {
3337
+ remote = usageSummaryFromValidate(v);
3338
+ }
3339
+ }
3340
+ if (remote) {
3341
+ const used = remote.mutationsUsed;
3342
+ const cap = remote.monthlyMutationLimit;
3343
+ const remaining = remote.mutationsRemaining;
3344
+ const pct = cap > 0 && cap !== -1 ? Math.round(used / cap * 100) : 0;
3345
+ const lines = [];
3346
+ lines.push("");
3347
+ lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
2200
3348
  lines.push("");
2201
- } else if (tier === "free") {
2202
- lines.push(chalk5.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3349
+ lines.push(` Plan: ${chalk7.bold(remote.plan)}`);
3350
+ lines.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
2203
3351
  lines.push("");
3352
+ lines.push(` Security tokens:${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
3353
+ lines.push(` Used: ${pct}%`);
3354
+ lines.push(` Remaining: ${fmt3(remaining)}`);
3355
+ lines.push("");
3356
+ if (remote.plan === "FREE" && cap !== -1 && used >= cap * 0.8) {
3357
+ lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3358
+ lines.push("");
3359
+ } else if (remote.plan === "FREE") {
3360
+ lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3361
+ lines.push("");
3362
+ }
3363
+ process.stdout.write(lines.join("\n") + "\n");
3364
+ return;
2204
3365
  }
2205
- process.stdout.write(lines.join("\n") + "\n");
2206
- }
3366
+ process.stderr.write(
3367
+ chalk7.yellow(`
3368
+ [pretense] Could not reach server \u2014 usage data unavailable.
3369
+ `) + chalk7.gray(` Check your connection or run \`pretense login\` if your key has changed.
2207
3370
 
2208
- // src/commands/login.ts
2209
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
2210
- import { join as join4 } from "path";
2211
- import { homedir as homedir2 } from "os";
2212
- import chalk6 from "chalk";
2213
- var CONFIG_DIR_NAME2 = ".pretense";
2214
- var CONFIG_FILE2 = "config.json";
2215
- function getUserConfigDir() {
2216
- return join4(homedir2(), CONFIG_DIR_NAME2);
2217
- }
2218
- function isValidKeyShape(key) {
2219
- const trimmed = key.trim();
2220
- if (trimmed.length < 24) return false;
2221
- if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
2222
- return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_");
2223
- }
2224
- function readStdinIfPiped() {
2225
- if (process.stdin.isTTY) return "";
2226
- try {
2227
- const chunks = [];
2228
- const buf = Buffer.alloc(4096);
2229
- let bytesRead = 0;
2230
- do {
2231
- try {
2232
- bytesRead = readSync(0, buf, 0, buf.length, null);
2233
- } catch {
2234
- bytesRead = 0;
2235
- }
2236
- if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
2237
- } while (bytesRead === buf.length);
2238
- return Buffer.concat(chunks).toString("utf-8").trim();
2239
- } catch {
2240
- return "";
2241
- }
2242
- }
2243
- function maskKey(key) {
2244
- const trimmed = key.trim();
2245
- if (trimmed.length <= 12) return trimmed;
2246
- return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
3371
+ `)
3372
+ );
2247
3373
  }
2248
- function saveApiKey(key, configDir = getUserConfigDir()) {
2249
- if (!existsSync5(configDir)) {
2250
- mkdirSync5(configDir, { recursive: true });
2251
- }
2252
- const path = join4(configDir, CONFIG_FILE2);
2253
- let existing = {};
2254
- if (existsSync5(path)) {
3374
+
3375
+ // src/commands/logout.ts
3376
+ import readline2 from "readline/promises";
3377
+ import chalk8 from "chalk";
3378
+ async function runLogout(opts = {}) {
3379
+ if (!loadApiKey()) {
3380
+ console.log(chalk8.gray("No saved Pretense dashboard API key in ~/.pretense/config.json."));
3381
+ console.log(chalk8.gray("If you use PRETENSE_API_KEY in your shell, run unset PRETENSE_API_KEY."));
3382
+ return 0;
3383
+ }
3384
+ if (!opts.assumeYes) {
3385
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3386
+ console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
3387
+ console.error(chalk8.gray("Run `pretense logout --yes` to skip the prompt."));
3388
+ return 1;
3389
+ }
3390
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
2255
3391
  try {
2256
- existing = JSON.parse(readFileSync5(path, "utf-8"));
2257
- } catch {
2258
- existing = {};
3392
+ const raw = await rl.question(
3393
+ chalk8.bold("Log out and remove your saved API key? Type y for yes, n for no: ")
3394
+ );
3395
+ const answer = raw.trim().toLowerCase();
3396
+ if (answer !== "y" && answer !== "yes") {
3397
+ console.log(chalk8.yellow("Logout cancelled."));
3398
+ return 0;
3399
+ }
3400
+ } finally {
3401
+ rl.close();
2259
3402
  }
2260
3403
  }
2261
- const next = {
2262
- ...existing,
2263
- api_key: key.trim(),
2264
- saved_at: (/* @__PURE__ */ new Date()).toISOString()
2265
- };
2266
- writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
2267
- try {
2268
- chmodSync(path, 384);
2269
- } catch {
2270
- }
2271
- return path;
2272
- }
2273
- function runLogin(opts = {}) {
2274
- const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
2275
- if (!candidate) {
2276
- console.error(chalk6.red("Error: no API key provided."));
2277
- console.error(
2278
- chalk6.gray("Try: pretense login --key pk_live_xxxxx")
2279
- );
2280
- console.error(
2281
- chalk6.gray(" or: echo $PRETENSE_API_KEY | pretense login")
2282
- );
2283
- return 1;
2284
- }
2285
- if (!isValidKeyShape(candidate)) {
2286
- console.error(chalk6.red("Error: that does not look like a Pretense API key."));
2287
- console.error(
2288
- chalk6.gray("Keys start with pk_live_ and are at least 24 characters long.")
2289
- );
2290
- console.error(
2291
- chalk6.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
2292
- );
2293
- return 1;
3404
+ const hadEnvDashboardKey = !!process.env["PRETENSE_API_KEY"]?.trim();
3405
+ const { removed, path } = removeSavedDashboardCredentials();
3406
+ clearValidationCache();
3407
+ delete process.env["PRETENSE_API_KEY"];
3408
+ if (removed) {
3409
+ console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
3410
+ console.log(chalk8.gray("Other terminals: run unset PRETENSE_API_KEY if that variable is still exported (e.g. in ~/.zshrc)."));
3411
+ if (hadEnvDashboardKey) {
3412
+ console.log(
3413
+ chalk8.yellow(
3414
+ "This shell had PRETENSE_API_KEY set; it was cleared for this process only. Restart any running pretense proxy (Ctrl+C, then pretense start)."
3415
+ )
3416
+ );
3417
+ }
2294
3418
  }
2295
- const path = saveApiKey(candidate, opts.configDir);
2296
- console.log(chalk6.green("Saved API key to"), chalk6.bold(path));
2297
- console.log(chalk6.gray(` ${maskKey(candidate)}`));
2298
- console.log(chalk6.gray(" File mode 600 \u2014 readable only by you."));
2299
3419
  return 0;
2300
3420
  }
2301
3421
 
3422
+ // src/commands/install.ts
3423
+ import { Command } from "commander";
3424
+ import chalk9 from "chalk";
3425
+ import {
3426
+ existsSync as existsSync7,
3427
+ writeFileSync as writeFileSync5,
3428
+ chmodSync as chmodSync2,
3429
+ readFileSync as readFileSync7,
3430
+ mkdirSync as mkdirSync6
3431
+ } from "fs";
3432
+ import { join as join5, resolve as resolve2 } from "path";
3433
+ var HOOK_SCRIPTS = {
3434
+ "pre-commit": `#!/bin/sh
3435
+ # Pretense AI Firewall \u2014 pre-commit hook
3436
+ # Installed by: pretense install --mode pre-commit
3437
+
3438
+ pretense scan pre-commit
3439
+ exit $?
3440
+ `,
3441
+ "pre-push": `#!/bin/sh
3442
+ # Pretense AI Firewall \u2014 pre-push hook
3443
+ # Installed by: pretense install --mode pre-push
3444
+
3445
+ pretense scan pre-push
3446
+ exit $?
3447
+ `
3448
+ };
3449
+ var VALID_MODES = ["pre-commit", "pre-push"];
3450
+ function findGitDir(cwd) {
3451
+ const gitDir = join5(cwd, ".git");
3452
+ if (existsSync7(gitDir)) return gitDir;
3453
+ return null;
3454
+ }
3455
+ function installHook(hooksDir, mode, force) {
3456
+ const hookPath = join5(hooksDir, mode);
3457
+ const script = HOOK_SCRIPTS[mode];
3458
+ if (existsSync7(hookPath) && !force) {
3459
+ const existing = readFileSync7(hookPath, "utf-8");
3460
+ if (existing.includes("Pretense AI Firewall")) {
3461
+ return { installed: false, path: hookPath, skipped: true };
3462
+ }
3463
+ return { installed: false, path: hookPath };
3464
+ }
3465
+ writeFileSync5(hookPath, script, "utf-8");
3466
+ chmodSync2(hookPath, 493);
3467
+ return { installed: true, path: hookPath };
3468
+ }
3469
+ function installCommand() {
3470
+ return new Command("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option("-m, --mode <mode>", "Hook mode: pre-commit, pre-push (omit to install both)").option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
3471
+ const dir = resolve2(opts.dir);
3472
+ const force = opts.force;
3473
+ const modeArg = opts.mode;
3474
+ if (modeArg && !VALID_MODES.includes(modeArg)) {
3475
+ console.error(
3476
+ chalk9.red(`
3477
+ Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
3478
+ `)
3479
+ );
3480
+ process.exit(1);
3481
+ }
3482
+ const modes = modeArg ? [modeArg] : [...VALID_MODES];
3483
+ const gitDir = findGitDir(dir);
3484
+ if (!gitDir) {
3485
+ console.error(
3486
+ chalk9.red(`
3487
+ Error: No .git directory found in ${dir}
3488
+ Run this command from a git repository root.
3489
+ `)
3490
+ );
3491
+ process.exit(1);
3492
+ }
3493
+ const hooksDir = join5(gitDir, "hooks");
3494
+ mkdirSync6(hooksDir, { recursive: true });
3495
+ console.log(chalk9.cyan("\n Pretense hook installer\n"));
3496
+ let hasError = false;
3497
+ for (const mode of modes) {
3498
+ const result = installHook(hooksDir, mode, force);
3499
+ if (result.installed) {
3500
+ console.log(chalk9.green(` Installed ${mode} hook at ${result.path}`));
3501
+ } else if (result.skipped) {
3502
+ console.log(chalk9.dim(` Pretense ${mode} hook already installed at ${result.path}`));
3503
+ } else {
3504
+ console.error(chalk9.yellow(` Hook already exists at ${result.path}. Use --force to overwrite.`));
3505
+ hasError = true;
3506
+ }
3507
+ }
3508
+ if (hasError) {
3509
+ console.log("");
3510
+ process.exit(1);
3511
+ }
3512
+ console.log(chalk9.bold.green("\n Git hooks installed successfully.\n"));
3513
+ });
3514
+ }
3515
+
2302
3516
  // src/index.ts
2303
3517
  {
2304
3518
  const major = parseInt(process.versions.node.split(".")[0], 10);
@@ -2312,7 +3526,7 @@ function runLogin(opts = {}) {
2312
3526
  process.exit(1);
2313
3527
  }
2314
3528
  }
2315
- var VERSION = "0.3.0";
3529
+ var VERSION = getCliSemver();
2316
3530
  var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
2317
3531
  var PROGRESS_INTERVAL_MS = 500;
2318
3532
  var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
@@ -2435,18 +3649,35 @@ function validateLanguage(lang) {
2435
3649
  return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
2436
3650
  }
2437
3651
  function collectFiles(target) {
2438
- const abs = resolve2(target);
2439
- if (!existsSync6(abs)) {
3652
+ const abs = resolve3(target);
3653
+ if (!existsSync8(abs)) {
2440
3654
  return [];
2441
3655
  }
2442
3656
  const stat = statSync(abs);
2443
3657
  if (stat.isFile()) return [abs];
2444
3658
  const files = [];
2445
3659
  function walk(dir) {
2446
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
2447
- const fullPath = join5(dir, entry.name);
3660
+ let entries;
3661
+ try {
3662
+ entries = readdirSync(dir, { withFileTypes: true });
3663
+ } catch {
3664
+ return;
3665
+ }
3666
+ const skipDirs = /* @__PURE__ */ new Set([
3667
+ "node_modules",
3668
+ ".git",
3669
+ "dist",
3670
+ "build",
3671
+ ".pretest",
3672
+ ".pretense",
3673
+ ".next",
3674
+ "__pycache__",
3675
+ ".Trash"
3676
+ ]);
3677
+ for (const entry of entries) {
3678
+ const fullPath = join6(dir, entry.name);
2448
3679
  if (entry.isDirectory()) {
2449
- if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
3680
+ if (skipDirs.has(entry.name)) continue;
2450
3681
  walk(fullPath);
2451
3682
  } else if (entry.isFile() && isScannableFile(entry.name)) {
2452
3683
  files.push(fullPath);
@@ -2456,13 +3687,13 @@ function collectFiles(target) {
2456
3687
  walk(abs);
2457
3688
  return files;
2458
3689
  }
2459
- var program = new Command();
3690
+ var program = new Command2();
2460
3691
  program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
2461
3692
  printBanner();
2462
3693
  });
2463
3694
  program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
2464
- const dir = resolve2(opts.dir);
2465
- console.log(chalk7.cyan("Initializing Pretense in"), chalk7.bold(dir));
3695
+ const dir = resolve3(opts.dir);
3696
+ console.log(chalk10.cyan("Initializing Pretense in"), chalk10.bold(dir));
2466
3697
  const configDir = initConfig(dir);
2467
3698
  const files = collectFiles(dir);
2468
3699
  const langCounts = {};
@@ -2470,23 +3701,25 @@ program.command("init").description("Initialize .pretense/ config in current dir
2470
3701
  const lang = langFromExt(f);
2471
3702
  langCounts[lang] = (langCounts[lang] ?? 0) + 1;
2472
3703
  }
2473
- console.log(chalk7.green("\n .pretense/ created at"), chalk7.bold(configDir));
2474
- console.log(chalk7.gray(`
3704
+ console.log(chalk10.green("\n .pretense/ created at"), chalk10.bold(configDir));
3705
+ console.log(chalk10.gray(`
2475
3706
  Scanned ${files.length} source files:`));
2476
3707
  for (const [lang, count] of Object.entries(langCounts)) {
2477
- console.log(chalk7.gray(` ${lang}: ${count} files`));
3708
+ console.log(chalk10.gray(` ${lang}: ${count} files`));
2478
3709
  }
2479
- console.log(chalk7.cyan("\n Next: run"), chalk7.bold("pretense start"), chalk7.cyan("to launch the proxy"));
3710
+ console.log(chalk10.cyan("\n Next: run"), chalk10.bold("pretense start"), chalk10.cyan("to launch the proxy"));
2480
3711
  console.log();
2481
3712
  });
2482
- program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action((opts) => {
3713
+ 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) => {
2483
3714
  const config = loadConfig();
2484
3715
  const port = validatePort(opts.port);
2485
3716
  if (port === null) {
2486
- console.error(chalk7.red("Error: Invalid port number:"), chalk7.bold(opts.port));
2487
- console.error(chalk7.gray("Port must be an integer between 1 and 65535."));
3717
+ console.error(chalk10.red("Error: Invalid port number:"), chalk10.bold(opts.port));
3718
+ console.error(chalk10.gray("Port must be an integer between 1 and 65535."));
2488
3719
  process.exit(1);
2489
3720
  }
3721
+ await ensureDashboardKeyForStart();
3722
+ logDashboardCredentialHint();
2490
3723
  startProxy({ config, port, verbose: opts.verbose });
2491
3724
  });
2492
3725
  async function scanOneFile(file, opts) {
@@ -2595,30 +3828,30 @@ function parseSizeOption(value) {
2595
3828
  return Math.floor(n * mult);
2596
3829
  }
2597
3830
  program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).option("--max-file-size <size>", "Skip files larger than this (e.g. 10mb, 500k)", "10mb").option("--concurrency <n>", "Max files scanned in parallel", String(SCAN_CONCURRENCY)).option("--quiet", "Suppress progress output", false).action(async (target, opts) => {
2598
- const absTarget = resolve2(target);
2599
- if (!existsSync6(absTarget)) {
2600
- console.error(chalk7.red("Error: Path not found:"), chalk7.bold(absTarget));
3831
+ const absTarget = resolve3(target);
3832
+ if (!existsSync8(absTarget)) {
3833
+ console.error(chalk10.red("Error: Path not found:"), chalk10.bold(absTarget));
2601
3834
  process.exit(1);
2602
3835
  }
2603
3836
  const maxFileSize = parseSizeOption(opts.maxFileSize);
2604
3837
  if (maxFileSize === null) {
2605
- console.error(chalk7.red("Error: Invalid --max-file-size value:"), chalk7.bold(opts.maxFileSize));
2606
- console.error(chalk7.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
3838
+ console.error(chalk10.red("Error: Invalid --max-file-size value:"), chalk10.bold(opts.maxFileSize));
3839
+ console.error(chalk10.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
2607
3840
  process.exit(1);
2608
3841
  }
2609
3842
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
2610
3843
  if (!opts.quiet && !opts.json) {
2611
- process.stderr.write(chalk7.gray(`Discovering files in ${absTarget}...
3844
+ process.stderr.write(chalk10.gray(`Discovering files in ${absTarget}...
2612
3845
  `));
2613
3846
  }
2614
3847
  const files = collectFiles(target);
2615
3848
  if (files.length === 0) {
2616
- console.error(chalk7.red("Error: No source files found at"), chalk7.bold(absTarget));
2617
- console.error(chalk7.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3849
+ console.error(chalk10.red("Error: No source files found at"), chalk10.bold(absTarget));
3850
+ console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
2618
3851
  process.exit(1);
2619
3852
  }
2620
3853
  if (!opts.quiet && !opts.json) {
2621
- process.stderr.write(chalk7.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
3854
+ process.stderr.write(chalk10.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
2622
3855
  `));
2623
3856
  }
2624
3857
  const startedAt = Date.now();
@@ -2644,34 +3877,34 @@ program.command("scan <target>").description("Scan file or directory for identif
2644
3877
  interrupted
2645
3878
  }, null, 2));
2646
3879
  } else {
2647
- console.log(chalk7.cyan("\nPretense Scan Results\n"));
3880
+ console.log(chalk10.cyan("\nPretense Scan Results\n"));
2648
3881
  for (const r of allResults) {
2649
3882
  if (r.skipped) {
2650
3883
  if (r.skipped === "too-large") {
2651
3884
  const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
2652
- console.log(chalk7.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
3885
+ console.log(chalk10.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
2653
3886
  }
2654
3887
  continue;
2655
3888
  }
2656
- const secretBadge = r.secrets > 0 ? chalk7.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3889
+ const secretBadge = r.secrets > 0 ? chalk10.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
2657
3890
  const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
2658
3891
  if (!showRow) continue;
2659
3892
  console.log(
2660
- chalk7.gray(" ") + chalk7.white(r.file) + chalk7.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
3893
+ chalk10.gray(" ") + chalk10.white(r.file) + chalk10.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
2661
3894
  );
2662
3895
  if (r.secrets > 0) {
2663
3896
  for (const t of r.secretTypes) {
2664
- console.log(chalk7.red(` ! ${t}`));
3897
+ console.log(chalk10.red(` ! ${t}`));
2665
3898
  }
2666
3899
  }
2667
3900
  }
2668
- const skipNote = skippedLarge + skippedBinary > 0 ? chalk7.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
3901
+ const skipNote = skippedLarge + skippedBinary > 0 ? chalk10.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
2669
3902
  console.log(
2670
- chalk7.gray(`
2671
- Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk7.red(`${totalSecrets} secrets found`) : chalk7.green("0 secrets")) + skipNote
3903
+ chalk10.gray(`
3904
+ Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk10.red(`${totalSecrets} secrets found`) : chalk10.green("0 secrets")) + skipNote
2672
3905
  );
2673
3906
  if (interrupted) {
2674
- console.log(chalk7.yellow("\n Interrupted \u2014 partial results shown above."));
3907
+ console.log(chalk10.yellow("\n Interrupted \u2014 partial results shown above."));
2675
3908
  }
2676
3909
  console.log();
2677
3910
  }
@@ -2685,68 +3918,81 @@ program.command("scan <target>").description("Scan file or directory for identif
2685
3918
  process.exit(2);
2686
3919
  }
2687
3920
  });
2688
- program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
2689
- const absPath = resolve2(file);
2690
- if (!existsSync6(absPath)) {
2691
- console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
3921
+ 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) => {
3922
+ const absPath = resolve3(file);
3923
+ if (!existsSync8(absPath)) {
3924
+ console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
2692
3925
  process.exit(1);
2693
3926
  }
2694
3927
  if (opts.language) {
2695
3928
  const validLang = validateLanguage(opts.language);
2696
3929
  if (!validLang) {
2697
- console.error(chalk7.red("Error: Unsupported language:"), chalk7.bold(opts.language));
2698
- console.error(chalk7.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
3930
+ console.error(chalk10.red("Error: Unsupported language:"), chalk10.bold(opts.language));
3931
+ console.error(chalk10.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
2699
3932
  process.exit(1);
2700
3933
  }
2701
3934
  }
2702
3935
  if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
2703
- console.error(chalk7.red("Error: Not a supported source file:"), chalk7.bold(absPath));
2704
- console.error(chalk7.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3936
+ console.error(chalk10.red("Error: Not a supported source file:"), chalk10.bold(absPath));
3937
+ console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
2705
3938
  process.exit(1);
2706
3939
  }
2707
- const code = readFileSync6(absPath, "utf-8");
3940
+ const code = readFileSync8(absPath, "utf-8");
2708
3941
  const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
2709
- const result = mutate(code, lang, opts.seed);
3942
+ const secretScan = scan2(code);
3943
+ const effectiveSeed = effectiveSeedForMutation(opts.seed);
3944
+ const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
3945
+ const secretsRedacted = secretsMap.size;
3946
+ const result = mutate(redacted, lang, opts.seed);
2710
3947
  if (opts.save) {
2711
3948
  const configDir = getConfigDir();
2712
- const store = new MutationStore(join5(configDir, "mutation-map.json"));
3949
+ const store = new MutationStore(join6(configDir, "mutation-map.json"));
2713
3950
  store.load();
2714
3951
  store.save({
2715
3952
  id: absPath,
2716
3953
  originalHash: absPath,
2717
3954
  mapEntries: MutationStore.fromMap(result.map),
3955
+ secretsMapEntries: [...secretsMap.entries()],
2718
3956
  timestamp: Date.now(),
2719
3957
  language: lang
2720
3958
  });
2721
3959
  store.persist();
2722
- process.stderr.write(chalk7.gray(`Mutation map saved to ${configDir}/mutation-map.json
3960
+ process.stderr.write(chalk10.gray(`Mutation map saved to ${configDir}/mutation-map.json
2723
3961
  `));
2724
3962
  }
2725
3963
  if (opts.json) {
2726
3964
  console.log(JSON.stringify({
2727
3965
  mutatedCode: result.mutatedCode,
2728
3966
  map: Object.fromEntries(result.map),
2729
- stats: result.stats
3967
+ stats: result.stats,
3968
+ secretsRedacted,
3969
+ secretsMap: Object.fromEntries(secretsMap)
2730
3970
  }, null, 2));
2731
3971
  } else {
2732
3972
  process.stdout.write(result.mutatedCode);
2733
- process.stderr.write(chalk7.gray(`
2734
- --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
3973
+ if (secretsRedacted > 0) {
3974
+ process.stderr.write(
3975
+ chalk10.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before code protection ---
3976
+ `)
3977
+ );
3978
+ }
3979
+ process.stderr.write(chalk10.gray(`
3980
+ --- ${result.stats.tokensMutated} identifiers protected in ${result.stats.durationMs}ms ---
2735
3981
  `));
2736
3982
  printTokenFooter(1, result.stats.tokensMutated);
2737
3983
  }
2738
3984
  });
2739
3985
  program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
2740
- const absPath = resolve2(file);
2741
- if (!existsSync6(absPath)) {
2742
- console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
3986
+ const absPath = resolve3(file);
3987
+ if (!existsSync8(absPath)) {
3988
+ console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
2743
3989
  process.exit(1);
2744
3990
  }
2745
- const mutatedCode = readFileSync6(absPath, "utf-8");
2746
- const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
2747
- if (!existsSync6(mapPath)) {
2748
- console.error(chalk7.red("Error: Mutation map not found:"), chalk7.bold(mapPath));
2749
- console.error(chalk7.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
3991
+ const mutatedCode = readFileSync8(absPath, "utf-8");
3992
+ const mapPath = opts.map ? resolve3(opts.map) : join6(getConfigDir(), "mutation-map.json");
3993
+ if (!existsSync8(mapPath)) {
3994
+ console.error(chalk10.red("Error: Mutation map not found:"), chalk10.bold(mapPath));
3995
+ console.error(chalk10.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
2750
3996
  process.exit(1);
2751
3997
  }
2752
3998
  let store;
@@ -2754,21 +4000,24 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
2754
4000
  store = new MutationStore(mapPath);
2755
4001
  store.load();
2756
4002
  } catch {
2757
- console.error(chalk7.red("Error: Failed to load mutation map:"), chalk7.bold(mapPath));
2758
- console.error(chalk7.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
4003
+ console.error(chalk10.red("Error: Failed to load mutation map:"), chalk10.bold(mapPath));
4004
+ console.error(chalk10.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
2759
4005
  process.exit(1);
2760
4006
  }
2761
4007
  const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
2762
4008
  if (!entry) {
2763
- console.error(chalk7.red("Error: No mutation map entries found."));
2764
- console.error(chalk7.gray("Run 'pretense mutate --save <file>' first to generate a map."));
4009
+ console.error(chalk10.red("Error: No mutation map entries found."));
4010
+ console.error(chalk10.gray("Run 'pretense mutate --save <file>' first to generate a map."));
2765
4011
  process.exit(1);
2766
4012
  }
2767
4013
  const map = MutationStore.toMap(entry);
2768
- const restored = reverse(mutatedCode, map);
4014
+ const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
4015
+ const restored = reverse(mutatedCode, map, secretsMap);
2769
4016
  process.stdout.write(restored);
2770
- process.stderr.write(chalk7.gray(`
2771
- --- Reversed ${map.size} mutations ---
4017
+ const secretsCount = secretsMap?.size ?? 0;
4018
+ const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
4019
+ process.stderr.write(chalk10.gray(`
4020
+ --- Reversed ${map.size} security tokens${secretsSuffix} ---
2772
4021
  `));
2773
4022
  });
2774
4023
  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) => {
@@ -2781,30 +4030,35 @@ program.command("audit").description("View the audit log").option("--json", "Out
2781
4030
  program.command("version").description("Print Pretense CLI version").action(() => {
2782
4031
  console.log(`@pretense/cli v${VERSION}`);
2783
4032
  });
2784
- program.command("status").description("Show current plan, monthly quota, seat usage").action(() => {
2785
- runStatus();
4033
+ program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
4034
+ await runStatus();
2786
4035
  });
2787
- program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(() => {
2788
- runStatus();
4036
+ program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
4037
+ await runStatus();
2789
4038
  });
2790
- program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(() => {
2791
- runCredits();
4039
+ program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining security token budget").action(async () => {
4040
+ await runCredits();
2792
4041
  });
2793
4042
  program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
2794
4043
  runUpgrade();
2795
4044
  });
2796
- program.command("credits").description("Show remaining mutation/scan budget for the current month").action(() => {
2797
- runCredits();
4045
+ program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
4046
+ await runCredits();
2798
4047
  });
2799
- program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
4048
+ program.command("login").description("Save a dashboard 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) => {
2800
4049
  const code = runLogin({ key: opts.key });
2801
4050
  if (code !== 0) process.exit(code);
2802
4051
  });
4052
+ program.command("logout").description("Remove the saved dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
4053
+ const code = await runLogout({ assumeYes: opts.yes === true });
4054
+ if (code !== 0) process.exit(code);
4055
+ });
4056
+ program.addCommand(installCommand());
2803
4057
  function handleFatalError(err) {
2804
4058
  if (err instanceof Error && err.message) {
2805
- console.error(chalk7.red("Error:"), err.message);
4059
+ console.error(chalk10.red("Error:"), err.message);
2806
4060
  } else {
2807
- console.error(chalk7.red("An unexpected error occurred."));
4061
+ console.error(chalk10.red("An unexpected error occurred."));
2808
4062
  }
2809
4063
  process.exit(1);
2810
4064
  }