agentapprove 0.1.10 → 0.1.11

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.
Files changed (2) hide show
  1. package/dist/cli.js +464 -105
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2327,12 +2327,185 @@ var source_default = chalk;
2327
2327
 
2328
2328
  // src/cli.ts
2329
2329
  var import_qrcode_terminal = __toESM(require_main(), 1);
2330
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, readdirSync, statSync } from "fs";
2330
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, readdirSync as readdirSync2, statSync, lstatSync, realpathSync as realpathSync2, rmSync as rmSync2 } from "fs";
2331
2331
  import { homedir, hostname, platform } from "os";
2332
- import { join, dirname, basename } from "path";
2332
+ import { join, dirname as dirname2 } from "path";
2333
2333
  import { execSync, spawnSync } from "child_process";
2334
2334
  import { randomBytes, createHash } from "crypto";
2335
- var VERSION = "0.1.10";
2335
+
2336
+ // src/uninstall-utils.ts
2337
+ import { existsSync, readdirSync, realpathSync, rmSync } from "fs";
2338
+ import { basename, dirname, sep } from "path";
2339
+ var INSTALLER_COMMAND_DESCRIPTIONS = {
2340
+ uninstall: "Remove hooks/plugins from agent configs and keep local Agent Approve state",
2341
+ purge: "Remove hooks/plugins and delete local Agent Approve state"
2342
+ };
2343
+ function isPathWithinBase(baseDir, candidatePath) {
2344
+ const safeBase = sanitizeAbsolutePath(baseDir);
2345
+ const safeCandidate = sanitizeAbsolutePath(candidatePath);
2346
+ if (!safeBase || !safeCandidate) {
2347
+ return false;
2348
+ }
2349
+ return safeCandidate === safeBase || safeCandidate.startsWith(`${safeBase}${sep}`);
2350
+ }
2351
+ function safeJoinWithinBase(baseDir, ...parts) {
2352
+ const safeBase = sanitizeAbsolutePath(baseDir);
2353
+ if (!safeBase || parts.some((part) => !isSafeChildPathPart(part))) {
2354
+ return null;
2355
+ }
2356
+ const childSegments = parts.flatMap(splitPathSegments);
2357
+ const candidatePath = appendPathSegments(safeBase, childSegments);
2358
+ if (!isPathWithinBase(safeBase, candidatePath)) {
2359
+ return null;
2360
+ }
2361
+ return candidatePath;
2362
+ }
2363
+ function sanitizeAbsolutePath(path) {
2364
+ if (!path || path.includes("\x00")) {
2365
+ return null;
2366
+ }
2367
+ const root = getPathRoot(path);
2368
+ if (!root) {
2369
+ return null;
2370
+ }
2371
+ const segments = splitPathSegments(path);
2372
+ if (segments.includes("..")) {
2373
+ return null;
2374
+ }
2375
+ if (root === sep) {
2376
+ return segments.length > 0 ? `${sep}${segments.join(sep)}` : sep;
2377
+ }
2378
+ const [, ...rest] = segments;
2379
+ return rest.length > 0 ? `${root}${rest.join(sep)}` : root;
2380
+ }
2381
+ function isSafeChildPathPart(part) {
2382
+ if (!part || part.includes("\x00") || getPathRoot(part)) {
2383
+ return false;
2384
+ }
2385
+ const segments = splitPathSegments(part);
2386
+ if (segments.some((segment) => segment === "..")) {
2387
+ return false;
2388
+ }
2389
+ return true;
2390
+ }
2391
+ function getPathRoot(path) {
2392
+ if (path.startsWith("/") || path.startsWith("\\")) {
2393
+ return sep;
2394
+ }
2395
+ const windowsDrive = path.match(/^[A-Za-z]:[\\/]/);
2396
+ if (windowsDrive) {
2397
+ return `${windowsDrive[0][0]}:${sep}`;
2398
+ }
2399
+ return null;
2400
+ }
2401
+ function splitPathSegments(path) {
2402
+ return path.split(/[\\/]+/).filter(Boolean).filter((segment) => segment !== ".");
2403
+ }
2404
+ function appendPathSegments(basePath, childSegments) {
2405
+ if (childSegments.length === 0) {
2406
+ return basePath;
2407
+ }
2408
+ return basePath.endsWith(sep) ? `${basePath}${childSegments.join(sep)}` : `${basePath}${sep}${childSegments.join(sep)}`;
2409
+ }
2410
+ function createRemovalTarget(path, kind, recursive = false) {
2411
+ if (!existsSync(path)) {
2412
+ return null;
2413
+ }
2414
+ let actualPath = path;
2415
+ let displayPath = path;
2416
+ try {
2417
+ actualPath = realpathSync(path);
2418
+ if (actualPath !== path) {
2419
+ displayPath = `${path} -> ${actualPath}`;
2420
+ }
2421
+ } catch {}
2422
+ return { path, actualPath, displayPath, kind, recursive };
2423
+ }
2424
+ function removeRemovalTarget(target) {
2425
+ if (!existsSync(target.path) && !existsSync(target.actualPath)) {
2426
+ return false;
2427
+ }
2428
+ try {
2429
+ rmSync(target.actualPath, { recursive: target.recursive, force: true });
2430
+ if (target.path !== target.actualPath) {
2431
+ rmSync(target.path, { recursive: false, force: true });
2432
+ }
2433
+ return true;
2434
+ } catch {
2435
+ return false;
2436
+ }
2437
+ }
2438
+ function getOpenClawPluginTargets(openclawConfigPath) {
2439
+ const configDir = dirname(openclawConfigPath);
2440
+ const openclawPath = safeJoinWithinBase(configDir, "extensions", "openclaw");
2441
+ const legacyPath = safeJoinWithinBase(configDir, "extensions", "agentapprove");
2442
+ return [
2443
+ openclawPath ? createRemovalTarget(openclawPath, "plugin_artifact", true) : null,
2444
+ legacyPath ? createRemovalTarget(legacyPath, "plugin_artifact", true) : null
2445
+ ].filter((target) => target !== null);
2446
+ }
2447
+ function getOpenCodePluginTargets(opencodeConfigDir) {
2448
+ const pluginPath = safeJoinWithinBase(opencodeConfigDir, "node_modules", "@agentapprove", "opencode");
2449
+ return [
2450
+ pluginPath ? createRemovalTarget(pluginPath, "plugin_artifact", true) : null
2451
+ ].filter((target) => target !== null);
2452
+ }
2453
+ function collectBackupTargets(configPaths) {
2454
+ const targets = [];
2455
+ for (const configPath of configPaths) {
2456
+ const configDir = dirname(configPath);
2457
+ if (!existsSync(configDir)) {
2458
+ continue;
2459
+ }
2460
+ const configBase = basename(configPath);
2461
+ for (const entry of readdirSync(configDir)) {
2462
+ if (!entry.startsWith(`${configBase}.backup.`)) {
2463
+ continue;
2464
+ }
2465
+ const backupPath = safeJoinWithinBase(configDir, entry);
2466
+ const target = backupPath ? createRemovalTarget(backupPath, "backup") : null;
2467
+ if (target) {
2468
+ targets.push(target);
2469
+ }
2470
+ }
2471
+ }
2472
+ return targets;
2473
+ }
2474
+ function getManagedStateTargets(agentApproveDir) {
2475
+ const reversible = [
2476
+ safeJoinWithinBase(agentApproveDir, "env"),
2477
+ safeJoinWithinBase(agentApproveDir, "env.disabled"),
2478
+ safeJoinWithinBase(agentApproveDir, "hook-debug.log"),
2479
+ safeJoinWithinBase(agentApproveDir, "hooks"),
2480
+ safeJoinWithinBase(agentApproveDir, "install.sh"),
2481
+ safeJoinWithinBase(agentApproveDir, "copilot-cli-hooks.json")
2482
+ ].map((path) => path ? createRemovalTarget(path, "local_state", path.endsWith(`${sep}hooks`)) : null).filter((target) => target !== null);
2483
+ const crypto = [
2484
+ safeJoinWithinBase(agentApproveDir, "e2e-key"),
2485
+ safeJoinWithinBase(agentApproveDir, "e2e-root-key"),
2486
+ safeJoinWithinBase(agentApproveDir, "e2e-server-key"),
2487
+ safeJoinWithinBase(agentApproveDir, "e2e-rotation.json")
2488
+ ].map((path) => path ? createRemovalTarget(path, "crypto_material") : null).filter((target) => target !== null);
2489
+ if (existsSync(agentApproveDir)) {
2490
+ for (const entry of readdirSync(agentApproveDir)) {
2491
+ if (!/^e2e-key\.[^.]+\.bak$/.test(entry)) {
2492
+ continue;
2493
+ }
2494
+ const cryptoPath = safeJoinWithinBase(agentApproveDir, entry);
2495
+ const target = cryptoPath ? createRemovalTarget(cryptoPath, "crypto_material") : null;
2496
+ if (target) {
2497
+ crypto.push(target);
2498
+ }
2499
+ }
2500
+ }
2501
+ return { reversible, crypto };
2502
+ }
2503
+ function shouldDeleteCryptoMaterial(purgeConfirmed, firstKeyConfirmation, secondKeyConfirmation) {
2504
+ return purgeConfirmed && firstKeyConfirmation && secondKeyConfirmation;
2505
+ }
2506
+
2507
+ // src/cli.ts
2508
+ var VERSION = "0.1.11";
2336
2509
  function getApiUrl() {
2337
2510
  return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
2338
2511
  }
@@ -2352,7 +2525,7 @@ function hasFlag2(flag) {
2352
2525
  }
2353
2526
  function updateEnvValue(key, value) {
2354
2527
  const envPath = join(getAgentApproveDir(), "env");
2355
- if (!existsSync(envPath))
2528
+ if (!existsSync2(envPath))
2356
2529
  return;
2357
2530
  let content = readFileSync(envPath, "utf-8");
2358
2531
  const pattern = new RegExp(`^${key}=.*$`, "m");
@@ -2380,7 +2553,7 @@ function getCommand() {
2380
2553
  }
2381
2554
  return filtered[0] || "install";
2382
2555
  }
2383
- var OPENCODE_PLUGIN_VERSION = "0.1.6";
2556
+ var OPENCODE_PLUGIN_VERSION = "0.1.7";
2384
2557
  var OPENCLAW_PLUGIN_VERSION = "0.2.5";
2385
2558
  var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
2386
2559
  var AGENTS = {
@@ -2505,7 +2678,7 @@ function findGitBash() {
2505
2678
  join(process.env.ProgramW6432 || "", "Git", "bin", "bash.exe")
2506
2679
  ].filter(Boolean);
2507
2680
  for (const candidate of candidates) {
2508
- if (existsSync(candidate)) {
2681
+ if (existsSync2(candidate)) {
2509
2682
  return candidate;
2510
2683
  }
2511
2684
  }
@@ -2513,7 +2686,7 @@ function findGitBash() {
2513
2686
  const result = execSync("where bash", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
2514
2687
  const firstLine = result.trim().split(`
2515
2688
  `)[0];
2516
- if (firstLine && existsSync(firstLine)) {
2689
+ if (firstLine && existsSync2(firstLine)) {
2517
2690
  return firstLine;
2518
2691
  }
2519
2692
  } catch {}
@@ -2550,7 +2723,7 @@ function getVSCodeSettingsPaths() {
2550
2723
  return paths;
2551
2724
  }
2552
2725
  function findInstalledVSCodeVariants() {
2553
- return getVSCodeSettingsPaths().filter(({ path }) => existsSync(dirname(path)));
2726
+ return getVSCodeSettingsPaths().filter(({ path }) => existsSync2(dirname2(path)));
2554
2727
  }
2555
2728
  function stripJsonComments(content) {
2556
2729
  let result = "";
@@ -2599,7 +2772,7 @@ function stripJsonComments(content) {
2599
2772
  return result;
2600
2773
  }
2601
2774
  function readJsoncConfig(configPath) {
2602
- if (!existsSync(configPath))
2775
+ if (!existsSync2(configPath))
2603
2776
  return {};
2604
2777
  try {
2605
2778
  const content = readFileSync(configPath, "utf-8");
@@ -2640,7 +2813,7 @@ function removeFromVSCodeHookLocations() {
2640
2813
  const hookLocation = "~/.agentapprove/hooks";
2641
2814
  const modified = [];
2642
2815
  for (const { path: settingsPath, variant } of getVSCodeSettingsPaths()) {
2643
- if (!existsSync(settingsPath))
2816
+ if (!existsSync2(settingsPath))
2644
2817
  continue;
2645
2818
  try {
2646
2819
  const config = readJsoncConfig(settingsPath);
@@ -2667,24 +2840,24 @@ function detectInstalledAgents() {
2667
2840
  installed.push(id);
2668
2841
  }
2669
2842
  } else if (id === "copilot-cli") {
2670
- if (existsSync(join(homedir(), ".copilot"))) {
2843
+ if (existsSync2(join(homedir(), ".copilot"))) {
2671
2844
  installed.push(id);
2672
2845
  }
2673
2846
  } else if (id === "openclaw") {
2674
- if (existsSync(join(homedir(), ".openclaw"))) {
2847
+ if (existsSync2(join(homedir(), ".openclaw"))) {
2675
2848
  installed.push(id);
2676
2849
  }
2677
2850
  } else if (id === "codex") {
2678
- if (existsSync(join(homedir(), ".codex"))) {
2851
+ if (existsSync2(join(homedir(), ".codex"))) {
2679
2852
  installed.push(id);
2680
2853
  }
2681
2854
  } else if (id === "opencode") {
2682
- if (existsSync(getOpenCodeConfigDir())) {
2855
+ if (existsSync2(getOpenCodeConfigDir())) {
2683
2856
  installed.push(id);
2684
2857
  }
2685
2858
  } else {
2686
- const configDir = dirname(agent.configPath);
2687
- if (existsSync(configDir)) {
2859
+ const configDir = dirname2(agent.configPath);
2860
+ if (existsSync2(configDir)) {
2688
2861
  installed.push(id);
2689
2862
  }
2690
2863
  }
@@ -2694,9 +2867,124 @@ function detectInstalledAgents() {
2694
2867
  function getAgentApproveDir() {
2695
2868
  return join(homedir(), ".agentapprove");
2696
2869
  }
2870
+ function uniqueRemovalTargets(targets) {
2871
+ const seen = new Set;
2872
+ const unique = [];
2873
+ for (const target of targets) {
2874
+ const key = `${target.kind}:${target.actualPath}`;
2875
+ if (seen.has(key)) {
2876
+ continue;
2877
+ }
2878
+ seen.add(key);
2879
+ unique.push(target);
2880
+ }
2881
+ return unique;
2882
+ }
2883
+ function buildUninstallPlan() {
2884
+ const configuredAgents = detectInstalledAgents();
2885
+ const pluginArtifactTargets = uniqueRemovalTargets([
2886
+ ...getOpenClawPluginTargets(AGENTS.openclaw.configPath),
2887
+ ...getOpenCodePluginTargets(getOpenCodeConfigDir())
2888
+ ]);
2889
+ return {
2890
+ configuredAgents,
2891
+ pluginArtifactTargets,
2892
+ backupTargets: uniqueRemovalTargets(collectBackupTargets(Object.values(AGENTS).map((agent) => agent.configPath))),
2893
+ managedState: getManagedStateTargets(getAgentApproveDir())
2894
+ };
2895
+ }
2896
+ function hasUninstallWork(mode, plan) {
2897
+ if (plan.configuredAgents.length > 0 || plan.pluginArtifactTargets.length > 0) {
2898
+ return true;
2899
+ }
2900
+ if (mode === "purge") {
2901
+ return plan.backupTargets.length > 0 || plan.managedState.reversible.length > 0 || plan.managedState.crypto.length > 0;
2902
+ }
2903
+ return false;
2904
+ }
2905
+ function printRemovalTargets(title, targets) {
2906
+ if (targets.length === 0) {
2907
+ return;
2908
+ }
2909
+ console.log(source_default.dim(` ${title}`));
2910
+ for (const target of targets) {
2911
+ console.log(source_default.dim(` - ${target.displayPath}`));
2912
+ }
2913
+ }
2914
+ async function confirmUninstallPlan(mode, plan) {
2915
+ console.log();
2916
+ console.log(source_default.cyan(` ${mode === "purge" ? "Purge" : "Uninstall"} plan`));
2917
+ if (plan.configuredAgents.length > 0) {
2918
+ console.log(source_default.dim(` Agent configs: ${plan.configuredAgents.join(", ")}`));
2919
+ }
2920
+ printRemovalTargets("Plugin files to remove:", plan.pluginArtifactTargets);
2921
+ if (mode === "purge") {
2922
+ printRemovalTargets("Local Agent Approve files to remove:", plan.managedState.reversible);
2923
+ printRemovalTargets("Backups to remove:", plan.backupTargets);
2924
+ printRemovalTargets("Irreversible E2E material to remove:", plan.managedState.crypto);
2925
+ } else {
2926
+ console.log(source_default.dim(" Local Agent Approve state kept:"));
2927
+ console.log(source_default.dim(" - ~/.agentapprove/env and env.disabled"));
2928
+ console.log(source_default.dim(" - ~/.agentapprove/hooks, install.sh, and hook-debug.log"));
2929
+ console.log(source_default.dim(" - E2E keys and rotation metadata"));
2930
+ console.log(source_default.dim(" - OS credential-store token and config backups"));
2931
+ }
2932
+ console.log();
2933
+ const proceed = await ce({
2934
+ message: mode === "purge" ? "Proceed with purge? This removes agent hooks/plugins and local Agent Approve state." : "Proceed with uninstall? This removes agent hooks/plugins but keeps local Agent Approve state."
2935
+ });
2936
+ if (lD(proceed) || !proceed) {
2937
+ ge("Cancelled.");
2938
+ return false;
2939
+ }
2940
+ if (mode === "purge" && plan.managedState.crypto.length > 0) {
2941
+ const firstKeyConfirm = await ce({
2942
+ message: "Delete E2E keys and rotation metadata? You may lose access to encrypted local history on this machine."
2943
+ });
2944
+ if (lD(firstKeyConfirm) || !firstKeyConfirm) {
2945
+ v2.warn("Purge cancelled. Run `npx agentapprove uninstall` if you want to keep local keys and config.");
2946
+ return false;
2947
+ }
2948
+ const secondKeyConfirm = await ce({
2949
+ message: "Final confirmation: permanently delete the E2E keys and rotation metadata?"
2950
+ });
2951
+ if (lD(secondKeyConfirm) || !shouldDeleteCryptoMaterial(true, firstKeyConfirm === true, secondKeyConfirm === true)) {
2952
+ v2.warn("Purge cancelled. Run `npx agentapprove uninstall` if you want to keep local keys and config.");
2953
+ return false;
2954
+ }
2955
+ }
2956
+ return true;
2957
+ }
2958
+ function pruneEmptyDir(path) {
2959
+ if (!existsSync2(path)) {
2960
+ return;
2961
+ }
2962
+ try {
2963
+ if (readdirSync2(path).length === 0) {
2964
+ rmSync2(path, { recursive: false, force: true });
2965
+ }
2966
+ } catch {}
2967
+ }
2968
+ function finalizeAgentApproveDirPurge(agentApproveDir) {
2969
+ if (!existsSync2(agentApproveDir)) {
2970
+ return;
2971
+ }
2972
+ try {
2973
+ const stat = lstatSync(agentApproveDir);
2974
+ if (stat.isSymbolicLink()) {
2975
+ const actualDir = realpathSync2(agentApproveDir);
2976
+ if (existsSync2(actualDir) && readdirSync2(actualDir).length === 0) {
2977
+ rmSync2(actualDir, { recursive: false, force: true });
2978
+ }
2979
+ rmSync2(agentApproveDir, { recursive: false, force: true });
2980
+ return;
2981
+ }
2982
+ } catch {}
2983
+ pruneEmptyDir(agentApproveDir);
2984
+ }
2697
2985
  function readExistingConfig() {
2698
2986
  const envPath = join(getAgentApproveDir(), "env");
2699
- if (!existsSync(envPath)) {
2987
+ if (!existsSync2(envPath)) {
2700
2988
  return null;
2701
2989
  }
2702
2990
  try {
@@ -2727,6 +3015,9 @@ function readExistingConfig() {
2727
3015
  case "AGENTAPPROVE_E2E_MODE":
2728
3016
  config.e2eMode = value;
2729
3017
  break;
3018
+ case "AGENTAPPROVE_E2E_ENABLED":
3019
+ config.e2eEnabled = value === "true";
3020
+ break;
2730
3021
  case "AGENTAPPROVE_FAIL_BEHAVIOR":
2731
3022
  config.failBehavior = value;
2732
3023
  break;
@@ -2739,11 +3030,11 @@ function readExistingConfig() {
2739
3030
  }
2740
3031
  function discoverE2EKeys() {
2741
3032
  const dir = getAgentApproveDir();
2742
- if (!existsSync(dir))
3033
+ if (!existsSync2(dir))
2743
3034
  return [];
2744
3035
  const keys = [];
2745
3036
  const currentPath = join(dir, "e2e-key");
2746
- if (existsSync(currentPath)) {
3037
+ if (existsSync2(currentPath)) {
2747
3038
  try {
2748
3039
  const hex = readFileSync(currentPath, "utf-8").trim();
2749
3040
  if (hex.length === 64) {
@@ -2753,7 +3044,7 @@ function discoverE2EKeys() {
2753
3044
  } catch {}
2754
3045
  }
2755
3046
  try {
2756
- const files = readdirSync(dir).filter((f2) => f2.startsWith("e2e-key.") && f2.endsWith(".bak"));
3047
+ const files = readdirSync2(dir).filter((f2) => f2.startsWith("e2e-key.") && f2.endsWith(".bak"));
2757
3048
  for (const file of files) {
2758
3049
  const fullPath = join(dir, file);
2759
3050
  try {
@@ -2769,7 +3060,7 @@ function discoverE2EKeys() {
2769
3060
  }
2770
3061
  function readRotationConfig() {
2771
3062
  const configPath = join(getAgentApproveDir(), "e2e-rotation.json");
2772
- if (!existsSync(configPath))
3063
+ if (!existsSync2(configPath))
2773
3064
  return null;
2774
3065
  try {
2775
3066
  return JSON.parse(readFileSync(configPath, "utf-8"));
@@ -2783,11 +3074,11 @@ function writeRotationConfig(config) {
2783
3074
  }
2784
3075
  function ensureAgentApproveDir() {
2785
3076
  const dir = getAgentApproveDir();
2786
- if (!existsSync(dir)) {
3077
+ if (!existsSync2(dir)) {
2787
3078
  mkdirSync(dir, { recursive: true, mode: 448 });
2788
3079
  }
2789
3080
  const hooksDir = join(dir, "hooks");
2790
- if (!existsSync(hooksDir)) {
3081
+ if (!existsSync2(hooksDir)) {
2791
3082
  mkdirSync(hooksDir, { recursive: true, mode: 448 });
2792
3083
  }
2793
3084
  }
@@ -2796,14 +3087,14 @@ function migrateE2ERootKey() {
2796
3087
  const keyPath = join(dir, "e2e-key");
2797
3088
  const rootKeyPath = join(dir, "e2e-root-key");
2798
3089
  const rotationPath = join(dir, "e2e-rotation.json");
2799
- if (!existsSync(keyPath) || existsSync(rootKeyPath))
3090
+ if (!existsSync2(keyPath) || existsSync2(rootKeyPath))
2800
3091
  return;
2801
3092
  try {
2802
3093
  const keyHex = readFileSync(keyPath, "utf-8").trim();
2803
3094
  if (keyHex.length !== 64)
2804
3095
  return;
2805
3096
  writeFileSync(rootKeyPath, keyHex, { mode: 384 });
2806
- if (!existsSync(rotationPath)) {
3097
+ if (!existsSync2(rotationPath)) {
2807
3098
  const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
2808
3099
  writeRotationConfig({
2809
3100
  rootKeyId: keyId,
@@ -2815,7 +3106,7 @@ function migrateE2ERootKey() {
2815
3106
  } catch {}
2816
3107
  }
2817
3108
  function backupConfig(configPath) {
2818
- if (!existsSync(configPath)) {
3109
+ if (!existsSync2(configPath)) {
2819
3110
  return null;
2820
3111
  }
2821
3112
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
@@ -2824,7 +3115,7 @@ function backupConfig(configPath) {
2824
3115
  return backupPath;
2825
3116
  }
2826
3117
  function readJsonConfig(configPath) {
2827
- if (!existsSync(configPath)) {
3118
+ if (!existsSync2(configPath)) {
2828
3119
  return {};
2829
3120
  }
2830
3121
  try {
@@ -2835,21 +3126,21 @@ function readJsonConfig(configPath) {
2835
3126
  }
2836
3127
  }
2837
3128
  function writeJsonConfig(configPath, config) {
2838
- const dir = dirname(configPath);
2839
- if (!existsSync(dir)) {
3129
+ const dir = dirname2(configPath);
3130
+ if (!existsSync2(dir)) {
2840
3131
  mkdirSync(dir, { recursive: true, mode: 448 });
2841
3132
  }
2842
3133
  writeFileSync(configPath, JSON.stringify(config, null, 2) + `
2843
3134
  `);
2844
3135
  }
2845
3136
  function ensureCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.toml")) {
2846
- const dir = dirname(configPath);
2847
- if (!existsSync(dir)) {
3137
+ const dir = dirname2(configPath);
3138
+ if (!existsSync2(dir)) {
2848
3139
  mkdirSync(dir, { recursive: true, mode: 448 });
2849
3140
  }
2850
3141
  const sectionHeader = "[features]";
2851
3142
  const desiredLine = "codex_hooks = true";
2852
- if (!existsSync(configPath)) {
3143
+ if (!existsSync2(configPath)) {
2853
3144
  writeFileSync(configPath, `${sectionHeader}
2854
3145
  ${desiredLine}
2855
3146
  `, { mode: 384 });
@@ -2909,7 +3200,7 @@ ${desiredLine}
2909
3200
  return { updated: true, backupPath };
2910
3201
  }
2911
3202
  function disableCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.toml")) {
2912
- if (!existsSync(configPath)) {
3203
+ if (!existsSync2(configPath)) {
2913
3204
  return { updated: false, backupPath: null };
2914
3205
  }
2915
3206
  const original = readFileSync(configPath, "utf-8");
@@ -3212,13 +3503,13 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3212
3503
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
3213
3504
  try {
3214
3505
  let pkgJson = {};
3215
- if (existsSync(opencodePkgPath)) {
3506
+ if (existsSync2(opencodePkgPath)) {
3216
3507
  pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
3217
3508
  }
3218
3509
  if (!pkgJson.dependencies) {
3219
3510
  pkgJson.dependencies = {};
3220
3511
  }
3221
- pkgJson.dependencies["@agentapprove/opencode"] = "latest";
3512
+ pkgJson.dependencies["@agentapprove/opencode"] = OPENCODE_PLUGIN_VERSION;
3222
3513
  writeFileSync(opencodePkgPath, JSON.stringify(pkgJson, null, 2) + `
3223
3514
  `);
3224
3515
  } catch (err) {
@@ -3968,7 +4259,7 @@ async function installCommand() {
3968
4259
  const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
3969
4260
  const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
3970
4261
  let e2eLine = "";
3971
- if (existsSync(e2eKeyPath2)) {
4262
+ if (existsSync2(e2eKeyPath2)) {
3972
4263
  const keyHex = readFileSync(e2eKeyPath2, "utf-8").trim();
3973
4264
  const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
3974
4265
  e2eLine = `
@@ -4072,7 +4363,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4072
4363
  if (connectionMethod === "existing") {
4073
4364
  token = existingConfig.token;
4074
4365
  apiUrl = existingConfig.apiUrl || API_URL;
4075
- const e2eKeyExists = existsSync(join(getAgentApproveDir(), "e2e-key"));
4366
+ const e2eKeyExists = existsSync2(join(getAgentApproveDir(), "e2e-key"));
4076
4367
  useE2E = e2eKeyExists;
4077
4368
  v2.success("Using existing token");
4078
4369
  } else if (connectionMethod === "qr") {
@@ -4082,7 +4373,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4082
4373
  const existingKeyPath = join(agentApproveDir, "e2e-key");
4083
4374
  let e2eUserKey = null;
4084
4375
  if (useE2E) {
4085
- if (existsSync(existingKeyPath)) {
4376
+ if (existsSync2(existingKeyPath)) {
4086
4377
  const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
4087
4378
  const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
4088
4379
  v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
@@ -4230,7 +4521,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4230
4521
  downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files)`);
4231
4522
  }
4232
4523
  const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
4233
- const hasE2EKey = existsSync(e2eKeyPath);
4524
+ const hasE2EKey = existsSync2(e2eKeyPath);
4234
4525
  let installMode = "approval";
4235
4526
  if (hasE2EKey) {
4236
4527
  const modeChoice = await le({
@@ -4259,7 +4550,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4259
4550
  v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
4260
4551
  }
4261
4552
  const envPath = join(getAgentApproveDir(), "env");
4262
- if (existsSync(envPath)) {
4553
+ if (existsSync2(envPath)) {
4263
4554
  let envContent = readFileSync(envPath, "utf-8");
4264
4555
  if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
4265
4556
  envContent = envContent.replace(/AGENTAPPROVE_E2E_MODE=\w+/, `AGENTAPPROVE_E2E_MODE=${installMode}`);
@@ -4360,7 +4651,8 @@ Backups will be created with timestamp`, "Files to be modified");
4360
4651
  npx agentapprove pair Link a new iOS device
4361
4652
  npx agentapprove status Show current configuration
4362
4653
  npx agentapprove disable Temporarily disable hooks
4363
- npx agentapprove uninstall Remove all hooks and config
4654
+ npx agentapprove uninstall Remove hooks/plugins and keep local state
4655
+ npx agentapprove purge Remove hooks/plugins and delete local state
4364
4656
  npx agentapprove restore Restore original configs`, "Useful commands");
4365
4657
  ge(`${source_default.green("Agent Approve is ready!")} ${source_default.dim("Learn more at")} ${source_default.cyan("agentapprove.com")}`);
4366
4658
  }
@@ -4369,7 +4661,7 @@ async function statusCommand() {
4369
4661
  Agent Approve Status
4370
4662
  `));
4371
4663
  const envPath = join(getAgentApproveDir(), "env");
4372
- if (!existsSync(envPath)) {
4664
+ if (!existsSync2(envPath)) {
4373
4665
  console.log(source_default.yellow(" Not configured. Run `npx agentapprove` to set up.\n"));
4374
4666
  return;
4375
4667
  }
@@ -4386,18 +4678,18 @@ async function statusCommand() {
4386
4678
  }
4387
4679
  const e2eKeyFile = join(getAgentApproveDir(), "e2e-key");
4388
4680
  const e2eServerKeyFile = join(getAgentApproveDir(), "e2e-server-key");
4389
- if (existsSync(e2eKeyFile)) {
4681
+ if (existsSync2(e2eKeyFile)) {
4390
4682
  console.log(` ${source_default.green("✓")} E2E user key: installed`);
4391
4683
  }
4392
- if (existsSync(e2eServerKeyFile)) {
4684
+ if (existsSync2(e2eServerKeyFile)) {
4393
4685
  console.log(` ${source_default.green("✓")} E2E server key: installed`);
4394
4686
  }
4395
- if (!existsSync(e2eKeyFile) && !existsSync(e2eServerKeyFile)) {
4687
+ if (!existsSync2(e2eKeyFile) && !existsSync2(e2eServerKeyFile)) {
4396
4688
  console.log(` ${source_default.dim(" E2E encryption: not configured")}`);
4397
4689
  }
4398
4690
  console.log();
4399
4691
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4400
- if (existsSync(agent.configPath)) {
4692
+ if (existsSync2(agent.configPath)) {
4401
4693
  const config = readJsonConfig(agent.configPath);
4402
4694
  if (agentId === "opencode") {
4403
4695
  const pluginConfig = config.plugin;
@@ -4433,8 +4725,8 @@ async function statusCommand() {
4433
4725
  async function disableCommand() {
4434
4726
  const envPath = join(getAgentApproveDir(), "env");
4435
4727
  const disabledPath = join(getAgentApproveDir(), "env.disabled");
4436
- if (!existsSync(envPath)) {
4437
- if (existsSync(disabledPath)) {
4728
+ if (!existsSync2(envPath)) {
4729
+ if (existsSync2(disabledPath)) {
4438
4730
  console.log(source_default.yellow(`
4439
4731
  Already disabled.
4440
4732
  `));
@@ -4453,13 +4745,13 @@ async function disableCommand() {
4453
4745
  async function enableCommand() {
4454
4746
  const envPath = join(getAgentApproveDir(), "env");
4455
4747
  const disabledPath = join(getAgentApproveDir(), "env.disabled");
4456
- if (existsSync(envPath)) {
4748
+ if (existsSync2(envPath)) {
4457
4749
  console.log(source_default.yellow(`
4458
4750
  Already enabled.
4459
4751
  `));
4460
4752
  return;
4461
4753
  }
4462
- if (!existsSync(disabledPath)) {
4754
+ if (!existsSync2(disabledPath)) {
4463
4755
  console.log(source_default.yellow("\n Not configured. Run `npx agentapprove` to set up.\n"));
4464
4756
  return;
4465
4757
  }
@@ -4469,12 +4761,22 @@ async function enableCommand() {
4469
4761
  ✓ Agent Approve enabled.
4470
4762
  `));
4471
4763
  }
4472
- async function uninstallCommand() {
4764
+ async function performUninstall(mode) {
4765
+ const plan = buildUninstallPlan();
4766
+ if (!hasUninstallWork(mode, plan)) {
4767
+ console.log(source_default.yellow(`
4768
+ Nothing to ${mode}. Run \`npx agentapprove install\` to set up Agent Approve.
4769
+ `));
4770
+ return;
4771
+ }
4772
+ if (!await confirmUninstallPlan(mode, plan)) {
4773
+ return;
4774
+ }
4473
4775
  console.log(source_default.cyan(`
4474
- Uninstalling Agent Approve hooks...
4776
+ ${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
4475
4777
  `));
4476
4778
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4477
- if (!existsSync(agent.configPath))
4779
+ if (!existsSync2(agent.configPath))
4478
4780
  continue;
4479
4781
  const config = readJsonConfig(agent.configPath);
4480
4782
  const hooksConfig = config[agent.hooksKey] ?? {};
@@ -4494,7 +4796,7 @@ async function uninstallCommand() {
4494
4796
  }
4495
4797
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
4496
4798
  try {
4497
- if (existsSync(opencodePkgPath)) {
4799
+ if (existsSync2(opencodePkgPath)) {
4498
4800
  const pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
4499
4801
  const deps = pkgJson.dependencies;
4500
4802
  if (deps && deps["@agentapprove/opencode"]) {
@@ -4508,13 +4810,15 @@ async function uninstallCommand() {
4508
4810
  const pluginsConfig = hooksConfig;
4509
4811
  const entries = pluginsConfig.entries;
4510
4812
  const installs = pluginsConfig.installs;
4511
- if (entries && entries.openclaw) {
4512
- delete entries.openclaw;
4513
- modified = true;
4514
- }
4515
- if (installs && installs.openclaw) {
4516
- delete installs.openclaw;
4517
- modified = true;
4813
+ for (const key of ["openclaw", "agentapprove"]) {
4814
+ if (entries && entries[key]) {
4815
+ delete entries[key];
4816
+ modified = true;
4817
+ }
4818
+ if (installs && installs[key]) {
4819
+ delete installs[key];
4820
+ modified = true;
4821
+ }
4518
4822
  }
4519
4823
  } else {
4520
4824
  for (const hook of agent.hooks) {
@@ -4539,8 +4843,8 @@ async function uninstallCommand() {
4539
4843
  modified = true;
4540
4844
  }
4541
4845
  }
4542
- if (hooksConfig["Prompt"]) {
4543
- delete hooksConfig["Prompt"];
4846
+ if (hooksConfig.Prompt) {
4847
+ delete hooksConfig.Prompt;
4544
4848
  modified = true;
4545
4849
  }
4546
4850
  }
@@ -4555,50 +4859,72 @@ async function uninstallCommand() {
4555
4859
  }
4556
4860
  }
4557
4861
  }
4558
- const envPath = join(getAgentApproveDir(), "env");
4559
- if (existsSync(envPath)) {
4560
- const { unlinkSync } = await import("fs");
4561
- unlinkSync(envPath);
4562
- console.log(` ${source_default.green("✓")} Removed configuration`);
4563
- }
4564
- if (deleteTokenFromKeychain()) {
4565
- console.log(` ${source_default.green("✓")} Removed token from credential store`);
4566
- }
4567
4862
  const removedVSCode = removeFromVSCodeHookLocations();
4568
4863
  if (removedVSCode.length > 0) {
4569
4864
  for (const path of removedVSCode) {
4570
4865
  console.log(` ${source_default.green("✓")} Removed hook path from ${path}`);
4571
4866
  }
4572
4867
  }
4573
- const hooksDir = join(getAgentApproveDir(), "hooks");
4574
- const agentApproveDir = getAgentApproveDir();
4575
- for (const configPath of [
4576
- join(hooksDir, "vscode-hooks.json"),
4577
- join(agentApproveDir, "copilot-cli-hooks.json"),
4578
- join(hooksDir, "copilot-cli-hooks.json")
4579
- ]) {
4580
- if (existsSync(configPath)) {
4581
- const { unlinkSync } = await import("fs");
4582
- unlinkSync(configPath);
4583
- console.log(` ${source_default.green("✓")} Removed ${basename(configPath)}`);
4868
+ for (const target of plan.pluginArtifactTargets) {
4869
+ if (!removeRemovalTarget(target)) {
4870
+ continue;
4871
+ }
4872
+ console.log(` ${source_default.green("✓")} Removed plugin files at ${target.displayPath}`);
4873
+ if (target.actualPath.endsWith("/node_modules/@agentapprove/opencode")) {
4874
+ pruneEmptyDir(dirname2(target.actualPath));
4875
+ pruneEmptyDir(dirname2(dirname2(target.actualPath)));
4876
+ }
4877
+ if (target.actualPath.includes("/extensions/")) {
4878
+ pruneEmptyDir(dirname2(target.actualPath));
4584
4879
  }
4585
4880
  }
4881
+ if (mode === "purge") {
4882
+ for (const target of plan.backupTargets) {
4883
+ if (removeRemovalTarget(target)) {
4884
+ console.log(` ${source_default.green("✓")} Removed backup ${target.displayPath}`);
4885
+ }
4886
+ }
4887
+ for (const target of plan.managedState.reversible) {
4888
+ if (removeRemovalTarget(target)) {
4889
+ console.log(` ${source_default.green("✓")} Removed local file ${target.displayPath}`);
4890
+ }
4891
+ }
4892
+ for (const target of plan.managedState.crypto) {
4893
+ if (removeRemovalTarget(target)) {
4894
+ console.log(` ${source_default.green("✓")} Removed E2E material ${target.displayPath}`);
4895
+ }
4896
+ }
4897
+ if (deleteTokenFromKeychain()) {
4898
+ console.log(` ${source_default.green("✓")} Removed token from credential store`);
4899
+ }
4900
+ finalizeAgentApproveDirPurge(getAgentApproveDir());
4901
+ }
4586
4902
  console.log(source_default.green(`
4587
- Agent Approve uninstalled.`));
4588
- console.log(source_default.dim(" Hook scripts remain in ~/.agentapprove/hooks"));
4589
- console.log(source_default.dim(` Backups remain as .backup files
4903
+ Agent Approve ${mode === "purge" ? "purged" : "uninstalled"}.`));
4904
+ if (mode === "purge") {
4905
+ console.log(source_default.dim(` Local Agent Approve state and backups were removed.
4590
4906
  `));
4907
+ return;
4908
+ }
4909
+ console.log(source_default.dim(" Local Agent Approve config, logs, hooks, backups, and E2E keys were kept."));
4910
+ console.log(source_default.dim(" Run `npx agentapprove purge` if you want to remove all local Agent Approve traces.\n"));
4911
+ }
4912
+ async function uninstallCommand() {
4913
+ await performUninstall("uninstall");
4914
+ }
4915
+ async function purgeCommand() {
4916
+ await performUninstall("purge");
4591
4917
  }
4592
4918
  async function restoreCommand() {
4593
4919
  console.log(source_default.cyan(`
4594
4920
  Restoring original configurations...
4595
4921
  `));
4596
- const { readdirSync: readdirSync2, copyFileSync: copyFileSync2 } = await import("fs");
4922
+ const { readdirSync: readdirSync3, copyFileSync: copyFileSync2 } = await import("fs");
4597
4923
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4598
- const configDir = dirname(agent.configPath);
4599
- if (!existsSync(configDir))
4924
+ const configDir = dirname2(agent.configPath);
4925
+ if (!existsSync2(configDir))
4600
4926
  continue;
4601
- const files = readdirSync2(configDir);
4927
+ const files = readdirSync3(configDir);
4602
4928
  const backups = files.filter((f2) => f2.startsWith(agent.configPath.split("/").pop()) && f2.includes(".backup.")).sort().reverse();
4603
4929
  if (backups.length > 0) {
4604
4930
  const latestBackup = join(configDir, backups[0]);
@@ -4610,7 +4936,7 @@ async function restoreCommand() {
4610
4936
  }
4611
4937
  async function initRepoCommand() {
4612
4938
  const cliHooksPath = join(getAgentApproveDir(), "copilot-cli-hooks.json");
4613
- if (!existsSync(cliHooksPath)) {
4939
+ if (!existsSync2(cliHooksPath)) {
4614
4940
  console.log(source_default.yellow(`
4615
4941
  GitHub Copilot CLI hooks not found. Run the installer first with GitHub Copilot CLI selected.`));
4616
4942
  console.log(source_default.dim(` npx agentapprove install
@@ -4619,12 +4945,12 @@ async function initRepoCommand() {
4619
4945
  }
4620
4946
  let repoRoot = process.cwd();
4621
4947
  let found = false;
4622
- while (repoRoot !== dirname(repoRoot)) {
4623
- if (existsSync(join(repoRoot, ".git"))) {
4948
+ while (repoRoot !== dirname2(repoRoot)) {
4949
+ if (existsSync2(join(repoRoot, ".git"))) {
4624
4950
  found = true;
4625
4951
  break;
4626
4952
  }
4627
- repoRoot = dirname(repoRoot);
4953
+ repoRoot = dirname2(repoRoot);
4628
4954
  }
4629
4955
  if (!found) {
4630
4956
  console.log(source_default.yellow(`
@@ -4637,7 +4963,7 @@ async function initRepoCommand() {
4637
4963
  console.log(source_default.cyan(`
4638
4964
  Installing GitHub Copilot CLI hooks to ${targetFile}
4639
4965
  `));
4640
- if (existsSync(targetFile)) {
4966
+ if (existsSync2(targetFile)) {
4641
4967
  console.log(source_default.yellow(" agentapprove.json already exists in .github/hooks/"));
4642
4968
  const overwrite = await ce({
4643
4969
  message: "Overwrite existing file?"
@@ -4648,7 +4974,7 @@ async function initRepoCommand() {
4648
4974
  process.exit(0);
4649
4975
  }
4650
4976
  }
4651
- if (!existsSync(targetDir)) {
4977
+ if (!existsSync2(targetDir)) {
4652
4978
  mkdirSync(targetDir, { recursive: true });
4653
4979
  }
4654
4980
  copyFileSync(cliHooksPath, targetFile);
@@ -4666,13 +4992,14 @@ ${source_default.yellow("Usage:")}
4666
4992
 
4667
4993
  ${source_default.yellow("Commands:")}
4668
4994
  ${source_default.green("install")} Run the installation wizard (default)
4669
- ${source_default.green("pair")} Link a new iOS device with existing E2E keys
4670
- ${source_default.green("refresh")} Generate a new token (when expired)
4995
+ ${source_default.green("pair")} Link a new iOS device or repair E2E pairing
4996
+ ${source_default.green("refresh")} Generate a new token and reuse current pairing context
4671
4997
  ${source_default.green("init-repo")} Add GitHub Copilot CLI hooks to current repo (.github/hooks/)
4672
4998
  ${source_default.green("status")} Show current configuration and installed hooks
4673
4999
  ${source_default.green("disable")} Temporarily disable hooks
4674
5000
  ${source_default.green("enable")} Re-enable hooks after disabling
4675
- ${source_default.green("uninstall")} Remove all hooks from agent configs
5001
+ ${source_default.green("uninstall")} ${INSTALLER_COMMAND_DESCRIPTIONS.uninstall}
5002
+ ${source_default.green("purge")} ${INSTALLER_COMMAND_DESCRIPTIONS.purge}
4676
5003
  ${source_default.green("restore")} Restore original configs from backups
4677
5004
  ${source_default.green("help")} Show this help message
4678
5005
 
@@ -4700,13 +5027,34 @@ async function refreshCommand() {
4700
5027
  but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
4701
5028
  let token = null;
4702
5029
  let apiUrl = API_URL;
4703
- const session = await createPairingSession();
5030
+ const installedAgents = detectInstalledAgents();
5031
+ const e2eEnabled = existingConfig.e2eEnabled !== false;
5032
+ const e2eKeyPath = join(getAgentApproveDir(), "e2e-key");
5033
+ let e2eUserKey = null;
5034
+ let e2eKeyId;
5035
+ if (e2eEnabled && existsSync2(e2eKeyPath)) {
5036
+ const existingKey = readFileSync(e2eKeyPath, "utf-8").trim();
5037
+ if (existingKey.length === 64) {
5038
+ e2eUserKey = existingKey;
5039
+ e2eKeyId = createHash("sha256").update(Buffer.from(existingKey, "hex")).digest("hex").slice(0, 8);
5040
+ v2.info(`Refresh will reuse E2E key ${e2eKeyId}. Use "npx agentapprove pair" if you need to choose a different key or repair pairing on another device.`);
5041
+ }
5042
+ } else {
5043
+ v2.info('Refresh updates the token only. Use "npx agentapprove pair" if you need to repair or change E2E pairing.');
5044
+ }
5045
+ const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
4704
5046
  if (!session || session.error) {
4705
5047
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
4706
5048
  process.exit(1);
4707
5049
  }
5050
+ const machineHost = hostname();
5051
+ let qrUrl = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
5052
+ if (e2eUserKey) {
5053
+ const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
5054
+ qrUrl += `&e2eKey=${e2eKeyBase64url}`;
5055
+ }
4708
5056
  let qrDisplay = "";
4709
- import_qrcode_terminal.default.generate(session.qrUrl, { small: true }, (qr) => {
5057
+ import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
4710
5058
  qrDisplay = qr;
4711
5059
  });
4712
5060
  await new Promise((resolve) => setTimeout(resolve, 10));
@@ -4741,10 +5089,16 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4741
5089
  privacy,
4742
5090
  retentionDays,
4743
5091
  debugLog: existingConfig.debugLog,
5092
+ e2eMode: existingConfig.e2eMode,
5093
+ e2eEnabled,
4744
5094
  failBehavior,
4745
5095
  configSetAt
4746
5096
  });
4747
5097
  await storeTokenInKeychain(token);
5098
+ if (e2eEnabled && result.e2eServerKey) {
5099
+ const serverKeyPath = join(getAgentApproveDir(), "e2e-server-key");
5100
+ writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
5101
+ }
4748
5102
  pushConfigToCloud({
4749
5103
  apiUrl,
4750
5104
  token,
@@ -4890,7 +5244,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4890
5244
  pairingSpinner.stop(`Paired! ${result.email ? `Account: ${result.email}` : ""}`);
4891
5245
  if (e2eUserKey && e2eKeyId) {
4892
5246
  const rootKeyPath = join(getAgentApproveDir(), "e2e-root-key");
4893
- if (!existsSync(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
5247
+ if (!existsSync2(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
4894
5248
  writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
4895
5249
  }
4896
5250
  if (!readRotationConfig()) {
@@ -4914,12 +5268,14 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4914
5268
  privacy: existingConfig?.privacy || result.privacy || "full",
4915
5269
  retentionDays: existingConfig?.retentionDays ?? 30,
4916
5270
  debugLog: existingConfig?.debugLog,
5271
+ e2eMode: existingConfig?.e2eMode,
5272
+ e2eEnabled: !!e2eUserKey,
4917
5273
  failBehavior: existingConfig?.failBehavior || "ask",
4918
5274
  configSetAt
4919
5275
  });
4920
5276
  await storeTokenInKeychain(result.token);
4921
5277
  const hooksDir = join(getAgentApproveDir(), "hooks");
4922
- if (existsSync(hooksDir)) {
5278
+ if (existsSync2(hooksDir)) {
4923
5279
  const updateSpinner = _2();
4924
5280
  updateSpinner.start("Updating hook scripts with new token");
4925
5281
  try {
@@ -4941,7 +5297,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4941
5297
  }
4942
5298
  async function updateHookScriptsWithToken(token, apiUrl) {
4943
5299
  const hooksDir = join(getAgentApproveDir(), "hooks");
4944
- if (!existsSync(hooksDir)) {
5300
+ if (!existsSync2(hooksDir)) {
4945
5301
  throw new Error("Hooks directory not found");
4946
5302
  }
4947
5303
  const hookFiles = [
@@ -4964,7 +5320,7 @@ async function updateHookScriptsWithToken(token, apiUrl) {
4964
5320
  ];
4965
5321
  for (const file of hookFiles) {
4966
5322
  const filePath = join(hooksDir, file);
4967
- if (existsSync(filePath)) {
5323
+ if (existsSync2(filePath)) {
4968
5324
  let content = readFileSync(filePath, "utf-8");
4969
5325
  content = content.replace(/export AGENTAPPROVE_TOKEN=".*"/, `export AGENTAPPROVE_TOKEN="${token}"`);
4970
5326
  content = content.replace(/export AGENTAPPROVE_API=".*"/, `export AGENTAPPROVE_API="${apiUrl}"`);
@@ -4996,6 +5352,9 @@ async function main() {
4996
5352
  case "uninstall":
4997
5353
  await uninstallCommand();
4998
5354
  break;
5355
+ case "purge":
5356
+ await purgeCommand();
5357
+ break;
4999
5358
  case "restore":
5000
5359
  await restoreCommand();
5001
5360
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentapprove",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Approve AI agent actions from your iPhone or Apple Watch",
5
5
  "type": "module",
6
6
  "bin": {