agentapprove 0.1.9 → 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 +479 -110
  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.9";
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,8 +2553,8 @@ function getCommand() {
2380
2553
  }
2381
2554
  return filtered[0] || "install";
2382
2555
  }
2383
- var OPENCODE_PLUGIN_VERSION = "0.1.6";
2384
- var OPENCLAW_PLUGIN_VERSION = "0.2.3";
2556
+ var OPENCODE_PLUGIN_VERSION = "0.1.7";
2557
+ var OPENCLAW_PLUGIN_VERSION = "0.2.5";
2385
2558
  var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
2386
2559
  var AGENTS = {
2387
2560
  "claude-code": {
@@ -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");
@@ -3178,10 +3469,11 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3178
3469
  plugins.entries = {};
3179
3470
  }
3180
3471
  const entries = plugins.entries;
3181
- entries.agentapprove = {
3472
+ entries.openclaw = {
3182
3473
  enabled: mode === "approval",
3183
3474
  config: {
3184
3475
  apiUrl: API_URL,
3476
+ apiVersion: "v001",
3185
3477
  timeout: 300,
3186
3478
  failBehavior: "ask",
3187
3479
  privacyTier: "full"
@@ -3196,7 +3488,7 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3196
3488
  }
3197
3489
  hooks.internal.enabled = true;
3198
3490
  writeJsonConfig(agent.configPath, config);
3199
- installedHooks.push("agentapprove");
3491
+ installedHooks.push("openclaw");
3200
3492
  return { success: true, backupPath, hooks: installedHooks };
3201
3493
  }
3202
3494
  if (agentId === "opencode") {
@@ -3211,13 +3503,13 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3211
3503
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
3212
3504
  try {
3213
3505
  let pkgJson = {};
3214
- if (existsSync(opencodePkgPath)) {
3506
+ if (existsSync2(opencodePkgPath)) {
3215
3507
  pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
3216
3508
  }
3217
3509
  if (!pkgJson.dependencies) {
3218
3510
  pkgJson.dependencies = {};
3219
3511
  }
3220
- pkgJson.dependencies["@agentapprove/opencode"] = "latest";
3512
+ pkgJson.dependencies["@agentapprove/opencode"] = OPENCODE_PLUGIN_VERSION;
3221
3513
  writeFileSync(opencodePkgPath, JSON.stringify(pkgJson, null, 2) + `
3222
3514
  `);
3223
3515
  } catch (err) {
@@ -3477,8 +3769,8 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3477
3769
  } else if (agentId === "openclaw") {
3478
3770
  const pluginsObj = hooksConfig;
3479
3771
  const entries = pluginsObj?.entries;
3480
- if (entries?.agentapprove && typeof entries.agentapprove === "object") {
3481
- entries.agentapprove.enabled = false;
3772
+ if (entries?.openclaw && typeof entries.openclaw === "object") {
3773
+ entries.openclaw.enabled = false;
3482
3774
  }
3483
3775
  }
3484
3776
  }
@@ -3602,10 +3894,11 @@ codex_hooks = true`;
3602
3894
  const configJson = JSON.stringify({
3603
3895
  plugins: {
3604
3896
  entries: {
3605
- agentapprove: {
3897
+ openclaw: {
3606
3898
  enabled: true,
3607
3899
  config: {
3608
3900
  apiUrl: API_URL,
3901
+ apiVersion: "v001",
3609
3902
  timeout: 300,
3610
3903
  failBehavior: "ask",
3611
3904
  privacyTier: "full"
@@ -3626,6 +3919,8 @@ codex_hooks = true`;
3626
3919
  "",
3627
3920
  configJson,
3628
3921
  "",
3922
+ 'Hosted installs can also set config.token/config.apiUrl/etc. with OpenClaw env substitution such as "${AGENTAPPROVE_TOKEN}".',
3923
+ "",
3629
3924
  "Restart the OpenClaw gateway to activate."
3630
3925
  ].join(`
3631
3926
  `);
@@ -3964,7 +4259,7 @@ async function installCommand() {
3964
4259
  const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
3965
4260
  const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
3966
4261
  let e2eLine = "";
3967
- if (existsSync(e2eKeyPath2)) {
4262
+ if (existsSync2(e2eKeyPath2)) {
3968
4263
  const keyHex = readFileSync(e2eKeyPath2, "utf-8").trim();
3969
4264
  const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
3970
4265
  e2eLine = `
@@ -4068,7 +4363,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4068
4363
  if (connectionMethod === "existing") {
4069
4364
  token = existingConfig.token;
4070
4365
  apiUrl = existingConfig.apiUrl || API_URL;
4071
- const e2eKeyExists = existsSync(join(getAgentApproveDir(), "e2e-key"));
4366
+ const e2eKeyExists = existsSync2(join(getAgentApproveDir(), "e2e-key"));
4072
4367
  useE2E = e2eKeyExists;
4073
4368
  v2.success("Using existing token");
4074
4369
  } else if (connectionMethod === "qr") {
@@ -4078,7 +4373,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4078
4373
  const existingKeyPath = join(agentApproveDir, "e2e-key");
4079
4374
  let e2eUserKey = null;
4080
4375
  if (useE2E) {
4081
- if (existsSync(existingKeyPath)) {
4376
+ if (existsSync2(existingKeyPath)) {
4082
4377
  const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
4083
4378
  const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
4084
4379
  v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
@@ -4226,7 +4521,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4226
4521
  downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files)`);
4227
4522
  }
4228
4523
  const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
4229
- const hasE2EKey = existsSync(e2eKeyPath);
4524
+ const hasE2EKey = existsSync2(e2eKeyPath);
4230
4525
  let installMode = "approval";
4231
4526
  if (hasE2EKey) {
4232
4527
  const modeChoice = await le({
@@ -4255,7 +4550,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4255
4550
  v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
4256
4551
  }
4257
4552
  const envPath = join(getAgentApproveDir(), "env");
4258
- if (existsSync(envPath)) {
4553
+ if (existsSync2(envPath)) {
4259
4554
  let envContent = readFileSync(envPath, "utf-8");
4260
4555
  if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
4261
4556
  envContent = envContent.replace(/AGENTAPPROVE_E2E_MODE=\w+/, `AGENTAPPROVE_E2E_MODE=${installMode}`);
@@ -4356,7 +4651,8 @@ Backups will be created with timestamp`, "Files to be modified");
4356
4651
  npx agentapprove pair Link a new iOS device
4357
4652
  npx agentapprove status Show current configuration
4358
4653
  npx agentapprove disable Temporarily disable hooks
4359
- 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
4360
4656
  npx agentapprove restore Restore original configs`, "Useful commands");
4361
4657
  ge(`${source_default.green("Agent Approve is ready!")} ${source_default.dim("Learn more at")} ${source_default.cyan("agentapprove.com")}`);
4362
4658
  }
@@ -4365,7 +4661,7 @@ async function statusCommand() {
4365
4661
  Agent Approve Status
4366
4662
  `));
4367
4663
  const envPath = join(getAgentApproveDir(), "env");
4368
- if (!existsSync(envPath)) {
4664
+ if (!existsSync2(envPath)) {
4369
4665
  console.log(source_default.yellow(" Not configured. Run `npx agentapprove` to set up.\n"));
4370
4666
  return;
4371
4667
  }
@@ -4382,18 +4678,18 @@ async function statusCommand() {
4382
4678
  }
4383
4679
  const e2eKeyFile = join(getAgentApproveDir(), "e2e-key");
4384
4680
  const e2eServerKeyFile = join(getAgentApproveDir(), "e2e-server-key");
4385
- if (existsSync(e2eKeyFile)) {
4681
+ if (existsSync2(e2eKeyFile)) {
4386
4682
  console.log(` ${source_default.green("✓")} E2E user key: installed`);
4387
4683
  }
4388
- if (existsSync(e2eServerKeyFile)) {
4684
+ if (existsSync2(e2eServerKeyFile)) {
4389
4685
  console.log(` ${source_default.green("✓")} E2E server key: installed`);
4390
4686
  }
4391
- if (!existsSync(e2eKeyFile) && !existsSync(e2eServerKeyFile)) {
4687
+ if (!existsSync2(e2eKeyFile) && !existsSync2(e2eServerKeyFile)) {
4392
4688
  console.log(` ${source_default.dim(" E2E encryption: not configured")}`);
4393
4689
  }
4394
4690
  console.log();
4395
4691
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4396
- if (existsSync(agent.configPath)) {
4692
+ if (existsSync2(agent.configPath)) {
4397
4693
  const config = readJsonConfig(agent.configPath);
4398
4694
  if (agentId === "opencode") {
4399
4695
  const pluginConfig = config.plugin;
@@ -4404,8 +4700,8 @@ async function statusCommand() {
4404
4700
  }
4405
4701
  if (agentId === "openclaw") {
4406
4702
  const entries = config.plugins?.entries;
4407
- if (entries?.agentapprove) {
4408
- console.log(` ${source_default.green("✓")} ${agent.name}: agentapprove plugin`);
4703
+ if (entries?.openclaw) {
4704
+ console.log(` ${source_default.green("✓")} ${agent.name}: openclaw plugin`);
4409
4705
  }
4410
4706
  continue;
4411
4707
  }
@@ -4429,8 +4725,8 @@ async function statusCommand() {
4429
4725
  async function disableCommand() {
4430
4726
  const envPath = join(getAgentApproveDir(), "env");
4431
4727
  const disabledPath = join(getAgentApproveDir(), "env.disabled");
4432
- if (!existsSync(envPath)) {
4433
- if (existsSync(disabledPath)) {
4728
+ if (!existsSync2(envPath)) {
4729
+ if (existsSync2(disabledPath)) {
4434
4730
  console.log(source_default.yellow(`
4435
4731
  Already disabled.
4436
4732
  `));
@@ -4449,13 +4745,13 @@ async function disableCommand() {
4449
4745
  async function enableCommand() {
4450
4746
  const envPath = join(getAgentApproveDir(), "env");
4451
4747
  const disabledPath = join(getAgentApproveDir(), "env.disabled");
4452
- if (existsSync(envPath)) {
4748
+ if (existsSync2(envPath)) {
4453
4749
  console.log(source_default.yellow(`
4454
4750
  Already enabled.
4455
4751
  `));
4456
4752
  return;
4457
4753
  }
4458
- if (!existsSync(disabledPath)) {
4754
+ if (!existsSync2(disabledPath)) {
4459
4755
  console.log(source_default.yellow("\n Not configured. Run `npx agentapprove` to set up.\n"));
4460
4756
  return;
4461
4757
  }
@@ -4465,12 +4761,22 @@ async function enableCommand() {
4465
4761
  ✓ Agent Approve enabled.
4466
4762
  `));
4467
4763
  }
4468
- 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
+ }
4469
4775
  console.log(source_default.cyan(`
4470
- Uninstalling Agent Approve hooks...
4776
+ ${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
4471
4777
  `));
4472
4778
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4473
- if (!existsSync(agent.configPath))
4779
+ if (!existsSync2(agent.configPath))
4474
4780
  continue;
4475
4781
  const config = readJsonConfig(agent.configPath);
4476
4782
  const hooksConfig = config[agent.hooksKey] ?? {};
@@ -4490,7 +4796,7 @@ async function uninstallCommand() {
4490
4796
  }
4491
4797
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
4492
4798
  try {
4493
- if (existsSync(opencodePkgPath)) {
4799
+ if (existsSync2(opencodePkgPath)) {
4494
4800
  const pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
4495
4801
  const deps = pkgJson.dependencies;
4496
4802
  if (deps && deps["@agentapprove/opencode"]) {
@@ -4501,10 +4807,18 @@ async function uninstallCommand() {
4501
4807
  }
4502
4808
  } catch {}
4503
4809
  } else if (agentId === "openclaw") {
4504
- const entries = hooksConfig.entries;
4505
- if (entries && entries.agentapprove) {
4506
- delete entries.agentapprove;
4507
- modified = true;
4810
+ const pluginsConfig = hooksConfig;
4811
+ const entries = pluginsConfig.entries;
4812
+ const installs = pluginsConfig.installs;
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
+ }
4508
4822
  }
4509
4823
  } else {
4510
4824
  for (const hook of agent.hooks) {
@@ -4529,8 +4843,8 @@ async function uninstallCommand() {
4529
4843
  modified = true;
4530
4844
  }
4531
4845
  }
4532
- if (hooksConfig["Prompt"]) {
4533
- delete hooksConfig["Prompt"];
4846
+ if (hooksConfig.Prompt) {
4847
+ delete hooksConfig.Prompt;
4534
4848
  modified = true;
4535
4849
  }
4536
4850
  }
@@ -4545,50 +4859,72 @@ async function uninstallCommand() {
4545
4859
  }
4546
4860
  }
4547
4861
  }
4548
- const envPath = join(getAgentApproveDir(), "env");
4549
- if (existsSync(envPath)) {
4550
- const { unlinkSync } = await import("fs");
4551
- unlinkSync(envPath);
4552
- console.log(` ${source_default.green("✓")} Removed configuration`);
4553
- }
4554
- if (deleteTokenFromKeychain()) {
4555
- console.log(` ${source_default.green("✓")} Removed token from credential store`);
4556
- }
4557
4862
  const removedVSCode = removeFromVSCodeHookLocations();
4558
4863
  if (removedVSCode.length > 0) {
4559
4864
  for (const path of removedVSCode) {
4560
4865
  console.log(` ${source_default.green("✓")} Removed hook path from ${path}`);
4561
4866
  }
4562
4867
  }
4563
- const hooksDir = join(getAgentApproveDir(), "hooks");
4564
- const agentApproveDir = getAgentApproveDir();
4565
- for (const configPath of [
4566
- join(hooksDir, "vscode-hooks.json"),
4567
- join(agentApproveDir, "copilot-cli-hooks.json"),
4568
- join(hooksDir, "copilot-cli-hooks.json")
4569
- ]) {
4570
- if (existsSync(configPath)) {
4571
- const { unlinkSync } = await import("fs");
4572
- unlinkSync(configPath);
4573
- 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));
4879
+ }
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`);
4574
4899
  }
4900
+ finalizeAgentApproveDirPurge(getAgentApproveDir());
4575
4901
  }
4576
4902
  console.log(source_default.green(`
4577
- Agent Approve uninstalled.`));
4578
- console.log(source_default.dim(" Hook scripts remain in ~/.agentapprove/hooks"));
4579
- 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.
4580
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");
4581
4917
  }
4582
4918
  async function restoreCommand() {
4583
4919
  console.log(source_default.cyan(`
4584
4920
  Restoring original configurations...
4585
4921
  `));
4586
- const { readdirSync: readdirSync2, copyFileSync: copyFileSync2 } = await import("fs");
4922
+ const { readdirSync: readdirSync3, copyFileSync: copyFileSync2 } = await import("fs");
4587
4923
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4588
- const configDir = dirname(agent.configPath);
4589
- if (!existsSync(configDir))
4924
+ const configDir = dirname2(agent.configPath);
4925
+ if (!existsSync2(configDir))
4590
4926
  continue;
4591
- const files = readdirSync2(configDir);
4927
+ const files = readdirSync3(configDir);
4592
4928
  const backups = files.filter((f2) => f2.startsWith(agent.configPath.split("/").pop()) && f2.includes(".backup.")).sort().reverse();
4593
4929
  if (backups.length > 0) {
4594
4930
  const latestBackup = join(configDir, backups[0]);
@@ -4600,7 +4936,7 @@ async function restoreCommand() {
4600
4936
  }
4601
4937
  async function initRepoCommand() {
4602
4938
  const cliHooksPath = join(getAgentApproveDir(), "copilot-cli-hooks.json");
4603
- if (!existsSync(cliHooksPath)) {
4939
+ if (!existsSync2(cliHooksPath)) {
4604
4940
  console.log(source_default.yellow(`
4605
4941
  GitHub Copilot CLI hooks not found. Run the installer first with GitHub Copilot CLI selected.`));
4606
4942
  console.log(source_default.dim(` npx agentapprove install
@@ -4609,12 +4945,12 @@ async function initRepoCommand() {
4609
4945
  }
4610
4946
  let repoRoot = process.cwd();
4611
4947
  let found = false;
4612
- while (repoRoot !== dirname(repoRoot)) {
4613
- if (existsSync(join(repoRoot, ".git"))) {
4948
+ while (repoRoot !== dirname2(repoRoot)) {
4949
+ if (existsSync2(join(repoRoot, ".git"))) {
4614
4950
  found = true;
4615
4951
  break;
4616
4952
  }
4617
- repoRoot = dirname(repoRoot);
4953
+ repoRoot = dirname2(repoRoot);
4618
4954
  }
4619
4955
  if (!found) {
4620
4956
  console.log(source_default.yellow(`
@@ -4627,7 +4963,7 @@ async function initRepoCommand() {
4627
4963
  console.log(source_default.cyan(`
4628
4964
  Installing GitHub Copilot CLI hooks to ${targetFile}
4629
4965
  `));
4630
- if (existsSync(targetFile)) {
4966
+ if (existsSync2(targetFile)) {
4631
4967
  console.log(source_default.yellow(" agentapprove.json already exists in .github/hooks/"));
4632
4968
  const overwrite = await ce({
4633
4969
  message: "Overwrite existing file?"
@@ -4638,7 +4974,7 @@ async function initRepoCommand() {
4638
4974
  process.exit(0);
4639
4975
  }
4640
4976
  }
4641
- if (!existsSync(targetDir)) {
4977
+ if (!existsSync2(targetDir)) {
4642
4978
  mkdirSync(targetDir, { recursive: true });
4643
4979
  }
4644
4980
  copyFileSync(cliHooksPath, targetFile);
@@ -4656,13 +4992,14 @@ ${source_default.yellow("Usage:")}
4656
4992
 
4657
4993
  ${source_default.yellow("Commands:")}
4658
4994
  ${source_default.green("install")} Run the installation wizard (default)
4659
- ${source_default.green("pair")} Link a new iOS device with existing E2E keys
4660
- ${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
4661
4997
  ${source_default.green("init-repo")} Add GitHub Copilot CLI hooks to current repo (.github/hooks/)
4662
4998
  ${source_default.green("status")} Show current configuration and installed hooks
4663
4999
  ${source_default.green("disable")} Temporarily disable hooks
4664
5000
  ${source_default.green("enable")} Re-enable hooks after disabling
4665
- ${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}
4666
5003
  ${source_default.green("restore")} Restore original configs from backups
4667
5004
  ${source_default.green("help")} Show this help message
4668
5005
 
@@ -4690,13 +5027,34 @@ async function refreshCommand() {
4690
5027
  but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
4691
5028
  let token = null;
4692
5029
  let apiUrl = API_URL;
4693
- 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);
4694
5046
  if (!session || session.error) {
4695
5047
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
4696
5048
  process.exit(1);
4697
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
+ }
4698
5056
  let qrDisplay = "";
4699
- import_qrcode_terminal.default.generate(session.qrUrl, { small: true }, (qr) => {
5057
+ import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
4700
5058
  qrDisplay = qr;
4701
5059
  });
4702
5060
  await new Promise((resolve) => setTimeout(resolve, 10));
@@ -4731,10 +5089,16 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4731
5089
  privacy,
4732
5090
  retentionDays,
4733
5091
  debugLog: existingConfig.debugLog,
5092
+ e2eMode: existingConfig.e2eMode,
5093
+ e2eEnabled,
4734
5094
  failBehavior,
4735
5095
  configSetAt
4736
5096
  });
4737
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
+ }
4738
5102
  pushConfigToCloud({
4739
5103
  apiUrl,
4740
5104
  token,
@@ -4880,7 +5244,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4880
5244
  pairingSpinner.stop(`Paired! ${result.email ? `Account: ${result.email}` : ""}`);
4881
5245
  if (e2eUserKey && e2eKeyId) {
4882
5246
  const rootKeyPath = join(getAgentApproveDir(), "e2e-root-key");
4883
- if (!existsSync(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
5247
+ if (!existsSync2(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
4884
5248
  writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
4885
5249
  }
4886
5250
  if (!readRotationConfig()) {
@@ -4904,12 +5268,14 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4904
5268
  privacy: existingConfig?.privacy || result.privacy || "full",
4905
5269
  retentionDays: existingConfig?.retentionDays ?? 30,
4906
5270
  debugLog: existingConfig?.debugLog,
5271
+ e2eMode: existingConfig?.e2eMode,
5272
+ e2eEnabled: !!e2eUserKey,
4907
5273
  failBehavior: existingConfig?.failBehavior || "ask",
4908
5274
  configSetAt
4909
5275
  });
4910
5276
  await storeTokenInKeychain(result.token);
4911
5277
  const hooksDir = join(getAgentApproveDir(), "hooks");
4912
- if (existsSync(hooksDir)) {
5278
+ if (existsSync2(hooksDir)) {
4913
5279
  const updateSpinner = _2();
4914
5280
  updateSpinner.start("Updating hook scripts with new token");
4915
5281
  try {
@@ -4931,7 +5297,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4931
5297
  }
4932
5298
  async function updateHookScriptsWithToken(token, apiUrl) {
4933
5299
  const hooksDir = join(getAgentApproveDir(), "hooks");
4934
- if (!existsSync(hooksDir)) {
5300
+ if (!existsSync2(hooksDir)) {
4935
5301
  throw new Error("Hooks directory not found");
4936
5302
  }
4937
5303
  const hookFiles = [
@@ -4954,7 +5320,7 @@ async function updateHookScriptsWithToken(token, apiUrl) {
4954
5320
  ];
4955
5321
  for (const file of hookFiles) {
4956
5322
  const filePath = join(hooksDir, file);
4957
- if (existsSync(filePath)) {
5323
+ if (existsSync2(filePath)) {
4958
5324
  let content = readFileSync(filePath, "utf-8");
4959
5325
  content = content.replace(/export AGENTAPPROVE_TOKEN=".*"/, `export AGENTAPPROVE_TOKEN="${token}"`);
4960
5326
  content = content.replace(/export AGENTAPPROVE_API=".*"/, `export AGENTAPPROVE_API="${apiUrl}"`);
@@ -4986,6 +5352,9 @@ async function main() {
4986
5352
  case "uninstall":
4987
5353
  await uninstallCommand();
4988
5354
  break;
5355
+ case "purge":
5356
+ await purgeCommand();
5357
+ break;
4989
5358
  case "restore":
4990
5359
  await restoreCommand();
4991
5360
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentapprove",
3
- "version": "0.1.9",
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": {