@pretense/cli 0.3.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 ADDED
@@ -0,0 +1,1581 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+ import { readFileSync as readFileSync4, existsSync as existsSync4, readdirSync, statSync } from "fs";
7
+ import { resolve as resolve2, extname, join as join3 } from "path";
8
+
9
+ // src/types.ts
10
+ var DEFAULT_CONFIG = {
11
+ port: 9339,
12
+ targetApis: ["https://api.anthropic.com", "https://api.openai.com"],
13
+ languages: ["typescript", "javascript", "python", "go", "java"],
14
+ mutationRules: [
15
+ { tokenType: "Variable" /* Variable */, prefix: "_v" },
16
+ { tokenType: "Function" /* Function */, prefix: "_fn" },
17
+ { tokenType: "Class" /* Class */, prefix: "_cls" },
18
+ { tokenType: "String" /* String */ },
19
+ { tokenType: "Comment" /* Comment */, strip: true },
20
+ { tokenType: "Import" /* Import */ },
21
+ { tokenType: "Property" /* Property */, prefix: "_prop" },
22
+ { tokenType: "Method" /* Method */, prefix: "_fn" }
23
+ ],
24
+ storePath: `${process.env["HOME"] ?? "/tmp"}/.pretense`
25
+ };
26
+
27
+ // src/scanner.ts
28
+ var TS_JS_KEYWORDS = /* @__PURE__ */ new Set([
29
+ "abstract",
30
+ "any",
31
+ "as",
32
+ "async",
33
+ "await",
34
+ "boolean",
35
+ "break",
36
+ "case",
37
+ "catch",
38
+ "class",
39
+ "const",
40
+ "constructor",
41
+ "continue",
42
+ "debugger",
43
+ "declare",
44
+ "default",
45
+ "delete",
46
+ "do",
47
+ "else",
48
+ "enum",
49
+ "export",
50
+ "extends",
51
+ "false",
52
+ "finally",
53
+ "for",
54
+ "from",
55
+ "function",
56
+ "get",
57
+ "if",
58
+ "implements",
59
+ "import",
60
+ "in",
61
+ "instanceof",
62
+ "interface",
63
+ "keyof",
64
+ "let",
65
+ "module",
66
+ "namespace",
67
+ "never",
68
+ "new",
69
+ "null",
70
+ "number",
71
+ "object",
72
+ "of",
73
+ "package",
74
+ "private",
75
+ "protected",
76
+ "public",
77
+ "readonly",
78
+ "require",
79
+ "return",
80
+ "set",
81
+ "static",
82
+ "string",
83
+ "super",
84
+ "switch",
85
+ "symbol",
86
+ "this",
87
+ "throw",
88
+ "true",
89
+ "try",
90
+ "type",
91
+ "typeof",
92
+ "undefined",
93
+ "unknown",
94
+ "var",
95
+ "void",
96
+ "while",
97
+ "with",
98
+ "yield",
99
+ "satisfies",
100
+ "using",
101
+ "infer",
102
+ "asserts"
103
+ ]);
104
+ var PYTHON_KEYWORDS = /* @__PURE__ */ new Set([
105
+ "False",
106
+ "None",
107
+ "True",
108
+ "and",
109
+ "as",
110
+ "assert",
111
+ "async",
112
+ "await",
113
+ "break",
114
+ "class",
115
+ "continue",
116
+ "def",
117
+ "del",
118
+ "elif",
119
+ "else",
120
+ "except",
121
+ "finally",
122
+ "for",
123
+ "from",
124
+ "global",
125
+ "if",
126
+ "import",
127
+ "in",
128
+ "is",
129
+ "lambda",
130
+ "nonlocal",
131
+ "not",
132
+ "or",
133
+ "pass",
134
+ "raise",
135
+ "return",
136
+ "try",
137
+ "while",
138
+ "with",
139
+ "yield",
140
+ "self",
141
+ "cls"
142
+ ]);
143
+ var GO_KEYWORDS = /* @__PURE__ */ new Set([
144
+ "break",
145
+ "case",
146
+ "chan",
147
+ "const",
148
+ "continue",
149
+ "default",
150
+ "defer",
151
+ "else",
152
+ "fallthrough",
153
+ "for",
154
+ "func",
155
+ "go",
156
+ "goto",
157
+ "if",
158
+ "import",
159
+ "interface",
160
+ "map",
161
+ "package",
162
+ "range",
163
+ "return",
164
+ "select",
165
+ "struct",
166
+ "switch",
167
+ "type",
168
+ "var",
169
+ "nil",
170
+ "true",
171
+ "false",
172
+ "error",
173
+ "string",
174
+ "int",
175
+ "int8",
176
+ "int16",
177
+ "int32",
178
+ "int64",
179
+ "uint",
180
+ "uint8",
181
+ "uint16",
182
+ "uint32",
183
+ "uint64",
184
+ "float32",
185
+ "float64",
186
+ "complex64",
187
+ "complex128",
188
+ "bool",
189
+ "byte",
190
+ "rune",
191
+ "uintptr",
192
+ "make",
193
+ "new",
194
+ "len",
195
+ "cap",
196
+ "append",
197
+ "copy",
198
+ "close",
199
+ "delete",
200
+ "panic",
201
+ "recover",
202
+ "print",
203
+ "println"
204
+ ]);
205
+ var JAVA_KEYWORDS = /* @__PURE__ */ new Set([
206
+ "abstract",
207
+ "assert",
208
+ "boolean",
209
+ "break",
210
+ "byte",
211
+ "case",
212
+ "catch",
213
+ "char",
214
+ "class",
215
+ "const",
216
+ "continue",
217
+ "default",
218
+ "do",
219
+ "double",
220
+ "else",
221
+ "enum",
222
+ "extends",
223
+ "final",
224
+ "finally",
225
+ "float",
226
+ "for",
227
+ "goto",
228
+ "if",
229
+ "implements",
230
+ "import",
231
+ "instanceof",
232
+ "int",
233
+ "interface",
234
+ "long",
235
+ "native",
236
+ "new",
237
+ "package",
238
+ "private",
239
+ "protected",
240
+ "public",
241
+ "return",
242
+ "short",
243
+ "static",
244
+ "strictfp",
245
+ "super",
246
+ "switch",
247
+ "synchronized",
248
+ "this",
249
+ "throw",
250
+ "throws",
251
+ "transient",
252
+ "try",
253
+ "void",
254
+ "volatile",
255
+ "while",
256
+ "true",
257
+ "false",
258
+ "null",
259
+ "var",
260
+ "record",
261
+ "sealed",
262
+ "permits",
263
+ "yield"
264
+ ]);
265
+ function detectLanguage(code) {
266
+ if (/:[\s]*\w+[\s]*[;,{)]/.test(code) || /import.*from\s+['"]/.test(code) || /interface\s+\w+/.test(code)) return "typescript";
267
+ if (/\bdef\s+\w+\s*\(/.test(code) || /\bimport\s+\w+/.test(code) && /:$/.test(code)) return "python";
268
+ if (/\bfunc\s+\w+\s*\(/.test(code) || /\bpackage\s+\w+/.test(code)) return "go";
269
+ if (/\bpublic\s+class\s+\w+/.test(code) || /\bimport\s+java\./.test(code)) return "java";
270
+ if (/\bconst\s+\w+\s*=/.test(code) || /\bfunction\s+\w+\s*\(/.test(code)) return "javascript";
271
+ return "typescript";
272
+ }
273
+ function lineAt(code, index) {
274
+ let line = 1;
275
+ for (let i = 0; i < index && i < code.length; i++) {
276
+ if (code[i] === "\n") line++;
277
+ }
278
+ return line;
279
+ }
280
+ function scanTypeScript(code) {
281
+ const tokens = [];
282
+ if (!code.trim()) return tokens;
283
+ const patterns = [
284
+ // Single-line comment
285
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
286
+ // Multi-line comment
287
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
288
+ // Import source (the module path string)
289
+ { re: /\bimport\b[^'"]*['"]([^'"]+)['"]/g, type: "Import" /* Import */, groupIndex: 1 },
290
+ // Class name
291
+ { re: /\bclass\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Class" /* Class */, groupIndex: 1 },
292
+ // Function/method name (function keyword)
293
+ { re: /\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Function" /* Function */, groupIndex: 1 },
294
+ // Arrow function assigned to const/let/var
295
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g, type: "Function" /* Function */, groupIndex: 1 },
296
+ // Method: identifier followed by (
297
+ { re: /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
298
+ // Variable declarations
299
+ { re: /\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
300
+ // Template literals (preserve backtick wrapper)
301
+ { re: /`[^`]*`/g, type: "String" /* String */ },
302
+ // Double-quoted strings
303
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
304
+ // Single-quoted strings
305
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
306
+ ];
307
+ for (const { re, type, groupIndex } of patterns) {
308
+ re.lastIndex = 0;
309
+ let m;
310
+ while ((m = re.exec(code)) !== null) {
311
+ const fullMatch = m[0];
312
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
313
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
314
+ if (!captureValue || captureValue.trim() === "") continue;
315
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */ || type === "Method" /* Method */ || type === "Property" /* Property */) {
316
+ if (TS_JS_KEYWORDS.has(captureValue)) continue;
317
+ if (/^(console|process|Object|Array|Math|JSON|Promise|Error|Map|Set|Date|RegExp|Symbol|Proxy|Reflect|global|globalThis|window|document|module|exports|require|__dirname|__filename)$/.test(captureValue)) continue;
318
+ if (captureValue.length <= 1) continue;
319
+ }
320
+ tokens.push({
321
+ type,
322
+ value: captureValue,
323
+ start: captureStart,
324
+ end: captureStart + captureValue.length,
325
+ line: lineAt(code, captureStart)
326
+ });
327
+ }
328
+ }
329
+ return deduplicate(tokens);
330
+ }
331
+ function scanPython(code) {
332
+ const tokens = [];
333
+ if (!code.trim()) return tokens;
334
+ const patterns = [
335
+ // Triple-quoted docstrings
336
+ { re: /"""[\s\S]*?"""|'''[\s\S]*?'''/g, type: "Comment" /* Comment */ },
337
+ // Line comments
338
+ { re: /#[^\n]*/g, type: "Comment" /* Comment */ },
339
+ // Import module name
340
+ { re: /\bimport\s+([A-Za-z_][A-Za-z0-9_.]*)/g, type: "Import" /* Import */, groupIndex: 1 },
341
+ { re: /\bfrom\s+([A-Za-z_.][A-Za-z0-9_.]*)\s+import/g, type: "Import" /* Import */, groupIndex: 1 },
342
+ // Class name
343
+ { re: /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Class" /* Class */, groupIndex: 1 },
344
+ // Function/method def
345
+ { re: /\bdef\s+([A-Za-z_][A-Za-z0-9_]*)/g, type: "Function" /* Function */, groupIndex: 1 },
346
+ // Variable assignment (simple name = ...)
347
+ { re: /^([A-Za-z_][A-Za-z0-9_]*)\s*=/gm, type: "Variable" /* Variable */, groupIndex: 1 },
348
+ // Double-quoted strings
349
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
350
+ // Single-quoted strings
351
+ { re: /'(?:[^'\\]|\\.)*'/g, type: "String" /* String */ }
352
+ ];
353
+ for (const { re, type, groupIndex } of patterns) {
354
+ re.lastIndex = 0;
355
+ let m;
356
+ while ((m = re.exec(code)) !== null) {
357
+ const fullMatch = m[0];
358
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
359
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
360
+ if (!captureValue || captureValue.trim() === "") continue;
361
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
362
+ if (PYTHON_KEYWORDS.has(captureValue)) continue;
363
+ if (captureValue.length <= 1) continue;
364
+ if (captureValue.startsWith("__") && captureValue.endsWith("__")) continue;
365
+ }
366
+ tokens.push({
367
+ type,
368
+ value: captureValue,
369
+ start: captureStart,
370
+ end: captureStart + captureValue.length,
371
+ line: lineAt(code, captureStart)
372
+ });
373
+ }
374
+ }
375
+ return deduplicate(tokens);
376
+ }
377
+ function scanGo(code) {
378
+ const tokens = [];
379
+ if (!code.trim()) return tokens;
380
+ const patterns = [
381
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
382
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
383
+ // Import path
384
+ { re: /import\s*\(\s*([\s\S]*?)\s*\)/g, type: "Import" /* Import */ },
385
+ { re: /import\s+"([^"]+)"/g, type: "Import" /* Import */, groupIndex: 1 },
386
+ // Type declaration
387
+ { re: /\btype\s+([A-Za-z_][A-Za-z0-9_]*)\s+(?:struct|interface)/g, type: "Class" /* Class */, groupIndex: 1 },
388
+ // Func name
389
+ { re: /\bfunc\s+(?:\([^)]*\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\(/g, type: "Function" /* Function */, groupIndex: 1 },
390
+ // Var/const declaration
391
+ { re: /\b(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g, type: "Variable" /* Variable */, groupIndex: 1 },
392
+ // Short variable declaration
393
+ { re: /\b([A-Za-z_][A-Za-z0-9_]*)\s*:=/g, type: "Variable" /* Variable */, groupIndex: 1 },
394
+ // Double-quoted strings
395
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ },
396
+ // Backtick raw strings
397
+ { re: /`[^`]*`/g, type: "String" /* String */ }
398
+ ];
399
+ for (const { re, type, groupIndex } of patterns) {
400
+ re.lastIndex = 0;
401
+ let m;
402
+ while ((m = re.exec(code)) !== null) {
403
+ const fullMatch = m[0];
404
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
405
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
406
+ if (!captureValue || captureValue.trim() === "") continue;
407
+ if (type === "Variable" /* Variable */ || type === "Function" /* Function */ || type === "Class" /* Class */) {
408
+ if (GO_KEYWORDS.has(captureValue)) continue;
409
+ if (captureValue.length <= 1) continue;
410
+ }
411
+ tokens.push({
412
+ type,
413
+ value: captureValue,
414
+ start: captureStart,
415
+ end: captureStart + captureValue.length,
416
+ line: lineAt(code, captureStart)
417
+ });
418
+ }
419
+ }
420
+ return deduplicate(tokens);
421
+ }
422
+ function scanJava(code) {
423
+ const tokens = [];
424
+ if (!code.trim()) return tokens;
425
+ const patterns = [
426
+ { re: /\/\/[^\n]*/g, type: "Comment" /* Comment */ },
427
+ { re: /\/\*[\s\S]*?\*\//g, type: "Comment" /* Comment */ },
428
+ // Import statement
429
+ { re: /\bimport\s+([\w.]+);/g, type: "Import" /* Import */, groupIndex: 1 },
430
+ // Class/interface/enum
431
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, type: "Class" /* Class */, groupIndex: 1 },
432
+ // Method declaration
433
+ { re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default)[\w\s<>\[\],]*\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, type: "Method" /* Method */, groupIndex: 1 },
434
+ // Variable declaration
435
+ { re: /\b(?:int|long|double|float|boolean|char|byte|short|String|Object|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=;,)]/g, type: "Variable" /* Variable */, groupIndex: 1 },
436
+ // Double-quoted strings
437
+ { re: /"(?:[^"\\]|\\.)*"/g, type: "String" /* String */ }
438
+ ];
439
+ for (const { re, type, groupIndex } of patterns) {
440
+ re.lastIndex = 0;
441
+ let m;
442
+ while ((m = re.exec(code)) !== null) {
443
+ const fullMatch = m[0];
444
+ const captureValue = groupIndex !== void 0 ? m[groupIndex] ?? fullMatch : fullMatch;
445
+ const captureStart = groupIndex !== void 0 ? m.index + fullMatch.indexOf(captureValue) : m.index;
446
+ if (!captureValue || captureValue.trim() === "") continue;
447
+ if (type === "Variable" /* Variable */ || type === "Method" /* Method */ || type === "Class" /* Class */) {
448
+ if (JAVA_KEYWORDS.has(captureValue)) continue;
449
+ if (captureValue.length <= 1) continue;
450
+ if (/^(String|Object|Integer|Long|Double|Boolean|List|Map|Set|Array|Exception|Error|System|Math|Thread|Class|Enum)$/.test(captureValue)) continue;
451
+ }
452
+ tokens.push({
453
+ type,
454
+ value: captureValue,
455
+ start: captureStart,
456
+ end: captureStart + captureValue.length,
457
+ line: lineAt(code, captureStart)
458
+ });
459
+ }
460
+ }
461
+ return deduplicate(tokens);
462
+ }
463
+ function deduplicate(tokens) {
464
+ tokens.sort((a, b) => a.start - b.start);
465
+ const seen = /* @__PURE__ */ new Set();
466
+ const result = [];
467
+ for (const token of tokens) {
468
+ const key = `${token.start}:${token.end}`;
469
+ if (!seen.has(key)) {
470
+ seen.add(key);
471
+ result.push(token);
472
+ }
473
+ }
474
+ return result;
475
+ }
476
+ function scan(code, language) {
477
+ if (!code || !code.trim()) {
478
+ return { tokens: [], language };
479
+ }
480
+ const lang = language.toLowerCase();
481
+ let tokens;
482
+ switch (lang) {
483
+ case "typescript":
484
+ case "javascript":
485
+ case "ts":
486
+ case "js":
487
+ case "tsx":
488
+ case "jsx":
489
+ tokens = scanTypeScript(code);
490
+ break;
491
+ case "python":
492
+ case "py":
493
+ tokens = scanPython(code);
494
+ break;
495
+ case "go":
496
+ tokens = scanGo(code);
497
+ break;
498
+ case "java":
499
+ tokens = scanJava(code);
500
+ break;
501
+ default:
502
+ tokens = scanTypeScript(code);
503
+ }
504
+ return { tokens, language: lang };
505
+ }
506
+
507
+ // src/mutator.ts
508
+ function hash32(str) {
509
+ let h = 5381;
510
+ for (let i = 0; i < str.length; i++) {
511
+ h = (h << 5) + h ^ str.charCodeAt(i);
512
+ }
513
+ return h >>> 0;
514
+ }
515
+ function toAlphaNum(n, len) {
516
+ const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
517
+ let result = "";
518
+ let v = n;
519
+ for (let i = 0; i < len; i++) {
520
+ result = CHARS[v % CHARS.length] + result;
521
+ v = Math.floor(v / CHARS.length);
522
+ }
523
+ return result;
524
+ }
525
+ function deterministicId(original, seed, prefix, len = 4) {
526
+ const combined = `${seed}:${prefix}:${original}`;
527
+ const h = hash32(combined);
528
+ return prefix + toAlphaNum(h, len);
529
+ }
530
+ function prefixForType(type) {
531
+ switch (type) {
532
+ case "Class" /* Class */:
533
+ return "_cls";
534
+ case "Function" /* Function */:
535
+ case "Method" /* Method */:
536
+ return "_fn";
537
+ case "Variable" /* Variable */:
538
+ return "_v";
539
+ case "Property" /* Property */:
540
+ return "_prop";
541
+ default:
542
+ return "_tok";
543
+ }
544
+ }
545
+ function mutate(code, language, seed = "pretense") {
546
+ const startMs = performance.now();
547
+ const lang = language ?? detectLanguage(code);
548
+ if (!code || !code.trim()) {
549
+ const emptyMap = /* @__PURE__ */ new Map();
550
+ return {
551
+ mutatedCode: code,
552
+ map: emptyMap,
553
+ stats: { tokensScanned: 0, tokensMutated: 0, durationMs: 0, language: lang }
554
+ };
555
+ }
556
+ const { tokens } = scan(code, lang);
557
+ const map = /* @__PURE__ */ new Map();
558
+ for (const token of tokens) {
559
+ if (map.has(token.value)) continue;
560
+ if (token.type === "Comment" /* Comment */) {
561
+ continue;
562
+ }
563
+ if (token.type === "String" /* String */) {
564
+ continue;
565
+ }
566
+ if (token.type === "Import" /* Import */) {
567
+ continue;
568
+ }
569
+ const prefix = prefixForType(token.type);
570
+ const synthetic = deterministicId(token.value, seed, prefix);
571
+ map.set(token.value, synthetic);
572
+ }
573
+ const entries = [...map.entries()].sort((a, b) => b[0].length - a[0].length);
574
+ let mutatedCode = code;
575
+ for (const [original, synthetic] of entries) {
576
+ if (!synthetic) continue;
577
+ const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
578
+ if (original.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/)) {
579
+ mutatedCode = mutatedCode.replace(new RegExp(`\\b${escaped}\\b`, "g"), synthetic);
580
+ } else {
581
+ mutatedCode = mutatedCode.split(original).join(synthetic);
582
+ }
583
+ }
584
+ const durationMs = Math.round((performance.now() - startMs) * 100) / 100;
585
+ return {
586
+ mutatedCode,
587
+ map,
588
+ stats: {
589
+ tokensScanned: tokens.length,
590
+ tokensMutated: map.size,
591
+ durationMs,
592
+ language: lang
593
+ }
594
+ };
595
+ }
596
+
597
+ // src/reverser.ts
598
+ function reverse(mutatedCode, map) {
599
+ if (!mutatedCode || map.size === 0) return mutatedCode;
600
+ const reverseMap = /* @__PURE__ */ new Map();
601
+ for (const [original, synthetic] of map.entries()) {
602
+ if (synthetic === "" || !synthetic) continue;
603
+ reverseMap.set(synthetic, original);
604
+ }
605
+ if (reverseMap.size === 0) return mutatedCode;
606
+ const sortedEntries = [...reverseMap.entries()].sort((a, b) => b[0].length - a[0].length);
607
+ let result = mutatedCode;
608
+ for (const [synthetic, original] of sortedEntries) {
609
+ const escaped = synthetic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
610
+ if (synthetic.match(/^[A-Za-z_$][A-Za-z0-9_$]*$/) || synthetic.match(/^_[a-z]+[a-z0-9]{4,}$/)) {
611
+ result = result.replace(new RegExp(`\\b${escaped}\\b`, "g"), original);
612
+ } else {
613
+ result = result.split(synthetic).join(original);
614
+ }
615
+ }
616
+ return result;
617
+ }
618
+
619
+ // src/secrets.ts
620
+ var SECRET_PATTERNS = [
621
+ { name: "anthropic-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,}/g },
622
+ { name: "openai-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sk-[A-Za-z0-9]{20,}/g },
623
+ { name: "aws-access-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /AKIA[0-9A-Z]{16}/g },
624
+ { name: "aws-secret-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/g },
625
+ { name: "github-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
626
+ { name: "github-fine-grained", category: "secret", severity: "critical", defaultAction: "block", pattern: /github_pat_[A-Za-z0-9_]{22,}/g },
627
+ { name: "stripe-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/g },
628
+ { name: "private-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
629
+ { name: "jwt-token", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
630
+ { name: "database-url", category: "credential", severity: "critical", defaultAction: "block", pattern: /(?:postgres|mysql|mongodb|redis|amqp):\/\/[^\s'"\\)]+:[^\s'"\\)]+@[^\s'"\\)]+/g },
631
+ { name: "generic-password", category: "credential", severity: "high", defaultAction: "redact", pattern: /(?:password|passwd|pwd|secret|token|api_key|apikey|api-key)\s*[=:]\s*['"][^\s'"]{8,}['"]/gi },
632
+ { name: "slack-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g },
633
+ { name: "google-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /AIza[A-Za-z0-9_-]{35}/g },
634
+ { name: "npm-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /npm_[A-Za-z0-9]{36}/g },
635
+ { name: "sendgrid-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
636
+ { name: "twilio-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /SK[a-f0-9]{32}/g },
637
+ // ─── Additional patterns (v0.3) ──────────────────────────────────────────────
638
+ { name: "azure-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /(?:AccountKey|SharedAccessKey)=[A-Za-z0-9+/=]{40,}/g },
639
+ { name: "mailgun-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /key-[a-f0-9]{32}/g },
640
+ { name: "twilio-sid", category: "secret", severity: "high", defaultAction: "block", pattern: /AC[a-f0-9]{32}/g },
641
+ { name: "heroku-api-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /[hH]eroku.*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g },
642
+ { name: "shopify-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /shpat_[a-fA-F0-9]{32}/g },
643
+ { name: "datadog-key", category: "secret", severity: "high", defaultAction: "block", pattern: /dd[a-z]{1,2}_[a-zA-Z0-9]{32,}/g },
644
+ { name: "vercel-token", category: "secret", severity: "high", defaultAction: "block", pattern: /vc_[a-zA-Z0-9]{24,}/g },
645
+ { name: "supabase-key", category: "secret", severity: "critical", defaultAction: "block", pattern: /sbp_[a-f0-9]{40}/g },
646
+ { name: "linear-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /lin_api_[A-Za-z0-9]{40}/g },
647
+ { name: "cloudflare-api-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /v1\.0-[a-f0-9]{24}-[a-f0-9]{146}/g },
648
+ { name: "discord-bot-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}/g },
649
+ { name: "grafana-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /eyJrIjoi[A-Za-z0-9_-]{40,}/g },
650
+ { name: "confluent-key", category: "secret", severity: "high", defaultAction: "block", pattern: /CONFLUENT[A-Z_]*\s*[=:]\s*['"]?[A-Za-z0-9/+=]{16,}['"]?/g },
651
+ { name: "digitalocean-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /dop_v1_[a-f0-9]{64}/g },
652
+ { name: "doppler-token", category: "secret", severity: "high", defaultAction: "block", pattern: /dp\.st\.[a-z0-9_-]{2,}\.[A-Za-z0-9]{40,}/g },
653
+ { name: "planetscale-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /pscale_tkn_[A-Za-z0-9_-]{32,}/g },
654
+ { name: "hashicorp-vault-token", category: "secret", severity: "critical", defaultAction: "block", pattern: /hvs\.[A-Za-z0-9_-]{24,}/g },
655
+ { name: "fastly-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /fastly_[A-Za-z0-9]{32}/g },
656
+ { name: "algolia-api-key", category: "secret", severity: "high", defaultAction: "block", pattern: /[a-f0-9]{32}/g, validate: (m) => /ALGOLIA|algolia/.test(m) },
657
+ { name: "pulumi-token", category: "secret", severity: "high", defaultAction: "block", pattern: /pul-[a-f0-9]{40}/g }
658
+ ];
659
+ var PII_PATTERNS = [
660
+ { name: "ssn", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
661
+ { name: "credit-card", category: "pii", severity: "critical", defaultAction: "redact", pattern: /\b(?:\d[ -]*?){13,19}\b/g, validate: luhnCheck },
662
+ { name: "email", category: "pii", severity: "medium", defaultAction: "warn", pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
663
+ { name: "phone-us", category: "pii", severity: "medium", defaultAction: "warn", pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
664
+ { name: "ip-address", category: "pii", severity: "low", defaultAction: "warn", pattern: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g, validate: (ip) => !ip.startsWith("127.") && !ip.startsWith("0.") && ip !== "255.255.255.255" }
665
+ ];
666
+ var ENV_PATTERNS = [
667
+ { name: "env-secret", category: "credential", severity: "high", defaultAction: "block", pattern: /(?:process\.env\.|os\.environ\[|ENV\[|getenv\()['"]?((?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE|AUTH)[A-Z_0-9]*)['"]?\]?\)?/gi }
668
+ ];
669
+ function luhnCheck(num) {
670
+ const digits = num.replace(/\D/g, "");
671
+ if (digits.length < 13 || digits.length > 19) return false;
672
+ let sum = 0;
673
+ let alternate = false;
674
+ for (let i = digits.length - 1; i >= 0; i--) {
675
+ let n = parseInt(digits[i], 10);
676
+ if (alternate) {
677
+ n *= 2;
678
+ if (n > 9) n -= 9;
679
+ }
680
+ sum += n;
681
+ alternate = !alternate;
682
+ }
683
+ return sum % 10 === 0;
684
+ }
685
+ function shannonEntropy(str) {
686
+ if (str.length === 0) return 0;
687
+ const freq = /* @__PURE__ */ new Map();
688
+ for (const ch of str) {
689
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
690
+ }
691
+ let entropy = 0;
692
+ const len = str.length;
693
+ for (const count of freq.values()) {
694
+ const p = count / len;
695
+ if (p > 0) entropy -= p * Math.log2(p);
696
+ }
697
+ return entropy;
698
+ }
699
+ function isHighEntropy(str, minLength = 20, threshold = 4.5) {
700
+ if (str.length <= minLength) return false;
701
+ return shannonEntropy(str) > threshold;
702
+ }
703
+ function maskValue(value, type) {
704
+ if (type.includes("key") || type.includes("token") || type.includes("secret")) return value.slice(0, 6) + "..." + value.slice(-4);
705
+ if (type === "ssn") return "***-**-" + value.slice(-4);
706
+ if (type === "credit-card") return "****-****-****-" + value.replace(/\D/g, "").slice(-4);
707
+ if (type === "email") {
708
+ const [local, domain] = value.split("@");
709
+ return (local?.[0] ?? "*") + "***@" + (domain ?? "");
710
+ }
711
+ if (value.length > 12) return value.slice(0, 4) + "..." + value.slice(-4);
712
+ return "***";
713
+ }
714
+ var matchCounter = 0;
715
+ function scan2(text, actionOverrides) {
716
+ const start = performance.now();
717
+ const matches = [];
718
+ const allPatterns = [...SECRET_PATTERNS, ...PII_PATTERNS, ...ENV_PATTERNS];
719
+ for (const def of allPatterns) {
720
+ def.pattern.lastIndex = 0;
721
+ let m;
722
+ while ((m = def.pattern.exec(text)) !== null) {
723
+ const value = m[0];
724
+ if (def.validate && !def.validate(value)) continue;
725
+ const action = actionOverrides?.[def.name] ?? def.defaultAction;
726
+ matchCounter++;
727
+ matches.push({
728
+ id: `scan_${matchCounter}`,
729
+ category: def.category,
730
+ type: def.name,
731
+ value,
732
+ masked: maskValue(value, def.name),
733
+ start: m.index,
734
+ end: m.index + value.length,
735
+ severity: def.severity,
736
+ action
737
+ });
738
+ }
739
+ }
740
+ const highEntropyPattern = /\b[A-Za-z0-9_\-/.+=$]{21,}\b/g;
741
+ highEntropyPattern.lastIndex = 0;
742
+ let hem;
743
+ while ((hem = highEntropyPattern.exec(text)) !== null) {
744
+ const value = hem[0];
745
+ const alreadyMatched = matches.some(
746
+ (m) => m.start <= hem.index && m.end >= hem.index + value.length
747
+ );
748
+ if (alreadyMatched) continue;
749
+ if (isHighEntropy(value)) {
750
+ matchCounter++;
751
+ matches.push({
752
+ id: `scan_${matchCounter}`,
753
+ category: "secret",
754
+ type: "high-entropy-string",
755
+ value,
756
+ masked: value.slice(0, 6) + "..." + value.slice(-4),
757
+ start: hem.index,
758
+ end: hem.index + value.length,
759
+ severity: "medium",
760
+ action: actionOverrides?.["high-entropy-string"] ?? "warn"
761
+ });
762
+ }
763
+ }
764
+ const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
765
+ matches.sort((a, b) => a.start - b.start || severityRank[b.severity] - severityRank[a.severity]);
766
+ const deduped = [];
767
+ let lastEnd = -1;
768
+ for (const match of matches) {
769
+ if (match.start >= lastEnd) {
770
+ deduped.push(match);
771
+ lastEnd = match.end;
772
+ }
773
+ }
774
+ return {
775
+ clean: deduped.filter((m) => m.action === "block" || m.action === "redact").length === 0,
776
+ matches: deduped,
777
+ scannedAt: Date.now(),
778
+ durationMs: Math.round((performance.now() - start) * 100) / 100
779
+ };
780
+ }
781
+ function applyRedactions(text, matches) {
782
+ const sorted = [...matches].sort((a, b) => b.start - a.start);
783
+ let result = text;
784
+ for (const match of sorted) {
785
+ if (match.action === "block" || match.action === "redact") {
786
+ result = result.slice(0, match.start) + `[PRETENSE:${match.type.toUpperCase()}]` + result.slice(match.end);
787
+ }
788
+ }
789
+ return result;
790
+ }
791
+
792
+ // src/store.ts
793
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
794
+ import { dirname } from "path";
795
+ var MutationStore = class {
796
+ entries = /* @__PURE__ */ new Map();
797
+ filePath;
798
+ constructor(filePath) {
799
+ this.filePath = filePath;
800
+ }
801
+ /**
802
+ * Persist a mutation map with associated metadata.
803
+ *
804
+ * @example
805
+ * store.save({ id: "req-1", originalHash: "abc", mapEntries: [...], timestamp: Date.now(), language: "typescript" });
806
+ */
807
+ save(entry) {
808
+ this.entries.set(entry.id, { ...entry });
809
+ }
810
+ /**
811
+ * Retrieve a stored entry by request ID.
812
+ *
813
+ * @example
814
+ * const entry = store.get("req-1");
815
+ */
816
+ get(id) {
817
+ return this.entries.get(id) ?? null;
818
+ }
819
+ /**
820
+ * Find the most recent entry whose originalHash matches.
821
+ *
822
+ * @example
823
+ * const entry = store.getByHash("sha256-abc123");
824
+ */
825
+ getByHash(hash) {
826
+ for (const entry of [...this.entries.values()].reverse()) {
827
+ if (entry.originalHash === hash) return entry;
828
+ }
829
+ return null;
830
+ }
831
+ /**
832
+ * Return all stored entries, newest first, up to `limit`.
833
+ *
834
+ * @example
835
+ * store.list(10) // last 10 entries
836
+ */
837
+ list(limit = 50) {
838
+ const all = [...this.entries.values()].reverse();
839
+ return all.slice(0, limit);
840
+ }
841
+ /**
842
+ * Delete an entry by ID. Returns true if it existed.
843
+ */
844
+ delete(id) {
845
+ return this.entries.delete(id);
846
+ }
847
+ /**
848
+ * Remove all entries from memory.
849
+ */
850
+ clear() {
851
+ this.entries.clear();
852
+ }
853
+ /**
854
+ * Write current in-memory state to the JSON file.
855
+ */
856
+ persist() {
857
+ const dir = dirname(this.filePath);
858
+ if (!existsSync(dir)) {
859
+ mkdirSync(dir, { recursive: true });
860
+ }
861
+ const data = JSON.stringify([...this.entries.values()], null, 2);
862
+ writeFileSync(this.filePath, data, "utf-8");
863
+ }
864
+ /**
865
+ * Load entries from the JSON file into memory.
866
+ * No-ops if the file doesn't exist.
867
+ */
868
+ load() {
869
+ if (!existsSync(this.filePath)) return;
870
+ try {
871
+ const raw = readFileSync(this.filePath, "utf-8");
872
+ const parsed = JSON.parse(raw);
873
+ this.entries.clear();
874
+ for (const entry of parsed) {
875
+ this.entries.set(entry.id, entry);
876
+ }
877
+ } catch {
878
+ this.entries.clear();
879
+ }
880
+ }
881
+ /** Total entries in memory */
882
+ get size() {
883
+ return this.entries.size;
884
+ }
885
+ /**
886
+ * Reconstruct a MutationMap from a stored entry.
887
+ *
888
+ * @example
889
+ * const map = store.toMap(entry);
890
+ * reverse(mutatedCode, map);
891
+ */
892
+ static toMap(entry) {
893
+ return new Map(entry.mapEntries);
894
+ }
895
+ /**
896
+ * Serialize a MutationMap to the format stored in StoreEntry.
897
+ */
898
+ static fromMap(map) {
899
+ return [...map.entries()];
900
+ }
901
+ };
902
+
903
+ // src/config.ts
904
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
905
+ import { join, resolve } from "path";
906
+ var CONFIG_DIR_NAME = ".pretense";
907
+ var CONFIG_FILE = "config.json";
908
+ var MUTATION_MAP_FILE = "mutation-map.json";
909
+ var AUDIT_LOG_FILE = "audit.log";
910
+ var USAGE_FILE = "usage.json";
911
+ function getConfigDir(baseDir) {
912
+ const base = baseDir ?? process.cwd();
913
+ return resolve(base, CONFIG_DIR_NAME);
914
+ }
915
+ function initConfig(dir) {
916
+ const configDir = getConfigDir(dir);
917
+ if (!existsSync2(configDir)) {
918
+ mkdirSync2(configDir, { recursive: true });
919
+ }
920
+ const configPath = join(configDir, CONFIG_FILE);
921
+ if (!existsSync2(configPath)) {
922
+ const config = {
923
+ ...DEFAULT_CONFIG,
924
+ storePath: configDir
925
+ };
926
+ writeFileSync2(configPath, JSON.stringify(config, null, 2), "utf-8");
927
+ }
928
+ const mapPath = join(configDir, MUTATION_MAP_FILE);
929
+ if (!existsSync2(mapPath)) {
930
+ writeFileSync2(mapPath, "[]", "utf-8");
931
+ }
932
+ const auditPath = join(configDir, AUDIT_LOG_FILE);
933
+ if (!existsSync2(auditPath)) {
934
+ writeFileSync2(auditPath, "", "utf-8");
935
+ }
936
+ const usagePath = join(configDir, USAGE_FILE);
937
+ if (!existsSync2(usagePath)) {
938
+ writeFileSync2(
939
+ usagePath,
940
+ JSON.stringify({ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), mutations: 0, firstUseDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }, null, 2),
941
+ "utf-8"
942
+ );
943
+ }
944
+ return configDir;
945
+ }
946
+ function loadConfig(dir) {
947
+ const configDir = getConfigDir(dir);
948
+ const configPath = join(configDir, CONFIG_FILE);
949
+ if (!existsSync2(configPath)) {
950
+ return { ...DEFAULT_CONFIG };
951
+ }
952
+ try {
953
+ const raw = readFileSync2(configPath, "utf-8");
954
+ const parsed = JSON.parse(raw);
955
+ return { ...DEFAULT_CONFIG, ...parsed };
956
+ } catch {
957
+ return { ...DEFAULT_CONFIG };
958
+ }
959
+ }
960
+ function loadUsage(dir) {
961
+ const configDir = getConfigDir(dir);
962
+ const usagePath = join(configDir, USAGE_FILE);
963
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
964
+ if (!existsSync2(usagePath)) {
965
+ return { date: today, mutations: 0, firstUseDate: today };
966
+ }
967
+ try {
968
+ const raw = readFileSync2(usagePath, "utf-8");
969
+ const data = JSON.parse(raw);
970
+ if (data.date !== today) {
971
+ return { date: today, mutations: 0, firstUseDate: data.firstUseDate ?? today };
972
+ }
973
+ return data;
974
+ } catch {
975
+ return { date: today, mutations: 0, firstUseDate: today };
976
+ }
977
+ }
978
+ function saveUsage(usage, dir) {
979
+ const configDir = getConfigDir(dir);
980
+ if (!existsSync2(configDir)) {
981
+ mkdirSync2(configDir, { recursive: true });
982
+ }
983
+ const usagePath = join(configDir, USAGE_FILE);
984
+ writeFileSync2(usagePath, JSON.stringify(usage, null, 2), "utf-8");
985
+ }
986
+ function detectTier() {
987
+ const key = process.env["PRETENSE_LICENSE_KEY"] ?? "";
988
+ if (key.startsWith("pre_ent_")) return "enterprise";
989
+ if (key.startsWith("pre_pro_")) return "pro";
990
+ return "free";
991
+ }
992
+ function getTierLimits(tier) {
993
+ switch (tier) {
994
+ case "enterprise":
995
+ return { dailyMutations: Infinity, auditRetentionDays: Infinity };
996
+ case "pro":
997
+ return { dailyMutations: Infinity, auditRetentionDays: 90 };
998
+ case "free":
999
+ default:
1000
+ return { dailyMutations: 500, auditRetentionDays: 3 };
1001
+ }
1002
+ }
1003
+
1004
+ // src/audit.ts
1005
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync3 } from "fs";
1006
+ import { join as join2, dirname as dirname2 } from "path";
1007
+ function writeAuditEntry(entry, dir) {
1008
+ const configDir = getConfigDir(dir);
1009
+ const auditPath = join2(configDir, "audit.log");
1010
+ const logDir = dirname2(auditPath);
1011
+ if (!existsSync3(logDir)) {
1012
+ mkdirSync3(logDir, { recursive: true });
1013
+ }
1014
+ const line = JSON.stringify(entry) + "\n";
1015
+ appendFileSync(auditPath, line, "utf-8");
1016
+ }
1017
+ function createAuditEntry(sessionId, file, identifiersMutated, secretsBlocked, llmProvider, latencyMs) {
1018
+ return {
1019
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1020
+ sessionId,
1021
+ file,
1022
+ identifiersMutated,
1023
+ secretsBlocked,
1024
+ llmProvider,
1025
+ latencyMs
1026
+ };
1027
+ }
1028
+ function readAuditLog(options = {}) {
1029
+ const { limit, dir } = options;
1030
+ const configDir = getConfigDir(dir);
1031
+ const auditPath = join2(configDir, "audit.log");
1032
+ if (!existsSync3(auditPath)) {
1033
+ return [];
1034
+ }
1035
+ const raw = readFileSync3(auditPath, "utf-8");
1036
+ const lines = raw.trim().split("\n").filter(Boolean);
1037
+ let entries = [];
1038
+ for (const line of lines) {
1039
+ try {
1040
+ entries.push(JSON.parse(line));
1041
+ } catch {
1042
+ }
1043
+ }
1044
+ const tier = detectTier();
1045
+ const { auditRetentionDays } = getTierLimits(tier);
1046
+ if (auditRetentionDays !== Infinity) {
1047
+ const cutoff = Date.now() - auditRetentionDays * 24 * 60 * 60 * 1e3;
1048
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() >= cutoff);
1049
+ }
1050
+ entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1051
+ if (limit && limit > 0) {
1052
+ entries = entries.slice(0, limit);
1053
+ }
1054
+ return entries;
1055
+ }
1056
+ function formatJson(entries) {
1057
+ return JSON.stringify(entries, null, 2);
1058
+ }
1059
+ function formatCsv(entries) {
1060
+ const headers = "timestamp,sessionId,file,identifiersMutated,secretsBlocked,llmProvider,latencyMs";
1061
+ const rows = entries.map(
1062
+ (e) => [
1063
+ e.timestamp,
1064
+ e.sessionId,
1065
+ `"${e.file.replace(/"/g, '""')}"`,
1066
+ e.identifiersMutated,
1067
+ e.secretsBlocked,
1068
+ e.llmProvider,
1069
+ e.latencyMs
1070
+ ].join(",")
1071
+ );
1072
+ return [headers, ...rows].join("\n");
1073
+ }
1074
+ function formatText(entries) {
1075
+ if (entries.length === 0) return "No audit entries found.";
1076
+ const lines = entries.map((e) => {
1077
+ const date = new Date(e.timestamp).toLocaleString();
1078
+ return `[${date}] ${e.file} | ${e.identifiersMutated} mutated | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
1079
+ });
1080
+ return lines.join("\n");
1081
+ }
1082
+ function formatAudit(entries, format = "text") {
1083
+ switch (format) {
1084
+ case "json":
1085
+ return formatJson(entries);
1086
+ case "csv":
1087
+ return formatCsv(entries);
1088
+ case "text":
1089
+ default:
1090
+ return formatText(entries);
1091
+ }
1092
+ }
1093
+
1094
+ // src/proxy.ts
1095
+ import { Hono } from "hono";
1096
+ import { serve } from "@hono/node-server";
1097
+ import { nanoid } from "nanoid";
1098
+ function detectUpstream(path) {
1099
+ if (path.includes("/v1/messages")) return "https://api.anthropic.com";
1100
+ if (path.includes("/v1/chat/completions") || path.includes("/v1/completions")) return "https://api.openai.com";
1101
+ if (path.includes("/v1beta/") || path.includes("/v1/models")) return "https://generativelanguage.googleapis.com";
1102
+ return "https://api.openai.com";
1103
+ }
1104
+ function detectProvider(path) {
1105
+ if (path.includes("/v1/messages")) return "anthropic";
1106
+ if (path.includes("/v1/chat/completions") || path.includes("/v1/completions")) return "openai";
1107
+ if (path.includes("/v1beta/") || path.includes("/v1/models")) return "google";
1108
+ return "openai";
1109
+ }
1110
+ function extractText(body) {
1111
+ const parts = [];
1112
+ if (typeof body["system"] === "string") parts.push(body["system"]);
1113
+ if (Array.isArray(body["messages"])) {
1114
+ for (const msg of body["messages"]) {
1115
+ if (typeof msg["content"] === "string") parts.push(msg["content"]);
1116
+ else if (Array.isArray(msg["content"])) {
1117
+ for (const block of msg["content"]) {
1118
+ if (block["type"] === "text" && typeof block["text"] === "string") parts.push(block["text"]);
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ return parts.join("\n");
1124
+ }
1125
+ function extractCodeBlocks(text) {
1126
+ const blocks = [];
1127
+ const re = /```(\w*)\n([\s\S]*?)```/g;
1128
+ let m;
1129
+ while ((m = re.exec(text)) !== null) {
1130
+ blocks.push({
1131
+ code: m[2] ?? "",
1132
+ language: m[1] ?? "typescript",
1133
+ start: m.index,
1134
+ end: m.index + m[0].length
1135
+ });
1136
+ }
1137
+ return blocks;
1138
+ }
1139
+ function replaceCodeBlocks(text, blocks, replacements) {
1140
+ let result = text;
1141
+ for (let i = blocks.length - 1; i >= 0; i--) {
1142
+ const block = blocks[i];
1143
+ const replacement = replacements[i] ?? block.code;
1144
+ const lang = block.language ?? "typescript";
1145
+ result = result.slice(0, block.start) + "```" + lang + "\n" + replacement + "```" + result.slice(block.end);
1146
+ }
1147
+ return result;
1148
+ }
1149
+ function applyToBody(body, transform) {
1150
+ const mutateField = (val) => {
1151
+ if (typeof val === "string") return transform(val);
1152
+ if (Array.isArray(val)) return val.map(mutateField);
1153
+ if (val && typeof val === "object") {
1154
+ const obj = val;
1155
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, mutateField(v)]));
1156
+ }
1157
+ return val;
1158
+ };
1159
+ return mutateField(body);
1160
+ }
1161
+ var stats = {
1162
+ requestsProcessed: 0,
1163
+ totalTokensMutated: 0,
1164
+ totalSecretsBlocked: 0,
1165
+ startedAt: Date.now()
1166
+ };
1167
+ function daysSinceFirstUse() {
1168
+ const usage = loadUsage();
1169
+ if (!usage.firstUseDate) return 0;
1170
+ const first = new Date(usage.firstUseDate).getTime();
1171
+ return Math.floor((Date.now() - first) / (24 * 60 * 60 * 1e3));
1172
+ }
1173
+ function printUpgradeNudge(reason) {
1174
+ process.stdout.write(
1175
+ `
1176
+ \x1B[33m[PRETENSE] ${reason}\x1B[0m
1177
+ \x1B[33m[PRETENSE] Upgrade to Pro: https://pretense.ai/pricing\x1B[0m
1178
+
1179
+ `
1180
+ );
1181
+ }
1182
+ function createProxy(opts = {}) {
1183
+ const config = { ...DEFAULT_CONFIG, ...opts.config };
1184
+ const store = opts.store ?? new MutationStore(`${config.storePath}/proxy-store.json`);
1185
+ const verbose = opts.verbose ?? false;
1186
+ const app = new Hono();
1187
+ app.get(
1188
+ "/health",
1189
+ (c) => c.json({
1190
+ status: "ok",
1191
+ version: "0.3.0",
1192
+ uptime: Math.floor((Date.now() - stats.startedAt) / 1e3),
1193
+ stats: {
1194
+ requestsProcessed: stats.requestsProcessed,
1195
+ totalTokensMutated: stats.totalTokensMutated
1196
+ }
1197
+ })
1198
+ );
1199
+ app.get(
1200
+ "/stats",
1201
+ (c) => c.json({
1202
+ ...stats,
1203
+ storeSize: store.size
1204
+ })
1205
+ );
1206
+ app.get("/audit", (c) => {
1207
+ const limit = parseInt(c.req.query("limit") ?? "50");
1208
+ return c.json(store.list(limit));
1209
+ });
1210
+ app.all("/*", async (c) => {
1211
+ if (c.req.method !== "POST") {
1212
+ const upstream2 = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
1213
+ const url = upstream2 + c.req.path;
1214
+ const headers2 = {};
1215
+ c.req.raw.headers.forEach((v, k) => {
1216
+ headers2[k] = v;
1217
+ });
1218
+ delete headers2["host"];
1219
+ headers2["host"] = new URL(upstream2).host;
1220
+ const resp = await fetch(url, { method: c.req.method, headers: headers2 });
1221
+ return new Response(resp.body, { status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
1222
+ }
1223
+ const requestStart = performance.now();
1224
+ let body;
1225
+ try {
1226
+ body = await c.req.json();
1227
+ } catch {
1228
+ return c.json({ error: { type: "invalid_request", message: "Invalid JSON body" } }, 400);
1229
+ }
1230
+ if (!body || Object.keys(body).length === 0) {
1231
+ return c.json({ error: { type: "invalid_request", message: "Empty request body" } }, 400);
1232
+ }
1233
+ const requestId = nanoid(12);
1234
+ const upstream = c.req.header("x-pretense-upstream") ?? detectUpstream(c.req.path);
1235
+ const provider = detectProvider(c.req.path);
1236
+ const tier = detectTier();
1237
+ const { dailyMutations } = getTierLimits(tier);
1238
+ const usage = loadUsage();
1239
+ if (tier === "free" && usage.mutations >= dailyMutations) {
1240
+ return c.json(
1241
+ {
1242
+ error: {
1243
+ type: "pretense_rate_limit",
1244
+ message: `Daily mutation limit reached (${dailyMutations}/day). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
1245
+ }
1246
+ },
1247
+ 429
1248
+ );
1249
+ }
1250
+ const fullText = extractText(body);
1251
+ const secretScan = scan2(fullText);
1252
+ const blockedSecrets = secretScan.matches.filter((m) => m.action === "block");
1253
+ if (blockedSecrets.length > 0) {
1254
+ stats.totalSecretsBlocked += blockedSecrets.length;
1255
+ if (tier === "free") {
1256
+ printUpgradeNudge(`${blockedSecrets.length} secret(s) blocked. Pro tier includes real-time secret alerts via Slack/PagerDuty.`);
1257
+ }
1258
+ return c.json(
1259
+ {
1260
+ error: {
1261
+ type: "pretense_blocked",
1262
+ message: `Request blocked: ${blockedSecrets.length} secret(s) detected. Types: ${[...new Set(blockedSecrets.map((m) => m.type))].join(", ")}`
1263
+ }
1264
+ },
1265
+ 400
1266
+ );
1267
+ }
1268
+ const processedBody = applyToBody(body, (text) => applyRedactions(text, secretScan.matches));
1269
+ const mutationMap = /* @__PURE__ */ new Map();
1270
+ let tokensMutated = 0;
1271
+ const mutatedBody = applyToBody(processedBody, (text) => {
1272
+ const blocks = extractCodeBlocks(text);
1273
+ if (blocks.length === 0) return text;
1274
+ const mutatedBlocks = [];
1275
+ for (const block of blocks) {
1276
+ const result = mutate(block.code, block.language);
1277
+ for (const [k, v] of result.map.entries()) mutationMap.set(k, v);
1278
+ tokensMutated += result.stats.tokensMutated;
1279
+ mutatedBlocks.push(result.mutatedCode);
1280
+ }
1281
+ return replaceCodeBlocks(text, blocks, mutatedBlocks);
1282
+ });
1283
+ usage.mutations += tokensMutated;
1284
+ saveUsage(usage);
1285
+ if (tier === "free" && usage.mutations >= 400 && usage.mutations - tokensMutated < 400) {
1286
+ printUpgradeNudge(`${usage.mutations}/${dailyMutations} daily mutations used. Running low!`);
1287
+ }
1288
+ if (tier === "free" && daysSinceFirstUse() >= 7) {
1289
+ printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
1290
+ }
1291
+ const entry = {
1292
+ id: requestId,
1293
+ originalHash: requestId,
1294
+ mapEntries: [...mutationMap.entries()],
1295
+ timestamp: Date.now(),
1296
+ language: "mixed"
1297
+ };
1298
+ store.save(entry);
1299
+ stats.requestsProcessed++;
1300
+ stats.totalTokensMutated += tokensMutated;
1301
+ const latencyMs = Math.round((performance.now() - requestStart) * 100) / 100;
1302
+ writeAuditEntry(
1303
+ createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
1304
+ );
1305
+ if (verbose && tokensMutated > 0) {
1306
+ process.stdout.write(`[PRETENSE] ${tokensMutated} tokens mutated for request ${requestId}
1307
+ `);
1308
+ }
1309
+ const headers = {};
1310
+ c.req.raw.headers.forEach((v, k) => {
1311
+ headers[k] = v;
1312
+ });
1313
+ delete headers["host"];
1314
+ delete headers["content-length"];
1315
+ headers["host"] = new URL(upstream).host;
1316
+ headers["x-pretense-request-id"] = requestId;
1317
+ const forwardBody = JSON.stringify(mutatedBody);
1318
+ let upstreamResp;
1319
+ try {
1320
+ upstreamResp = await fetch(upstream + c.req.path, {
1321
+ method: "POST",
1322
+ headers: { ...headers, "content-type": "application/json", "content-length": String(Buffer.byteLength(forwardBody)) },
1323
+ body: forwardBody
1324
+ });
1325
+ } catch {
1326
+ return c.json({ error: { type: "pretense_upstream_error", message: "Failed to reach upstream provider" } }, 502);
1327
+ }
1328
+ if (body["stream"] === true) {
1329
+ return new Response(upstreamResp.body, {
1330
+ status: upstreamResp.status,
1331
+ headers: {
1332
+ "content-type": upstreamResp.headers.get("content-type") ?? "text/event-stream",
1333
+ "cache-control": "no-cache",
1334
+ "x-pretense-request-id": requestId,
1335
+ "x-pretense-protected": "true",
1336
+ "x-pretense-mutations": String(tokensMutated)
1337
+ }
1338
+ });
1339
+ }
1340
+ const reverseMap = new Map(mutationMap);
1341
+ let respBody;
1342
+ try {
1343
+ respBody = await upstreamResp.json();
1344
+ } catch {
1345
+ return new Response(null, {
1346
+ status: upstreamResp.status,
1347
+ headers: {
1348
+ "x-pretense-protected": "true",
1349
+ "x-pretense-mutations": String(tokensMutated)
1350
+ }
1351
+ });
1352
+ }
1353
+ const reversedBody = applyToBody(respBody, (text) => {
1354
+ const blocks = extractCodeBlocks(text);
1355
+ if (blocks.length === 0) return reverse(text, reverseMap);
1356
+ const reversedBlocks = blocks.map((b) => reverse(b.code, reverseMap));
1357
+ return replaceCodeBlocks(text, blocks, reversedBlocks);
1358
+ });
1359
+ c.header("x-pretense-request-id", requestId);
1360
+ c.header("x-pretense-protected", "true");
1361
+ c.header("x-pretense-mutations", String(tokensMutated));
1362
+ return c.json(reversedBody, upstreamResp.status);
1363
+ });
1364
+ return app;
1365
+ }
1366
+ function startProxy(opts = {}) {
1367
+ const port = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
1368
+ const app = createProxy(opts);
1369
+ const tier = detectTier();
1370
+ serve({ fetch: app.fetch, port }, () => {
1371
+ process.stdout.write(`
1372
+ \x1B[36m\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
1373
+ \u2502 \u2502
1374
+ \u2502 \x1B[1mPretense\x1B[0m\x1B[36m v0.3.0 [${tier.toUpperCase()}]${" ".repeat(20 - tier.length)}\u2502
1375
+ \u2502 Your code hits AI naked. We fix that. \u2502
1376
+ \u2502 \u2502
1377
+ \u2502 Proxy: http://localhost:${port} \u2502
1378
+ \u2502 Health: http://localhost:${port}/health \u2502
1379
+ \u2502 Stats: http://localhost:${port}/stats \u2502
1380
+ \u2502 \u2502
1381
+ \u2502 ANTHROPIC_BASE_URL=http://localhost:${port} \u2502
1382
+ \u2502 OPENAI_BASE_URL=http://localhost:${port}/v1 \u2502
1383
+ \u2502 \u2502
1384
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\x1B[0m
1385
+ `);
1386
+ });
1387
+ }
1388
+
1389
+ // src/index.ts
1390
+ var VERSION = "0.3.0";
1391
+ function langFromExt(filePath) {
1392
+ const ext = extname(filePath).toLowerCase();
1393
+ const map = {
1394
+ ".ts": "typescript",
1395
+ ".tsx": "typescript",
1396
+ ".js": "javascript",
1397
+ ".jsx": "javascript",
1398
+ ".mjs": "javascript",
1399
+ ".cjs": "javascript",
1400
+ ".py": "python",
1401
+ ".go": "go",
1402
+ ".java": "java"
1403
+ };
1404
+ return map[ext] ?? "typescript";
1405
+ }
1406
+ function collectFiles(target) {
1407
+ const abs = resolve2(target);
1408
+ if (!existsSync4(abs)) {
1409
+ return [];
1410
+ }
1411
+ const stat = statSync(abs);
1412
+ if (stat.isFile()) return [abs];
1413
+ const files = [];
1414
+ const supportedExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java"]);
1415
+ function walk(dir) {
1416
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1417
+ const fullPath = join3(dir, entry.name);
1418
+ if (entry.isDirectory()) {
1419
+ if (["node_modules", ".git", "dist", "build", ".pretense", ".next", "__pycache__"].includes(entry.name)) continue;
1420
+ walk(fullPath);
1421
+ } else if (entry.isFile() && supportedExts.has(extname(entry.name).toLowerCase())) {
1422
+ files.push(fullPath);
1423
+ }
1424
+ }
1425
+ }
1426
+ walk(abs);
1427
+ return files;
1428
+ }
1429
+ var program = new Command();
1430
+ program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION);
1431
+ program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
1432
+ const dir = resolve2(opts.dir);
1433
+ console.log(chalk.cyan("Initializing Pretense in"), chalk.bold(dir));
1434
+ const configDir = initConfig(dir);
1435
+ const files = collectFiles(dir);
1436
+ const langCounts = {};
1437
+ for (const f of files) {
1438
+ const lang = langFromExt(f);
1439
+ langCounts[lang] = (langCounts[lang] ?? 0) + 1;
1440
+ }
1441
+ console.log(chalk.green("\n .pretense/ created at"), chalk.bold(configDir));
1442
+ console.log(chalk.gray(`
1443
+ Scanned ${files.length} source files:`));
1444
+ for (const [lang, count] of Object.entries(langCounts)) {
1445
+ console.log(chalk.gray(` ${lang}: ${count} files`));
1446
+ }
1447
+ console.log(chalk.cyan("\n Next: run"), chalk.bold("pretense start"), chalk.cyan("to launch the proxy"));
1448
+ console.log();
1449
+ });
1450
+ program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action((opts) => {
1451
+ const config = loadConfig();
1452
+ const port = parseInt(opts.port) || config.port;
1453
+ startProxy({ config, port, verbose: opts.verbose });
1454
+ });
1455
+ program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).action((target, opts) => {
1456
+ const files = collectFiles(target);
1457
+ if (files.length === 0) {
1458
+ console.error(chalk.red("No source files found at"), chalk.bold(target));
1459
+ process.exit(1);
1460
+ }
1461
+ let totalIdentifiers = 0;
1462
+ let totalSecrets = 0;
1463
+ const allResults = [];
1464
+ for (const file of files) {
1465
+ const code = readFileSync4(file, "utf-8");
1466
+ const lang = langFromExt(file);
1467
+ let identifiers = 0;
1468
+ if (!opts.secretsOnly) {
1469
+ const scanResult = scan(code, lang);
1470
+ identifiers = scanResult.tokens.length;
1471
+ totalIdentifiers += identifiers;
1472
+ }
1473
+ const secretResult = scan2(code);
1474
+ const blocked = secretResult.matches.filter((m) => m.action === "block" || m.action === "redact");
1475
+ totalSecrets += blocked.length;
1476
+ allResults.push({
1477
+ file,
1478
+ identifiers,
1479
+ secrets: blocked.length,
1480
+ secretTypes: blocked.map((m) => m.type)
1481
+ });
1482
+ }
1483
+ if (opts.json) {
1484
+ console.log(JSON.stringify({ files: allResults, totalIdentifiers, totalSecrets }, null, 2));
1485
+ } else {
1486
+ console.log(chalk.cyan("\nPretense Scan Results\n"));
1487
+ for (const r of allResults) {
1488
+ const secretBadge = r.secrets > 0 ? chalk.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
1489
+ console.log(
1490
+ chalk.gray(" ") + chalk.white(r.file) + chalk.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
1491
+ );
1492
+ if (r.secrets > 0) {
1493
+ for (const t of r.secretTypes) {
1494
+ console.log(chalk.red(` ! ${t}`));
1495
+ }
1496
+ }
1497
+ }
1498
+ console.log(chalk.gray(`
1499
+ Total: ${files.length} files, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk.red(`${totalSecrets} secrets found`) : chalk.green("0 secrets")));
1500
+ console.log();
1501
+ }
1502
+ if (totalSecrets > 0) {
1503
+ process.exit(2);
1504
+ }
1505
+ });
1506
+ program.command("mutate <file>").description("Mutate a file and print result to stdout").option("-s, --seed <seed>", "Mutation seed", "pretense").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
1507
+ const absPath = resolve2(file);
1508
+ if (!existsSync4(absPath)) {
1509
+ console.error(chalk.red("File not found:"), chalk.bold(absPath));
1510
+ process.exit(1);
1511
+ }
1512
+ const code = readFileSync4(absPath, "utf-8");
1513
+ const lang = langFromExt(absPath);
1514
+ const result = mutate(code, lang, opts.seed);
1515
+ if (opts.save) {
1516
+ const configDir = getConfigDir();
1517
+ const store = new MutationStore(join3(configDir, "mutation-map.json"));
1518
+ store.load();
1519
+ store.save({
1520
+ id: absPath,
1521
+ originalHash: absPath,
1522
+ mapEntries: MutationStore.fromMap(result.map),
1523
+ timestamp: Date.now(),
1524
+ language: lang
1525
+ });
1526
+ store.persist();
1527
+ process.stderr.write(chalk.gray(`Mutation map saved to ${configDir}/mutation-map.json
1528
+ `));
1529
+ }
1530
+ if (opts.json) {
1531
+ console.log(JSON.stringify({
1532
+ mutatedCode: result.mutatedCode,
1533
+ map: Object.fromEntries(result.map),
1534
+ stats: result.stats
1535
+ }, null, 2));
1536
+ } else {
1537
+ process.stdout.write(result.mutatedCode);
1538
+ process.stderr.write(chalk.gray(`
1539
+ --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
1540
+ `));
1541
+ }
1542
+ });
1543
+ program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
1544
+ const absPath = resolve2(file);
1545
+ if (!existsSync4(absPath)) {
1546
+ console.error(chalk.red("File not found:"), chalk.bold(absPath));
1547
+ process.exit(1);
1548
+ }
1549
+ const mutatedCode = readFileSync4(absPath, "utf-8");
1550
+ const mapPath = opts.map ? resolve2(opts.map) : join3(getConfigDir(), "mutation-map.json");
1551
+ if (!existsSync4(mapPath)) {
1552
+ console.error(chalk.red("Mutation map not found:"), chalk.bold(mapPath));
1553
+ console.error(chalk.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
1554
+ process.exit(1);
1555
+ }
1556
+ const store = new MutationStore(mapPath);
1557
+ store.load();
1558
+ const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
1559
+ if (!entry) {
1560
+ console.error(chalk.red("No mutation map found for this file."));
1561
+ process.exit(1);
1562
+ }
1563
+ const map = MutationStore.toMap(entry);
1564
+ const restored = reverse(mutatedCode, map);
1565
+ process.stdout.write(restored);
1566
+ process.stderr.write(chalk.gray(`
1567
+ --- Reversed ${map.size} mutations ---
1568
+ `));
1569
+ });
1570
+ 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) => {
1571
+ const format = opts.json ? "json" : opts.csv ? "csv" : "text";
1572
+ const limit = parseInt(opts.limit) || 50;
1573
+ const entries = readAuditLog({ limit, format });
1574
+ const output = formatAudit(entries, format);
1575
+ console.log(output);
1576
+ });
1577
+ program.command("version").description("Print Pretense CLI version").action(() => {
1578
+ console.log(`@pretense/cli v${VERSION}`);
1579
+ });
1580
+ program.parse();
1581
+ //# sourceMappingURL=index.js.map