@pretense/cli 0.6.13 → 0.6.15

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/README.md CHANGED
@@ -35,6 +35,10 @@ pretending.
35
35
  docs-exec:skip "curl -fsSL https://www.pretense.ai/install.sh | sh" — downloads and installs a binary from the network
36
36
  docs-exec:skip "pretense start" — long-running server; it would outlive the suite holding port 9339
37
37
  docs-exec:skip "pretense start --port 9500" — long-running server; it would outlive the suite holding port 9500
38
+ docs-exec:skip `pretense run claude "explain this repo"` — starts a proxy and a third-party tool that would outlive the suite
39
+ docs-exec:skip "pretense run cursor" — starts a proxy and a third-party tool that would outlive the suite
40
+ docs-exec:skip `pretense run gemini "summarise the changes"` — starts a proxy and a third-party tool that would outlive the suite
41
+ docs-exec:skip "pretense run --provider openai mytool" — starts a proxy and a third-party tool that would outlive the suite
38
42
  docs-exec:skip "curl -s http://localhost:9339/health" — needs the proxy from the preceding line, which cannot be started here
39
43
  docs-exec:skip "claude" — third-party CLI that is not a dependency of this repo
40
44
  docs-exec:skip `curl -fsSLO "${BASE}/SHA256SUMS"` — network fetch from the release host
@@ -103,20 +107,30 @@ binary has no Node requirement.
103
107
 
104
108
  ```bash
105
109
  pretense scan <file|dir> # what would leak
106
- pretense mutate <file> --map m.json -i # swap it before the AI sees it
107
- pretense reverse <file> --map m.json -i # put the real values back
110
+ pretense mutate <file> # swap secrets in place, keep a reversal map (auto)
111
+ pretense reverse <file> # put the real values back (finds the map itself)
108
112
  pretense start # proxy mode (npm install only, see below)
109
113
  pretense scan <file> --policy hipaa # the flag most people type
110
114
  ```
111
115
 
116
+ `mutate <file>` then `reverse <file>` is a byte-exact round trip with **no
117
+ flags**: `mutate` writes an encrypted reversal map to
118
+ `~/.pretense/maps/<hash>.json` (derived from the file's absolute path, mode
119
+ 0600), and `reverse` recomputes that same location and finds it. To run any AI
120
+ tool through the mutating proxy with zero setup, use `pretense run` (see
121
+ [Zero-export proxy runner](#zero-export-proxy-runner)).
122
+
112
123
  Two honest caveats about the verbs:
113
124
 
114
125
  - `mutate` and `reverse` take **one file at a time**. `pretense mutate .` and
115
126
  `pretense mutate src` both fail with `Cannot read file: .` and exit 1.
116
127
  Directory mutation is not available yet. `scan` does accept a directory.
117
- - `mutate` **previews by default** it prints to stdout and leaves the file
118
- untouched. `-i` writes in place. `-i` without `--map` is irreversible; there
119
- is no global map store to recover from.
128
+ - `mutate` **mutates in place by default** and auto-writes the reversal map, so
129
+ the round trip is reversible with no `--map` on either side. Use `--stdout` to
130
+ preview to stdout without touching the file, `-o <path>` to write a copy, and
131
+ `--map <path>` to override where the map is written. The map lives under
132
+ `~/.pretense/maps`, deliberately out of the working tree so it is never
133
+ committed by accident.
120
134
 
121
135
  ## The 30-second walkthrough
122
136
 
@@ -273,6 +287,28 @@ claude
273
287
 
274
288
  `OPENAI_BASE_URL` works the same way, as does any client that accepts a base URL.
275
289
 
290
+ ## Zero-export proxy runner
291
+
292
+ The manual dance above — start a proxy, remember the port, export the *right*
293
+ base-URL variable for your tool, remember to stop it — is what `pretense run`
294
+ does for you. It ensures a healthy proxy exists (reusing a running one, else
295
+ starting one on an auto-found free port), sets the correct base-URL env var, runs
296
+ your tool, and stops the proxy it started when the tool exits.
297
+
298
+ ```bash
299
+ pretense run claude "explain this repo" # Claude → ANTHROPIC_BASE_URL
300
+ pretense run cursor # Cursor / OpenAI-compatible → OPENAI_BASE_URL
301
+ pretense run gemini "summarise the changes" # Gemini → GEMINI_BASE_URL
302
+ pretense run --provider openai mytool # force the provider mapping
303
+ ```
304
+
305
+ `pretense with` is an alias for `pretense run`. The tool name selects the
306
+ provider automatically (`claude` → Anthropic, `cursor`/`code` → OpenAI-compatible,
307
+ `gemini` → Google); pass `--provider anthropic|openai|google` to override it, and
308
+ `--port <n>` to prefer a port when a proxy has to be started. If the tool is not
309
+ on your `PATH`, `run` prints an install hint and exits non-zero instead of
310
+ crashing — and it never tears down a proxy it did not start.
311
+
276
312
  Read this before you rely on the proxy:
277
313
 
278
314
  - **npm install only.** `start` crashes on the `curl | sh` binary
@@ -383,8 +419,10 @@ pretense init # 0 — writes pretense.yaml + .prete
383
419
  pretense scan <file|dir> # 0 clean / 1 findings
384
420
  pretense scan <file> --policy <preset> # 0 clean / 1 findings
385
421
  pretense scan <file> --severity high # 0 clean / 1 findings
386
- pretense mutate <file> # 0 — preview to stdout, file untouched
387
- pretense mutate <file> --map m.json -i # 0 — in place, reversible
422
+ pretense mutate <file> --stdout # 0 — preview to stdout, file untouched
423
+ pretense mutate <file> # 0 — in place + auto-map (the default)
424
+ pretense reverse <file> # 0 — in place, auto-finds the map
425
+ pretense mutate <file> --map m.json -i # 0 — explicit map-path override
388
426
  pretense reverse <file> --map m.json -i # 0 — byte-exact restore
389
427
  pretense status # 0
390
428
  pretense audit --limit 20 # 0
@@ -451,9 +489,13 @@ has edited will not reverse today. Token-substitution reversal is on the roadmap
451
489
 
452
490
  ## Where the reversal map lives
453
491
 
454
- In the CLI path, a map exists only where you ask for one, via
455
- `pretense mutate --map <path>`. It is AES-256-GCM encrypted and written mode
456
- `0600`.
492
+ In the CLI path, `pretense mutate <file>` auto-writes the map to a deterministic
493
+ per-file location `~/.pretense/maps/<sha256(absolute-path)>.json` and
494
+ `pretense reverse <file>` recomputes that same path to find it, so neither verb
495
+ needs `--map`. Pass `pretense mutate --map <path>` to override the location. The
496
+ map is AES-256-GCM encrypted and written mode `0600`, and it lives under
497
+ `~/.pretense/maps` (not in the working tree) so it is never committed by
498
+ accident.
457
499
 
458
500
  In the proxy path the reverse map is held in memory for the life of the request
459
501
  and is never written to disk — deliberately, so Pretense never becomes an
@@ -17,7 +17,7 @@ function versionFromPackageJson() {
17
17
  return void 0;
18
18
  }
19
19
  }
20
- var PRETENSE_VERSION = (true ? "0.6.13" : void 0) ?? versionFromPackageJson() ?? "0.0.0-unknown";
20
+ var PRETENSE_VERSION = (true ? "0.6.15" : void 0) ?? versionFromPackageJson() ?? "0.0.0-unknown";
21
21
 
22
22
  export {
23
23
  PRETENSE_VERSION
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PRETENSE_VERSION
4
- } from "./chunk-HWSWJ4EC.js";
4
+ } from "./chunk-AULCU5QN.js";
5
5
  import {
6
6
  __commonJS,
7
7
  __toESM,
@@ -221,8 +221,8 @@ var require_brace_expansion = __commonJS({
221
221
 
222
222
  // src/index.ts
223
223
  init_esm_shims();
224
- import { Command as Command21 } from "commander";
225
- import chalk20 from "chalk";
224
+ import { Command as Command22 } from "commander";
225
+ import chalk21 from "chalk";
226
226
 
227
227
  // src/commands/init.ts
228
228
  init_esm_shims();
@@ -16445,11 +16445,11 @@ function versionFromCliPackageJson() {
16445
16445
  return void 0;
16446
16446
  }
16447
16447
  function productVersion() {
16448
- return override ?? (true ? "0.6.13" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
16448
+ return override ?? (true ? "0.6.15" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
16449
16449
  }
16450
16450
  var UNKNOWN_COMMIT = "unknown";
16451
16451
  function bakedCommitSha() {
16452
- return "a36bf5d80a91cfd8fbf57e6c5a6a9b9114826309".length > 0 ? "a36bf5d80a91cfd8fbf57e6c5a6a9b9114826309" : UNKNOWN_COMMIT;
16452
+ return "538df7d688ce6d0c9f2b39578c555bb4b8281a55".length > 0 ? "538df7d688ce6d0c9f2b39578c555bb4b8281a55" : UNKNOWN_COMMIT;
16453
16453
  }
16454
16454
  var FEATURE_TIERS = {
16455
16455
  compliance_export: "pro",
@@ -17901,7 +17901,10 @@ function startProxy(opts = {}) {
17901
17901
  function renderClientEnvLines(port, protocol) {
17902
17902
  const base = `${protocol}://localhost:${port}`;
17903
17903
  return [
17904
- " Point your AI client at Pretense:",
17904
+ " Easiest \u2014 let Pretense wire the env var and run the tool for you:",
17905
+ ' pretense run claude "build me X" # or: cursor, gemini, any tool',
17906
+ "",
17907
+ " Or point your AI client at Pretense yourself:",
17905
17908
  ` ANTHROPIC_BASE_URL=${base}`,
17906
17909
  ` OPENAI_BASE_URL=${base}/v1`,
17907
17910
  ` GOOGLE_GEMINI_BASE_URL=${base}`
@@ -17931,6 +17934,33 @@ if (isMain) {
17931
17934
  startProxy();
17932
17935
  }
17933
17936
 
17937
+ // src/free-port.ts
17938
+ init_esm_shims();
17939
+ import net from "net";
17940
+ function isPortFree(port) {
17941
+ return new Promise((resolve7) => {
17942
+ const srv = net.createServer();
17943
+ srv.once("error", () => {
17944
+ srv.close(() => resolve7(false));
17945
+ resolve7(false);
17946
+ });
17947
+ srv.once("listening", () => {
17948
+ srv.close(() => resolve7(true));
17949
+ });
17950
+ srv.listen(port);
17951
+ });
17952
+ }
17953
+ async function findFreePort(start, opts = {}) {
17954
+ const maxTries = opts.maxTries ?? 100;
17955
+ const end = Math.min(start + maxTries - 1, 65535);
17956
+ for (let p = start; p <= end; p += 1) {
17957
+ if (await isPortFree(p)) return p;
17958
+ }
17959
+ throw new Error(
17960
+ `No free port found in [${start}, ${end}] after ${maxTries} attempts.`
17961
+ );
17962
+ }
17963
+
17934
17964
  // src/proxy-pidfile.ts
17935
17965
  init_esm_shims();
17936
17966
  import { existsSync as existsSync7, readFileSync as readFileSync6, unlinkSync } from "fs";
@@ -18054,15 +18084,15 @@ function explainAddrInUse(port) {
18054
18084
  );
18055
18085
  }
18056
18086
  function startCommand() {
18057
- return new Command2("start").description("Start the Pretense proxy server").option("-p, --port <port>", "Port to listen on", "9339").option("--verbose", "Enable verbose logging", false).option("-c, --config <path>", "Path to pretense.yaml").action((opts) => {
18058
- const port = parseInt(opts.port, 10);
18087
+ return new Command2("start").description("Start the Pretense proxy server").option("-p, --port <port>", "Port to listen on", "9339").option("--verbose", "Enable verbose logging", false).option("-c, --config <path>", "Path to pretense.yaml").option("--strict-port", "Fail if the requested port is busy instead of auto-advancing", false).action(async (opts) => {
18088
+ const requested = parseInt(opts.port, 10);
18059
18089
  const existing = readProxyRecord();
18060
18090
  if (existing) {
18061
18091
  if (isProcessAlive(existing.pid)) {
18062
- if (existing.port === port) {
18092
+ if (existing.port === requested) {
18063
18093
  console.error(
18064
18094
  chalk2.red(`
18065
- x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) + chalk2.dim("\n Stop it with ") + chalk2.cyan("pretense stop") + chalk2.dim(", or start this one on another port with ") + chalk2.cyan(`--port ${port + 1}`) + "\n"
18095
+ x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) + chalk2.dim("\n Stop it with ") + chalk2.cyan("pretense stop") + chalk2.dim(", or start this one on another port with ") + chalk2.cyan(`--port ${requested + 1}`) + "\n"
18066
18096
  );
18067
18097
  process.exit(1);
18068
18098
  }
@@ -18070,6 +18100,25 @@ function startCommand() {
18070
18100
  removeProxyRecord();
18071
18101
  }
18072
18102
  }
18103
+ let port = requested;
18104
+ if (!opts.strictPort) {
18105
+ try {
18106
+ port = await findFreePort(requested);
18107
+ } catch (err) {
18108
+ console.error(chalk2.red(`
18109
+ x ${err.message}
18110
+ `));
18111
+ process.exit(1);
18112
+ }
18113
+ if (port !== requested) {
18114
+ console.error(
18115
+ chalk2.yellow(`
18116
+ ! Port ${requested} is busy \u2014 starting Pretense on port ${port} instead.`) + chalk2.dim(`
18117
+ Pass --strict-port to fail instead of auto-advancing.
18118
+ `)
18119
+ );
18120
+ }
18121
+ }
18073
18122
  process.on("uncaughtException", (err) => {
18074
18123
  if (err?.code === "EADDRINUSE") {
18075
18124
  const rec = readProxyRecord();
@@ -20570,11 +20619,13 @@ function quickstartCommand() {
20570
20619
  }
20571
20620
  console.log(chalk13.dim(" [4/4] Done.\n"));
20572
20621
  console.log(chalk13.bold(" Next steps:\n"));
20573
- console.log(` ${chalk13.green("1.")} ${chalk13.cyan("pretense start")} Start proxy on localhost:9339`);
20574
- console.log(` ${chalk13.green("2.")} ${chalk13.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
20575
- console.log(` ${chalk13.green("3.")} Set your LLM client to use the proxy:`);
20622
+ console.log(` ${chalk13.green("1.")} ${chalk13.cyan('pretense run claude "build me X"')} Run any AI tool through the proxy \u2014 zero setup`);
20623
+ console.log(` ${chalk13.dim("(auto-starts the proxy on a free port and sets the base-URL env var for you)")}`);
20624
+ console.log(` ${chalk13.green("2.")} ${chalk13.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
20625
+ console.log(` ${chalk13.green("3.")} ${chalk13.dim("Prefer to wire it by hand? Start the proxy and set the base URL yourself:")}`);
20626
+ console.log(` ${chalk13.dim("pretense start")}`);
20576
20627
  console.log(` ${chalk13.dim("ANTHROPIC_BASE_URL=http://localhost:9339 claude")}`);
20577
- console.log(` ${chalk13.dim("OPENAI_BASE_URL=http://localhost:9339/v1 openai ...")}`);
20628
+ console.log(` ${chalk13.dim("OPENAI_BASE_URL=http://localhost:9339/v1 cursor ...")}`);
20578
20629
  console.log(` ${chalk13.dim("GOOGLE_GEMINI_BASE_URL=http://localhost:9339 gemini ...")}
20579
20630
  `);
20580
20631
  console.log(chalk13.bold.green(" Protected. Happy coding.\n"));
@@ -20586,7 +20637,7 @@ init_esm_shims();
20586
20637
  import { Command as Command15, Option as Option2 } from "commander";
20587
20638
  import chalk14 from "chalk";
20588
20639
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, chmodSync as chmodSync4, existsSync as existsSync15 } from "fs";
20589
- import { createHash as createHash2 } from "crypto";
20640
+ import { createHash as createHash3 } from "crypto";
20590
20641
  import { resolve as resolve5, extname as extname4 } from "path";
20591
20642
 
20592
20643
  // src/secret-crypto.ts
@@ -20694,6 +20745,22 @@ function writeFileAtomicSync(targetPath, data, opts = {}) {
20694
20745
  }
20695
20746
  }
20696
20747
 
20748
+ // src/default-map.ts
20749
+ init_esm_shims();
20750
+ import { createHash as createHash2 } from "crypto";
20751
+ import { join as join20 } from "path";
20752
+ function mapsDir(override2) {
20753
+ return override2 ?? join20(pretenseHome(), "maps");
20754
+ }
20755
+ function defaultMapPath(absFilePath, override2) {
20756
+ const digest = createHash2("sha256").update(absFilePath, "utf8").digest("hex");
20757
+ return join20(mapsDir(override2), `${digest}.json`);
20758
+ }
20759
+ function ensureDefaultMapPath(absFilePath, override2) {
20760
+ ensureDirOwnerOnly(mapsDir(override2));
20761
+ return defaultMapPath(absFilePath, override2);
20762
+ }
20763
+
20697
20764
  // src/commands/mutate.ts
20698
20765
  var REVERSAL_MAP_VERSION = 3;
20699
20766
  function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
@@ -20758,7 +20825,7 @@ function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
20758
20825
  };
20759
20826
  }
20760
20827
  function sha256Hex2(s) {
20761
- return createHash2("sha256").update(s, "utf8").digest("hex");
20828
+ return createHash3("sha256").update(s, "utf8").digest("hex");
20762
20829
  }
20763
20830
  var L2_LANGUAGE_BY_EXT2 = {
20764
20831
  ".ts": "typescript",
@@ -20863,6 +20930,8 @@ async function runMutate(file, opts = {}) {
20863
20930
  );
20864
20931
  return 1;
20865
20932
  }
20933
+ const preview = opts.stdout === true;
20934
+ const inPlace = !preview && !opts.output;
20866
20935
  const { findings, unmutatable } = await collectMutationFindings(
20867
20936
  content,
20868
20937
  file,
@@ -20875,16 +20944,24 @@ async function runMutate(file, opts = {}) {
20875
20944
  } else {
20876
20945
  reportUnmutatable(unmutatable);
20877
20946
  }
20878
- if (!opts.inPlace && !opts.output) process.stdout.write(content);
20947
+ if (preview) process.stdout.write(content);
20879
20948
  return 0;
20880
20949
  }
20881
20950
  reportUnmutatable(unmutatable);
20882
20951
  const absFile = resolve5(file);
20883
20952
  const projectRoot = findProjectRoot(absFile);
20884
20953
  const result = mutateSecrets(content, findings, { projectRoot, keysDir: opts.keysDir });
20885
- if (opts.map) {
20954
+ let effectiveMap;
20955
+ if (preview) {
20956
+ effectiveMap = void 0;
20957
+ } else if (inPlace) {
20958
+ effectiveMap = opts.map ?? ensureDefaultMapPath(absFile, opts.mapsDir);
20959
+ } else {
20960
+ effectiveMap = opts.map;
20961
+ }
20962
+ if (effectiveMap) {
20886
20963
  const contentSha = sha256Hex2(content);
20887
- const pre = preflightReversalMap(opts.map, absFile, content, contentSha, opts.forceMap === true);
20964
+ const pre = preflightReversalMap(effectiveMap, absFile, content, contentSha, opts.forceMap === true);
20888
20965
  if (pre.action === "refuse") {
20889
20966
  console.error(
20890
20967
  chalk14.red(`
@@ -20899,7 +20976,7 @@ async function runMutate(file, opts = {}) {
20899
20976
  const isNoop = pre.action === "keep" && pre.expectedMutatedSha === mutatedSha;
20900
20977
  if (isNoop) {
20901
20978
  console.error(
20902
- chalk14.dim(` = Reversal map ${opts.map} already describes ${file} at this exact content`) + chalk14.dim(`
20979
+ chalk14.dim(` = Reversal map ${effectiveMap} already describes ${file} at this exact content`) + chalk14.dim(`
20903
20980
  hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) + chalk14.dim(`
20904
20981
  byte-identical).`)
20905
20982
  );
@@ -20922,7 +20999,7 @@ async function runMutate(file, opts = {}) {
20922
20999
  type: m.type
20923
21000
  }))
20924
21001
  };
20925
- const written = writeReversalMap(opts.map, mapFile);
21002
+ const written = writeReversalMap(effectiveMap, mapFile);
20926
21003
  if (written !== 0) return written;
20927
21004
  if (pre.action === "write" && pre.generation > 1) {
20928
21005
  console.error(
@@ -20936,18 +21013,17 @@ async function runMutate(file, opts = {}) {
20936
21013
  }
20937
21014
  }
20938
21015
  }
20939
- if (opts.inPlace && !opts.map) {
21016
+ if (inPlace && !effectiveMap) {
20940
21017
  console.error(
20941
- chalk14.yellow(`
20942
- ! IRREVERSIBLE: overwriting ${file} in place without a reversal map.`) + chalk14.dim(`
20943
- Synthetic values are one-way \u2014 this cannot be undone and no copy of`) + chalk14.dim(`
20944
- the original secrets is kept. Re-run with --map <path> to be able to`) + chalk14.dim(`
20945
- restore them later with \`pretense reverse\`.
21018
+ chalk14.red(`
21019
+ x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk14.dim(`
21020
+ Pass --map <path>, or report this as a bug.
20946
21021
  `)
20947
21022
  );
21023
+ return 1;
20948
21024
  }
20949
21025
  try {
20950
- if (opts.inPlace) {
21026
+ if (inPlace) {
20951
21027
  writeFileAtomicSync(file, result.content);
20952
21028
  console.error(chalk14.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
20953
21029
  } else if (opts.output) {
@@ -20972,8 +21048,11 @@ async function runMutate(file, opts = {}) {
20972
21048
  }
20973
21049
  function mutateCommand() {
20974
21050
  return new Command15("mutate").description(
20975
- "Preview mutated secrets on stdout (add -i to write back in place; pair with --map to keep it reversible)"
20976
- ).argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file (irreversible unless --map is also given)").option("-o, --output <path>", "Write mutated content to a specific file").option("--map <path>", "Write the encrypted reversal map (required to ever run `pretense reverse`)").option(
21051
+ "Mutate secrets IN PLACE and auto-write an encrypted reversal map (reverse with `pretense reverse <file>`; --stdout to preview instead)"
21052
+ ).argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file (this is the default; kept for compatibility)").option("-o, --output <path>", "Write mutated content to a specific file instead of in place").option("--stdout", "Preview mutated content on stdout and write NOTHING (no file, no map)").option(
21053
+ "--map <path>",
21054
+ "Override where the encrypted reversal map is written (default: ~/.pretense/maps/<hash>.json)"
21055
+ ).option(
20977
21056
  "--force-map",
20978
21057
  "Overwrite an existing reversal map that still covers recoverable data (DESTRUCTIVE: the map it replaces may be the only way back to the real secrets)"
20979
21058
  ).option("--show-map", "Print mutation map JSON to stderr").addOption(
@@ -21005,9 +21084,9 @@ import { Command as Command16 } from "commander";
21005
21084
  import chalk15 from "chalk";
21006
21085
  import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync6 } from "fs";
21007
21086
  import { resolve as resolve6 } from "path";
21008
- import { createHash as createHash3 } from "crypto";
21087
+ import { createHash as createHash4 } from "crypto";
21009
21088
  function sha256Hex3(s) {
21010
- return createHash3("sha256").update(s, "utf8").digest("hex");
21089
+ return createHash4("sha256").update(s, "utf8").digest("hex");
21011
21090
  }
21012
21091
  var SYNTHETIC_MARKERS = [
21013
21092
  /pretense_mut_u[0-9a-z]{6,}:pretense_mut_p[0-9a-z]{6,}@/,
@@ -21063,22 +21142,25 @@ function loadReversalMap(mapPath) {
21063
21142
  }
21064
21143
  function runReverse(file, opts = {}) {
21065
21144
  const absPath = resolve6(file);
21066
- if (!opts.map) {
21067
- console.error(
21068
- chalk15.red(`
21069
- x reverse requires a reversal map: --map <file>`) + chalk15.dim(`
21070
- Produce one with: ${chalk15.cyan(`pretense mutate ${file} -i --map <file>`)}
21071
- `)
21072
- );
21073
- return 1;
21074
- }
21075
21145
  if (!existsSync16(absPath) || !statSync6(absPath).isFile()) {
21076
21146
  console.error(chalk15.red(`
21077
21147
  x File not found: ${file}
21078
21148
  `));
21079
21149
  return 1;
21080
21150
  }
21081
- const mapFile = loadReversalMap(opts.map);
21151
+ const mapPath = opts.map ?? defaultMapPath(absPath, opts.mapsDir);
21152
+ if (!opts.map && !existsSync16(mapPath)) {
21153
+ console.error(
21154
+ chalk15.red(`
21155
+ x No reversal map found for ${absPath}`) + chalk15.dim(`
21156
+ Looked in the default location: ${mapPath}`) + chalk15.dim(`
21157
+ Mutate the file first with ${chalk15.cyan(`pretense mutate ${file}`)}, or pass`) + chalk15.dim(`
21158
+ an explicit ${chalk15.cyan("--map <path>")} if you wrote the map somewhere else.
21159
+ `)
21160
+ );
21161
+ return 1;
21162
+ }
21163
+ const mapFile = loadReversalMap(mapPath);
21082
21164
  let plainMutations;
21083
21165
  if (mapFile.encrypted) {
21084
21166
  const projectRoot = findProjectRoot(mapFile.file ?? absPath);
@@ -21184,7 +21266,9 @@ function runReverse(file, opts = {}) {
21184
21266
  }
21185
21267
  const mark = lineage.ok ? chalk15.green(" \u2713 Restored") : chalk15.yellow(" ! Partially restored");
21186
21268
  const detail = `${appliedCount} of ${plainMutations.length} mapped tokens` + (lineage.ok ? "" : " \u2014 to an earlier MUTATED generation, not the original");
21187
- if (opts.inPlace) {
21269
+ const preview = opts.stdout === true;
21270
+ const inPlace = !preview && !opts.output;
21271
+ if (inPlace) {
21188
21272
  writeFileAtomicSync(absPath, restored);
21189
21273
  console.error(`${mark} ${absPath} (${detail}, in-place)`);
21190
21274
  } else if (opts.output) {
@@ -21211,8 +21295,11 @@ function runReverse(file, opts = {}) {
21211
21295
  }
21212
21296
  function reverseCommand() {
21213
21297
  return new Command16("reverse").description(
21214
- "Restore a mutated file to its EXACT original bytes using an encrypted reversal map (fails if the file was edited after mutation)"
21215
- ).argument("<file>", "Mutated file to restore (must be byte-identical to the mutate output)").requiredOption("--map <path>", "Reversal map produced by `pretense mutate --map <path>`").option("-i, --in-place", "Write the restored content back to the input file").option("-o, --output <path>", "Write restored content to a specific file").option("--json", "Emit a JSON summary to stderr").option(
21298
+ "Restore a mutated file to its EXACT original bytes IN PLACE, auto-finding its reversal map (fails if the file was edited after mutation)"
21299
+ ).argument("<file>", "Mutated file to restore (must be byte-identical to the mutate output)").option(
21300
+ "--map <path>",
21301
+ "Override the reversal map to use (default: the one `pretense mutate <file>` auto-wrote)"
21302
+ ).option("-i, --in-place", "Write the restored content back to the input file (this is the default)").option("-o, --output <path>", "Write restored content to a specific file instead of in place").option("--stdout", "Preview restored content on stdout instead of writing the file").option("--json", "Emit a JSON summary to stderr").option(
21216
21303
  "--allow-partial",
21217
21304
  "Apply a map whose originals are themselves synthetics (writes the PREVIOUS MUTATED generation, not the original)"
21218
21305
  ).action((file, opts) => {
@@ -21220,10 +21307,191 @@ function reverseCommand() {
21220
21307
  });
21221
21308
  }
21222
21309
 
21223
- // src/commands/policy.ts
21310
+ // src/commands/run.ts
21224
21311
  init_esm_shims();
21225
21312
  import { Command as Command17 } from "commander";
21226
21313
  import chalk16 from "chalk";
21314
+ import { spawn } from "child_process";
21315
+ import { basename as basename3 } from "path";
21316
+ import { fileURLToPath as fileURLToPath4 } from "url";
21317
+ var TOOL_PROVIDER = {
21318
+ claude: "anthropic",
21319
+ "claude-code": "anthropic",
21320
+ anthropic: "anthropic",
21321
+ cursor: "openai",
21322
+ code: "openai",
21323
+ codex: "openai",
21324
+ openai: "openai",
21325
+ aider: "openai",
21326
+ gemini: "google",
21327
+ google: "google"
21328
+ };
21329
+ var PROVIDER_ALIASES = {
21330
+ anthropic: "anthropic",
21331
+ claude: "anthropic",
21332
+ openai: "openai",
21333
+ "openai-compatible": "openai",
21334
+ cursor: "openai",
21335
+ google: "google",
21336
+ gemini: "google"
21337
+ };
21338
+ function resolveProvider(tool, override2) {
21339
+ if (override2 !== void 0) {
21340
+ const p = PROVIDER_ALIASES[override2.toLowerCase()];
21341
+ if (!p) {
21342
+ throw new Error(
21343
+ `Unknown --provider "${override2}". Expected one of: anthropic, openai, google.`
21344
+ );
21345
+ }
21346
+ return p;
21347
+ }
21348
+ return TOOL_PROVIDER[basename3(tool).toLowerCase()] ?? "anthropic";
21349
+ }
21350
+ function injectedEnvFor(provider, port) {
21351
+ const base = `http://localhost:${port}`;
21352
+ switch (provider) {
21353
+ case "anthropic":
21354
+ return { ANTHROPIC_BASE_URL: base };
21355
+ case "openai":
21356
+ return { OPENAI_BASE_URL: `${base}/v1` };
21357
+ case "google":
21358
+ return { GEMINI_BASE_URL: base, GOOGLE_GEMINI_BASE_URL: base };
21359
+ }
21360
+ }
21361
+ function primaryEnvVar(provider) {
21362
+ return provider === "anthropic" ? "ANTHROPIC_BASE_URL" : provider === "openai" ? "OPENAI_BASE_URL" : "GEMINI_BASE_URL";
21363
+ }
21364
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
21365
+ async function isProxyHealthy(port, timeoutMs = 800) {
21366
+ const ctrl = new AbortController();
21367
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
21368
+ try {
21369
+ const res = await fetch(`http://localhost:${port}/health`, { signal: ctrl.signal });
21370
+ return res.ok;
21371
+ } catch {
21372
+ return false;
21373
+ } finally {
21374
+ clearTimeout(t);
21375
+ }
21376
+ }
21377
+ async function waitHealthy(port, deadlineMs, pollMs = 150) {
21378
+ const deadline = Date.now() + deadlineMs;
21379
+ while (Date.now() < deadline) {
21380
+ if (await isProxyHealthy(port)) return true;
21381
+ await sleep2(pollMs);
21382
+ }
21383
+ return false;
21384
+ }
21385
+ function spawnProxy(port) {
21386
+ const cliEntry = process.argv[1] ?? fileURLToPath4(import.meta.url);
21387
+ return spawn(process.execPath, [cliEntry, "start", "--port", String(port), "--strict-port"], {
21388
+ stdio: ["ignore", "ignore", "pipe"],
21389
+ env: process.env
21390
+ });
21391
+ }
21392
+ function runCommand() {
21393
+ return new Command17("run").alias("with").description(
21394
+ "Run an AI tool THROUGH the Pretense proxy with zero setup \u2014 auto-starts the proxy, sets the right base-URL env var, and stops the proxy it started when the tool exits"
21395
+ ).argument("<tool>", "The AI CLI to run (claude, cursor, gemini, \u2026)").argument("[args...]", "Arguments passed through to <tool>").option("--provider <name>", "Force the provider mapping: anthropic | openai | google").option("-p, --port <port>", "Preferred proxy port when starting one (default 9339)").option("--strict-port", "Fail instead of auto-advancing when starting a proxy on a busy port", false).passThroughOptions().allowUnknownOption().action(async (tool, args, opts) => {
21396
+ let provider;
21397
+ try {
21398
+ provider = resolveProvider(tool, opts.provider);
21399
+ } catch (err) {
21400
+ console.error(chalk16.red(`
21401
+ x ${err.message}
21402
+ `));
21403
+ process.exit(1);
21404
+ }
21405
+ let port;
21406
+ let proxyChild;
21407
+ let startedByUs = false;
21408
+ const rec = readProxyRecord();
21409
+ if (rec && isProcessAlive(rec.pid) && await isProxyHealthy(rec.port)) {
21410
+ port = rec.port;
21411
+ console.error(chalk16.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
21412
+ } else {
21413
+ const preferred = opts.port ? parseInt(opts.port, 10) : 9339;
21414
+ try {
21415
+ port = await findFreePort(preferred);
21416
+ } catch (err) {
21417
+ console.error(chalk16.red(`
21418
+ x ${err.message}
21419
+ `));
21420
+ process.exit(1);
21421
+ }
21422
+ proxyChild = spawnProxy(port);
21423
+ startedByUs = true;
21424
+ let proxyStderr = "";
21425
+ proxyChild.stderr?.on("data", (d) => {
21426
+ proxyStderr += d.toString();
21427
+ });
21428
+ const ready = await waitHealthy(port, 15e3);
21429
+ if (!ready) {
21430
+ console.error(
21431
+ chalk16.red(`
21432
+ x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk16.dim(`
21433
+ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
21434
+ );
21435
+ proxyChild.kill("SIGTERM");
21436
+ process.exit(1);
21437
+ }
21438
+ console.error(chalk16.green(` \u2713 Started Pretense proxy on port ${port}.`));
21439
+ }
21440
+ const stopStartedProxy = () => {
21441
+ if (startedByUs && proxyChild && !proxyChild.killed) {
21442
+ proxyChild.kill("SIGTERM");
21443
+ }
21444
+ };
21445
+ const inject = injectedEnvFor(provider, port);
21446
+ console.error(
21447
+ chalk16.dim(
21448
+ ` \u2192 Routing ${basename3(tool)} through the proxy (${primaryEnvVar(provider)}=http://localhost:${port}).
21449
+ `
21450
+ )
21451
+ );
21452
+ const child = spawn(tool, args, {
21453
+ stdio: "inherit",
21454
+ env: { ...process.env, ...inject }
21455
+ });
21456
+ const forward = (sig) => () => {
21457
+ if (!child.killed) child.kill(sig);
21458
+ };
21459
+ const onInt = forward("SIGINT");
21460
+ const onTerm = forward("SIGTERM");
21461
+ process.on("SIGINT", onInt);
21462
+ process.on("SIGTERM", onTerm);
21463
+ child.on("error", (err) => {
21464
+ if (err.code === "ENOENT") {
21465
+ console.error(
21466
+ chalk16.red(`
21467
+ x "${tool}" is not on your PATH.`) + chalk16.dim(`
21468
+ Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) + chalk16.dim(`
21469
+ You can still point a client at the proxy yourself:`) + chalk16.dim(`
21470
+ ${primaryEnvVar(provider)}=http://localhost:${port}
21471
+ `)
21472
+ );
21473
+ } else {
21474
+ console.error(chalk16.red(`
21475
+ x Could not run "${tool}": ${err.message}
21476
+ `));
21477
+ }
21478
+ stopStartedProxy();
21479
+ process.exit(err.code === "ENOENT" ? 127 : 1);
21480
+ });
21481
+ child.on("exit", (code, signal) => {
21482
+ stopStartedProxy();
21483
+ process.removeListener("SIGINT", onInt);
21484
+ process.removeListener("SIGTERM", onTerm);
21485
+ if (typeof code === "number") process.exit(code);
21486
+ process.exit(signal ? 1 : 0);
21487
+ });
21488
+ });
21489
+ }
21490
+
21491
+ // src/commands/policy.ts
21492
+ init_esm_shims();
21493
+ import { Command as Command18 } from "commander";
21494
+ import chalk17 from "chalk";
21227
21495
  function listAction(opts) {
21228
21496
  const ids = listPresetIds();
21229
21497
  if (opts.json) {
@@ -21246,21 +21514,21 @@ function listAction(opts) {
21246
21514
  );
21247
21515
  return;
21248
21516
  }
21249
- console.log(chalk16.cyan("\n Pretense Compliance Frameworks\n"));
21517
+ console.log(chalk17.cyan("\n Pretense Compliance Frameworks\n"));
21250
21518
  console.log(
21251
- chalk16.dim(
21519
+ chalk17.dim(
21252
21520
  " " + "ID".padEnd(8) + "NAME".padEnd(14) + "TIER".padEnd(12) + "AUDIT".padEnd(8) + "DETECTORS"
21253
21521
  )
21254
21522
  );
21255
- console.log(chalk16.dim(" " + "\u2500".repeat(64)));
21523
+ console.log(chalk17.dim(" " + "\u2500".repeat(64)));
21256
21524
  for (const id of ids) {
21257
21525
  const p = COMPLIANCE_PRESETS[id];
21258
21526
  console.log(
21259
- " " + chalk16.bold(p.id.padEnd(8)) + p.name.padEnd(14) + chalk16.dim(p.minTier.padEnd(12)) + (p.auditRequired ? chalk16.green("yes".padEnd(8)) : chalk16.dim("no".padEnd(8))) + chalk16.dim(`${Object.keys(p.actionOverrides).length} overrides`)
21527
+ " " + chalk17.bold(p.id.padEnd(8)) + p.name.padEnd(14) + chalk17.dim(p.minTier.padEnd(12)) + (p.auditRequired ? chalk17.green("yes".padEnd(8)) : chalk17.dim("no".padEnd(8))) + chalk17.dim(`${Object.keys(p.actionOverrides).length} overrides`)
21260
21528
  );
21261
21529
  }
21262
- console.log(chalk16.dim(`
21263
- Run ${chalk16.cyan("pretense policy show <id>")} for detector-level detail.
21530
+ console.log(chalk17.dim(`
21531
+ Run ${chalk17.cyan("pretense policy show <id>")} for detector-level detail.
21264
21532
  `));
21265
21533
  }
21266
21534
  function showAction(id, opts) {
@@ -21269,8 +21537,8 @@ function showAction(id, opts) {
21269
21537
  preset = loadPreset(id);
21270
21538
  } catch (err) {
21271
21539
  console.error(
21272
- chalk16.red(`
21273
- x ${err.message}`) + chalk16.dim(`
21540
+ chalk17.red(`
21541
+ x ${err.message}`) + chalk17.dim(`
21274
21542
  Valid frameworks: ${listPresetIds().join(", ")}
21275
21543
  `)
21276
21544
  );
@@ -21280,24 +21548,24 @@ function showAction(id, opts) {
21280
21548
  console.log(JSON.stringify(preset, null, 2));
21281
21549
  return;
21282
21550
  }
21283
- console.log(chalk16.cyan(`
21284
- ${preset.name} `) + chalk16.dim(`(${preset.id})`));
21285
- console.log(chalk16.dim(` ${preset.description}
21551
+ console.log(chalk17.cyan(`
21552
+ ${preset.name} `) + chalk17.dim(`(${preset.id})`));
21553
+ console.log(chalk17.dim(` ${preset.description}
21286
21554
  `));
21287
- console.log(` Minimum tier: ${chalk16.bold(preset.minTier)}`);
21288
- console.log(` Audit required: ${preset.auditRequired ? chalk16.green("yes") : chalk16.dim("no")}`);
21289
- console.log(` Detector sets: ${chalk16.dim(preset.detectorSets.join(", "))}
21555
+ console.log(` Minimum tier: ${chalk17.bold(preset.minTier)}`);
21556
+ console.log(` Audit required: ${preset.auditRequired ? chalk17.green("yes") : chalk17.dim("no")}`);
21557
+ console.log(` Detector sets: ${chalk17.dim(preset.detectorSets.join(", "))}
21290
21558
  `);
21291
21559
  const overrides = Object.entries(preset.actionOverrides);
21292
- console.log(chalk16.dim(` Detector actions (${overrides.length}):`));
21560
+ console.log(chalk17.dim(` Detector actions (${overrides.length}):`));
21293
21561
  for (const [detector, action] of overrides) {
21294
- const color = action === "block" ? chalk16.red : action === "redact" ? chalk16.yellow : chalk16.dim;
21562
+ const color = action === "block" ? chalk17.red : action === "redact" ? chalk17.yellow : chalk17.dim;
21295
21563
  console.log(` ${detector.padEnd(24)} ${color(action)}`);
21296
21564
  }
21297
21565
  console.log();
21298
21566
  }
21299
21567
  function policyCommand() {
21300
- const cmd = new Command17("policy").description(
21568
+ const cmd = new Command18("policy").description(
21301
21569
  "List and inspect the built-in compliance presets (5: hipaa, gdpr, soc2, nist, pci)"
21302
21570
  );
21303
21571
  cmd.command("list").description("List the 5 built-in compliance presets").option("--json", "Output as JSON").action((opts) => listAction(opts));
@@ -21308,10 +21576,10 @@ function policyCommand() {
21308
21576
 
21309
21577
  // src/commands/audit.ts
21310
21578
  init_esm_shims();
21311
- import { Command as Command18 } from "commander";
21312
- import chalk17 from "chalk";
21579
+ import { Command as Command19 } from "commander";
21580
+ import chalk18 from "chalk";
21313
21581
  import { homedir as homedir10 } from "os";
21314
- import { join as join20 } from "path";
21582
+ import { join as join21 } from "path";
21315
21583
  var VALID_FORMATS2 = ["table", "csv", "json", "jsonl"];
21316
21584
  function toSafeAuditRow(e) {
21317
21585
  return {
@@ -21382,18 +21650,18 @@ function resolveFormat2(opts) {
21382
21650
  const raw2 = opts.format?.trim().toLowerCase();
21383
21651
  if (raw2 !== void 0 && !VALID_FORMATS2.includes(raw2)) {
21384
21652
  console.error(
21385
- chalk17.red(`
21653
+ chalk18.red(`
21386
21654
  x Unknown --format: "${opts.format}"
21387
- `) + chalk17.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
21655
+ `) + chalk18.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
21388
21656
  );
21389
21657
  return null;
21390
21658
  }
21391
21659
  const fromFormat = raw2 === void 0 ? void 0 : raw2 === "jsonl" ? "json" : raw2;
21392
21660
  if (opts.json && fromFormat !== void 0 && fromFormat !== "json") {
21393
21661
  console.error(
21394
- chalk17.red(`
21662
+ chalk18.red(`
21395
21663
  x --json and --format ${fromFormat} request different outputs.
21396
- `) + chalk17.dim(" Pick one.\n")
21664
+ `) + chalk18.dim(" Pick one.\n")
21397
21665
  );
21398
21666
  return null;
21399
21667
  }
@@ -21401,7 +21669,7 @@ function resolveFormat2(opts) {
21401
21669
  return opts.json ? "json" : "table";
21402
21670
  }
21403
21671
  function auditCommand() {
21404
- return new Command18("audit").description(
21672
+ return new Command19("audit").description(
21405
21673
  "Show recent mutation/tokenization audit entries (leak-proof; JSONL with --json, RFC 4180 CSV with --format csv)"
21406
21674
  ).option("-l, --limit <n>", "Number of entries to show", "20").option("--json", "Output as JSONL (one JSON object per line, jq-pipeable)", false).option("--db <path>", "Path to the audit database (default: ~/.pretense/audit.db)").option(
21407
21675
  "--framework <name>",
@@ -21421,21 +21689,21 @@ function auditCommand() {
21421
21689
  framework = resolveFrameworkName(opts.framework);
21422
21690
  if (!framework) {
21423
21691
  console.error(
21424
- chalk17.red(`
21692
+ chalk18.red(`
21425
21693
  x Unknown framework: "${opts.framework}"
21426
- `) + chalk17.dim(" Run ") + chalk17.cyan("pretense audit --list-frameworks") + chalk17.dim(" to see all 36.\n")
21694
+ `) + chalk18.dim(" Run ") + chalk18.cyan("pretense audit --list-frameworks") + chalk18.dim(" to see all 36.\n")
21427
21695
  );
21428
21696
  process.exit(1);
21429
21697
  }
21430
21698
  }
21431
21699
  const limit = parseInt(opts.limit ?? "20", 10);
21432
21700
  if (!Number.isFinite(limit) || limit <= 0) {
21433
- console.error(chalk17.red(`
21701
+ console.error(chalk18.red(`
21434
21702
  x Invalid --limit: "${opts.limit}". Provide a positive integer.
21435
21703
  `));
21436
21704
  process.exit(1);
21437
21705
  }
21438
- const dbPath = opts.db ?? join20(homedir10(), ".pretense", "audit.db");
21706
+ const dbPath = opts.db ?? join21(homedir10(), ".pretense", "audit.db");
21439
21707
  let rows;
21440
21708
  try {
21441
21709
  const store = new AuditStore(dbPath);
@@ -21455,8 +21723,8 @@ function auditCommand() {
21455
21723
  console.error("pretense: no audit data \u2014 the audit database has not been initialized yet.");
21456
21724
  return;
21457
21725
  }
21458
- console.log(chalk17.dim("\n No audit data available. The audit database has not been initialized yet."));
21459
- console.log(chalk17.dim(" Run " + chalk17.cyan("pretense start") + " and make some requests first.\n"));
21726
+ console.log(chalk18.dim("\n No audit data available. The audit database has not been initialized yet."));
21727
+ console.log(chalk18.dim(" Run " + chalk18.cyan("pretense start") + " and make some requests first.\n"));
21460
21728
  return;
21461
21729
  }
21462
21730
  if (format === "csv") {
@@ -21468,23 +21736,23 @@ function auditCommand() {
21468
21736
  return;
21469
21737
  }
21470
21738
  if (rows.length === 0) {
21471
- console.log(chalk17.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
21739
+ console.log(chalk18.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
21472
21740
  return;
21473
21741
  }
21474
- console.log(chalk17.cyan(`
21742
+ console.log(chalk18.cyan(`
21475
21743
  Pretense Mutation Audit \u2014 last ${rows.length} entries
21476
21744
  `));
21477
21745
  console.log(
21478
- chalk17.dim(
21746
+ chalk18.dim(
21479
21747
  " " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATED".padEnd(10) + "BLOCKED".padEnd(10) + "PII".padEnd(8) + "MS"
21480
21748
  )
21481
21749
  );
21482
- console.log(chalk17.dim(" " + "\u2500".repeat(72)));
21750
+ console.log(chalk18.dim(" " + "\u2500".repeat(72)));
21483
21751
  for (const row of rows) {
21484
21752
  const time = new Date(row.timestamp).toTimeString().slice(0, 8);
21485
- const actionColor = row.action === "blocked" ? chalk17.red : row.action === "mutated" ? chalk17.green : row.action === "redacted" ? chalk17.yellow : row.action === "error" ? chalk17.red : chalk17.dim;
21753
+ const actionColor = row.action === "blocked" ? chalk18.red : row.action === "mutated" ? chalk18.green : row.action === "redacted" ? chalk18.yellow : row.action === "error" ? chalk18.red : chalk18.dim;
21486
21754
  console.log(
21487
- " " + chalk17.dim(time.padEnd(10)) + actionColor(row.action.padEnd(12)) + chalk17.dim(row.provider.padEnd(14)) + chalk17.green(String(row.mutationsApplied).padEnd(10)) + (row.secretsBlocked > 0 ? chalk17.red(String(row.secretsBlocked).padEnd(10)) : chalk17.dim("0".padEnd(10))) + chalk17.dim(String(row.piiRedacted).padEnd(8)) + chalk17.dim(String(row.scanDurationMs).slice(0, 5))
21755
+ " " + chalk18.dim(time.padEnd(10)) + actionColor(row.action.padEnd(12)) + chalk18.dim(row.provider.padEnd(14)) + chalk18.green(String(row.mutationsApplied).padEnd(10)) + (row.secretsBlocked > 0 ? chalk18.red(String(row.secretsBlocked).padEnd(10)) : chalk18.dim("0".padEnd(10))) + chalk18.dim(String(row.piiRedacted).padEnd(8)) + chalk18.dim(String(row.scanDurationMs).slice(0, 5))
21488
21756
  );
21489
21757
  }
21490
21758
  console.log();
@@ -21493,9 +21761,9 @@ function auditCommand() {
21493
21761
 
21494
21762
  // src/commands/credits.ts
21495
21763
  init_esm_shims();
21496
- import { Command as Command19 } from "commander";
21497
- import chalk18 from "chalk";
21498
- import { join as join21 } from "path";
21764
+ import { Command as Command20 } from "commander";
21765
+ import chalk19 from "chalk";
21766
+ import { join as join24 } from "path";
21499
21767
  import { homedir as homedir11 } from "os";
21500
21768
  function remaining(used, limit) {
21501
21769
  if (limit === null) return null;
@@ -21505,16 +21773,16 @@ function fmt(n) {
21505
21773
  return n === null ? "unlimited" : n.toLocaleString("en-US");
21506
21774
  }
21507
21775
  function bar(used, limit, width = 24) {
21508
- if (limit === null) return chalk18.green("\u2588".repeat(width));
21776
+ if (limit === null) return chalk19.green("\u2588".repeat(width));
21509
21777
  const pct = limit <= 0 ? 1 : Math.min(1, used / limit);
21510
21778
  const filled = Math.min(width, Math.round(pct * width));
21511
21779
  const s = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
21512
- if (pct >= 1) return chalk18.red(s);
21513
- if (pct >= 0.8) return chalk18.yellow(s);
21514
- return chalk18.green(s);
21780
+ if (pct >= 1) return chalk19.red(s);
21781
+ if (pct >= 0.8) return chalk19.yellow(s);
21782
+ return chalk19.green(s);
21515
21783
  }
21516
21784
  function creditsCommand() {
21517
- return new Command19("credits").alias("tokens").description("Show remaining mutation budget for the current period").option("--db <path>", "Path to audit database", join21(homedir11(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
21785
+ return new Command20("credits").alias("tokens").description("Show remaining mutation budget for the current period").option("--db <path>", "Path to audit database", join24(homedir11(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
21518
21786
  const resolution = await resolvePlanTier({ offline: opts.offline });
21519
21787
  const tier = resolution.tier;
21520
21788
  const collected = collectUsage(opts.db);
@@ -21527,7 +21795,7 @@ function creditsCommand() {
21527
21795
  periodEnd: null
21528
21796
  };
21529
21797
  if (!collected && !opts.json) {
21530
- console.log(chalk18.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
21798
+ console.log(chalk19.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
21531
21799
  }
21532
21800
  const mutationLimit = tier ? PLAN_CONFIGS[tier].mutationsPerPeriod : null;
21533
21801
  const scanLimit = tier ? PLAN_CONFIGS[tier].scansPerPeriod : null;
@@ -21559,12 +21827,12 @@ function creditsCommand() {
21559
21827
  );
21560
21828
  return;
21561
21829
  }
21562
- console.log(chalk18.cyan("\n Pretense Credits"));
21563
- console.log(chalk18.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
21830
+ console.log(chalk19.cyan("\n Pretense Credits"));
21831
+ console.log(chalk19.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
21564
21832
  `));
21565
21833
  if (!tier) {
21566
21834
  for (const line of unverifiedNote(resolution.detail)) {
21567
- console.log(chalk18.yellow(` ${line}`));
21835
+ console.log(chalk19.yellow(` ${line}`));
21568
21836
  }
21569
21837
  console.log();
21570
21838
  console.log(" " + "Mutations used".padEnd(20) + m.mutationsApplied.toLocaleString("en-US"));
@@ -21573,18 +21841,18 @@ function creditsCommand() {
21573
21841
  return;
21574
21842
  }
21575
21843
  console.log(
21576
- " " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk18.bold(fmt(mutationsLeft)) + chalk18.dim(` left of ${fmt(mutationLimit)}`)
21844
+ " " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk19.bold(fmt(mutationsLeft)) + chalk19.dim(` left of ${fmt(mutationLimit)}`)
21577
21845
  );
21578
21846
  console.log(
21579
- " " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk18.bold(fmt(scansLeft)) + chalk18.dim(` left of ${fmt(scanLimit)}`)
21847
+ " " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk19.bold(fmt(scansLeft)) + chalk19.dim(` left of ${fmt(scanLimit)}`)
21580
21848
  );
21581
21849
  if (tier !== "enterprise" && mutationLimit !== null && mutationsLeft === 0) {
21582
21850
  console.log(
21583
- chalk18.red("\n Mutation budget exhausted for this period.") + chalk18.dim(" Run ") + chalk18.cyan("pretense upgrade") + chalk18.dim(" to compare plans.")
21851
+ chalk19.red("\n Mutation budget exhausted for this period.") + chalk19.dim(" Run ") + chalk19.cyan("pretense upgrade") + chalk19.dim(" to compare plans.")
21584
21852
  );
21585
21853
  } else if (tier !== "enterprise" && mutationLimit !== null && m.mutationsApplied >= mutationLimit * 0.8) {
21586
21854
  console.log(
21587
- chalk18.yellow("\n Running low.") + chalk18.dim(" Run ") + chalk18.cyan("pretense upgrade") + chalk18.dim(" to compare plans.")
21855
+ chalk19.yellow("\n Running low.") + chalk19.dim(" Run ") + chalk19.cyan("pretense upgrade") + chalk19.dim(" to compare plans.")
21588
21856
  );
21589
21857
  }
21590
21858
  console.log();
@@ -21593,8 +21861,8 @@ function creditsCommand() {
21593
21861
 
21594
21862
  // src/commands/upgrade.ts
21595
21863
  init_esm_shims();
21596
- import { Command as Command20 } from "commander";
21597
- import chalk19 from "chalk";
21864
+ import { Command as Command21 } from "commander";
21865
+ import chalk20 from "chalk";
21598
21866
  var PRICING_URL = "https://pretense.ai/pricing";
21599
21867
  var TIERS = ["free", "pro", "enterprise"];
21600
21868
  function quota(n) {
@@ -21607,7 +21875,7 @@ function yesNo(b) {
21607
21875
  return b ? "yes" : "\u2014";
21608
21876
  }
21609
21877
  function upgradeCommand() {
21610
- return new Command20("upgrade").description("Compare Pretense plans and upgrade").option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
21878
+ return new Command21("upgrade").description("Compare Pretense plans and upgrade").option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
21611
21879
  const resolution = await resolvePlanTier({ offline: opts.offline });
21612
21880
  const current = resolution.tier;
21613
21881
  if (opts.json) {
@@ -21635,11 +21903,11 @@ function upgradeCommand() {
21635
21903
  );
21636
21904
  return;
21637
21905
  }
21638
- console.log(chalk19.cyan("\n Pretense Plans\n"));
21906
+ console.log(chalk20.cyan("\n Pretense Plans\n"));
21639
21907
  const col = 16;
21640
21908
  const header = " " + "".padEnd(18) + TIERS.map((t) => tierName(t).padEnd(col)).join("");
21641
- console.log(chalk19.bold(header));
21642
- console.log(" " + chalk19.dim("\u2500".repeat(18 + col * TIERS.length)));
21909
+ console.log(chalk20.bold(header));
21910
+ console.log(" " + chalk20.dim("\u2500".repeat(18 + col * TIERS.length)));
21643
21911
  const rows = [
21644
21912
  ["Price", (t) => priceLabel(t)],
21645
21913
  ["Mutations/7d", (t) => quota(PLAN_CONFIGS[t].mutationsPerPeriod)],
@@ -21651,24 +21919,24 @@ function upgradeCommand() {
21651
21919
  for (const [label, cell] of rows) {
21652
21920
  console.log(" " + label.padEnd(18) + TIERS.map((t) => cell(t).padEnd(col)).join(""));
21653
21921
  }
21654
- console.log(" " + chalk19.dim("\u2500".repeat(18 + col * TIERS.length)));
21922
+ console.log(" " + chalk20.dim("\u2500".repeat(18 + col * TIERS.length)));
21655
21923
  if (current) {
21656
- console.log(chalk19.dim(`
21924
+ console.log(chalk20.dim(`
21657
21925
  Current plan: ${tierName(current)}`));
21658
21926
  if (current === "enterprise") {
21659
- console.log(chalk19.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
21927
+ console.log(chalk20.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
21660
21928
  return;
21661
21929
  }
21662
21930
  } else {
21663
21931
  console.log(
21664
- chalk19.yellow(
21932
+ chalk20.yellow(
21665
21933
  `
21666
21934
  Could not confirm your current plan: ${resolution.detail ?? "unknown reason"}.`
21667
21935
  )
21668
21936
  );
21669
21937
  }
21670
21938
  const from = current ?? "unknown";
21671
- console.log(chalk19.bold(`
21939
+ console.log(chalk20.bold(`
21672
21940
  \u2192 ${PRICING_URL}?ref=cli_upgrade&from=${from}
21673
21941
  `));
21674
21942
  });
@@ -21701,34 +21969,35 @@ function findClosestCommand(input, knownCommands) {
21701
21969
  }
21702
21970
  return bestDist <= 3 ? best : null;
21703
21971
  }
21704
- var program = new Command21();
21705
- program.name("pretense").description(chalk20.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
21706
- ${chalk20.bold("Quick start:")}
21707
- ${chalk20.cyan("pretense scan")} ${chalk20.dim("<file|dir>")} Scan for secrets and PII
21708
- ${chalk20.cyan("pretense mutate")} ${chalk20.dim("<file>")} Preview code mutations
21709
- ${chalk20.cyan("pretense init")} Scan codebase and build protection profile
21972
+ var program = new Command22();
21973
+ program.name("pretense").enablePositionalOptions().description(chalk21.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
21974
+ ${chalk21.bold("Quick start:")}
21975
+ ${chalk21.cyan("pretense run claude")} ${chalk21.dim('"build X"')} Run an AI tool through the proxy (zero setup)
21976
+ ${chalk21.cyan("pretense mutate")} ${chalk21.dim("<file>")} Mutate secrets in place (reverse with the same path)
21977
+ ${chalk21.cyan("pretense scan")} ${chalk21.dim("<file|dir>")} Scan for secrets and PII
21710
21978
 
21711
- ${chalk20.dim("All commands:")}
21712
- ${chalk20.cyan("pretense init")} Scan codebase and build protection profile
21713
- ${chalk20.cyan("pretense start")} Start proxy on localhost:9339
21714
- ${chalk20.cyan("pretense stop")} Stop the running proxy
21715
- ${chalk20.cyan("pretense scan src/api.ts")} Scan file for secrets
21716
- ${chalk20.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
21717
- ${chalk20.cyan("pretense status")} Show protection status
21718
- ${chalk20.cyan("pretense review")} Preview mutations that would be applied
21719
- ${chalk20.cyan("pretense review --json")} Output review as JSON
21720
- ${chalk20.cyan("pretense mutate f --map m.json")} Mutate secrets + write reversal map
21721
- ${chalk20.cyan("pretense reverse f --map m.json")} Restore a mutated file
21722
- ${chalk20.cyan("pretense policy list")} List the 5 compliance presets
21723
- ${chalk20.cyan("pretense audit")} Show mutation audit log
21724
- ${chalk20.cyan("pretense usage")} Plan usage vs limits
21725
- ${chalk20.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
21726
- ${chalk20.cyan("pretense upgrade")} Compare plans / upgrade
21727
- ${chalk20.cyan("pretense auth login")} Authenticate with Pretense
21728
- ${chalk20.cyan("pretense install")} Install git hooks
21729
- ${chalk20.cyan("pretense logs --limit 20")} Show recent audit entries
21979
+ ${chalk21.dim("All commands:")}
21980
+ ${chalk21.cyan("pretense init")} Scan codebase and build protection profile
21981
+ ${chalk21.cyan("pretense run <tool>")} Run an AI tool through the proxy, no manual export
21982
+ ${chalk21.cyan("pretense start")} Start proxy on localhost:9339
21983
+ ${chalk21.cyan("pretense stop")} Stop the running proxy
21984
+ ${chalk21.cyan("pretense scan src/api.ts")} Scan file for secrets
21985
+ ${chalk21.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
21986
+ ${chalk21.cyan("pretense status")} Show protection status
21987
+ ${chalk21.cyan("pretense review")} Preview mutations that would be applied
21988
+ ${chalk21.cyan("pretense review --json")} Output review as JSON
21989
+ ${chalk21.cyan("pretense mutate <file>")} Mutate secrets in place + auto-write reversal map
21990
+ ${chalk21.cyan("pretense reverse <file>")} Restore a mutated file (auto-finds its map)
21991
+ ${chalk21.cyan("pretense policy list")} List the 5 compliance presets
21992
+ ${chalk21.cyan("pretense audit")} Show mutation audit log
21993
+ ${chalk21.cyan("pretense usage")} Plan usage vs limits
21994
+ ${chalk21.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
21995
+ ${chalk21.cyan("pretense upgrade")} Compare plans / upgrade
21996
+ ${chalk21.cyan("pretense auth login")} Authenticate with Pretense
21997
+ ${chalk21.cyan("pretense install")} Install git hooks
21998
+ ${chalk21.cyan("pretense logs --limit 20")} Show recent audit entries
21730
21999
 
21731
- ${chalk20.dim("Run")} ${chalk20.cyan("pretense <command> --help")} ${chalk20.dim("for command-specific help.")}
22000
+ ${chalk21.dim("Run")} ${chalk21.cyan("pretense <command> --help")} ${chalk21.dim("for command-specific help.")}
21732
22001
  `);
21733
22002
  var KNOWN_COMMANDS = [
21734
22003
  "init",
@@ -21747,6 +22016,8 @@ var KNOWN_COMMANDS = [
21747
22016
  "quickstart",
21748
22017
  "mutate",
21749
22018
  "reverse",
22019
+ "run",
22020
+ "with",
21750
22021
  "policy",
21751
22022
  "audit",
21752
22023
  "version",
@@ -21782,6 +22053,7 @@ program.addCommand(completionCommand());
21782
22053
  program.addCommand(quickstartCommand());
21783
22054
  program.addCommand(mutateCommand());
21784
22055
  program.addCommand(reverseCommand());
22056
+ program.addCommand(runCommand());
21785
22057
  program.addCommand(policyCommand());
21786
22058
  program.addCommand(auditCommand());
21787
22059
  program.addCommand(creditsCommand());
@@ -21789,13 +22061,13 @@ program.addCommand(upgradeCommand());
21789
22061
  program.on("command:*", (operands) => {
21790
22062
  const unknown = operands[0] ?? "";
21791
22063
  const suggestion = findClosestCommand(unknown, KNOWN_COMMANDS);
21792
- console.error(chalk20.red(`
22064
+ console.error(chalk21.red(`
21793
22065
  x Unknown command: "${unknown}"`));
21794
22066
  if (suggestion) {
21795
- console.error(chalk20.yellow(` Did you mean: ${chalk20.bold(`pretense ${suggestion}`)}`));
22067
+ console.error(chalk21.yellow(` Did you mean: ${chalk21.bold(`pretense ${suggestion}`)}`));
21796
22068
  }
21797
- console.error(chalk20.dim(`
21798
- Run ${chalk20.cyan("pretense --help")} to see all available commands.
22069
+ console.error(chalk21.dim(`
22070
+ Run ${chalk21.cyan("pretense --help")} to see all available commands.
21799
22071
  `));
21800
22072
  process.exit(1);
21801
22073
  });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PRETENSE_VERSION
4
- } from "./chunk-HWSWJ4EC.js";
4
+ } from "./chunk-AULCU5QN.js";
5
5
  import {
6
6
  init_esm_shims
7
7
  } from "./chunk-TWMRHZGM.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pretense/cli",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Local-first AI firewall that mutates proprietary code before sending to LLM APIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,14 +23,14 @@
23
23
  "tsx": "^4.0.0",
24
24
  "typescript": "^5.4.0",
25
25
  "vitest": "^1.0.0",
26
- "@pretense/protocol": "0.1.0",
27
- "@pretense/learner": "0.2.0",
26
+ "@pretense/billing": "0.1.0",
28
27
  "@pretense/compliance-engine": "0.1.0",
28
+ "@pretense/learner": "0.2.0",
29
29
  "@pretense/mutator": "0.2.0",
30
- "@pretense/billing": "0.1.0",
31
30
  "@pretense/proxy": "0.1.0",
32
31
  "@pretense/scanner": "0.2.0",
33
32
  "@pretense/scanner-rs": "0.2.0",
33
+ "@pretense/protocol": "0.1.0",
34
34
  "@pretense/store": "0.2.0"
35
35
  },
36
36
  "publishConfig": {