@pretense/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
5
- import chalk9 from "chalk";
4
+ import { Command as Command2 } from "commander";
5
+ import chalk10 from "chalk";
6
6
  import {
7
- readFileSync as readFileSync7,
8
- existsSync as existsSync7,
7
+ readFileSync as readFileSync8,
8
+ existsSync as existsSync8,
9
9
  readdirSync,
10
10
  statSync,
11
11
  openSync,
@@ -13,7 +13,7 @@ import {
13
13
  closeSync
14
14
  } from "fs";
15
15
  import { readFile, stat as fsStat, open as fsOpen } from "fs/promises";
16
- import { resolve as resolve2, extname, join as join5, basename } from "path";
16
+ import { resolve as resolve3, extname, join as join6, basename } from "path";
17
17
  import os from "os";
18
18
 
19
19
  // src/types.ts
@@ -901,6 +901,12 @@ function getMutationSalt() {
901
901
  function buildSaltedSeed(baseSeed) {
902
902
  return `${baseSeed}:${getMutationSalt()}`;
903
903
  }
904
+ function effectiveSeedForMutation(userSeed) {
905
+ if (userSeed === "pretense" || userSeed === "pretest") {
906
+ return buildSaltedSeed("pretense");
907
+ }
908
+ return userSeed;
909
+ }
904
910
 
905
911
  // src/deterministic-id.ts
906
912
  function hash32(str) {
@@ -1517,15 +1523,6 @@ function loadUsage(dir) {
1517
1523
  return { month: currentMonth, mutations: 0, firstUseDate: today };
1518
1524
  }
1519
1525
  }
1520
- function saveUsage(usage, dir) {
1521
- const configDir = getConfigDir(dir);
1522
- if (!existsSync3(configDir)) {
1523
- mkdirSync3(configDir, { recursive: true });
1524
- }
1525
- const usagePath = join2(configDir, USAGE_FILE);
1526
- const checksum = computeUsageChecksum(usage);
1527
- writeFileSync3(usagePath, JSON.stringify({ ...usage, checksum }, null, 2), "utf-8");
1528
- }
1529
1526
  var MIN_LICENSE_KEY_LENGTH = 40;
1530
1527
  var LICENSE_PAYLOAD_REGEX = /^[a-zA-Z0-9]+$/;
1531
1528
  function isValidLicenseKey(key, prefix) {
@@ -1641,7 +1638,7 @@ function formatText(entries) {
1641
1638
  if (entries.length === 0) return "No audit entries found.";
1642
1639
  const lines = entries.map((e) => {
1643
1640
  const date = new Date(e.timestamp).toLocaleString();
1644
- return `[${date}] ${e.file} | ${e.identifiersMutated} mutated | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
1641
+ return `[${date}] ${e.file} | ${e.identifiersMutated} protected | ${e.secretsBlocked} secrets blocked | ${e.llmProvider} | ${e.latencyMs}ms`;
1645
1642
  });
1646
1643
  return lines.join("\n");
1647
1644
  }
@@ -2016,6 +2013,27 @@ function runLogin(opts = {}) {
2016
2013
  return 0;
2017
2014
  }
2018
2015
 
2016
+ // src/dashboard-env.ts
2017
+ var DASHBOARD_API_KEY_ENVS = ["PRETENSE_API_KEY", "PRETEST_API_KEY"];
2018
+ var DASHBOARD_API_URL_ENVS = ["PRETENSE_API_URL", "PRETEST_API_URL"];
2019
+ function firstNonEmptyEnv(names) {
2020
+ for (const name of names) {
2021
+ const v = process.env[name]?.trim();
2022
+ if (v) return v;
2023
+ }
2024
+ return "";
2025
+ }
2026
+ function clearDashboardApiKeyFromProcess() {
2027
+ delete process.env["PRETEST_API_KEY"];
2028
+ delete process.env["PRETENSE_API_KEY"];
2029
+ }
2030
+ function hasDashboardApiKeyInEnv() {
2031
+ return DASHBOARD_API_KEY_ENVS.some((n) => !!process.env[n]?.trim());
2032
+ }
2033
+
2034
+ // src/publish-info.ts
2035
+ var PUBLISH_PACKAGE_URL = "https://www.npmjs.com/package/@pretense/cli";
2036
+
2019
2037
  // src/backend-client.ts
2020
2038
  function usageSummaryFromValidate(v) {
2021
2039
  return {
@@ -2031,9 +2049,9 @@ function usageSummaryFromValidate(v) {
2031
2049
  };
2032
2050
  }
2033
2051
  var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
2034
- var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
2052
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
2035
2053
  function getConfiguredApiRoot(rootOverride) {
2036
- const raw = (rootOverride ?? process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
2054
+ const raw = (rootOverride?.trim() || firstNonEmptyEnv(DASHBOARD_API_URL_ENVS) || DEFAULT_PRETENSE_API_URL).trim();
2037
2055
  if (!raw) {
2038
2056
  return DEFAULT_PRETENSE_API_URL.replace(/\/$/, "");
2039
2057
  }
@@ -2043,7 +2061,7 @@ function getConfiguredApiRoot(rootOverride) {
2043
2061
  }
2044
2062
  try {
2045
2063
  const u = new URL(normalized);
2046
- const origin = /^pretense\.ai$/i.test(u.hostname) ? "https://api.pretense.ai" : u.origin;
2064
+ const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL.replace(/\/$/, "") : u.origin;
2047
2065
  const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
2048
2066
  return `${origin}${path}`;
2049
2067
  } catch {
@@ -2056,20 +2074,20 @@ function getCliApiUrl(pathWithinCli, rootOverride) {
2056
2074
  return `${root}/api/cli/${tail}`;
2057
2075
  }
2058
2076
  function getApiRequestTimeoutMs() {
2059
- const raw = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
2077
+ const raw = process.env["PRETEST_API_TIMEOUT_MS"]?.trim() || process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
2060
2078
  if (!raw || !/^\d+$/.test(raw)) return DEFAULT_REQUEST_TIMEOUT_MS;
2061
2079
  const n = parseInt(raw, 10);
2062
2080
  return Math.min(Math.max(n, 3e3), 12e4);
2063
2081
  }
2064
2082
  function getApiKey() {
2065
- const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
2083
+ const fromEnv = firstNonEmptyEnv(DASHBOARD_API_KEY_ENVS);
2066
2084
  if (fromEnv) return fromEnv;
2067
2085
  const fromFile = loadApiKey();
2068
2086
  if (!fromFile) return null;
2069
2087
  const t = fromFile.trim();
2070
2088
  return t.length > 0 ? t : null;
2071
2089
  }
2072
- function getPretenseApiKey() {
2090
+ function getDashboardApiKey() {
2073
2091
  return getApiKey();
2074
2092
  }
2075
2093
  var cachedValidation = null;
@@ -2092,6 +2110,15 @@ function bumpCachedUsageAfterLog(identifiersMutated) {
2092
2110
  );
2093
2111
  }
2094
2112
  }
2113
+ function syncCachedUsageFromServer(codeProtectionsUsed, limit) {
2114
+ if (!cachedValidation) return;
2115
+ if (codeProtectionsUsed < cachedValidation.mutationsUsed) return;
2116
+ cachedValidation.mutationsUsed = codeProtectionsUsed;
2117
+ cachedValidation.monthlyMutationLimit = limit;
2118
+ if (limit !== -1) {
2119
+ cachedValidation.mutationsRemaining = Math.max(0, limit - codeProtectionsUsed);
2120
+ }
2121
+ }
2095
2122
  function makeBackendReachabilityError(code, message) {
2096
2123
  const err = new Error(message);
2097
2124
  err.code = code;
@@ -2109,7 +2136,7 @@ async function validateKey(opts = {}) {
2109
2136
  const apiKey = getApiKey();
2110
2137
  if (!apiKey) {
2111
2138
  if (enforceReachability) {
2112
- const err = new Error("Missing Pretense dashboard API key");
2139
+ const err = new Error("Missing dashboard API key");
2113
2140
  err.code = "MISSING_API_KEY";
2114
2141
  cachedValidation = null;
2115
2142
  lastValidationSuccessAt = 0;
@@ -2130,16 +2157,16 @@ async function validateKey(opts = {}) {
2130
2157
  signal: controller.signal
2131
2158
  });
2132
2159
  if (resp.status === 401 || resp.status === 403) {
2133
- const raw = await resp.json().catch(() => ({}));
2134
- const msg = typeof raw.message === "string" ? raw.message : "API key rejected by server";
2160
+ const raw2 = await resp.json().catch(() => ({}));
2161
+ const msg = typeof raw2.message === "string" ? raw2.message : "API key rejected by server";
2135
2162
  const err = new Error(msg);
2136
- err.code = typeof raw.code === "string" ? raw.code : "INVALID_API_KEY";
2163
+ err.code = typeof raw2.code === "string" ? raw2.code : "INVALID_API_KEY";
2137
2164
  cachedValidation = null;
2138
2165
  lastValidationSuccessAt = 0;
2139
2166
  throw err;
2140
2167
  }
2141
2168
  if (!resp.ok) {
2142
- const msg = `Pretense API returned HTTP ${resp.status} from ${validateUrl}`;
2169
+ const msg = `Dashboard API returned HTTP ${resp.status} from ${validateUrl}`;
2143
2170
  if (enforceReachability) {
2144
2171
  cachedValidation = null;
2145
2172
  lastValidationSuccessAt = 0;
@@ -2147,9 +2174,9 @@ async function validateKey(opts = {}) {
2147
2174
  }
2148
2175
  return null;
2149
2176
  }
2150
- let data;
2177
+ let raw;
2151
2178
  try {
2152
- data = await resp.json();
2179
+ raw = await resp.json();
2153
2180
  } catch {
2154
2181
  if (enforceReachability) {
2155
2182
  cachedValidation = null;
@@ -2161,6 +2188,12 @@ async function validateKey(opts = {}) {
2161
2188
  }
2162
2189
  return null;
2163
2190
  }
2191
+ const data = {
2192
+ ...raw,
2193
+ mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
2194
+ mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
2195
+ monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
2196
+ };
2164
2197
  if (typeof data.valid === "boolean" && data.valid === false) {
2165
2198
  const err = new Error("API key is not valid");
2166
2199
  err.code = "INVALID_API_KEY";
@@ -2176,7 +2209,7 @@ async function validateKey(opts = {}) {
2176
2209
  throw err;
2177
2210
  }
2178
2211
  if (enforceReachability) {
2179
- const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS)` : `Cannot reach Pretense API at ${validateUrl}: ${err.message}`;
2212
+ const msg = err.name === "AbortError" ? `Timed out after ${timeoutMs}ms reaching ${validateUrl} (raise PRETENSE_API_TIMEOUT_MS or PRETEST_API_TIMEOUT_MS)` : `Cannot reach dashboard API at ${validateUrl}: ${err.message}`;
2180
2213
  cachedValidation = null;
2181
2214
  lastValidationSuccessAt = 0;
2182
2215
  throw makeBackendReachabilityError("BACKEND_UNAVAILABLE", msg);
@@ -2205,7 +2238,13 @@ async function fetchUsageSummary(opts = {}) {
2205
2238
  signal: controller.signal
2206
2239
  });
2207
2240
  if (!resp.ok) return null;
2208
- return await resp.json();
2241
+ const raw = await resp.json();
2242
+ return {
2243
+ ...raw,
2244
+ mutationsUsed: raw["mutationsUsed"] ?? raw["codeProtectionsUsed"] ?? 0,
2245
+ mutationsRemaining: raw["mutationsRemaining"] ?? raw["codeProtectionsRemaining"] ?? -1,
2246
+ monthlyMutationLimit: raw["monthlyMutationLimit"] ?? raw["monthlyCodeProtectionLimit"] ?? -1
2247
+ };
2209
2248
  } catch (err) {
2210
2249
  if (opts.verbose) {
2211
2250
  process.stderr.write(
@@ -2222,27 +2261,91 @@ async function isWithinLimits() {
2222
2261
  const v = getCachedValidation();
2223
2262
  if (!v) return { allowed: true };
2224
2263
  if (v.mutationsRemaining !== -1 && v.mutationsRemaining <= 0) {
2225
- return {
2226
- allowed: false,
2227
- reason: `Monthly mutation limit reached (${v.mutationsUsed}/${v.monthlyMutationLimit}). Upgrade at https://pretense.ai/pricing`
2228
- };
2264
+ const fresh = await validateKey({ force: true }).catch(() => null);
2265
+ const check = fresh ?? v;
2266
+ if (check.mutationsRemaining !== -1 && check.mutationsRemaining <= 0) {
2267
+ return {
2268
+ allowed: false,
2269
+ reason: `Monthly security token limit reached (${check.mutationsUsed}/${check.monthlyMutationLimit}). See ${PUBLISH_PACKAGE_URL}`
2270
+ };
2271
+ }
2272
+ return { allowed: true };
2229
2273
  }
2230
2274
  return { allowed: true };
2231
2275
  }
2232
2276
 
2233
2277
  // src/log-uploader.ts
2234
- async function uploadLog(event, options) {
2278
+ var FLUSH_INTERVAL_MS = 5e3;
2279
+ var buffer = [];
2280
+ var flushTimer = null;
2281
+ function startFlushTimer(verbose) {
2282
+ if (flushTimer !== null) return;
2283
+ flushTimer = setInterval(() => {
2284
+ void flushBuffer(verbose);
2285
+ }, FLUSH_INTERVAL_MS);
2286
+ if (typeof flushTimer.unref === "function") flushTimer.unref();
2287
+ }
2288
+ function attachExitFlush() {
2289
+ const flush = () => {
2290
+ void flushBuffer(false);
2291
+ };
2292
+ process.once("exit", flush);
2293
+ process.once("SIGINT", () => {
2294
+ flush();
2295
+ process.exit(0);
2296
+ });
2297
+ process.once("SIGTERM", () => {
2298
+ flush();
2299
+ process.exit(0);
2300
+ });
2301
+ }
2302
+ var exitFlushAttached = false;
2303
+ async function flushBuffer(verbose) {
2304
+ if (buffer.length === 0) return;
2305
+ const snapshot = buffer;
2306
+ buffer = [];
2307
+ const last = snapshot[snapshot.length - 1];
2308
+ const { options } = last;
2309
+ let file_count = 0;
2310
+ let identifiers_mutated = 0;
2311
+ let secrets_blocked = 0;
2312
+ let scan_tokens = 0;
2313
+ let mutation_tokens = 0;
2314
+ let read_tokens = 0;
2315
+ let llm_provider = "unknown";
2316
+ let cli_version;
2317
+ for (const { event } of snapshot) {
2318
+ file_count += event.file_count ?? 0;
2319
+ identifiers_mutated += event.identifiers_mutated ?? 0;
2320
+ secrets_blocked += event.secrets_blocked ?? 0;
2321
+ scan_tokens += event.scan_tokens ?? 0;
2322
+ mutation_tokens += event.mutation_tokens ?? 0;
2323
+ read_tokens += event.read_tokens ?? 0;
2324
+ if (event.llm_provider) llm_provider = event.llm_provider;
2325
+ if (event.cli_version) cli_version = event.cli_version;
2326
+ }
2235
2327
  const apiRoot = options.apiUrl?.trim() || void 0;
2236
2328
  const endpoint = getCliApiUrl("log", apiRoot);
2237
- const { apiKey, verbose = false, timeoutMs = getApiRequestTimeoutMs() } = options;
2329
+ const { apiKey, timeoutMs = getApiRequestTimeoutMs(), onResponse } = options;
2238
2330
  if (!apiKey) return;
2239
2331
  const gitCtx = options.gitContext ?? await collectGitContextCached(options.cwd);
2332
+ const idempotency_key = crypto.randomUUID();
2240
2333
  const body = {
2241
- ...event,
2242
- ...gitCtx,
2334
+ file_count,
2335
+ identifiers_mutated,
2336
+ identifiers_code_protected: identifiers_mutated,
2337
+ secrets_blocked,
2338
+ llm_provider,
2339
+ scan_tokens,
2340
+ code_protection_tokens: mutation_tokens,
2341
+ read_tokens,
2342
+ idempotency_key,
2343
+ ...cli_version ? { cli_version } : {},
2344
+ git_remote: gitCtx.git_remote,
2243
2345
  repo_remote_url: gitCtx.git_remote,
2244
2346
  git_branch: gitCtx.git_branch,
2245
- commit_sha: gitCtx.git_commit_sha
2347
+ commit_sha: gitCtx.git_commit_sha,
2348
+ git_commit_sha: gitCtx.git_commit_sha
2246
2349
  };
2247
2350
  const controller = new AbortController();
2248
2351
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -2256,8 +2359,14 @@ async function uploadLog(event, options) {
2256
2359
  body: JSON.stringify(body),
2257
2360
  signal: controller.signal
2258
2361
  });
2259
- if (!resp.ok && verbose) {
2260
- process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status}
2362
+ if (resp.ok) {
2363
+ const data = await resp.json().catch(() => null);
2364
+ if (data && typeof data.codeProtectionsUsed === "number" && typeof data.limit === "number") {
2365
+ onResponse?.(data.codeProtectionsUsed, data.limit);
2366
+ }
2367
+ } else if (verbose) {
2368
+ const detail = await resp.text().catch(() => "");
2369
+ process.stderr.write(`[PRETENSE] log upload HTTP ${resp.status} (batch of ${snapshot.length}): ${detail}
2261
2370
  `);
2262
2371
  }
2263
2372
  } catch (err) {
@@ -2270,6 +2379,15 @@ async function uploadLog(event, options) {
2270
2379
  clearTimeout(timer);
2271
2380
  }
2272
2381
  }
2382
+ async function uploadLog(event, options) {
2383
+ if (!options.apiKey) return;
2384
+ buffer.push({ event, options });
2385
+ startFlushTimer(options.verbose ?? false);
2386
+ if (!exitFlushAttached) {
2387
+ exitFlushAttached = true;
2388
+ attachExitFlush();
2389
+ }
2390
+ }
2273
2391
 
2274
2392
  // src/start-client-instructions.ts
2275
2393
  import chalk2 from "chalk";
@@ -2328,6 +2446,10 @@ import { fileURLToPath } from "url";
2328
2446
  var cached = null;
2329
2447
  function getCliSemver() {
2330
2448
  if (cached !== null) return cached;
2449
+ if (typeof __BINARY_VERSION__ !== "undefined" && __BINARY_VERSION__) {
2450
+ cached = __BINARY_VERSION__;
2451
+ return cached;
2452
+ }
2331
2453
  try {
2332
2454
  const url = new URL("../package.json", import.meta.url);
2333
2455
  const raw = readFileSync6(fileURLToPath(url), "utf-8");
@@ -2349,7 +2471,7 @@ function attachDashboardLogoutWatcher(enabled) {
2349
2471
  if (!fk) {
2350
2472
  if (dashboardSavedCredentialsRevoked) return;
2351
2473
  dashboardSavedCredentialsRevoked = true;
2352
- delete process.env["PRETENSE_API_KEY"];
2474
+ clearDashboardApiKeyFromProcess();
2353
2475
  clearValidationCache();
2354
2476
  try {
2355
2477
  process.stderr.write(
@@ -2480,7 +2602,7 @@ function printUpgradeNudge(reason) {
2480
2602
  process.stdout.write(
2481
2603
  `
2482
2604
  \x1B[33m[PRETENSE] ${reason}\x1B[0m
2483
- \x1B[33m[PRETENSE] Upgrade to Pro: https://pretense.ai/pricing\x1B[0m
2605
+ \x1B[33m[PRETENSE] Upgrade to Pro: ${PUBLISH_PACKAGE_URL}\x1B[0m
2484
2606
 
2485
2607
  `
2486
2608
  );
@@ -2505,7 +2627,7 @@ function createProxy(opts = {}) {
2505
2627
  "POST /*": "Proxy to upstream LLM API (Anthropic, OpenAI, Google auto-detected)"
2506
2628
  },
2507
2629
  languages: ["TypeScript", "JavaScript", "Python", "Go", "Java", "C#", "Ruby", "Rust"],
2508
- docs: "https://pretense.ai/docs"
2630
+ docs: PUBLISH_PACKAGE_URL
2509
2631
  })
2510
2632
  );
2511
2633
  app.get(
@@ -2564,19 +2686,19 @@ function createProxy(opts = {}) {
2564
2686
  {
2565
2687
  error: {
2566
2688
  type: "pretense_session_logged_out",
2567
- message: "Saved Pretense dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with PRETENSE_API_KEY in this shell, stop it (Ctrl+C) and start again after logout."
2689
+ message: "Saved dashboard credentials were removed (for example pretense logout). Run pretense login, then restart pretense start. If you started the proxy with a dashboard API key in this shell, stop it (Ctrl+C) and start again after logout."
2568
2690
  }
2569
2691
  },
2570
2692
  401
2571
2693
  );
2572
2694
  }
2573
- const pretenseDashboardKey = getPretenseApiKey();
2574
- if (!pretenseDashboardKey) {
2695
+ const dashboardApiKey = getDashboardApiKey();
2696
+ if (!dashboardApiKey) {
2575
2697
  return c.json(
2576
2698
  {
2577
2699
  error: {
2578
2700
  type: "pretense_dashboard_key_required",
2579
- message: "Configure your Pretense dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
2701
+ message: "Configure your dashboard API key: run `pretense login --key prtns_live_...` or set PRETENSE_API_KEY."
2580
2702
  }
2581
2703
  },
2582
2704
  401
@@ -2622,26 +2744,13 @@ function createProxy(opts = {}) {
2622
2744
  getCachedValidation()?.plan,
2623
2745
  detectTier()
2624
2746
  );
2625
- const { monthlyMutations } = getTierLimits(tier);
2626
- const usage = loadUsage();
2627
- if (tier === "free" && usage.mutations >= monthlyMutations) {
2628
- return c.json(
2629
- {
2630
- error: {
2631
- type: "pretense_rate_limit",
2632
- message: `Monthly mutation limit reached (${monthlyMutations}/month). Upgrade to Pro for unlimited: https://pretense.ai/pricing`
2633
- }
2634
- },
2635
- 429
2636
- );
2637
- }
2638
2747
  const backendLimits = await isWithinLimits();
2639
2748
  if (!backendLimits.allowed) {
2640
2749
  return c.json(
2641
2750
  {
2642
2751
  error: {
2643
2752
  type: "pretense_rate_limit",
2644
- message: backendLimits.reason ?? "Mutation limit exceeded."
2753
+ message: backendLimits.reason ?? "Security token limit exceeded."
2645
2754
  }
2646
2755
  },
2647
2756
  429
@@ -2706,10 +2815,9 @@ function createProxy(opts = {}) {
2706
2815
  }
2707
2816
  return replaceCodeBlocks(text, blocks, mutatedBlocks);
2708
2817
  });
2709
- usage.mutations += tokensMutated;
2710
- saveUsage(usage);
2711
2818
  const remoteLogDisabled = process.env["PRETENSE_DISABLE_REMOTE_LOG"] === "1";
2712
- if (pretenseDashboardKey && !remoteLogDisabled) {
2819
+ if (dashboardApiKey && !remoteLogDisabled) {
2820
+ bumpCachedUsageAfterLog(tokensMutated);
2713
2821
  void uploadLog(
2714
2822
  {
2715
2823
  file_count: 0,
@@ -2718,16 +2826,26 @@ function createProxy(opts = {}) {
2718
2826
  llm_provider: provider,
2719
2827
  cli_version: getCliSemver()
2720
2828
  },
2721
- { apiKey: pretenseDashboardKey, verbose }
2829
+ {
2830
+ apiKey: dashboardApiKey,
2831
+ verbose,
2832
+ onResponse: (codeProtectionsUsed, limit) => {
2833
+ syncCachedUsageFromServer(codeProtectionsUsed, limit);
2834
+ }
2835
+ }
2722
2836
  );
2723
- bumpCachedUsageAfterLog(tokensMutated);
2724
2837
  }
2725
- const warningThreshold = Math.floor(monthlyMutations * 0.8);
2726
- if (tier === "free" && usage.mutations >= warningThreshold && usage.mutations - tokensMutated < warningThreshold) {
2727
- printUpgradeNudge(`${usage.mutations}/${monthlyMutations} monthly mutations used. Running low!`);
2838
+ const warnCache = getCachedValidation();
2839
+ if (warnCache && warnCache.monthlyMutationLimit !== -1 && tier === "free") {
2840
+ const limit = warnCache.monthlyMutationLimit;
2841
+ const used = warnCache.mutationsUsed;
2842
+ const warningThreshold = Math.floor(limit * 0.8);
2843
+ if (used >= warningThreshold && used - tokensMutated < warningThreshold) {
2844
+ printUpgradeNudge(`${used}/${limit} monthly security tokens used. Running low!`);
2845
+ }
2728
2846
  }
2729
2847
  if (tier === "free" && daysSinceFirstUse() >= 7) {
2730
- printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited mutations, 90-day audit, Slack alerts.");
2848
+ printUpgradeNudge("You've been using Pretense for 7+ days. Unlock Pro features: unlimited security tokens, 90-day audit, Slack alerts.");
2731
2849
  }
2732
2850
  const entry = {
2733
2851
  id: requestId,
@@ -2744,7 +2862,7 @@ function createProxy(opts = {}) {
2744
2862
  createAuditEntry(requestId, "proxy-request", tokensMutated, blockedSecrets.length, provider, latencyMs)
2745
2863
  );
2746
2864
  if (verbose && tokensMutated > 0) {
2747
- process.stdout.write(`[PRETENSE] ${tokensMutated} tokens mutated for request ${requestId}
2865
+ process.stdout.write(`[PRETENSE] ${tokensMutated} security tokens protected for request ${requestId}
2748
2866
  `);
2749
2867
  }
2750
2868
  const headers = {};
@@ -2784,12 +2902,12 @@ function createProxy(opts = {}) {
2784
2902
  }
2785
2903
  const decoder = new TextDecoder();
2786
2904
  const encoder = new TextEncoder();
2787
- let buffer = "";
2905
+ let buffer2 = "";
2788
2906
  const transform = new TransformStream({
2789
2907
  transform(chunk, controller) {
2790
- buffer += decoder.decode(chunk, { stream: true });
2791
- const lines = buffer.split("\n");
2792
- buffer = lines.pop() ?? "";
2908
+ buffer2 += decoder.decode(chunk, { stream: true });
2909
+ const lines = buffer2.split("\n");
2910
+ buffer2 = lines.pop() ?? "";
2793
2911
  for (const line of lines) {
2794
2912
  if (line.startsWith("data: ") && line !== "data: [DONE]") {
2795
2913
  try {
@@ -2806,8 +2924,8 @@ function createProxy(opts = {}) {
2806
2924
  }
2807
2925
  },
2808
2926
  flush(controller) {
2809
- if (buffer.length > 0) {
2810
- controller.enqueue(encoder.encode(buffer));
2927
+ if (buffer2.length > 0) {
2928
+ controller.enqueue(encoder.encode(buffer2));
2811
2929
  }
2812
2930
  }
2813
2931
  });
@@ -2851,9 +2969,9 @@ function createProxy(opts = {}) {
2851
2969
  return app;
2852
2970
  }
2853
2971
  function isPortAvailable(port) {
2854
- return new Promise((resolve3) => {
2855
- const tester = createServer().once("error", () => resolve3(false)).once("listening", () => {
2856
- tester.once("close", () => resolve3(true)).close();
2972
+ return new Promise((resolve4) => {
2973
+ const tester = createServer().once("error", () => resolve4(false)).once("listening", () => {
2974
+ tester.once("close", () => resolve4(true)).close();
2857
2975
  }).listen(port, "127.0.0.1");
2858
2976
  });
2859
2977
  }
@@ -2871,12 +2989,12 @@ function startProxy(opts = {}) {
2871
2989
  const requestedPort = opts.port ?? opts.config?.port ?? DEFAULT_CONFIG.port;
2872
2990
  const app = createProxy(opts);
2873
2991
  void (async () => {
2874
- const pretenseDashboardKeyFromEnvAtLaunch = !!process.env["PRETENSE_API_KEY"]?.trim();
2875
- const dashboardKey = getPretenseApiKey();
2992
+ const dashboardKeyFromEnvAtLaunch = hasDashboardApiKeyInEnv();
2993
+ const dashboardKey = getDashboardApiKey();
2876
2994
  if (!dashboardKey || !isValidKeyShape(dashboardKey)) {
2877
2995
  process.stderr.write(
2878
2996
  `\x1B[31m[PRETENSE] API key required or invalid shape. Run \`pretense login --key prtns_live_...\`\x1B[0m
2879
- \x1B[31m[PRETENSE] or set PRETENSE_API_KEY. Get a key at https://pretense.ai/dashboard/settings/api-keys\x1B[0m
2997
+ \x1B[31m[PRETENSE] or set PRETENSE_API_KEY. See ${PUBLISH_PACKAGE_URL}\x1B[0m
2880
2998
  `
2881
2999
  );
2882
3000
  process.exit(1);
@@ -2899,7 +3017,7 @@ function startProxy(opts = {}) {
2899
3017
  const plan = validation.plan ?? "FREE";
2900
3018
  const remaining = validation.mutationsRemaining === -1 ? "unlimited" : String(validation.mutationsRemaining);
2901
3019
  process.stdout.write(
2902
- `\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} mutations remaining\x1B[0m
3020
+ `\x1B[32m[PRETENSE] Key validated: ${plan} plan` + (validation.organizationName ? ` (${validation.organizationName})` : "") + ` \u2014 ${remaining} security tokens remaining\x1B[0m
2903
3021
  `
2904
3022
  );
2905
3023
  break;
@@ -2909,7 +3027,7 @@ function startProxy(opts = {}) {
2909
3027
  if (code === "BACKEND_UNAVAILABLE" || code === "AUTH_BACKEND_REJECTED") {
2910
3028
  process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2911
3029
  `);
2912
- process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL if using a custom API.\x1B[0m
3030
+ process.stderr.write(`\x1B[31m[PRETENSE] Fix network or set PRETENSE_API_URL (or PRETEST_API_URL) if using a custom API.\x1B[0m
2913
3031
  `);
2914
3032
  process.exit(1);
2915
3033
  }
@@ -2925,7 +3043,7 @@ function startProxy(opts = {}) {
2925
3043
  process.stderr.write(`\x1B[31m[PRETENSE] ${msg}\x1B[0m
2926
3044
  `);
2927
3045
  process.stderr.write(
2928
- `\x1B[33m[PRETENSE] Enter your Pretense dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
3046
+ `\x1B[33m[PRETENSE] Enter your dashboard API key and press Enter (Ctrl+C to quit).\x1B[0m
2929
3047
  `
2930
3048
  );
2931
3049
  let entered = null;
@@ -2938,7 +3056,7 @@ function startProxy(opts = {}) {
2938
3056
  }
2939
3057
  if (isValidKeyShape(entered)) break;
2940
3058
  process.stderr.write(
2941
- `\x1B[31m[PRETENSE] That does not look like a Pretense dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
3059
+ `\x1B[31m[PRETENSE] That does not look like a dashboard key (expect prtns_live_\u2026 or pk_live_\u2026).\x1B[0m
2942
3060
  `
2943
3061
  );
2944
3062
  }
@@ -2962,7 +3080,7 @@ function startProxy(opts = {}) {
2962
3080
  process.exit(1);
2963
3081
  }
2964
3082
  }
2965
- attachDashboardLogoutWatcher(!pretenseDashboardKeyFromEnvAtLaunch);
3083
+ attachDashboardLogoutWatcher(!dashboardKeyFromEnvAtLaunch);
2966
3084
  const bannerTier = tierFromDashboardPlan(
2967
3085
  getCachedValidation()?.plan,
2968
3086
  detectTier()
@@ -3007,7 +3125,7 @@ function startProxy(opts = {}) {
3007
3125
  `);
3008
3126
  setInterval(() => {
3009
3127
  if (dashboardSavedCredentialsRevoked) return;
3010
- if (!getPretenseApiKey()) return;
3128
+ if (!getDashboardApiKey()) return;
3011
3129
  void validateKey({
3012
3130
  force: true,
3013
3131
  verbose: opts.verbose,
@@ -3031,39 +3149,43 @@ var PLAN_LIMITS = {
3031
3149
  pricePerMonth: "$99/seat/month"
3032
3150
  }
3033
3151
  };
3034
- function getPlanLimits(tier) {
3035
- return PLAN_LIMITS[tier];
3036
- }
3037
3152
  function fmt(n) {
3038
- if (!Number.isFinite(n)) return "unlimited";
3153
+ if (n === -1 || !Number.isFinite(n)) return "unlimited";
3039
3154
  return n.toLocaleString("en-US");
3040
3155
  }
3041
3156
  function printTokenFooter(filesScanned, mutationsMade) {
3042
- const tier = detectTier();
3043
- const limits = getPlanLimits(tier);
3044
- const usage = loadUsage();
3045
- if (tier === "enterprise") {
3157
+ const cached2 = getCachedValidation();
3158
+ if (!cached2) {
3159
+ process.stderr.write(chalk3.gray(`
3160
+ ${filesScanned} files, ${mutationsMade} security tokens
3161
+
3162
+ `));
3163
+ return;
3164
+ }
3165
+ const plan = cached2.plan;
3166
+ if (plan === "ENTERPRISE") {
3046
3167
  process.stderr.write(
3047
3168
  chalk3.gray(`
3048
- ${filesScanned} files, ${mutationsMade} mutations \xB7 Plan: Enterprise (unlimited)
3169
+ ${filesScanned} files, ${mutationsMade} security tokens \xB7 Plan: Enterprise (unlimited)
3049
3170
 
3050
3171
  `)
3051
3172
  );
3052
3173
  return;
3053
3174
  }
3054
- const used = usage.mutations;
3055
- const cap = limits.monthlyMutations;
3056
- const pct = cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
3057
- const usageStr = pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
3175
+ const used = cached2.mutationsUsed;
3176
+ const cap = cached2.monthlyMutationLimit;
3177
+ const unlimited = cap === -1 || !Number.isFinite(cap);
3178
+ const pct = !unlimited && cap > 0 ? Math.min(100, Math.round(used / cap * 100)) : 0;
3179
+ const usageStr = unlimited ? chalk3.gray(`${fmt(used)} / unlimited`) : pct >= 80 ? chalk3.yellow(`${fmt(used)} / ${fmt(cap)}`) : chalk3.gray(`${fmt(used)} / ${fmt(cap)}`);
3058
3180
  process.stderr.write(
3059
3181
  `
3060
- ${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " mutations")} \xB7 Plan: ${chalk3.bold(tier.toUpperCase())} \xB7 Mutations this month: ${usageStr} (${pct}%)
3182
+ ${chalk3.cyan(filesScanned + " files")}, ${chalk3.cyan(mutationsMade + " security tokens")} \xB7 Plan: ${chalk3.bold(plan)} \xB7 Security tokens this month: ${usageStr}${!unlimited ? ` (${pct}%)` : ""}
3061
3183
  `
3062
3184
  );
3063
- if (tier === "free") {
3185
+ if (plan === "FREE") {
3064
3186
  if (pct >= 80) {
3065
3187
  process.stderr.write(
3066
- chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold("pretense upgrade")}
3188
+ chalk3.yellow(` \u26A0 Approaching free-tier limit. Upgrade: ${chalk3.bold(PUBLISH_PACKAGE_URL)}
3067
3189
 
3068
3190
  `)
3069
3191
  );
@@ -3116,11 +3238,6 @@ function printBanner() {
3116
3238
 
3117
3239
  // src/commands/status.ts
3118
3240
  import chalk5 from "chalk";
3119
- var PLAN_LABEL = {
3120
- free: "Free",
3121
- pro: "Pro",
3122
- enterprise: "Enterprise"
3123
- };
3124
3241
  function fmt2(n) {
3125
3242
  if (n === -1 || !Number.isFinite(n)) return "unlimited";
3126
3243
  return n.toLocaleString("en-US");
@@ -3135,50 +3252,36 @@ async function runStatus() {
3135
3252
  }
3136
3253
  if (remote) {
3137
3254
  const plan = remote.plan;
3138
- const tierBadge2 = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3139
- const lines2 = [];
3140
- lines2.push("");
3141
- lines2.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge2} ${chalk5.green("(synced)")}`);
3142
- lines2.push("");
3143
- lines2.push(` Plan: ${chalk5.bold(plan)}`);
3144
- lines2.push(` Mutations: ${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
3145
- lines2.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
3146
- lines2.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
3147
- lines2.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
3148
- lines2.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
3149
- lines2.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3255
+ const tierBadge = plan === "ENTERPRISE" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : plan === "PRO" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3256
+ const lines = [];
3257
+ lines.push("");
3258
+ lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.green("(synced)")}`);
3259
+ lines.push("");
3260
+ lines.push(` Plan: ${chalk5.bold(plan)}`);
3261
+ lines.push(` Security tokens:${fmt2(remote.mutationsUsed)} / ${fmt2(remote.monthlyMutationLimit)} this month`);
3262
+ lines.push(` Remaining: ${fmt2(remote.mutationsRemaining)}`);
3263
+ lines.push(` Requests: ${fmt2(remote.requestsThisPeriod)} this period`);
3264
+ lines.push(` Secrets: ${fmt2(remote.secretsBlockedThisPeriod)} blocked`);
3265
+ lines.push(` Key status: ${remote.keyStatus === "active" ? chalk5.green("active") : chalk5.red("revoked")}`);
3266
+ lines.push(` Resets at: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3150
3267
  if (remote.keyExpiresAt) {
3151
- lines2.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
3268
+ lines.push(` Key expires: ${new Date(remote.keyExpiresAt).toLocaleDateString()}`);
3152
3269
  }
3153
- lines2.push("");
3270
+ lines.push("");
3154
3271
  if (plan === "FREE") {
3155
- lines2.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3156
- lines2.push("");
3272
+ lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3273
+ lines.push("");
3157
3274
  }
3158
- process.stdout.write(lines2.join("\n") + "\n");
3275
+ process.stdout.write(lines.join("\n") + "\n");
3159
3276
  return;
3160
3277
  }
3161
- const tier = detectTier();
3162
- const planLimits = getPlanLimits(tier);
3163
- const tierLimits = getTierLimits(tier);
3164
- const usage = loadUsage();
3165
- const tierBadge = tier === "enterprise" ? chalk5.bgMagenta.white.bold(" ENTERPRISE ") : tier === "pro" ? chalk5.bgBlue.white.bold(" PRO ") : chalk5.bgGray.white.bold(" FREE ");
3166
- const lines = [];
3167
- lines.push("");
3168
- lines.push(`${chalk5.cyan("Pretense")} ${chalk5.gray(`v${getCliSemver()}`)} ${tierBadge} ${chalk5.yellow("(offline)")}`);
3169
- lines.push("");
3170
- lines.push(` Plan: ${chalk5.bold(PLAN_LABEL[tier])} ${chalk5.gray("(" + planLimits.pricePerMonth + ")")}`);
3171
- lines.push(` Mutations: ${fmt2(usage.mutations)} / ${fmt2(planLimits.monthlyMutations)} this month`);
3172
- lines.push(` Scans: 0 / ${fmt2(planLimits.monthlyScans)} this month`);
3173
- lines.push(` Seats: 1 / ${fmt2(planLimits.seats)}`);
3174
- lines.push(` Audit log: ${Number.isFinite(tierLimits.auditRetentionDays) ? `${tierLimits.auditRetentionDays} days` : "unlimited"}`);
3175
- lines.push("");
3176
- if (tier === "free") {
3177
- lines.push(` ${chalk5.gray("Upgrade:")} ${chalk5.bold("pretense upgrade")}`);
3178
- lines.push(` ${chalk5.gray("Credits:")} ${chalk5.bold("pretense credits")}`);
3179
- lines.push("");
3180
- }
3181
- process.stdout.write(lines.join("\n") + "\n");
3278
+ process.stderr.write(
3279
+ chalk5.yellow(`
3280
+ [pretense] Could not reach server \u2014 usage data unavailable.
3281
+ `) + chalk5.gray(` Check your connection or run \`pretense login\` if your key has changed.
3282
+
3283
+ `)
3284
+ );
3182
3285
  }
3183
3286
 
3184
3287
  // src/commands/upgrade.ts
@@ -3214,11 +3317,6 @@ function runUpgrade() {
3214
3317
 
3215
3318
  // src/commands/credits.ts
3216
3319
  import chalk7 from "chalk";
3217
- var PLAN_LABEL2 = {
3218
- free: "Free",
3219
- pro: "Pro",
3220
- enterprise: "Enterprise"
3221
- };
3222
3320
  function fmt3(n) {
3223
3321
  if (n === -1 || !Number.isFinite(n)) return "unlimited";
3224
3322
  return n.toLocaleString("en-US");
@@ -3240,57 +3338,38 @@ async function runCredits() {
3240
3338
  }
3241
3339
  }
3242
3340
  if (remote) {
3243
- const used2 = remote.mutationsUsed;
3244
- const cap2 = remote.monthlyMutationLimit;
3245
- const remaining2 = remote.mutationsRemaining;
3246
- const pct2 = cap2 > 0 && cap2 !== -1 ? Math.round(used2 / cap2 * 100) : 0;
3247
- const lines2 = [];
3248
- lines2.push("");
3249
- lines2.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
3250
- lines2.push("");
3251
- lines2.push(` Plan: ${chalk7.bold(remote.plan)}`);
3252
- lines2.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3253
- lines2.push("");
3254
- lines2.push(` Mutations: ${progressBar(used2, cap2)} ${fmt3(used2)} / ${fmt3(cap2)}`);
3255
- lines2.push(` Used: ${pct2}%`);
3256
- lines2.push(` Remaining: ${fmt3(remaining2)}`);
3257
- lines2.push("");
3258
- if (remote.plan === "FREE" && cap2 !== -1 && used2 >= cap2 * 0.8) {
3259
- lines2.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3260
- lines2.push("");
3341
+ const used = remote.mutationsUsed;
3342
+ const cap = remote.monthlyMutationLimit;
3343
+ const remaining = remote.mutationsRemaining;
3344
+ const pct = cap > 0 && cap !== -1 ? Math.round(used / cap * 100) : 0;
3345
+ const lines = [];
3346
+ lines.push("");
3347
+ lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.green("(synced)"));
3348
+ lines.push("");
3349
+ lines.push(` Plan: ${chalk7.bold(remote.plan)}`);
3350
+ lines.push(` Resets: ${new Date(remote.periodResetsAt).toLocaleDateString()}`);
3351
+ lines.push("");
3352
+ lines.push(` Security tokens:${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
3353
+ lines.push(` Used: ${pct}%`);
3354
+ lines.push(` Remaining: ${fmt3(remaining)}`);
3355
+ lines.push("");
3356
+ if (remote.plan === "FREE" && cap !== -1 && used >= cap * 0.8) {
3357
+ lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3358
+ lines.push("");
3261
3359
  } else if (remote.plan === "FREE") {
3262
- lines2.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3263
- lines2.push("");
3360
+ lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3361
+ lines.push("");
3264
3362
  }
3265
- process.stdout.write(lines2.join("\n") + "\n");
3363
+ process.stdout.write(lines.join("\n") + "\n");
3266
3364
  return;
3267
3365
  }
3268
- const tier = detectTier();
3269
- const limits = getPlanLimits(tier);
3270
- const usage = loadUsage();
3271
- const used = usage.mutations;
3272
- const cap = limits.monthlyMutations;
3273
- const remaining = Number.isFinite(cap) ? Math.max(0, cap - used) : Number.POSITIVE_INFINITY;
3274
- const pct = Number.isFinite(cap) && cap > 0 ? Math.round(used / cap * 100) : 0;
3275
- const lines = [];
3276
- lines.push("");
3277
- lines.push(chalk7.cyan.bold(" Pretense Credits") + " " + chalk7.yellow("(offline)"));
3278
- lines.push("");
3279
- lines.push(` Plan: ${chalk7.bold(PLAN_LABEL2[tier])}`);
3280
- lines.push(` Month: ${usage.month}`);
3281
- lines.push("");
3282
- lines.push(` Mutations: ${progressBar(used, cap)} ${fmt3(used)} / ${fmt3(cap)}`);
3283
- lines.push(` Used: ${pct}%`);
3284
- lines.push(` Remaining: ${fmt3(remaining)}`);
3285
- lines.push("");
3286
- if (tier === "free" && Number.isFinite(cap) && used >= cap * 0.8) {
3287
- lines.push(chalk7.yellow(" Warning: Approaching free-tier limit. Run 'pretense upgrade' for unlimited."));
3288
- lines.push("");
3289
- } else if (tier === "free") {
3290
- lines.push(chalk7.gray(" Tip: Run 'pretense upgrade' to compare plans."));
3291
- lines.push("");
3292
- }
3293
- process.stdout.write(lines.join("\n") + "\n");
3366
+ process.stderr.write(
3367
+ chalk7.yellow(`
3368
+ [pretense] Could not reach server \u2014 usage data unavailable.
3369
+ `) + chalk7.gray(` Check your connection or run \`pretense login\` if your key has changed.
3370
+
3371
+ `)
3372
+ );
3294
3373
  }
3295
3374
 
3296
3375
  // src/commands/logout.ts
@@ -3340,6 +3419,100 @@ async function runLogout(opts = {}) {
3340
3419
  return 0;
3341
3420
  }
3342
3421
 
3422
+ // src/commands/install.ts
3423
+ import { Command } from "commander";
3424
+ import chalk9 from "chalk";
3425
+ import {
3426
+ existsSync as existsSync7,
3427
+ writeFileSync as writeFileSync5,
3428
+ chmodSync as chmodSync2,
3429
+ readFileSync as readFileSync7,
3430
+ mkdirSync as mkdirSync6
3431
+ } from "fs";
3432
+ import { join as join5, resolve as resolve2 } from "path";
3433
+ var HOOK_SCRIPTS = {
3434
+ "pre-commit": `#!/bin/sh
3435
+ # Pretense AI Firewall \u2014 pre-commit hook
3436
+ # Installed by: pretense install --mode pre-commit
3437
+
3438
+ pretense scan pre-commit
3439
+ exit $?
3440
+ `,
3441
+ "pre-push": `#!/bin/sh
3442
+ # Pretense AI Firewall \u2014 pre-push hook
3443
+ # Installed by: pretense install --mode pre-push
3444
+
3445
+ pretense scan pre-push
3446
+ exit $?
3447
+ `
3448
+ };
3449
+ var VALID_MODES = ["pre-commit", "pre-push"];
3450
+ function findGitDir(cwd) {
3451
+ const gitDir = join5(cwd, ".git");
3452
+ if (existsSync7(gitDir)) return gitDir;
3453
+ return null;
3454
+ }
3455
+ function installHook(hooksDir, mode, force) {
3456
+ const hookPath = join5(hooksDir, mode);
3457
+ const script = HOOK_SCRIPTS[mode];
3458
+ if (existsSync7(hookPath) && !force) {
3459
+ const existing = readFileSync7(hookPath, "utf-8");
3460
+ if (existing.includes("Pretense AI Firewall")) {
3461
+ return { installed: false, path: hookPath, skipped: true };
3462
+ }
3463
+ return { installed: false, path: hookPath };
3464
+ }
3465
+ writeFileSync5(hookPath, script, "utf-8");
3466
+ chmodSync2(hookPath, 493);
3467
+ return { installed: true, path: hookPath };
3468
+ }
3469
+ function installCommand() {
3470
+ return new Command("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option("-m, --mode <mode>", "Hook mode: pre-commit, pre-push (omit to install both)").option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
3471
+ const dir = resolve2(opts.dir);
3472
+ const force = opts.force;
3473
+ const modeArg = opts.mode;
3474
+ if (modeArg && !VALID_MODES.includes(modeArg)) {
3475
+ console.error(
3476
+ chalk9.red(`
3477
+ Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
3478
+ `)
3479
+ );
3480
+ process.exit(1);
3481
+ }
3482
+ const modes = modeArg ? [modeArg] : [...VALID_MODES];
3483
+ const gitDir = findGitDir(dir);
3484
+ if (!gitDir) {
3485
+ console.error(
3486
+ chalk9.red(`
3487
+ Error: No .git directory found in ${dir}
3488
+ Run this command from a git repository root.
3489
+ `)
3490
+ );
3491
+ process.exit(1);
3492
+ }
3493
+ const hooksDir = join5(gitDir, "hooks");
3494
+ mkdirSync6(hooksDir, { recursive: true });
3495
+ console.log(chalk9.cyan("\n Pretense hook installer\n"));
3496
+ let hasError = false;
3497
+ for (const mode of modes) {
3498
+ const result = installHook(hooksDir, mode, force);
3499
+ if (result.installed) {
3500
+ console.log(chalk9.green(` Installed ${mode} hook at ${result.path}`));
3501
+ } else if (result.skipped) {
3502
+ console.log(chalk9.dim(` Pretense ${mode} hook already installed at ${result.path}`));
3503
+ } else {
3504
+ console.error(chalk9.yellow(` Hook already exists at ${result.path}. Use --force to overwrite.`));
3505
+ hasError = true;
3506
+ }
3507
+ }
3508
+ if (hasError) {
3509
+ console.log("");
3510
+ process.exit(1);
3511
+ }
3512
+ console.log(chalk9.bold.green("\n Git hooks installed successfully.\n"));
3513
+ });
3514
+ }
3515
+
3343
3516
  // src/index.ts
3344
3517
  {
3345
3518
  const major = parseInt(process.versions.node.split(".")[0], 10);
@@ -3476,8 +3649,8 @@ function validateLanguage(lang) {
3476
3649
  return SUPPORTED_LANGUAGES.includes(resolved) ? resolved : null;
3477
3650
  }
3478
3651
  function collectFiles(target) {
3479
- const abs = resolve2(target);
3480
- if (!existsSync7(abs)) {
3652
+ const abs = resolve3(target);
3653
+ if (!existsSync8(abs)) {
3481
3654
  return [];
3482
3655
  }
3483
3656
  const stat = statSync(abs);
@@ -3495,13 +3668,14 @@ function collectFiles(target) {
3495
3668
  ".git",
3496
3669
  "dist",
3497
3670
  "build",
3671
+ ".pretest",
3498
3672
  ".pretense",
3499
3673
  ".next",
3500
3674
  "__pycache__",
3501
3675
  ".Trash"
3502
3676
  ]);
3503
3677
  for (const entry of entries) {
3504
- const fullPath = join5(dir, entry.name);
3678
+ const fullPath = join6(dir, entry.name);
3505
3679
  if (entry.isDirectory()) {
3506
3680
  if (skipDirs.has(entry.name)) continue;
3507
3681
  walk(fullPath);
@@ -3513,13 +3687,13 @@ function collectFiles(target) {
3513
3687
  walk(abs);
3514
3688
  return files;
3515
3689
  }
3516
- var program = new Command();
3690
+ var program = new Command2();
3517
3691
  program.name("pretense").description("AI firewall CLI \u2014 mutates proprietary code before LLM API calls").version(VERSION).hook("preAction", () => {
3518
3692
  printBanner();
3519
3693
  });
3520
3694
  program.command("init").description("Initialize .pretense/ config in current directory").option("-d, --dir <path>", "Target directory", process.cwd()).action((opts) => {
3521
- const dir = resolve2(opts.dir);
3522
- console.log(chalk9.cyan("Initializing Pretense in"), chalk9.bold(dir));
3695
+ const dir = resolve3(opts.dir);
3696
+ console.log(chalk10.cyan("Initializing Pretense in"), chalk10.bold(dir));
3523
3697
  const configDir = initConfig(dir);
3524
3698
  const files = collectFiles(dir);
3525
3699
  const langCounts = {};
@@ -3527,21 +3701,21 @@ program.command("init").description("Initialize .pretense/ config in current dir
3527
3701
  const lang = langFromExt(f);
3528
3702
  langCounts[lang] = (langCounts[lang] ?? 0) + 1;
3529
3703
  }
3530
- console.log(chalk9.green("\n .pretense/ created at"), chalk9.bold(configDir));
3531
- console.log(chalk9.gray(`
3704
+ console.log(chalk10.green("\n .pretense/ created at"), chalk10.bold(configDir));
3705
+ console.log(chalk10.gray(`
3532
3706
  Scanned ${files.length} source files:`));
3533
3707
  for (const [lang, count] of Object.entries(langCounts)) {
3534
- console.log(chalk9.gray(` ${lang}: ${count} files`));
3708
+ console.log(chalk10.gray(` ${lang}: ${count} files`));
3535
3709
  }
3536
- console.log(chalk9.cyan("\n Next: run"), chalk9.bold("pretense start"), chalk9.cyan("to launch the proxy"));
3710
+ console.log(chalk10.cyan("\n Next: run"), chalk10.bold("pretense start"), chalk10.cyan("to launch the proxy"));
3537
3711
  console.log();
3538
3712
  });
3539
3713
  program.command("start").description("Start the Pretense proxy server").option("-p, --port <number>", "Port number", "9339").option("-v, --verbose", "Enable verbose logging", false).action(async (opts) => {
3540
3714
  const config = loadConfig();
3541
3715
  const port = validatePort(opts.port);
3542
3716
  if (port === null) {
3543
- console.error(chalk9.red("Error: Invalid port number:"), chalk9.bold(opts.port));
3544
- console.error(chalk9.gray("Port must be an integer between 1 and 65535."));
3717
+ console.error(chalk10.red("Error: Invalid port number:"), chalk10.bold(opts.port));
3718
+ console.error(chalk10.gray("Port must be an integer between 1 and 65535."));
3545
3719
  process.exit(1);
3546
3720
  }
3547
3721
  await ensureDashboardKeyForStart();
@@ -3654,30 +3828,30 @@ function parseSizeOption(value) {
3654
3828
  return Math.floor(n * mult);
3655
3829
  }
3656
3830
  program.command("scan <target>").description("Scan file or directory for identifiers and secrets (no mutation)").option("--secrets-only", "Only scan for secrets/credentials", false).option("--json", "Output as JSON", false).option("--max-file-size <size>", "Skip files larger than this (e.g. 10mb, 500k)", "10mb").option("--concurrency <n>", "Max files scanned in parallel", String(SCAN_CONCURRENCY)).option("--quiet", "Suppress progress output", false).action(async (target, opts) => {
3657
- const absTarget = resolve2(target);
3658
- if (!existsSync7(absTarget)) {
3659
- console.error(chalk9.red("Error: Path not found:"), chalk9.bold(absTarget));
3831
+ const absTarget = resolve3(target);
3832
+ if (!existsSync8(absTarget)) {
3833
+ console.error(chalk10.red("Error: Path not found:"), chalk10.bold(absTarget));
3660
3834
  process.exit(1);
3661
3835
  }
3662
3836
  const maxFileSize = parseSizeOption(opts.maxFileSize);
3663
3837
  if (maxFileSize === null) {
3664
- console.error(chalk9.red("Error: Invalid --max-file-size value:"), chalk9.bold(opts.maxFileSize));
3665
- console.error(chalk9.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
3838
+ console.error(chalk10.red("Error: Invalid --max-file-size value:"), chalk10.bold(opts.maxFileSize));
3839
+ console.error(chalk10.gray("Use bytes or a unit suffix, e.g. 10mb, 500k, 1048576"));
3666
3840
  process.exit(1);
3667
3841
  }
3668
3842
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || SCAN_CONCURRENCY);
3669
3843
  if (!opts.quiet && !opts.json) {
3670
- process.stderr.write(chalk9.gray(`Discovering files in ${absTarget}...
3844
+ process.stderr.write(chalk10.gray(`Discovering files in ${absTarget}...
3671
3845
  `));
3672
3846
  }
3673
3847
  const files = collectFiles(target);
3674
3848
  if (files.length === 0) {
3675
- console.error(chalk9.red("Error: No source files found at"), chalk9.bold(absTarget));
3676
- console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3849
+ console.error(chalk10.red("Error: No source files found at"), chalk10.bold(absTarget));
3850
+ console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3677
3851
  process.exit(1);
3678
3852
  }
3679
3853
  if (!opts.quiet && !opts.json) {
3680
- process.stderr.write(chalk9.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
3854
+ process.stderr.write(chalk10.gray(`Found ${files.length} candidate files. Scanning with concurrency=${concurrency}...
3681
3855
  `));
3682
3856
  }
3683
3857
  const startedAt = Date.now();
@@ -3703,34 +3877,34 @@ program.command("scan <target>").description("Scan file or directory for identif
3703
3877
  interrupted
3704
3878
  }, null, 2));
3705
3879
  } else {
3706
- console.log(chalk9.cyan("\nPretense Scan Results\n"));
3880
+ console.log(chalk10.cyan("\nPretense Scan Results\n"));
3707
3881
  for (const r of allResults) {
3708
3882
  if (r.skipped) {
3709
3883
  if (r.skipped === "too-large") {
3710
3884
  const mb = ((r.sizeBytes ?? 0) / 1024 / 1024).toFixed(1);
3711
- console.log(chalk9.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
3885
+ console.log(chalk10.gray(` ${r.file} \u2014 skipped (${mb} MB > limit)`));
3712
3886
  }
3713
3887
  continue;
3714
3888
  }
3715
- const secretBadge = r.secrets > 0 ? chalk9.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3889
+ const secretBadge = r.secrets > 0 ? chalk10.red(` [${r.secrets} SECRET${r.secrets > 1 ? "S" : ""}]`) : "";
3716
3890
  const showRow = r.secrets > 0 || !opts.secretsOnly && r.identifiers > 0;
3717
3891
  if (!showRow) continue;
3718
3892
  console.log(
3719
- chalk9.gray(" ") + chalk9.white(r.file) + chalk9.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
3893
+ chalk10.gray(" ") + chalk10.white(r.file) + chalk10.gray(` \u2014 ${r.identifiers} identifiers`) + secretBadge
3720
3894
  );
3721
3895
  if (r.secrets > 0) {
3722
3896
  for (const t of r.secretTypes) {
3723
- console.log(chalk9.red(` ! ${t}`));
3897
+ console.log(chalk10.red(` ! ${t}`));
3724
3898
  }
3725
3899
  }
3726
3900
  }
3727
- const skipNote = skippedLarge + skippedBinary > 0 ? chalk9.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
3901
+ const skipNote = skippedLarge + skippedBinary > 0 ? chalk10.gray(` (skipped ${skippedLarge} large, ${skippedBinary} binary)`) : "";
3728
3902
  console.log(
3729
- chalk9.gray(`
3730
- Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk9.red(`${totalSecrets} secrets found`) : chalk9.green("0 secrets")) + skipNote
3903
+ chalk10.gray(`
3904
+ Total: ${allResults.length} files in ${(elapsedMs / 1e3).toFixed(2)}s, ${totalIdentifiers} identifiers, `) + (totalSecrets > 0 ? chalk10.red(`${totalSecrets} secrets found`) : chalk10.green("0 secrets")) + skipNote
3731
3905
  );
3732
3906
  if (interrupted) {
3733
- console.log(chalk9.yellow("\n Interrupted \u2014 partial results shown above."));
3907
+ console.log(chalk10.yellow("\n Interrupted \u2014 partial results shown above."));
3734
3908
  }
3735
3909
  console.log();
3736
3910
  }
@@ -3745,34 +3919,34 @@ program.command("scan <target>").description("Scan file or directory for identif
3745
3919
  }
3746
3920
  });
3747
3921
  program.command("mutate <file>").description("Mutate identifiers for stdout (scanner redacts secrets/PII in the source first)").option("-s, --seed <seed>", "Mutation seed", "pretense").option("-l, --language <lang>", "Source language (typescript, javascript, python, go, java, csharp, ruby, rust)").option("--save", "Save mutation map to .pretense/", false).option("--json", "Output as JSON (includes map + stats)", false).action((file, opts) => {
3748
- const absPath = resolve2(file);
3749
- if (!existsSync7(absPath)) {
3750
- console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
3922
+ const absPath = resolve3(file);
3923
+ if (!existsSync8(absPath)) {
3924
+ console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
3751
3925
  process.exit(1);
3752
3926
  }
3753
3927
  if (opts.language) {
3754
3928
  const validLang = validateLanguage(opts.language);
3755
3929
  if (!validLang) {
3756
- console.error(chalk9.red("Error: Unsupported language:"), chalk9.bold(opts.language));
3757
- console.error(chalk9.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
3930
+ console.error(chalk10.red("Error: Unsupported language:"), chalk10.bold(opts.language));
3931
+ console.error(chalk10.gray("Supported languages: " + SUPPORTED_LANGUAGES.join(", ")));
3758
3932
  process.exit(1);
3759
3933
  }
3760
3934
  }
3761
3935
  if (!SUPPORTED_EXTENSIONS.has(extname(absPath).toLowerCase()) && isBinaryFile(absPath)) {
3762
- console.error(chalk9.red("Error: Not a supported source file:"), chalk9.bold(absPath));
3763
- console.error(chalk9.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3936
+ console.error(chalk10.red("Error: Not a supported source file:"), chalk10.bold(absPath));
3937
+ console.error(chalk10.gray("Supported extensions: " + [...SUPPORTED_EXTENSIONS].join(", ")));
3764
3938
  process.exit(1);
3765
3939
  }
3766
- const code = readFileSync7(absPath, "utf-8");
3940
+ const code = readFileSync8(absPath, "utf-8");
3767
3941
  const lang = opts.language ? validateLanguage(opts.language) : langFromExt(absPath);
3768
3942
  const secretScan = scan2(code);
3769
- const effectiveSeed = opts.seed === "pretense" ? buildSaltedSeed(opts.seed) : opts.seed;
3943
+ const effectiveSeed = effectiveSeedForMutation(opts.seed);
3770
3944
  const { redactedCode: redacted, secretsMap } = applyRedactionsTracked(code, secretScan.matches, effectiveSeed);
3771
3945
  const secretsRedacted = secretsMap.size;
3772
3946
  const result = mutate(redacted, lang, opts.seed);
3773
3947
  if (opts.save) {
3774
3948
  const configDir = getConfigDir();
3775
- const store = new MutationStore(join5(configDir, "mutation-map.json"));
3949
+ const store = new MutationStore(join6(configDir, "mutation-map.json"));
3776
3950
  store.load();
3777
3951
  store.save({
3778
3952
  id: absPath,
@@ -3783,7 +3957,7 @@ program.command("mutate <file>").description("Mutate identifiers for stdout (sca
3783
3957
  language: lang
3784
3958
  });
3785
3959
  store.persist();
3786
- process.stderr.write(chalk9.gray(`Mutation map saved to ${configDir}/mutation-map.json
3960
+ process.stderr.write(chalk10.gray(`Mutation map saved to ${configDir}/mutation-map.json
3787
3961
  `));
3788
3962
  }
3789
3963
  if (opts.json) {
@@ -3798,27 +3972,27 @@ program.command("mutate <file>").description("Mutate identifiers for stdout (sca
3798
3972
  process.stdout.write(result.mutatedCode);
3799
3973
  if (secretsRedacted > 0) {
3800
3974
  process.stderr.write(
3801
- chalk9.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before identifier mutation ---
3975
+ chalk10.gray(`--- ${secretsRedacted} secret/PII span(s) redacted before code protection ---
3802
3976
  `)
3803
3977
  );
3804
3978
  }
3805
- process.stderr.write(chalk9.gray(`
3806
- --- ${result.stats.tokensMutated} identifiers mutated in ${result.stats.durationMs}ms ---
3979
+ process.stderr.write(chalk10.gray(`
3980
+ --- ${result.stats.tokensMutated} identifiers protected in ${result.stats.durationMs}ms ---
3807
3981
  `));
3808
3982
  printTokenFooter(1, result.stats.tokensMutated);
3809
3983
  }
3810
3984
  });
3811
3985
  program.command("reverse <file>").description("Reverse mutations using stored map").option("-m, --map <path>", "Path to mutation map JSON file").action((file, opts) => {
3812
- const absPath = resolve2(file);
3813
- if (!existsSync7(absPath)) {
3814
- console.error(chalk9.red("Error: File not found:"), chalk9.bold(absPath));
3986
+ const absPath = resolve3(file);
3987
+ if (!existsSync8(absPath)) {
3988
+ console.error(chalk10.red("Error: File not found:"), chalk10.bold(absPath));
3815
3989
  process.exit(1);
3816
3990
  }
3817
- const mutatedCode = readFileSync7(absPath, "utf-8");
3818
- const mapPath = opts.map ? resolve2(opts.map) : join5(getConfigDir(), "mutation-map.json");
3819
- if (!existsSync7(mapPath)) {
3820
- console.error(chalk9.red("Error: Mutation map not found:"), chalk9.bold(mapPath));
3821
- console.error(chalk9.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
3991
+ const mutatedCode = readFileSync8(absPath, "utf-8");
3992
+ const mapPath = opts.map ? resolve3(opts.map) : join6(getConfigDir(), "mutation-map.json");
3993
+ if (!existsSync8(mapPath)) {
3994
+ console.error(chalk10.red("Error: Mutation map not found:"), chalk10.bold(mapPath));
3995
+ console.error(chalk10.gray("Run 'pretense mutate --save' first, or specify --map <path>"));
3822
3996
  process.exit(1);
3823
3997
  }
3824
3998
  let store;
@@ -3826,14 +4000,14 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
3826
4000
  store = new MutationStore(mapPath);
3827
4001
  store.load();
3828
4002
  } catch {
3829
- console.error(chalk9.red("Error: Failed to load mutation map:"), chalk9.bold(mapPath));
3830
- console.error(chalk9.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
4003
+ console.error(chalk10.red("Error: Failed to load mutation map:"), chalk10.bold(mapPath));
4004
+ console.error(chalk10.gray("The file may be corrupted. Run 'pretense mutate --save' to regenerate."));
3831
4005
  process.exit(1);
3832
4006
  }
3833
4007
  const entry = store.get(absPath) ?? store.getByHash(absPath) ?? store.list(1)[0];
3834
4008
  if (!entry) {
3835
- console.error(chalk9.red("Error: No mutation map entries found."));
3836
- console.error(chalk9.gray("Run 'pretense mutate --save <file>' first to generate a map."));
4009
+ console.error(chalk10.red("Error: No mutation map entries found."));
4010
+ console.error(chalk10.gray("Run 'pretense mutate --save <file>' first to generate a map."));
3837
4011
  process.exit(1);
3838
4012
  }
3839
4013
  const map = MutationStore.toMap(entry);
@@ -3842,8 +4016,8 @@ program.command("reverse <file>").description("Reverse mutations using stored ma
3842
4016
  process.stdout.write(restored);
3843
4017
  const secretsCount = secretsMap?.size ?? 0;
3844
4018
  const secretsSuffix = secretsCount > 0 ? ` + ${secretsCount} secret(s)` : "";
3845
- process.stderr.write(chalk9.gray(`
3846
- --- Reversed ${map.size} mutations${secretsSuffix} ---
4019
+ process.stderr.write(chalk10.gray(`
4020
+ --- Reversed ${map.size} security tokens${secretsSuffix} ---
3847
4021
  `));
3848
4022
  });
3849
4023
  program.command("audit").description("View the audit log").option("--json", "Output as JSON", false).option("--csv", "Output as CSV", false).option("-l, --limit <number>", "Limit entries", "50").action((opts) => {
@@ -3862,7 +4036,7 @@ program.command("status").description("Show current plan, monthly quota, seat us
3862
4036
  program.command("usage").description("Alias for `pretense status` \u2014 show monthly quota usage").action(async () => {
3863
4037
  await runStatus();
3864
4038
  });
3865
- program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining mutation budget").action(async () => {
4039
+ program.command("tokens").description("Alias for `pretense credits` \u2014 show remaining security token budget").action(async () => {
3866
4040
  await runCredits();
3867
4041
  });
3868
4042
  program.command("upgrade").description("Compare plans and upgrade to Pro or Enterprise").action(() => {
@@ -3871,19 +4045,20 @@ program.command("upgrade").description("Compare plans and upgrade to Pro or Ente
3871
4045
  program.command("credits").description("Show remaining mutation/scan budget for the current month").action(async () => {
3872
4046
  await runCredits();
3873
4047
  });
3874
- program.command("login").description("Save a Pretense API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
4048
+ program.command("login").description("Save a dashboard API key to ~/.pretense/config.json for future CLI runs").option("-k, --key <key>", "API key (prtns_live_... or pk_live_...). If omitted, read from $PRETENSE_API_KEY or stdin.").action((opts) => {
3875
4049
  const code = runLogin({ key: opts.key });
3876
4050
  if (code !== 0) process.exit(code);
3877
4051
  });
3878
- program.command("logout").description("Remove the saved Pretense dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
4052
+ program.command("logout").description("Remove the saved dashboard API key from ~/.pretense/config.json").option("-y, --yes", "Skip confirmation prompt", false).action(async (opts) => {
3879
4053
  const code = await runLogout({ assumeYes: opts.yes === true });
3880
4054
  if (code !== 0) process.exit(code);
3881
4055
  });
4056
+ program.addCommand(installCommand());
3882
4057
  function handleFatalError(err) {
3883
4058
  if (err instanceof Error && err.message) {
3884
- console.error(chalk9.red("Error:"), err.message);
4059
+ console.error(chalk10.red("Error:"), err.message);
3885
4060
  } else {
3886
- console.error(chalk9.red("An unexpected error occurred."));
4061
+ console.error(chalk10.red("An unexpected error occurred."));
3887
4062
  }
3888
4063
  process.exit(1);
3889
4064
  }