@pretense/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  // src/index.ts
4
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";
5
+ import chalk9 from "chalk";
6
+ import {
7
+ readFileSync as readFileSync7,
8
+ existsSync as existsSync7,
9
+ readdirSync,
10
+ statSync,
11
+ openSync,
12
+ readSync as readSync2,
13
+ closeSync
14
+ } from "fs";
7
15
  import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
8
16
  import { resolve as resolve2, extname, join as join5, basename } from "path";
9
17
  import os from "os";
@@ -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 = [];
@@ -850,7 +902,7 @@ function buildSaltedSeed(baseSeed) {
850
902
  return `${baseSeed}:${getMutationSalt()}`;
851
903
  }
852
904
 
853
- // src/mutator.ts
905
+ // src/deterministic-id.ts
854
906
  function hash32(str) {
855
907
  let h = 5381;
856
908
  for (let i = 0; i < str.length; i++) {
@@ -868,11 +920,13 @@ function toAlphaNum(n, len) {
868
920
  }
869
921
  return result;
870
922
  }
871
- function deterministicId(original, seed, prefix, len = 4) {
923
+ function deterministicSyntheticId(original, seed, prefix, len = 4) {
872
924
  const combined = `${seed}:${prefix}:${original}`;
873
925
  const h = hash32(combined);
874
926
  return prefix + toAlphaNum(h, len);
875
927
  }
928
+
929
+ // src/mutator.ts
876
930
  function prefixForType(type) {
877
931
  switch (type) {
878
932
  case "Class" /* Class */:
@@ -884,6 +938,8 @@ function prefixForType(type) {
884
938
  return "_v";
885
939
  case "Property" /* Property */:
886
940
  return "_prop";
941
+ case "HttpHeaderString" /* HttpHeaderString */:
942
+ return "_hk";
887
943
  default:
888
944
  return "_tok";
889
945
  }
@@ -910,11 +966,24 @@ function mutate(code, language, seed = "pretense") {
910
966
  if (token.type === "String" /* String */) {
911
967
  continue;
912
968
  }
969
+ if (token.type === "HttpHeaderString" /* HttpHeaderString */) {
970
+ const qm = /^("|')(.+)\1$/.exec(token.value);
971
+ if (qm) {
972
+ const inner = qm[2];
973
+ const q = qm[1];
974
+ const syntheticInner = deterministicSyntheticId(inner, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
975
+ map.set(token.value, `${q}${syntheticInner}${q}`);
976
+ } else {
977
+ const syntheticInner = deterministicSyntheticId(token.value, effectiveSeed, prefixForType("HttpHeaderString" /* HttpHeaderString */));
978
+ map.set(token.value, syntheticInner);
979
+ }
980
+ continue;
981
+ }
913
982
  if (token.type === "Import" /* Import */) {
914
983
  continue;
915
984
  }
916
985
  const prefix = prefixForType(token.type);
917
- const synthetic = deterministicId(token.value, effectiveSeed, prefix);
986
+ const synthetic = deterministicSyntheticId(token.value, effectiveSeed, prefix);
918
987
  map.set(token.value, synthetic);
919
988
  }
920
989
  const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
@@ -942,22 +1011,31 @@ function mutate(code, language, seed = "pretense") {
942
1011
  }
943
1012
 
944
1013
  // 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);
1014
+ function reverse(mutatedCode, map, secretsMap) {
1015
+ if (!mutatedCode) return mutatedCode;
954
1016
  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);
1017
+ if (map.size > 0) {
1018
+ const reverseMap = /* @__PURE__ */ new Map();
1019
+ for (const [original, synthetic] of map.entries()) {
1020
+ if (synthetic === "" || !synthetic) continue;
1021
+ reverseMap.set(synthetic, original);
1022
+ }
1023
+ if (reverseMap.size > 0) {
1024
+ const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
1025
+ for (const [synthetic, original] of sortedEntries) {
1026
+ const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1027
+ if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
1028
+ result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
1029
+ } else {
1030
+ result = result.split(synthetic).join(original);
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ if (secretsMap && secretsMap.size > 0) {
1036
+ const sortedSecrets = [...secretsMap.entries()].sort((a, b) => b[0].length - a[0].length);
1037
+ for (const [placeholder, original] of sortedSecrets) {
1038
+ result = result.split(placeholder).join(original);
961
1039
  }
962
1040
  }
963
1041
  return result;
@@ -966,16 +1044,40 @@ function reverse(mutatedCode, map) {
966
1044
  // src/secrets.ts
967
1045
  var SECRET_PATTERNS = [
968
1046
  { 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 },
1047
+ /** Legacy `sk-…` and modern `sk-proj-…` (hyphenated body); aligned with @pretense/scanner */
1048
+ { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
970
1049
  { 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 },
1050
+ /** `AWS_SECRET = '…40 chars…'` (not `AWS_SECRET_ACCESS_KEY`, which is covered below). */
1051
+ { name: "aws-secret-assignment", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bAWS_SECRET\s*=\s*['"]([A-Za-z0-9/+=]{40})['"]/gd, valueGroup: 1 },
1052
+ /**
1053
+ * Env-style labels with `-` or `.` (e.g. `AWS-SECRET-KEY`, `AWS.SECRET.KEY`, long `ACCESS` forms).
1054
+ * Case-insensitive `AWS` / `SECRET` / `KEY`. Value: 40-char secret; quotes optional (matches aws-secret-key).
1055
+ */
1056
+ {
1057
+ name: "aws-secret-key-delimited",
1058
+ category: "secret",
1059
+ severity: "critical",
1060
+ defaultAction: "block",
1061
+ pattern: /\bAWS[-.]SECRET(?:[-.]ACCESS)?[-.]KEY\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gdi,
1062
+ valueGroup: 1
1063
+ },
1064
+ { name: "aws-secret-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gd, valueGroup: 1 },
972
1065
  { name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
973
1066
  { name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
974
1067
  { 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 },
1068
+ { name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
1069
+ /** Three base64url JWT segments; payload segment is not required to start with `eyJ` (Supabase and many issuers use compact payloads). */
1070
+ { name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{4,}/g },
977
1071
  { 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 },
1072
+ {
1073
+ name: "generic-password",
1074
+ category: "credential",
1075
+ severity: "high",
1076
+ defaultAction: "redact",
1077
+ // Do not match `token` / `secret` inside identifiers (e.g. GITHUB_TOKEN, AWS_SECRET): require no word/underscore before keyword.
1078
+ pattern: /(?<![A-Za-z0-9_])(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"]([^\s'"]{8,})['"]/gid,
1079
+ valueGroup: 1
1080
+ },
979
1081
  { name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
980
1082
  { name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
981
1083
  { name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
@@ -1009,7 +1111,28 @@ var SECRET_PATTERNS = [
1009
1111
  { name: "x509-certificate", category: "secret", severity: "high", defaultAction: "redact", pattern: /-----BEGIN CERTIFICATE-----/g },
1010
1112
  { name: "launchdarkly-sdk-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bsdk-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
1011
1113
  { name: "launchdarkly-mobile-key", category: "secret", severity: "high", defaultAction: "block", pattern: /\bmob-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/g },
1012
- { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{32,}\b/g }
1114
+ /** Stripe signing secrets are often 32+ chars; allow shorter synthetic/test tails so redaction still applies. */
1115
+ { name: "stripe-webhook-secret", category: "secret", severity: "critical", defaultAction: "block", pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g },
1116
+ /** Stripe Price / product IDs (`price_…`) — identifiers, not card data, but leak billing context. */
1117
+ { name: "stripe-price-id", category: "secret", severity: "high", defaultAction: "block", pattern: /\bprice_[A-Za-z0-9_]{14,}\b/g },
1118
+ /** `FOO_PASSWORD = '…'` / `NEXT_PUBLIC_*_PASSWORD` — generic-password skips when `password` is preceded by `_`. */
1119
+ {
1120
+ name: "env-password-assignment",
1121
+ category: "credential",
1122
+ severity: "high",
1123
+ defaultAction: "block",
1124
+ pattern: /\b[A-Z][A-Z0-9_]*PASSWORD\s*=\s*['"]([^'"]{6,})['"]/gd,
1125
+ valueGroup: 1
1126
+ },
1127
+ /** Quoted common DB dialect names — stack hints when mutating env-style config. */
1128
+ {
1129
+ name: "quoted-db-dialect",
1130
+ category: "credential",
1131
+ severity: "medium",
1132
+ defaultAction: "block",
1133
+ pattern: /(['"])(postgres|mysql|redis)\1/gd,
1134
+ valueGroup: 2
1135
+ }
1013
1136
  ];
1014
1137
  var PII_PATTERNS = [
1015
1138
  { name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
@@ -1076,7 +1199,17 @@ function scan2(text, actionOverrides) {
1076
1199
  def.pattern.lastIndex = 0;
1077
1200
  let m;
1078
1201
  while ((m = def.pattern.exec(text)) !== null) {
1079
- const value = m[0];
1202
+ let value = m[0];
1203
+ let start2 = m.index;
1204
+ let end = start2 + value.length;
1205
+ if (def.valueGroup !== void 0) {
1206
+ const span = m.indices?.[def.valueGroup];
1207
+ if (!span) continue;
1208
+ const [gs, ge] = span;
1209
+ value = text.slice(gs, ge);
1210
+ start2 = gs;
1211
+ end = ge;
1212
+ }
1080
1213
  if (def.validate && !def.validate(value)) continue;
1081
1214
  const action = actionOverrides?.[def.name] ?? def.defaultAction;
1082
1215
  matchCounter++;
@@ -1086,8 +1219,8 @@ function scan2(text, actionOverrides) {
1086
1219
  type: def.name,
1087
1220
  value,
1088
1221
  masked: maskValue(value, def.name),
1089
- start: m.index,
1090
- end: m.index + value.length,
1222
+ start: start2,
1223
+ end,
1091
1224
  severity: def.severity,
1092
1225
  action
1093
1226
  });
@@ -1151,14 +1284,26 @@ function scan2(text, actionOverrides) {
1151
1284
  };
1152
1285
  }
1153
1286
  function applyRedactions(text, matches) {
1154
- const sorted = [...matches].sort((a, b) => b.start - a.start);
1287
+ return applyRedactionsTracked(text, matches).redactedCode;
1288
+ }
1289
+ var SECRET_LITERAL_PREFIX = "_sl";
1290
+ function applyRedactionsTracked(text, matches, mutationSeed = buildSaltedSeed("pretense")) {
1291
+ const actionable = matches.filter((m) => m.action === "block" || m.action === "redact");
1292
+ const sorted = [...actionable].sort((a, b) => b.start - a.start);
1293
+ const secretsMap = /* @__PURE__ */ new Map();
1294
+ const placeholders = /* @__PURE__ */ new Map();
1295
+ const forward = [...actionable].sort((a, b) => a.start - b.start);
1296
+ for (const match of forward) {
1297
+ const tag = deterministicSyntheticId(`${match.id}:${match.start}`, mutationSeed, SECRET_LITERAL_PREFIX);
1298
+ placeholders.set(match.id, tag);
1299
+ secretsMap.set(tag, match.value);
1300
+ }
1155
1301
  let result = text;
1156
1302
  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
- }
1303
+ const tag = placeholders.get(match.id);
1304
+ result = result.slice(0, match.start) + tag + result.slice(match.end);
1160
1305
  }
1161
- return result;
1306
+ return { redactedCode: result, secretsMap };
1162
1307
  }
1163
1308
 
1164
1309
  // src/store.ts
@@ -1405,6 +1550,11 @@ function detectTier() {
1405
1550
  if (key.startsWith("pre_pro_") && isValidLicenseKey(key, "pre_pro_")) return "pro";
1406
1551
  return "free";
1407
1552
  }
1553
+ function tierFromDashboardPlan(plan, fallback) {
1554
+ if (plan === "ENTERPRISE") return "enterprise";
1555
+ if (plan === "PRO") return "pro";
1556
+ return fallback;
1557
+ }
1408
1558
  function getTierLimits(tier) {
1409
1559
  switch (tier) {
1410
1560
  case "enterprise":
@@ -1512,6 +1662,7 @@ import { Hono } from "hono";
1512
1662
  import { serve } from "@hono/node-server";
1513
1663
  import { nanoid } from "nanoid";
1514
1664
  import { createServer } from "net";
1665
+ import { existsSync as existsSync6, watch } from "fs";
1515
1666
 
1516
1667
  // src/auth.ts
1517
1668
  import { createHash } from "crypto";
@@ -1575,7 +1726,657 @@ function sessionCount() {
1575
1726
  return sessions.size;
1576
1727
  }
1577
1728
 
1729
+ // src/git-context.ts
1730
+ import { execFile } from "child_process";
1731
+ import { promisify } from "util";
1732
+ var execFileAsync = promisify(execFile);
1733
+ var GIT_TIMEOUT_MS = 2e3;
1734
+ async function runGit(args, cwd) {
1735
+ try {
1736
+ const { stdout } = await execFileAsync("git", args, {
1737
+ cwd,
1738
+ timeout: GIT_TIMEOUT_MS,
1739
+ windowsHide: true
1740
+ });
1741
+ const trimmed = stdout.trim();
1742
+ return trimmed.length > 0 ? trimmed : void 0;
1743
+ } catch {
1744
+ return void 0;
1745
+ }
1746
+ }
1747
+ function sanitizeRemote(raw) {
1748
+ if (/^[\w.-]+@[^:]+:/.test(raw) && !raw.includes("://")) {
1749
+ return raw;
1750
+ }
1751
+ try {
1752
+ const url = new URL(raw);
1753
+ url.username = "";
1754
+ url.password = "";
1755
+ return url.toString();
1756
+ } catch {
1757
+ return raw;
1758
+ }
1759
+ }
1760
+ async function collectGitContext(cwd = process.cwd()) {
1761
+ const insideRepo = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1762
+ if (insideRepo !== "true") return {};
1763
+ const [remote, branch, sha] = await Promise.all([
1764
+ runGit(["remote", "get-url", "origin"], cwd),
1765
+ runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
1766
+ runGit(["rev-parse", "HEAD"], cwd)
1767
+ ]);
1768
+ const ctx = {};
1769
+ if (remote) ctx.git_remote = sanitizeRemote(remote);
1770
+ if (branch && branch !== "HEAD") ctx.git_branch = branch;
1771
+ if (sha) ctx.git_commit_sha = sha;
1772
+ return ctx;
1773
+ }
1774
+ var CACHE_TTL_MS = 3e4;
1775
+ var cache = /* @__PURE__ */ new Map();
1776
+ async function collectGitContextCached(cwd = process.cwd(), now = Date.now()) {
1777
+ const hit = cache.get(cwd);
1778
+ if (hit && hit.expiresAt > now) return hit.value;
1779
+ const value = await collectGitContext(cwd);
1780
+ cache.set(cwd, { value, expiresAt: now + CACHE_TTL_MS });
1781
+ return value;
1782
+ }
1783
+
1784
+ // src/commands/login.ts
1785
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
1786
+ import { join as join4 } from "path";
1787
+ import { homedir as homedir2 } from "os";
1788
+ import readline from "readline/promises";
1789
+ import chalk from "chalk";
1790
+ var CONFIG_DIR_NAME2 = ".pretense";
1791
+ var CONFIG_FILE2 = "config.json";
1792
+ function getUserDashboardConfigDir() {
1793
+ return join4(homedir2(), CONFIG_DIR_NAME2);
1794
+ }
1795
+ function getUserDashboardConfigPath() {
1796
+ return join4(getUserDashboardConfigDir(), CONFIG_FILE2);
1797
+ }
1798
+ function isValidKeyShape(key) {
1799
+ const trimmed = key.trim();
1800
+ if (trimmed.length < 24) return false;
1801
+ if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
1802
+ return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_") || trimmed.startsWith("prtns_live_");
1803
+ }
1804
+ function readStdinIfPiped() {
1805
+ if (process.stdin.isTTY) return "";
1806
+ try {
1807
+ const chunks = [];
1808
+ const buf = Buffer.alloc(4096);
1809
+ let bytesRead = 0;
1810
+ do {
1811
+ try {
1812
+ bytesRead = readSync(0, buf, 0, buf.length, null);
1813
+ } catch {
1814
+ bytesRead = 0;
1815
+ }
1816
+ if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
1817
+ } while (bytesRead === buf.length);
1818
+ return Buffer.concat(chunks).toString("utf-8").trim();
1819
+ } catch {
1820
+ return "";
1821
+ }
1822
+ }
1823
+ function maskKey(key) {
1824
+ const trimmed = key.trim();
1825
+ if (trimmed.length <= 12) return trimmed;
1826
+ return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
1827
+ }
1828
+ function logDashboardCredentialHint() {
1829
+ const env = process.env["PRETENSE_API_KEY"]?.trim() ?? "";
1830
+ const envOk = env.length > 0 && isValidKeyShape(env);
1831
+ const abs = getUserDashboardConfigPath();
1832
+ const fk = loadApiKey();
1833
+ if (envOk) {
1834
+ console.error(
1835
+ chalk.gray(
1836
+ `[pretense] Dashboard key source: PRETENSE_API_KEY in environment (${maskKey(env)}) \u2014 unset this variable after logout if you expect no key.`
1837
+ )
1838
+ );
1839
+ } else if (fk && isValidKeyShape(fk)) {
1840
+ console.error(chalk.gray(`[pretense] Dashboard key source: ${abs} (${maskKey(fk)})`));
1841
+ } else {
1842
+ console.error(chalk.gray("[pretense] Dashboard key source: none configured."));
1843
+ }
1844
+ }
1845
+ function saveApiKey(key, configDir = getUserDashboardConfigDir()) {
1846
+ if (!existsSync5(configDir)) {
1847
+ mkdirSync5(configDir, { recursive: true });
1848
+ }
1849
+ const path = join4(configDir, CONFIG_FILE2);
1850
+ let existing = {};
1851
+ if (existsSync5(path)) {
1852
+ try {
1853
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1854
+ } catch {
1855
+ existing = {};
1856
+ }
1857
+ }
1858
+ const next = {
1859
+ ...existing,
1860
+ api_key: key.trim(),
1861
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
1862
+ };
1863
+ writeFileSync4(path, JSON.stringify(next, null, 2), { encoding: "utf-8", mode: 384 });
1864
+ try {
1865
+ chmodSync(path, 384);
1866
+ } catch {
1867
+ }
1868
+ return path;
1869
+ }
1870
+ function removeSavedDashboardCredentials(configDir = getUserDashboardConfigDir()) {
1871
+ const path = join4(configDir, CONFIG_FILE2);
1872
+ if (!existsSync5(path)) return { removed: false, path };
1873
+ let existing = {};
1874
+ try {
1875
+ existing = JSON.parse(readFileSync5(path, "utf-8"));
1876
+ } catch {
1877
+ return { removed: false, path };
1878
+ }
1879
+ const ak = existing.api_key;
1880
+ const hadKey = typeof ak === "string" && ak.trim().length > 0;
1881
+ if (!hadKey) return { removed: false, path };
1882
+ delete existing.api_key;
1883
+ delete existing.saved_at;
1884
+ writeFileSync4(path, JSON.stringify(existing, null, 2), { encoding: "utf-8", mode: 384 });
1885
+ try {
1886
+ chmodSync(path, 384);
1887
+ } catch {
1888
+ }
1889
+ return { removed: true, path };
1890
+ }
1891
+ function loadApiKey(configDir = getUserDashboardConfigDir()) {
1892
+ const path = join4(configDir, CONFIG_FILE2);
1893
+ if (!existsSync5(path)) return null;
1894
+ try {
1895
+ const raw = readFileSync5(path, "utf-8");
1896
+ const parsed = JSON.parse(raw);
1897
+ if (typeof parsed.api_key === "string") {
1898
+ const t = parsed.api_key.trim();
1899
+ if (t.length > 0) return t;
1900
+ }
1901
+ } catch {
1902
+ }
1903
+ return null;
1904
+ }
1905
+ function resolveDashboardKeyCandidate() {
1906
+ const env = (process.env["PRETENSE_API_KEY"] ?? "").trim();
1907
+ if (env.length > 0) {
1908
+ if (!isValidKeyShape(env)) {
1909
+ return { key: null, fromFile: false, badEnv: true };
1910
+ }
1911
+ return { key: env, fromFile: false, badEnv: false };
1912
+ }
1913
+ const fileKey = loadApiKey();
1914
+ if (fileKey && isValidKeyShape(fileKey)) {
1915
+ return { key: fileKey.trim(), fromFile: true, badEnv: false };
1916
+ }
1917
+ return { key: null, fromFile: Boolean(fileKey), badEnv: false };
1918
+ }
1919
+ function installDashboardKeyForCurrentProcess(key) {
1920
+ const trimmed = key.trim();
1921
+ const path = saveApiKey(trimmed);
1922
+ process.env["PRETENSE_API_KEY"] = trimmed;
1923
+ return path;
1924
+ }
1925
+ async function promptDashboardApiKeyAfterFailure() {
1926
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
1927
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1928
+ try {
1929
+ const line = (await rl.question(chalk.bold("Pretense dashboard API key: "))).trim();
1930
+ return line.length > 0 ? line : null;
1931
+ } finally {
1932
+ rl.close();
1933
+ }
1934
+ }
1935
+ async function ensureDashboardKeyForStart() {
1936
+ const { key, fromFile, badEnv } = resolveDashboardKeyCandidate();
1937
+ if (badEnv) {
1938
+ console.error(chalk.red("Error: PRETENSE_API_KEY is set but is not a valid Pretense dashboard API key shape."));
1939
+ console.error(
1940
+ chalk.gray("Keys look like prtns_live_\u2026 or pk_live_\u2026. Fix the variable or unset it to use ~/.pretense/config.json.")
1941
+ );
1942
+ process.exit(1);
1943
+ }
1944
+ if (key) {
1945
+ return;
1946
+ }
1947
+ if (fromFile) {
1948
+ console.warn(
1949
+ chalk.yellow(
1950
+ "Ignoring invalid `api_key` in ~/.pretense/config.json (wrong shape). Enter a new key or run `pretense login --key ...`."
1951
+ )
1952
+ );
1953
+ }
1954
+ const stdinOk = process.stdin.isTTY;
1955
+ const stdoutOk = process.stdout.isTTY;
1956
+ if (!stdinOk || !stdoutOk) {
1957
+ console.error(chalk.red("Error: Pretense API key required to start the proxy."));
1958
+ console.error(
1959
+ chalk.gray(
1960
+ "Save one with `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
1961
+ )
1962
+ );
1963
+ process.exit(1);
1964
+ }
1965
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1966
+ try {
1967
+ console.log(chalk.cyan("No Pretense API key found in ~/.pretense/config.json or $PRETENSE_API_KEY."));
1968
+ const entered = (await rl.question(chalk.bold("Enter your Pretense API key: "))).trim();
1969
+ if (!entered) {
1970
+ console.error(chalk.red("No key entered; exiting."));
1971
+ process.exit(1);
1972
+ }
1973
+ if (!isValidKeyShape(entered)) {
1974
+ console.error(chalk.red("That does not look like a Pretense API key (expect prtns_live_\u2026 or pk_live_\u2026)."));
1975
+ console.error(chalk.gray("Get one from your Pretense dashboard, then run `pretense login --key ...`."));
1976
+ process.exit(1);
1977
+ }
1978
+ const path = installDashboardKeyForCurrentProcess(entered);
1979
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
1980
+ console.log(chalk.gray(` ${maskKey(entered)}`));
1981
+ } finally {
1982
+ rl.close();
1983
+ }
1984
+ const persisted = resolveDashboardKeyCandidate();
1985
+ if (!persisted.key || persisted.badEnv) {
1986
+ console.error(chalk.red("Error: Pretense dashboard API key could not be confirmed after login prompt."));
1987
+ process.exit(1);
1988
+ }
1989
+ }
1990
+ function runLogin(opts = {}) {
1991
+ const candidate = opts.key?.trim() || (process.env["PRETENSE_API_KEY"] ?? "").trim() || readStdinIfPiped();
1992
+ if (!candidate) {
1993
+ console.error(chalk.red("Error: no API key provided."));
1994
+ console.error(
1995
+ chalk.gray("Try: pretense login --key pk_live_xxxxx")
1996
+ );
1997
+ console.error(
1998
+ chalk.gray(" or: echo $PRETENSE_API_KEY | pretense login")
1999
+ );
2000
+ return 1;
2001
+ }
2002
+ if (!isValidKeyShape(candidate)) {
2003
+ console.error(chalk.red("Error: that does not look like a Pretense API key."));
2004
+ console.error(
2005
+ chalk.gray("Keys start with prtns_live_ (or pk_live_) and are at least 24 characters long.")
2006
+ );
2007
+ console.error(
2008
+ chalk.gray("Get one at https://pretense.ai/dashboard/settings/api-keys")
2009
+ );
2010
+ return 1;
2011
+ }
2012
+ const path = saveApiKey(candidate, opts.configDir);
2013
+ console.log(chalk.green("Saved API key to"), chalk.bold(path));
2014
+ console.log(chalk.gray(` ${maskKey(candidate)}`));
2015
+ console.log(chalk.gray(" File mode 600 \u2014 readable only by you."));
2016
+ return 0;
2017
+ }
2018
+
2019
+ // src/backend-client.ts
2020
+ function usageSummaryFromValidate(v) {
2021
+ return {
2022
+ plan: v.plan,
2023
+ monthlyMutationLimit: v.monthlyMutationLimit,
2024
+ mutationsUsed: v.mutationsUsed,
2025
+ mutationsRemaining: v.mutationsRemaining,
2026
+ requestsThisPeriod: 0,
2027
+ secretsBlockedThisPeriod: 0,
2028
+ periodResetsAt: v.periodResetsAt,
2029
+ keyExpiresAt: null,
2030
+ keyStatus: "active"
2031
+ };
2032
+ }
2033
+ var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
2034
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
2035
+ function getConfiguredApiRoot(rootOverride) {
2036
+ const raw = (rootOverride ?? process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
2037
+ if (!raw) {
2038
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2039
+ }
2040
+ let normalized = raw;
2041
+ if (!/^https?:\/\//i.test(normalized)) {
2042
+ normalized = `https://${normalized}`;
2043
+ }
2044
+ try {
2045
+ const u = new URL(normalized);
2046
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? "https://api.pretense.ai" : u.origin;
2047
+ const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
2048
+ return `${origin}${path}`;
2049
+ } catch {
2050
+ return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2051
+ }
2052
+ }
2053
+ function getCliApiUrl(pathWithinCli, rootOverride) {
2054
+ const root = getConfiguredApiRoot(rootOverride);
2055
+ const tail = pathWithinCli.replace(/^\/+/, "");
2056
+ return `${root}/api/cli/${tail}`;
2057
+ }
2058
+ function getApiRequestTimeoutMs() {
2059
+ const raw = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
2060
+ if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
2061
+ const n = parseInt(raw, 10);
2062
+ return Math.min(Math.max(n, 3e3), 12e4);
2063
+ }
2064
+ function getApiKey() {
2065
+ const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
2066
+ if (fromEnv) return fromEnv;
2067
+ const fromFile = loadApiKey();
2068
+ if (!fromFile) return null;
2069
+ const t = fromFile.trim();
2070
+ return t.length > 0 ? t : null;
2071
+ }
2072
+ function getPretenseApiKey() {
2073
+ return getApiKey();
2074
+ }
2075
+ var cachedValidation = null;
2076
+ var lastValidationSuccessAt = 0;
2077
+ var VALIDATION_CACHE_FRESH_MS = 45e3;
2078
+ function getCachedValidation() {
2079
+ return cachedValidation;
2080
+ }
2081
+ function clearValidationCache() {
2082
+ cachedValidation = null;
2083
+ lastValidationSuccessAt = 0;
2084
+ }
2085
+ function bumpCachedUsageAfterLog(identifiersMutated) {
2086
+ if (!cachedValidation || identifiersMutated <= 0) return;
2087
+ cachedValidation.mutationsUsed += identifiersMutated;
2088
+ if (cachedValidation.mutationsRemaining !== -1) {
2089
+ cachedValidation.mutationsRemaining = Math.max(
2090
+ 0,
2091
+ cachedValidation.mutationsRemaining - identifiersMutated
2092
+ );
2093
+ }
2094
+ }
2095
+ function makeBackendReachabilityError(code, message) {
2096
+ const err = new Error(message);
2097
+ err.code = code;
2098
+ return err;
2099
+ }
2100
+ async function validateKey(opts = {}) {
2101
+ const enforceReachability = opts.enforceReachability === true;
2102
+ const now = Date.now();
2103
+ const cacheFresh = cachedValidation !== null && now - lastValidationSuccessAt < VALIDATION_CACHE_FRESH_MS;
2104
+ if (!opts.force && cacheFresh) {
2105
+ return cachedValidation;
2106
+ }
2107
+ const validateUrl = getCliApiUrl("auth/validate");
2108
+ const timeoutMs = getApiRequestTimeoutMs();
2109
+ const apiKey = getApiKey();
2110
+ if (!apiKey) {
2111
+ if (enforceReachability) {
2112
+ const err = new Error("Missing Pretense dashboard API key");
2113
+ err.code = "MISSING_API_KEY";
2114
+ cachedValidation = null;
2115
+ lastValidationSuccessAt = 0;
2116
+ throw err;
2117
+ }
2118
+ return null;
2119
+ }
2120
+ const controller = new AbortController();
2121
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2122
+ try {
2123
+ const resp = await fetch(validateUrl, {
2124
+ method: "POST",
2125
+ headers: {
2126
+ "content-type": "application/json",
2127
+ authorization: `Bearer ${apiKey}`
2128
+ },
2129
+ body: "{}",
2130
+ signal: controller.signal
2131
+ });
2132
+ if (resp.status === 401 || resp.status === 403) {
2133
+ const raw = await resp.json().catch(() => ({}));
2134
+ const msg = typeof raw.message === "string" ? raw.message : "API key rejected by server";
2135
+ const err = new Error(msg);
2136
+ err.code = typeof raw.code === "string" ? raw.code : "INVALID_API_KEY";
2137
+ cachedValidation = null;
2138
+ lastValidationSuccessAt = 0;
2139
+ throw err;
2140
+ }
2141
+ if (!resp.ok) {
2142
+ const msg = `Pretense API returned HTTP ${resp.status} from ${validateUrl}`;
2143
+ if (enforceReachability) {
2144
+ cachedValidation = null;
2145
+ lastValidationSuccessAt = 0;
2146
+ throw makeBackendReachabilityError("AUTH_BACKEND_REJECTED", msg);
2147
+ }
2148
+ return null;
2149
+ }
2150
+ let data;
2151
+ try {
2152
+ data = await resp.json();
2153
+ } catch {
2154
+ if (enforceReachability) {
2155
+ cachedValidation = null;
2156
+ lastValidationSuccessAt = 0;
2157
+ throw makeBackendReachabilityError(
2158
+ "AUTH_BACKEND_REJECTED",
2159
+ `Invalid JSON from ${validateUrl}`
2160
+ );
2161
+ }
2162
+ return null;
2163
+ }
2164
+ if (typeof data.valid === "boolean" && data.valid === false) {
2165
+ const err = new Error("API key is not valid");
2166
+ err.code = "INVALID_API_KEY";
2167
+ cachedValidation = null;
2168
+ lastValidationSuccessAt = 0;
2169
+ throw err;
2170
+ }
2171
+ cachedValidation = data;
2172
+ lastValidationSuccessAt = Date.now();
2173
+ return data;
2174
+ } catch (err) {
2175
+ if (err.code === "KEY_REVOKED" || err.code === "KEY_EXPIRED" || err.code === "INVALID_API_KEY" || err.code === "AUTH_ERROR" || err.code === "ORG_INACTIVE" || err.code === "MISSING_API_KEY" || err.code === "BACKEND_UNAVAILABLE" || err.code === "AUTH_BACKEND_REJECTED") {
2176
+ throw err;
2177
+ }
2178
+ if (enforceReachability) {
2179
+ const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS)` : `Cannot reach Pretense API at ${validateUrl}: ${err.message}`;
2180
+ cachedValidation = null;
2181
+ lastValidationSuccessAt = 0;
2182
+ throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
2183
+ }
2184
+ if (opts.verbose) {
2185
+ process.stderr.write(
2186
+ `[PRETENSE] Backend validation failed: ${err.message}
2187
+ `
2188
+ );
2189
+ }
2190
+ return null;
2191
+ } finally {
2192
+ clearTimeout(timer);
2193
+ }
2194
+ }
2195
+ async function fetchUsageSummary(opts = {}) {
2196
+ const summaryUrl = getCliApiUrl("usage/summary");
2197
+ const apiKey = getApiKey();
2198
+ if (!apiKey) return null;
2199
+ const controller = new AbortController();
2200
+ const timer = setTimeout(() => controller.abort(), getApiRequestTimeoutMs());
2201
+ try {
2202
+ const resp = await fetch(summaryUrl, {
2203
+ method: "GET",
2204
+ headers: { authorization: `Bearer ${apiKey}` },
2205
+ signal: controller.signal
2206
+ });
2207
+ if (!resp.ok) return null;
2208
+ return await resp.json();
2209
+ } catch (err) {
2210
+ if (opts.verbose) {
2211
+ process.stderr.write(
2212
+ `[PRETENSE] Usage summary fetch failed: ${err.message}
2213
+ `
2214
+ );
2215
+ }
2216
+ return null;
2217
+ } finally {
2218
+ clearTimeout(timer);
2219
+ }
2220
+ }
2221
+ async function isWithinLimits() {
2222
+ const v = getCachedValidation();
2223
+ if (!v) return { allowed: true };
2224
+ if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
2225
+ return {
2226
+ allowed: false,
2227
+ reason: `Monthly mutation limit reached (${v.mutationsUsed}/${v.monthlyMutationLimit}). Upgrade at https://pretense.ai/pricing`
2228
+ };
2229
+ }
2230
+ return { allowed: true };
2231
+ }
2232
+
2233
+ // src/log-uploader.ts
2234
+ async function uploadLog(event, options) {
2235
+ const apiRoot = options.apiUrl?.trim() || void 0;
2236
+ const endpoint = getCliApiUrl("log", apiRoot);
2237
+ const { apiKey, verbose = false, timeoutMs = getApiRequestTimeoutMs() } = options;
2238
+ if (!apiKey) return;
2239
+ const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
2240
+ const body = {
2241
+ ...event,
2242
+ ...gitCtx,
2243
+ repo_remote_url: gitCtx.git_remote,
2244
+ git_branch: gitCtx.git_branch,
2245
+ commit_sha: gitCtx.git_commit_sha
2246
+ };
2247
+ const controller = new AbortController();
2248
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2249
+ try {
2250
+ const resp = await fetch(endpoint, {
2251
+ method: "POST",
2252
+ headers: {
2253
+ "content-type": "application/json",
2254
+ authorization: `Bearer ${apiKey}`
2255
+ },
2256
+ body: JSON.stringify(body),
2257
+ signal: controller.signal
2258
+ });
2259
+ if (!resp.ok && verbose) {
2260
+ process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status}
2261
+ `);
2262
+ }
2263
+ } catch (err) {
2264
+ if (verbose) {
2265
+ const msg = err instanceof Error ? err.message : String(err);
2266
+ process.stderr.write(`[PRETENSE] log upload failed: ${msg}
2267
+ `);
2268
+ }
2269
+ } finally {
2270
+ clearTimeout(timer);
2271
+ }
2272
+ }
2273
+
2274
+ // src/start-client-instructions.ts
2275
+ import chalk2 from "chalk";
2276
+ function printStartClientInstructions(port) {
2277
+ const host = `http://localhost:${port}`;
2278
+ console.log();
2279
+ console.log(chalk2.cyan.bold("\u2500\u2500 Client setup (environment variables) \u2500\u2500"));
2280
+ console.log(chalk2.gray("Point the CLI at the Pretense backend API (optional if using default):"));
2281
+ console.log(
2282
+ chalk2.white(`export PRETENSE_API_URL='https://api.pretense.ai'`)
2283
+ );
2284
+ console.log();
2285
+ console.log(chalk2.gray("Log in with your Pretense dashboard API key:"));
2286
+ console.log(chalk2.white("pretense login --key your-pretense-api-key"));
2287
+ console.log();
2288
+ console.log(
2289
+ chalk2.gray("Set the LLM base URL for your provider (proxy below):")
2290
+ );
2291
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL="${host}"`));
2292
+ console.log(chalk2.white(`export ANTHROPIC_BASE_URL=${host}`));
2293
+ console.log(chalk2.white(`export OPENAI_BASE_URL=${host}/v1`));
2294
+ console.log(chalk2.white(`export GEMINI_BASE_URL=${host}/v1beta`));
2295
+ console.log(chalk2.white(`export OLLAMA_HOST=${host}`));
2296
+ console.log();
2297
+ console.log(chalk2.gray("Set your real upstream LLM API key (unchanged):"));
2298
+ console.log(chalk2.white('export ANTHROPIC_API_KEY="sk-ant-API-KEY"'));
2299
+ console.log();
2300
+ console.log(chalk2.cyan.bold("\u2500\u2500 Stop & log out \u2500\u2500"));
2301
+ console.log(
2302
+ chalk2.gray(
2303
+ "Stop the proxy: press Ctrl+C in the terminal where pretense start is running."
2304
+ )
2305
+ );
2306
+ console.log(
2307
+ chalk2.gray(
2308
+ "Remove the saved Pretense dashboard key from ~/.pretense/config.json:"
2309
+ )
2310
+ );
2311
+ console.log(chalk2.white("pretense logout"));
2312
+ console.log(
2313
+ chalk2.gray(
2314
+ "(confirm with y / cancel with n.) Mutations stop immediately unless this proxy was started with"
2315
+ )
2316
+ );
2317
+ console.log(
2318
+ chalk2.gray(
2319
+ "PRETENSE_API_KEY set in that same terminal \u2014 then press Ctrl+C and run pretense start again."
2320
+ )
2321
+ );
2322
+ console.log();
2323
+ }
2324
+
2325
+ // src/version.ts
2326
+ import { readFileSync as readFileSync6 } from "fs";
2327
+ import { fileURLToPath } from "url";
2328
+ var cached = null;
2329
+ function getCliSemver() {
2330
+ if (cached !== null) return cached;
2331
+ try {
2332
+ const url = new URL("../package.json", import.meta.url);
2333
+ const raw = readFileSync6(fileURLToPath(url), "utf-8");
2334
+ cached = JSON.parse(raw).version ?? "0.0.0";
2335
+ } catch {
2336
+ cached = "0.0.0";
2337
+ }
2338
+ return cached;
2339
+ }
2340
+
1578
2341
  // src/proxy.ts
2342
+ var dashboardSavedCredentialsRevoked = false;
2343
+ function attachDashboardLogoutWatcher(enabled) {
2344
+ if (!enabled) return;
2345
+ const dir = getUserDashboardConfigDir();
2346
+ let debounceTimer;
2347
+ const applyFilesystemCredentialState = () => {
2348
+ const fk = loadApiKey();
2349
+ if (!fk) {
2350
+ if (dashboardSavedCredentialsRevoked) return;
2351
+ dashboardSavedCredentialsRevoked = true;
2352
+ delete process.env["PRETENSE_API_KEY"];
2353
+ clearValidationCache();
2354
+ try {
2355
+ process.stderr.write(
2356
+ `\x1B[33m[PRETENSE] Saved dashboard API key removed (~/.pretense/config.json, e.g. pretense logout). LLM proxy POST requests are disabled until you run pretense login and restart pretense start.\x1B[0m
2357
+ `
2358
+ );
2359
+ } catch {
2360
+ }
2361
+ return;
2362
+ }
2363
+ if (dashboardSavedCredentialsRevoked) {
2364
+ dashboardSavedCredentialsRevoked = false;
2365
+ clearValidationCache();
2366
+ }
2367
+ };
2368
+ const schedule = () => {
2369
+ if (debounceTimer) clearTimeout(debounceTimer);
2370
+ debounceTimer = setTimeout(applyFilesystemCredentialState, 120);
2371
+ };
2372
+ try {
2373
+ if (!existsSync6(dir)) return;
2374
+ watch(dir, () => {
2375
+ schedule();
2376
+ });
2377
+ } catch {
2378
+ }
2379
+ }
1579
2380
  function anthropicUpstream() {
1580
2381
  return process.env["ANTHROPIC_UPSTREAM_URL"] ?? "https://api.anthropic.com";
1581
2382
  }
@@ -1606,6 +2407,12 @@ function detectProvider(path) {
1606
2407
  if (path.startsWith("/api/chat") || path.startsWith("/api/generate")) return "ollama";
1607
2408
  return "anthropic";
1608
2409
  }
2410
+ function stripPretenseOnlyForwardingHeaders(headers) {
2411
+ const drop = /* @__PURE__ */ new Set(["x-pretense-api-key", "x-pretense-key"]);
2412
+ for (const k of [...Object.keys(headers)]) {
2413
+ if (drop.has(k.toLowerCase())) delete headers[k];
2414
+ }
2415
+ }
1609
2416
  function extractText(body) {
1610
2417
  const parts = [];
1611
2418
  if (typeof body["system"] === "string") parts.push(body["system"]);
@@ -1688,7 +2495,7 @@ function createProxy(opts = {}) {
1688
2495
  "/",
1689
2496
  (c) => c.json({
1690
2497
  name: "pretense",
1691
- version: "0.3.0",
2498
+ version: getCliSemver(),
1692
2499
  status: "running",
1693
2500
  routes: {
1694
2501
  "GET /": "This welcome page",
@@ -1705,7 +2512,7 @@ function createProxy(opts = {}) {
1705
2512
  "/health",
1706
2513
  (c) => c.json({
1707
2514
  status: "ok",
1708
- version: "0.3.0",
2515
+ version: getCliSemver(),
1709
2516
  uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
1710
2517
  stats: {
1711
2518
  requestsProcessed: stats.requestsProcessed,
@@ -1735,6 +2542,7 @@ function createProxy(opts = {}) {
1735
2542
  });
1736
2543
  delete headers2["host"];
1737
2544
  headers2["host"] = new URL(upstream2).host;
2545
+ stripPretenseOnlyForwardingHeaders(headers2);
1738
2546
  const resp = await fetch(url, { method: c.req.method, headers: headers2 });
1739
2547
  return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
1740
2548
  }
@@ -1751,9 +2559,69 @@ function createProxy(opts = {}) {
1751
2559
  const requestId = nanoid(12);
1752
2560
  const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
1753
2561
  const provider = detectProvider(c.req.path);
2562
+ if (dashboardSavedCredentialsRevoked) {
2563
+ return c.json(
2564
+ {
2565
+ error: {
2566
+ type: "pretense_session_logged_out",
2567
+ message: "Saved Pretense dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with PRETENSE_API_KEY in this shell, stop it (Ctrl+C) and start again after logout."
2568
+ }
2569
+ },
2570
+ 401
2571
+ );
2572
+ }
2573
+ const pretenseDashboardKey = getPretenseApiKey();
2574
+ if (!pretenseDashboardKey) {
2575
+ return c.json(
2576
+ {
2577
+ error: {
2578
+ type: "pretense_dashboard_key_required",
2579
+ message: "Configure your Pretense dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
2580
+ }
2581
+ },
2582
+ 401
2583
+ );
2584
+ }
2585
+ try {
2586
+ await validateKey({ verbose, force: false, enforceReachability: true });
2587
+ } catch (err) {
2588
+ const code = err.code ?? "";
2589
+ const msg = err.message;
2590
+ const knownAuthCodes = /* @__PURE__ */ new Set([
2591
+ "KEY_REVOKED",
2592
+ "KEY_EXPIRED",
2593
+ "INVALID_API_KEY",
2594
+ "MISSING_API_KEY"
2595
+ ]);
2596
+ if (knownAuthCodes.has(code)) {
2597
+ return c.json(
2598
+ { error: { type: "pretense_auth_error", code, message: msg } },
2599
+ 401
2600
+ );
2601
+ }
2602
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2603
+ return c.json(
2604
+ { error: { type: "pretense_auth_backend_error", code, message: msg } },
2605
+ 503
2606
+ );
2607
+ }
2608
+ if (code === "ORG_INACTIVE") {
2609
+ return c.json(
2610
+ { error: { type: "pretense_org_inactive", code, message: msg } },
2611
+ 403
2612
+ );
2613
+ }
2614
+ return c.json(
2615
+ { error: { type: "pretense_auth_error", message: msg } },
2616
+ 401
2617
+ );
2618
+ }
1754
2619
  const sessHash = c.get("sessionHash");
1755
2620
  const session = getSession(sessHash);
1756
- const tier = detectTier();
2621
+ const tier = tierFromDashboardPlan(
2622
+ getCachedValidation()?.plan,
2623
+ detectTier()
2624
+ );
1757
2625
  const { monthlyMutations } = getTierLimits(tier);
1758
2626
  const usage = loadUsage();
1759
2627
  if (tier === "free" && usage.mutations >= monthlyMutations) {
@@ -1767,6 +2635,30 @@ function createProxy(opts = {}) {
1767
2635
  429
1768
2636
  );
1769
2637
  }
2638
+ const backendLimits = await isWithinLimits();
2639
+ if (!backendLimits.allowed) {
2640
+ return c.json(
2641
+ {
2642
+ error: {
2643
+ type: "pretense_rate_limit",
2644
+ message: backendLimits.reason ?? "Mutation limit exceeded."
2645
+ }
2646
+ },
2647
+ 429
2648
+ );
2649
+ }
2650
+ const cliAuth = getCachedValidation();
2651
+ if (cliAuth?.permission === "read_only") {
2652
+ return c.json(
2653
+ {
2654
+ error: {
2655
+ type: "pretense_forbidden",
2656
+ message: "This API key is read-only. Create a read_write key in the dashboard to use the Pretense proxy."
2657
+ }
2658
+ },
2659
+ 403
2660
+ );
2661
+ }
1770
2662
  const fullText = extractText(body);
1771
2663
  const secretScan = scan2(fullText);
1772
2664
  const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
@@ -1790,7 +2682,17 @@ function createProxy(opts = {}) {
1790
2682
  let tokensMutated = 0;
1791
2683
  const mutatedBody = applyToBody(processedBody, (text) => {
1792
2684
  const blocks = extractCodeBlocks(text);
1793
- if (blocks.length === 0) return text;
2685
+ if (blocks.length === 0) {
2686
+ const lang = detectLanguage(text);
2687
+ const result = mutate(text, lang);
2688
+ for (const [k, v] of result.map) {
2689
+ mutationMap.set(k, v);
2690
+ session.mutationMap.set(k, v);
2691
+ session.reverseMap.set(v, k);
2692
+ }
2693
+ tokensMutated += result.stats.tokensMutated;
2694
+ return result.mutatedCode;
2695
+ }
1794
2696
  const mutatedBlocks = [];
1795
2697
  for (const block of blocks) {
1796
2698
  const result = mutate(block.code, block.language);
@@ -1806,6 +2708,20 @@ function createProxy(opts = {}) {
1806
2708
  });
1807
2709
  usage.mutations += tokensMutated;
1808
2710
  saveUsage(usage);
2711
+ const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
2712
+ if (pretenseDashboardKey && !remoteLogDisabled) {
2713
+ void uploadLog(
2714
+ {
2715
+ file_count: 0,
2716
+ identifiers_mutated: tokensMutated,
2717
+ secrets_blocked: blockedSecrets.length,
2718
+ llm_provider: provider,
2719
+ cli_version: getCliSemver()
2720
+ },
2721
+ { apiKey: pretenseDashboardKey, verbose }
2722
+ );
2723
+ bumpCachedUsageAfterLog(tokensMutated);
2724
+ }
1809
2725
  const warningThreshold = Math.floor(monthlyMutations * 0.8);
1810
2726
  if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
1811
2727
  printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
@@ -1839,6 +2755,7 @@ function createProxy(opts = {}) {
1839
2755
  delete headers["content-length"];
1840
2756
  headers["host"] = new URL(upstream).host;
1841
2757
  headers["x-pretense-request-id"] = requestId;
2758
+ stripPretenseOnlyForwardingHeaders(headers);
1842
2759
  const forwardBody = JSON.stringify(mutatedBody);
1843
2760
  let upstreamResp;
1844
2761
  try {
@@ -1953,8 +2870,103 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
1953
2870
  function startProxy(opts = {}) {
1954
2871
  const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
1955
2872
  const app = createProxy(opts);
1956
- const tier = detectTier();
1957
2873
  void (async () => {
2874
+ const pretenseDashboardKeyFromEnvAtLaunch = !!process.env["PRETENSE_API_KEY"]?.trim();
2875
+ const dashboardKey = getPretenseApiKey();
2876
+ if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
2877
+ process.stderr.write(
2878
+ `\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
2879
+ \x1B[31m[PRETENSE] or set PRETENSE_API_KEY. Get a key at https://pretense.ai/dashboard/settings/api-keys\x1B[0m
2880
+ `
2881
+ );
2882
+ process.exit(1);
2883
+ }
2884
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2885
+ for (; ; ) {
2886
+ try {
2887
+ const validation = await validateKey({
2888
+ force: true,
2889
+ verbose: opts.verbose,
2890
+ enforceReachability: true
2891
+ });
2892
+ if (!validation) {
2893
+ process.stderr.write(
2894
+ `\x1B[31m[PRETENSE] Startup validation failed (no API key or unreachable backend). Cannot start proxy.\x1B[0m
2895
+ `
2896
+ );
2897
+ process.exit(1);
2898
+ }
2899
+ const plan = validation.plan ?? "FREE";
2900
+ const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
2901
+ process.stdout.write(
2902
+ `\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} mutations remaining\x1B[0m
2903
+ `
2904
+ );
2905
+ break;
2906
+ } catch (err) {
2907
+ const code = err.code;
2908
+ const msg = err.message;
2909
+ if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2910
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2911
+ `);
2912
+ process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL if using a custom API.\x1B[0m
2913
+ `);
2914
+ process.exit(1);
2915
+ }
2916
+ if (code === "ORG_INACTIVE") {
2917
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2918
+ `);
2919
+ process.stderr.write(`\x1B[31m[PRETENSE] Your organization is inactive; contact support.\x1B[0m
2920
+ `);
2921
+ process.exit(1);
2922
+ }
2923
+ const authRetryable = code === "INVALID_API_KEY" || code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "AUTH_ERROR" || code === "MISSING_API_KEY";
2924
+ if (authRetryable && interactive) {
2925
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2926
+ `);
2927
+ process.stderr.write(
2928
+ `\x1B[33m[PRETENSE] Enter your Pretense dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
2929
+ `
2930
+ );
2931
+ let entered = null;
2932
+ for (; ; ) {
2933
+ entered = await promptDashboardApiKeyAfterFailure();
2934
+ if (!entered) {
2935
+ process.stderr.write(`\x1B[31m[PRETENSE] No key entered; exiting.\x1B[0m
2936
+ `);
2937
+ process.exit(1);
2938
+ }
2939
+ if (isValidKeyShape(entered)) break;
2940
+ process.stderr.write(
2941
+ `\x1B[31m[PRETENSE] That does not look like a Pretense dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
2942
+ `
2943
+ );
2944
+ }
2945
+ const savedPath = installDashboardKeyForCurrentProcess(entered);
2946
+ process.stdout.write(`\x1B[32m[PRETENSE] Saved API key to ${savedPath}\x1B[0m
2947
+ `);
2948
+ clearValidationCache();
2949
+ continue;
2950
+ }
2951
+ if (code === "KEY_REVOKED" || code === "KEY_EXPIRED" || code === "INVALID_API_KEY" || code === "AUTH_ERROR" || code === "MISSING_API_KEY") {
2952
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2953
+ `);
2954
+ process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' to fix.\x1B[0m
2955
+ `);
2956
+ process.exit(1);
2957
+ }
2958
+ process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2959
+ `);
2960
+ process.stderr.write(`\x1B[31m[PRETENSE] Run 'pretense login --key <new-key>' or check connectivity.\x1B[0m
2961
+ `);
2962
+ process.exit(1);
2963
+ }
2964
+ }
2965
+ attachDashboardLogoutWatcher(!pretenseDashboardKeyFromEnvAtLaunch);
2966
+ const bannerTier = tierFromDashboardPlan(
2967
+ getCachedValidation()?.plan,
2968
+ detectTier()
2969
+ );
1958
2970
  let port;
1959
2971
  try {
1960
2972
  port = await findAvailablePort(requestedPort, 10);
@@ -1969,10 +2981,11 @@ function startProxy(opts = {}) {
1969
2981
  `
1970
2982
  );
1971
2983
  }
2984
+ printStartClientInstructions(port);
1972
2985
  serve({ fetch: app.fetch, port }, () => {
1973
2986
  const portStr = String(port);
1974
2987
  process.stdout.write(`
1975
- \x1B[36mPretense v0.3.0 [${tier.toUpperCase()}]\x1B[0m
2988
+ \x1B[36mPretense v${getCliSemver()} [${bannerTier.toUpperCase()}]\x1B[0m
1976
2989
  Your code hits AI naked. We fix that.
1977
2990
 
1978
2991
  Proxy: http://localhost:${portStr}
@@ -1992,12 +3005,22 @@ function startProxy(opts = {}) {
1992
3005
  /v1beta/... -> Google Gemini
1993
3006
  /api/chat, /api/generate -> Ollama (local)
1994
3007
  `);
3008
+ setInterval(() => {
3009
+ if (dashboardSavedCredentialsRevoked) return;
3010
+ if (!getPretenseApiKey()) return;
3011
+ void validateKey({
3012
+ force: true,
3013
+ verbose: opts.verbose,
3014
+ enforceReachability: true
3015
+ }).catch(() => {
3016
+ });
3017
+ }, 6e4);
1995
3018
  });
1996
3019
  })();
1997
3020
  }
1998
3021
 
1999
3022
  // src/token-footer.ts
2000
- import chalk from "chalk";
3023
+ import chalk3 from "chalk";
2001
3024
  var PLAN_LIMITS = {
2002
3025
  free: { monthlyMutations: 1e3, monthlyScans: 5e3, seats: 3, pricePerMonth: "$0/month" },
2003
3026
  pro: { monthlyMutations: 1e5, monthlyScans: 5e5, seats: 25, pricePerMonth: "$29/seat/month" },
@@ -2021,7 +3044,7 @@ function printTokenFooter(filesScanned, mutationsMade) {
2021
3044
  const usage = loadUsage();
2022
3045
  if (tier === "enterprise") {
2023
3046
  process.stderr.write(
2024
- chalk.gray(`
3047
+ chalk3.gray(`
2025
3048
  ${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
2026
3049
 
2027
3050
  `)
@@ -2031,22 +3054,22 @@ function printTokenFooter(filesScanned, mutationsMade) {
2031
3054
  const used = usage.mutations;
2032
3055
  const cap = limits.monthlyMutations;
2033
3056
  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)}`);
3057
+ const usageStr = pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
2035
3058
  process.stderr.write(
2036
3059
  `
2037
- ${chalk.cyan(filesScanned + " files")}, ${chalk.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
3060
+ ${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk3.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
2038
3061
  `
2039
3062
  );
2040
3063
  if (tier === "free") {
2041
3064
  if (pct >= 80) {
2042
3065
  process.stderr.write(
2043
- chalk.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk.bold("pretense upgrade")}
3066
+ chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretense upgrade")}
2044
3067
 
2045
3068
  `)
2046
3069
  );
2047
3070
  } else {
2048
3071
  process.stderr.write(
2049
- chalk.gray(` Run ${chalk.bold("pretense status")} for details, ${chalk.bold("pretense upgrade")} to remove limits.
3072
+ chalk3.gray(` Run ${chalk3.bold("pretense status")} for details, ${chalk3.bold("pretense upgrade")} to remove limits.
2050
3073
 
2051
3074
  `)
2052
3075
  );
@@ -2057,7 +3080,7 @@ function printTokenFooter(filesScanned, mutationsMade) {
2057
3080
  }
2058
3081
 
2059
3082
  // src/banner.ts
2060
- import chalk2 from "chalk";
3083
+ import chalk4 from "chalk";
2061
3084
  var BANNER_RAW = `\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 pretense
2062
3085
  \u2588\u2588 \u2588\u2588
2063
3086
  \u2588\u2588 \u2588\u2588 Stop your code from
@@ -2077,7 +3100,7 @@ function shouldPrint() {
2077
3100
  }
2078
3101
  function tint(line) {
2079
3102
  try {
2080
- return chalk2.hex(ICE_BLUE)(line);
3103
+ return chalk4.hex(ICE_BLUE)(line);
2081
3104
  } catch {
2082
3105
  return line;
2083
3106
  }
@@ -2092,53 +3115,85 @@ function printBanner() {
2092
3115
  }
2093
3116
 
2094
3117
  // src/commands/status.ts
2095
- import chalk3 from "chalk";
3118
+ import chalk5 from "chalk";
2096
3119
  var PLAN_LABEL = {
2097
3120
  free: "Free",
2098
3121
  pro: "Pro",
2099
3122
  enterprise: "Enterprise"
2100
3123
  };
2101
3124
  function fmt2(n) {
2102
- if (!Number.isFinite(n)) return "unlimited";
3125
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
2103
3126
  return n.toLocaleString("en-US");
2104
3127
  }
2105
- function runStatus() {
3128
+ async function runStatus() {
3129
+ let remote = await fetchUsageSummary();
3130
+ if (!remote) {
3131
+ const v = await validateKey({ force: true });
3132
+ if (v?.valid) {
3133
+ remote = usageSummaryFromValidate(v);
3134
+ }
3135
+ }
3136
+ if (remote) {
3137
+ const plan = remote.plan;
3138
+ const tierBadge2 = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3139
+ const lines2 = [];
3140
+ lines2.push("");
3141
+ lines2.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge2} ${chalk5.green("(synced)")}`);
3142
+ lines2.push("");
3143
+ lines2.push(` Plan: ${chalk5.bold(plan)}`);
3144
+ lines2.push(` Mutations: ${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
3145
+ lines2.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
3146
+ lines2.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
3147
+ lines2.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
3148
+ lines2.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
3149
+ lines2.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3150
+ if (remote.keyExpiresAt) {
3151
+ lines2.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
3152
+ }
3153
+ lines2.push("");
3154
+ if (plan === "FREE") {
3155
+ lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3156
+ lines2.push("");
3157
+ }
3158
+ process.stdout.write(lines2.join("\n") + "\n");
3159
+ return;
3160
+ }
2106
3161
  const tier = detectTier();
2107
3162
  const planLimits = getPlanLimits(tier);
2108
3163
  const tierLimits = getTierLimits(tier);
2109
3164
  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 ");
3165
+ const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
2111
3166
  const lines = [];
2112
3167
  lines.push("");
2113
- lines.push(`${chalk3.cyan("Pretense")} ${chalk3.gray("v0.3.0")} ${tierBadge}`);
3168
+ lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
2114
3169
  lines.push("");
2115
- lines.push(` Plan: ${chalk3.bold(PLAN_LABEL[tier])} ${chalk3.gray("(" + planLimits.pricePerMonth + ")")}`);
3170
+ lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
2116
3171
  lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
2117
3172
  lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
2118
3173
  lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
2119
3174
  lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
2120
3175
  lines.push("");
2121
3176
  if (tier === "free") {
2122
- lines.push(` ${chalk3.gray("Upgrade:")} ${chalk3.bold("pretense upgrade")}`);
2123
- lines.push(` ${chalk3.gray("Credits:")} ${chalk3.bold("pretense credits")}`);
3177
+ lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3178
+ lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretense credits")}`);
2124
3179
  lines.push("");
2125
3180
  }
2126
3181
  process.stdout.write(lines.join("\n") + "\n");
2127
3182
  }
2128
3183
 
2129
3184
  // src/commands/upgrade.ts
2130
- import chalk4 from "chalk";
3185
+ import chalk6 from "chalk";
2131
3186
  var PRICING_URL = "https://pretense.ai/pricing";
2132
3187
  function runUpgrade() {
2133
3188
  const tier = detectTier();
2134
3189
  const lines = [];
2135
3190
  lines.push("");
2136
- lines.push(chalk4.cyan.bold(" Pretense Plans"));
3191
+ lines.push(chalk6.cyan.bold(" Pretense Plans"));
2137
3192
  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`);
3193
+ lines.push(chalk6.gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
3194
+ lines.push(chalk6.gray(" \u2502 \u2502 Free \u2502 Pro \u2502 Enterprise \u2502"));
3195
+ lines.push(chalk6.gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
3196
+ lines.push(` \u2502 Price/seat \u2502 ${chalk6.bold("$0")} \u2502 ${chalk6.bold("$29")} \u2502 ${chalk6.bold("$99")} \u2502`);
2142
3197
  lines.push(" \u2502 Mutations/mo \u2502 1,000 \u2502 100,000 \u2502 unlimited \u2502");
2143
3198
  lines.push(" \u2502 Scans/mo \u2502 5,000 \u2502 500,000 \u2502 unlimited \u2502");
2144
3199
  lines.push(" \u2502 Seats \u2502 3 \u2502 25 \u2502 unlimited \u2502");
@@ -2146,37 +3201,70 @@ function runUpgrade() {
2146
3201
  lines.push(" \u2502 SSO/SAML \u2502 - \u2502 - \u2502 yes \u2502");
2147
3202
  lines.push(" \u2502 On-prem \u2502 - \u2502 - \u2502 yes \u2502");
2148
3203
  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"));
3204
+ lines.push(chalk6.gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2150
3205
  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)}`);
3206
+ lines.push(` ${chalk6.gray("Current plan:")} ${chalk6.bold(tier.toUpperCase())}`);
3207
+ lines.push(` ${chalk6.gray("Upgrade at:")} ${chalk6.cyan.underline(PRICING_URL)}`);
2153
3208
  lines.push("");
2154
- lines.push(chalk4.gray(" After purchase, set:"));
2155
- lines.push(chalk4.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
3209
+ lines.push(chalk6.gray(" After purchase, set:"));
3210
+ lines.push(chalk6.gray(" export PRETENSE_LICENSE_KEY=pre_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
2156
3211
  lines.push("");
2157
3212
  process.stdout.write(lines.join("\n") + "\n");
2158
3213
  }
2159
3214
 
2160
3215
  // src/commands/credits.ts
2161
- import chalk5 from "chalk";
3216
+ import chalk7 from "chalk";
2162
3217
  var PLAN_LABEL2 = {
2163
3218
  free: "Free",
2164
3219
  pro: "Pro",
2165
3220
  enterprise: "Enterprise"
2166
3221
  };
2167
3222
  function fmt3(n) {
2168
- if (!Number.isFinite(n)) return "unlimited";
3223
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
2169
3224
  return n.toLocaleString("en-US");
2170
3225
  }
2171
3226
  function progressBar(used, cap, width = 24) {
2172
- if (!Number.isFinite(cap) || cap <= 0) return chalk5.green("[" + "=".repeat(width) + "]");
3227
+ if (cap === -1 || !Number.isFinite(cap) || cap <= 0) return chalk7.green("[" + "=".repeat(width) + "]");
2173
3228
  const pct = Math.min(1, used / cap);
2174
3229
  const filled = Math.round(pct * width);
2175
3230
  const bar = "=".repeat(filled) + "-".repeat(width - filled);
2176
- const colored = pct >= 0.8 ? chalk5.yellow(bar) : pct >= 1 ? chalk5.red(bar) : chalk5.green(bar);
3231
+ const colored = pct >= 0.8 ? chalk7.yellow(bar) : pct >= 1 ? chalk7.red(bar) : chalk7.green(bar);
2177
3232
  return "[" + colored + "]";
2178
3233
  }
2179
- function runCredits() {
3234
+ async function runCredits() {
3235
+ let remote = await fetchUsageSummary();
3236
+ if (!remote) {
3237
+ const v = await validateKey({ force: true });
3238
+ if (v?.valid) {
3239
+ remote = usageSummaryFromValidate(v);
3240
+ }
3241
+ }
3242
+ if (remote) {
3243
+ const used2 = remote.mutationsUsed;
3244
+ const cap2 = remote.monthlyMutationLimit;
3245
+ const remaining2 = remote.mutationsRemaining;
3246
+ const pct2 = cap2 > 0 && cap2 !== -1 ? Math.round(used2 / cap2 * 100) : 0;
3247
+ const lines2 = [];
3248
+ lines2.push("");
3249
+ lines2.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
3250
+ lines2.push("");
3251
+ lines2.push(` Plan: ${chalk7.bold(remote.plan)}`);
3252
+ lines2.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3253
+ lines2.push("");
3254
+ lines2.push(` Mutations: ${progressBar(used2, cap2)} ${fmt3(used2)} / ${fmt3(cap2)}`);
3255
+ lines2.push(` Used: ${pct2}%`);
3256
+ lines2.push(` Remaining: ${fmt3(remaining2)}`);
3257
+ lines2.push("");
3258
+ if (remote.plan === "FREE" && cap2 !== -1 && used2 >= cap2 * 0.8) {
3259
+ lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3260
+ lines2.push("");
3261
+ } else if (remote.plan === "FREE") {
3262
+ lines2.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3263
+ lines2.push("");
3264
+ }
3265
+ process.stdout.write(lines2.join("\n") + "\n");
3266
+ return;
3267
+ }
2180
3268
  const tier = detectTier();
2181
3269
  const limits = getPlanLimits(tier);
2182
3270
  const usage = loadUsage();
@@ -2186,9 +3274,9 @@ function runCredits() {
2186
3274
  const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
2187
3275
  const lines = [];
2188
3276
  lines.push("");
2189
- lines.push(chalk5.cyan.bold(" Pretense Credits"));
3277
+ lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.yellow("(offline)"));
2190
3278
  lines.push("");
2191
- lines.push(` Plan: ${chalk5.bold(PLAN_LABEL2[tier])}`);
3279
+ lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
2192
3280
  lines.push(` Month: ${usage.month}`);
2193
3281
  lines.push("");
2194
3282
  lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
@@ -2196,106 +3284,59 @@ function runCredits() {
2196
3284
  lines.push(` Remaining: ${fmt3(remaining)}`);
2197
3285
  lines.push("");
2198
3286
  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."));
3287
+ lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
2200
3288
  lines.push("");
2201
3289
  } else if (tier === "free") {
2202
- lines.push(chalk5.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3290
+ lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
2203
3291
  lines.push("");
2204
3292
  }
2205
3293
  process.stdout.write(lines.join("\n") + "\n");
2206
3294
  }
2207
3295
 
2208
- // src/commands/login.ts
2209
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync5, chmodSync, readSync } from "fs";
2210
- import { join as join4 } from "path";
2211
- import { homedir as homedir2 } from "os";
2212
- import chalk6 from "chalk";
2213
- var CONFIG_DIR_NAME2 = ".pretense";
2214
- var CONFIG_FILE2 = "config.json";
2215
- function getUserConfigDir() {
2216
- return join4(homedir2(), CONFIG_DIR_NAME2);
2217
- }
2218
- function isValidKeyShape(key) {
2219
- const trimmed = key.trim();
2220
- if (trimmed.length < 24) return false;
2221
- if (!/^[A-Za-z0-9_]+$/.test(trimmed)) return false;
2222
- return trimmed.startsWith("pk_live_") || trimmed.startsWith("ptns_live_");
2223
- }
2224
- function readStdinIfPiped() {
2225
- if (process.stdin.isTTY) return "";
2226
- try {
2227
- const chunks = [];
2228
- const buf = Buffer.alloc(4096);
2229
- let bytesRead = 0;
2230
- do {
2231
- try {
2232
- bytesRead = readSync(0, buf, 0, buf.length, null);
2233
- } catch {
2234
- bytesRead = 0;
2235
- }
2236
- if (bytesRead > 0) chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
2237
- } while (bytesRead === buf.length);
2238
- return Buffer.concat(chunks).toString("utf-8").trim();
2239
- } catch {
2240
- return "";
2241
- }
2242
- }
2243
- function maskKey(key) {
2244
- const trimmed = key.trim();
2245
- if (trimmed.length <= 12) return trimmed;
2246
- return `${trimmed.slice(0, 12)}${"\u2022".repeat(8)}${trimmed.slice(-4)}`;
2247
- }
2248
- function saveApiKey(key, configDir = getUserConfigDir()) {
2249
- if (!existsSync5(configDir)) {
2250
- mkdirSync5(configDir, { recursive: true });
2251
- }
2252
- const path = join4(configDir, CONFIG_FILE2);
2253
- let existing = {};
2254
- if (existsSync5(path)) {
3296
+ // src/commands/logout.ts
3297
+ import readline2 from "readline/promises";
3298
+ import chalk8 from "chalk";
3299
+ async function runLogout(opts = {}) {
3300
+ if (!loadApiKey()) {
3301
+ console.log(chalk8.gray("No saved Pretense dashboard API key in ~/.pretense/config.json."));
3302
+ console.log(chalk8.gray("If you use PRETENSE_API_KEY in your shell, run unset PRETENSE_API_KEY."));
3303
+ return 0;
3304
+ }
3305
+ if (!opts.assumeYes) {
3306
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3307
+ console.error(chalk8.red("Error: confirmation requires an interactive terminal."));
3308
+ console.error(chalk8.gray("Run `pretense logout --yes` to skip the prompt."));
3309
+ return 1;
3310
+ }
3311
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
2255
3312
  try {
2256
- existing = JSON.parse(readFileSync5(path, "utf-8"));
2257
- } catch {
2258
- existing = {};
3313
+ const raw = await rl.question(
3314
+ chalk8.bold("Log out and remove your saved API key? Type y for yes, n for no: ")
3315
+ );
3316
+ const answer = raw.trim().toLowerCase();
3317
+ if (answer !== "y" && answer !== "yes") {
3318
+ console.log(chalk8.yellow("Logout cancelled."));
3319
+ return 0;
3320
+ }
3321
+ } finally {
3322
+ rl.close();
2259
3323
  }
2260
3324
  }
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;
3325
+ const hadEnvDashboardKey = !!process.env["PRETENSE_API_KEY"]?.trim();
3326
+ const { removed, path } = removeSavedDashboardCredentials();
3327
+ clearValidationCache();
3328
+ delete process.env["PRETENSE_API_KEY"];
3329
+ if (removed) {
3330
+ console.log(chalk8.green("Logged out. Removed saved API key from"), chalk8.bold(path));
3331
+ console.log(chalk8.gray("Other terminals: run unset PRETENSE_API_KEY if that variable is still exported (e.g. in ~/.zshrc)."));
3332
+ if (hadEnvDashboardKey) {
3333
+ console.log(
3334
+ chalk8.yellow(
3335
+ "This shell had PRETENSE_API_KEY set; it was cleared for this process only. Restart any running pretense proxy (Ctrl+C, then pretense start)."
3336
+ )
3337
+ );
3338
+ }
2294
3339
  }
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
3340
  return 0;
2300
3341
  }
2301
3342
 
@@ -2312,7 +3353,7 @@ function runLogin(opts = {}) {
2312
3353
  process.exit(1);
2313
3354
  }
2314
3355
  }
2315
- var VERSION = "0.3.0";
3356
+ var VERSION = getCliSemver();
2316
3357
  var SCAN_CONCURRENCY = Math.max(2, os.cpus().length);
2317
3358
  var PROGRESS_INTERVAL_MS = 500;
2318
3359
  var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "go", "java", "csharp", "ruby", "rust"];
@@ -2436,17 +3477,33 @@ function validateLanguage(lang) {
2436
3477
  }
2437
3478
  function collectFiles(target) {
2438
3479
  const abs = resolve2(target);
2439
- if (!existsSync6(abs)) {
3480
+ if (!existsSync7(abs)) {
2440
3481
  return [];
2441
3482
  }
2442
3483
  const stat = statSync(abs);
2443
3484
  if (stat.isFile()) return [abs];
2444
3485
  const files = [];
2445
3486
  function walk(dir) {
2446
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
3487
+ let entries;
3488
+ try {
3489
+ entries = readdirSync(dir, { withFileTypes: true });
3490
+ } catch {
3491
+ return;
3492
+ }
3493
+ const skipDirs = /* @__PURE__ */ new Set([
3494
+ "node_modules",
3495
+ ".git",
3496
+ "dist",
3497
+ "build",
3498
+ ".pretense",
3499
+ ".next",
3500
+ "__pycache__",
3501
+ ".Trash"
3502
+ ]);
3503
+ for (const entry of entries) {
2447
3504
  const fullPath = join5(dir, entry.name);
2448
3505
  if (entry.isDirectory()) {
2449
- if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
3506
+ if (skipDirs.has(entry.name)) continue;
2450
3507
  walk(fullPath);
2451
3508
  } else if (entry.isFile() && isScannableFile(entry.name)) {
2452
3509
  files.push(fullPath);
@@ -2462,7 +3519,7 @@ program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary
2462
3519
  });
2463
3520
  program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
2464
3521
  const dir = resolve2(opts.dir);
2465
- console.log(chalk7.cyan("Initializing Pretense in"), chalk7.bold(dir));
3522
+ console.log(chalk9.cyan("Initializing Pretense in"), chalk9.bold(dir));
2466
3523
  const configDir = initConfig(dir);
2467
3524
  const files = collectFiles(dir);
2468
3525
  const langCounts = {};
@@ -2470,23 +3527,25 @@ program.command("init").description("Initialize .pretense/ config in current dir
2470
3527
  const lang = langFromExt(f);
2471
3528
  langCounts[lang] = (langCounts[lang] ?? 0) + 1;
2472
3529
  }
2473
- console.log(chalk7.green("\n .pretense/ created at"), chalk7.bold(configDir));
2474
- console.log(chalk7.gray(`
3530
+ console.log(chalk9.green("\n .pretense/ created at"), chalk9.bold(configDir));
3531
+ console.log(chalk9.gray(`
2475
3532
  Scanned ${files.length} source files:`));
2476
3533
  for (const [lang, count] of Object.entries(langCounts)) {
2477
- console.log(chalk7.gray(` ${lang}: ${count} files`));
3534
+ console.log(chalk9.gray(` ${lang}: ${count} files`));
2478
3535
  }
2479
- console.log(chalk7.cyan("\n Next: run"), chalk7.bold("pretense start"), chalk7.cyan("to launch the proxy"));
3536
+ console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretense start"), chalk9.cyan("to launch the proxy"));
2480
3537
  console.log();
2481
3538
  });
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) => {
3539
+ program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action(async (opts) => {
2483
3540
  const config = loadConfig();
2484
3541
  const port = validatePort(opts.port);
2485
3542
  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."));
3543
+ console.error(chalk9.red("Error: Invalid port number:"), chalk9.bold(opts.port));
3544
+ console.error(chalk9.gray("Port must be an integer between 1 and 65535."));
2488
3545
  process.exit(1);
2489
3546
  }
3547
+ await ensureDashboardKeyForStart();
3548
+ logDashboardCredentialHint();
2490
3549
  startProxy({ config, port, verbose: opts.verbose });
2491
3550
  });
2492
3551
  async function scanOneFile(file, opts) {
@@ -2596,29 +3655,29 @@ function parseSizeOption(value) {
2596
3655
  }
2597
3656
  program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).option("--max-file-size <size>", "Skip files larger than this (e.g. 10mb, 500k)", "10mb").option("--concurrency <n>", "Max files scanned in parallel", String(SCAN_CONCURRENCY)).option("--quiet", "Suppress progress output", false).action(async (target, opts) => {
2598
3657
  const absTarget = resolve2(target);
2599
- if (!existsSync6(absTarget)) {
2600
- console.error(chalk7.red("Error: Path not found:"), chalk7.bold(absTarget));
3658
+ if (!existsSync7(absTarget)) {
3659
+ console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
2601
3660
  process.exit(1);
2602
3661
  }
2603
3662
  const maxFileSize = parseSizeOption(opts.maxFileSize);
2604
3663
  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"));
3664
+ console.error(chalk9.red("Error: Invalid --max-file-size value:"), chalk9.bold(opts.maxFileSize));
3665
+ console.error(chalk9.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
2607
3666
  process.exit(1);
2608
3667
  }
2609
3668
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
2610
3669
  if (!opts.quiet && !opts.json) {
2611
- process.stderr.write(chalk7.gray(`Discovering files in ${absTarget}...
3670
+ process.stderr.write(chalk9.gray(`Discovering files in ${absTarget}...
2612
3671
  `));
2613
3672
  }
2614
3673
  const files = collectFiles(target);
2615
3674
  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(", ")));
3675
+ console.error(chalk9.red("Error: No source files found at"), chalk9.bold(absTarget));
3676
+ console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
2618
3677
  process.exit(1);
2619
3678
  }
2620
3679
  if (!opts.quiet && !opts.json) {
2621
- process.stderr.write(chalk7.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
3680
+ process.stderr.write(chalk9.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
2622
3681
  `));
2623
3682
  }
2624
3683
  const startedAt = Date.now();
@@ -2644,34 +3703,34 @@ program.command("scan <target>").description("Scan file or directory for identif
2644
3703
  interrupted
2645
3704
  }, null, 2));
2646
3705
  } else {
2647
- console.log(chalk7.cyan("\nPretense Scan Results\n"));
3706
+ console.log(chalk9.cyan("\nPretense Scan Results\n"));
2648
3707
  for (const r of allResults) {
2649
3708
  if (r.skipped) {
2650
3709
  if (r.skipped === "too-large") {
2651
3710
  const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
2652
- console.log(chalk7.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
3711
+ console.log(chalk9.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
2653
3712
  }
2654
3713
  continue;
2655
3714
  }
2656
- const secretBadge = r.secrets > 0 ? chalk7.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3715
+ const secretBadge = r.secrets > 0 ? chalk9.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
2657
3716
  const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
2658
3717
  if (!showRow) continue;
2659
3718
  console.log(
2660
- chalk7.gray(" ") + chalk7.white(r.file) + chalk7.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
3719
+ chalk9.gray(" ") + chalk9.white(r.file) + chalk9.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
2661
3720
  );
2662
3721
  if (r.secrets > 0) {
2663
3722
  for (const t of r.secretTypes) {
2664
- console.log(chalk7.red(` ! ${t}`));
3723
+ console.log(chalk9.red(` ! ${t}`));
2665
3724
  }
2666
3725
  }
2667
3726
  }
2668
- const skipNote = skippedLarge + skippedBinary > 0 ? chalk7.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
3727
+ const skipNote = skippedLarge + skippedBinary > 0 ? chalk9.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
2669
3728
  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
3729
+ chalk9.gray(`
3730
+ Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk9.red(`${totalSecrets} secrets found`) : chalk9.green("0 secrets")) + skipNote
2672
3731
  );
2673
3732
  if (interrupted) {
2674
- console.log(chalk7.yellow("\n Interrupted \u2014 partial results shown above."));
3733
+ console.log(chalk9.yellow("\n Interrupted \u2014 partial results shown above."));
2675
3734
  }
2676
3735
  console.log();
2677
3736
  }
@@ -2685,28 +3744,32 @@ program.command("scan <target>").description("Scan file or directory for identif
2685
3744
  process.exit(2);
2686
3745
  }
2687
3746
  });
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) => {
3747
+ program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
2689
3748
  const absPath = resolve2(file);
2690
- if (!existsSync6(absPath)) {
2691
- console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
3749
+ if (!existsSync7(absPath)) {
3750
+ console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
2692
3751
  process.exit(1);
2693
3752
  }
2694
3753
  if (opts.language) {
2695
3754
  const validLang = validateLanguage(opts.language);
2696
3755
  if (!validLang) {
2697
- console.error(chalk7.red("Error: Unsupported language:"), chalk7.bold(opts.language));
2698
- console.error(chalk7.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
3756
+ console.error(chalk9.red("Error: Unsupported language:"), chalk9.bold(opts.language));
3757
+ console.error(chalk9.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
2699
3758
  process.exit(1);
2700
3759
  }
2701
3760
  }
2702
3761
  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(", ")));
3762
+ console.error(chalk9.red("Error: Not a supported source file:"), chalk9.bold(absPath));
3763
+ console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
2705
3764
  process.exit(1);
2706
3765
  }
2707
- const code = readFileSync6(absPath, "utf-8");
3766
+ const code = readFileSync7(absPath, "utf-8");
2708
3767
  const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
2709
- const result = mutate(code, lang, opts.seed);
3768
+ const secretScan = scan2(code);
3769
+ const effectiveSeed = opts.seed === "pretense" ? buildSaltedSeed(opts.seed) : opts.seed;
3770
+ const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
3771
+ const secretsRedacted = secretsMap.size;
3772
+ const result = mutate(redacted, lang, opts.seed);
2710
3773
  if (opts.save) {
2711
3774
  const configDir = getConfigDir();
2712
3775
  const store = new MutationStore(join5(configDir, "mutation-map.json"));
@@ -2715,22 +3778,31 @@ program.command("mutate <file>").description("Mutate a file and print result to
2715
3778
  id: absPath,
2716
3779
  originalHash: absPath,
2717
3780
  mapEntries: MutationStore.fromMap(result.map),
3781
+ secretsMapEntries: [...secretsMap.entries()],
2718
3782
  timestamp: Date.now(),
2719
3783
  language: lang
2720
3784
  });
2721
3785
  store.persist();
2722
- process.stderr.write(chalk7.gray(`Mutation map saved to ${configDir}/mutation-map.json
3786
+ process.stderr.write(chalk9.gray(`Mutation map saved to ${configDir}/mutation-map.json
2723
3787
  `));
2724
3788
  }
2725
3789
  if (opts.json) {
2726
3790
  console.log(JSON.stringify({
2727
3791
  mutatedCode: result.mutatedCode,
2728
3792
  map: Object.fromEntries(result.map),
2729
- stats: result.stats
3793
+ stats: result.stats,
3794
+ secretsRedacted,
3795
+ secretsMap: Object.fromEntries(secretsMap)
2730
3796
  }, null, 2));
2731
3797
  } else {
2732
3798
  process.stdout.write(result.mutatedCode);
2733
- process.stderr.write(chalk7.gray(`
3799
+ if (secretsRedacted > 0) {
3800
+ process.stderr.write(
3801
+ chalk9.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before identifier mutation ---
3802
+ `)
3803
+ );
3804
+ }
3805
+ process.stderr.write(chalk9.gray(`
2734
3806
  --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
2735
3807
  `));
2736
3808
  printTokenFooter(1, result.stats.tokensMutated);
@@ -2738,15 +3810,15 @@ program.command("mutate <file>").description("Mutate a file and print result to
2738
3810
  });
2739
3811
  program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
2740
3812
  const absPath = resolve2(file);
2741
- if (!existsSync6(absPath)) {
2742
- console.error(chalk7.red("Error: File not found:"), chalk7.bold(absPath));
3813
+ if (!existsSync7(absPath)) {
3814
+ console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
2743
3815
  process.exit(1);
2744
3816
  }
2745
- const mutatedCode = readFileSync6(absPath, "utf-8");
3817
+ const mutatedCode = readFileSync7(absPath, "utf-8");
2746
3818
  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>"));
3819
+ if (!existsSync7(mapPath)) {
3820
+ console.error(chalk9.red("Error: Mutation map not found:"), chalk9.bold(mapPath));
3821
+ console.error(chalk9.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
2750
3822
  process.exit(1);
2751
3823
  }
2752
3824
  let store;
@@ -2754,21 +3826,24 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
2754
3826
  store = new MutationStore(mapPath);
2755
3827
  store.load();
2756
3828
  } 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."));
3829
+ console.error(chalk9.red("Error: Failed to load mutation map:"), chalk9.bold(mapPath));
3830
+ console.error(chalk9.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
2759
3831
  process.exit(1);
2760
3832
  }
2761
3833
  const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
2762
3834
  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."));
3835
+ console.error(chalk9.red("Error: No mutation map entries found."));
3836
+ console.error(chalk9.gray("Run 'pretense mutate --save <file>' first to generate a map."));
2765
3837
  process.exit(1);
2766
3838
  }
2767
3839
  const map = MutationStore.toMap(entry);
2768
- const restored = reverse(mutatedCode, map);
3840
+ const secretsMap = entry.secretsMapEntries ? new Map(entry.secretsMapEntries) : void 0;
3841
+ const restored = reverse(mutatedCode, map, secretsMap);
2769
3842
  process.stdout.write(restored);
2770
- process.stderr.write(chalk7.gray(`
2771
- --- Reversed ${map.size} mutations ---
3843
+ const secretsCount = secretsMap?.size ?? 0;
3844
+ const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
3845
+ process.stderr.write(chalk9.gray(`
3846
+ --- Reversed ${map.size} mutations${secretsSuffix} ---
2772
3847
  `));
2773
3848
  });
2774
3849
  program.command("audit").description("View the audit log").option("--json", "Output as JSON", false).option("--csv", "Output as CSV", false).option("-l, --limit <number>", "Limit entries", "50").action((opts) => {
@@ -2781,30 +3856,34 @@ program.command("audit").description("View the audit log").option("--json", "Out
2781
3856
  program.command("version").description("Print Pretense CLI version").action(() => {
2782
3857
  console.log(`@pretense/cli v${VERSION}`);
2783
3858
  });
2784
- program.command("status").description("Show current plan, monthly quota, seat usage").action(() => {
2785
- runStatus();
3859
+ program.command("status").description("Show current plan, monthly quota, seat usage").action(async () => {
3860
+ await runStatus();
2786
3861
  });
2787
- program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(() => {
2788
- runStatus();
3862
+ program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
3863
+ await runStatus();
2789
3864
  });
2790
- program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(() => {
2791
- runCredits();
3865
+ program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(async () => {
3866
+ await runCredits();
2792
3867
  });
2793
3868
  program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
2794
3869
  runUpgrade();
2795
3870
  });
2796
- program.command("credits").description("Show remaining mutation/scan budget for the current month").action(() => {
2797
- runCredits();
3871
+ program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
3872
+ await runCredits();
2798
3873
  });
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) => {
3874
+ program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
2800
3875
  const code = runLogin({ key: opts.key });
2801
3876
  if (code !== 0) process.exit(code);
2802
3877
  });
3878
+ program.command("logout").description("Remove the saved Pretense dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
3879
+ const code = await runLogout({ assumeYes: opts.yes === true });
3880
+ if (code !== 0) process.exit(code);
3881
+ });
2803
3882
  function handleFatalError(err) {
2804
3883
  if (err instanceof Error && err.message) {
2805
- console.error(chalk7.red("Error:"), err.message);
3884
+ console.error(chalk9.red("Error:"), err.message);
2806
3885
  } else {
2807
- console.error(chalk7.red("An unexpected error occurred."));
3886
+ console.error(chalk9.red("An unexpected error occurred."));
2808
3887
  }
2809
3888
  process.exit(1);
2810
3889
  }