agentapprove 0.1.10 → 0.1.12

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 +668 -187
  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.12";
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.5";
2556
+ var OPENCODE_PLUGIN_VERSION = "0.1.8";
2557
+ var OPENCLAW_PLUGIN_VERSION = "0.2.7";
2385
2558
  var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
2386
2559
  var AGENTS = {
2387
2560
  "claude-code": {
@@ -2494,6 +2667,53 @@ var AGENTS = {
2494
2667
  ]
2495
2668
  }
2496
2669
  };
2670
+ var SHARED_HOOK_FILES = ["common.sh"];
2671
+ var STATUS_FIELD_LABELS = {
2672
+ AGENTAPPROVE_API: "API URL",
2673
+ AGENTAPPROVE_TOKEN: "Token",
2674
+ AGENTAPPROVE_PRIVACY: "Privacy",
2675
+ AGENTAPPROVE_RETENTION_DAYS: "Retention",
2676
+ AGENTAPPROVE_CONFIG_SET_AT: "Configured at",
2677
+ AGENTAPPROVE_DEBUG_LOG: "Debug logging",
2678
+ AGENTAPPROVE_E2E_MODE: "Security mode",
2679
+ AGENTAPPROVE_E2E_ENABLED: "E2E encryption",
2680
+ AGENTAPPROVE_FAIL_BEHAVIOR: "If unreachable"
2681
+ };
2682
+ var HOOK_STATUS_LABELS = {
2683
+ PreToolUse: "Tool approval",
2684
+ PostToolUse: "Tool completed",
2685
+ PostToolUseFailure: "Tool failed",
2686
+ PermissionRequest: "Permission request",
2687
+ UserPromptSubmit: "Prompt submitted",
2688
+ SessionStart: "Session start",
2689
+ SessionEnd: "Session end",
2690
+ Stop: "Stop",
2691
+ sessionStart: "Session start",
2692
+ sessionEnd: "Session end",
2693
+ beforeShellExecution: "Shell approval",
2694
+ beforeMCPExecution: "MCP approval",
2695
+ preToolUse: "Tool approval",
2696
+ afterShellExecution: "Shell completed",
2697
+ afterMCPExecution: "MCP completed",
2698
+ postToolUse: "Tool completed",
2699
+ beforeSubmitPrompt: "Prompt submitted",
2700
+ subagentStart: "Subagent start",
2701
+ subagentStop: "Subagent stop",
2702
+ preCompact: "Pre-compact",
2703
+ afterAgentThought: "Agent thinking",
2704
+ afterAgentResponse: "Agent response",
2705
+ BeforeTool: "Tool approval",
2706
+ AfterTool: "Tool completed",
2707
+ BeforeAgent: "Prompt submitted",
2708
+ AfterAgent: "Stop",
2709
+ BeforeModel: "Model request",
2710
+ AfterModel: "Model response",
2711
+ Notification: "Notification",
2712
+ userPromptSubmitted: "Prompt submitted",
2713
+ errorOccurred: "Error",
2714
+ SubagentStart: "Subagent start",
2715
+ SubagentStop: "Subagent stop"
2716
+ };
2497
2717
  function findGitBash() {
2498
2718
  if (!isWindows())
2499
2719
  return null;
@@ -2505,7 +2725,7 @@ function findGitBash() {
2505
2725
  join(process.env.ProgramW6432 || "", "Git", "bin", "bash.exe")
2506
2726
  ].filter(Boolean);
2507
2727
  for (const candidate of candidates) {
2508
- if (existsSync(candidate)) {
2728
+ if (existsSync2(candidate)) {
2509
2729
  return candidate;
2510
2730
  }
2511
2731
  }
@@ -2513,7 +2733,7 @@ function findGitBash() {
2513
2733
  const result = execSync("where bash", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
2514
2734
  const firstLine = result.trim().split(`
2515
2735
  `)[0];
2516
- if (firstLine && existsSync(firstLine)) {
2736
+ if (firstLine && existsSync2(firstLine)) {
2517
2737
  return firstLine;
2518
2738
  }
2519
2739
  } catch {}
@@ -2550,7 +2770,7 @@ function getVSCodeSettingsPaths() {
2550
2770
  return paths;
2551
2771
  }
2552
2772
  function findInstalledVSCodeVariants() {
2553
- return getVSCodeSettingsPaths().filter(({ path }) => existsSync(dirname(path)));
2773
+ return getVSCodeSettingsPaths().filter(({ path }) => existsSync2(dirname2(path)));
2554
2774
  }
2555
2775
  function stripJsonComments(content) {
2556
2776
  let result = "";
@@ -2599,7 +2819,7 @@ function stripJsonComments(content) {
2599
2819
  return result;
2600
2820
  }
2601
2821
  function readJsoncConfig(configPath) {
2602
- if (!existsSync(configPath))
2822
+ if (!existsSync2(configPath))
2603
2823
  return {};
2604
2824
  try {
2605
2825
  const content = readFileSync(configPath, "utf-8");
@@ -2640,7 +2860,7 @@ function removeFromVSCodeHookLocations() {
2640
2860
  const hookLocation = "~/.agentapprove/hooks";
2641
2861
  const modified = [];
2642
2862
  for (const { path: settingsPath, variant } of getVSCodeSettingsPaths()) {
2643
- if (!existsSync(settingsPath))
2863
+ if (!existsSync2(settingsPath))
2644
2864
  continue;
2645
2865
  try {
2646
2866
  const config = readJsoncConfig(settingsPath);
@@ -2667,36 +2887,238 @@ function detectInstalledAgents() {
2667
2887
  installed.push(id);
2668
2888
  }
2669
2889
  } else if (id === "copilot-cli") {
2670
- if (existsSync(join(homedir(), ".copilot"))) {
2890
+ if (existsSync2(join(homedir(), ".copilot"))) {
2671
2891
  installed.push(id);
2672
2892
  }
2673
2893
  } else if (id === "openclaw") {
2674
- if (existsSync(join(homedir(), ".openclaw"))) {
2894
+ if (existsSync2(join(homedir(), ".openclaw"))) {
2675
2895
  installed.push(id);
2676
2896
  }
2677
2897
  } else if (id === "codex") {
2678
- if (existsSync(join(homedir(), ".codex"))) {
2898
+ if (existsSync2(join(homedir(), ".codex"))) {
2679
2899
  installed.push(id);
2680
2900
  }
2681
2901
  } else if (id === "opencode") {
2682
- if (existsSync(getOpenCodeConfigDir())) {
2902
+ if (existsSync2(getOpenCodeConfigDir())) {
2683
2903
  installed.push(id);
2684
2904
  }
2685
2905
  } else {
2686
- const configDir = dirname(agent.configPath);
2687
- if (existsSync(configDir)) {
2906
+ const configDir = dirname2(agent.configPath);
2907
+ if (existsSync2(configDir)) {
2688
2908
  installed.push(id);
2689
2909
  }
2690
2910
  }
2691
2911
  }
2692
2912
  return installed;
2693
2913
  }
2914
+ function getDownloadableHookFilesForAgent(agentId) {
2915
+ const agent = AGENTS[agentId];
2916
+ if (!agent)
2917
+ return [];
2918
+ return agent.hooks.filter((hook) => !hook.isPlugin).map((hook) => hook.file);
2919
+ }
2920
+ function buildHookDownloadPlan(agentIds) {
2921
+ const files = new Set;
2922
+ const perAgent = [];
2923
+ for (const agentId of agentIds) {
2924
+ const agent = AGENTS[agentId];
2925
+ if (!agent)
2926
+ continue;
2927
+ const agentFiles = [...new Set(getDownloadableHookFilesForAgent(agentId))];
2928
+ if (agentFiles.length === 0) {
2929
+ continue;
2930
+ }
2931
+ perAgent.push({
2932
+ agentId,
2933
+ agentName: agent.name,
2934
+ count: agentFiles.length
2935
+ });
2936
+ for (const file of agentFiles) {
2937
+ files.add(file);
2938
+ }
2939
+ }
2940
+ const sharedCount = perAgent.length > 0 ? SHARED_HOOK_FILES.length : 0;
2941
+ if (sharedCount > 0) {
2942
+ for (const file of SHARED_HOOK_FILES) {
2943
+ files.add(file);
2944
+ }
2945
+ }
2946
+ return {
2947
+ files: [...files],
2948
+ perAgent,
2949
+ sharedCount
2950
+ };
2951
+ }
2952
+ function formatHookDownloadSummary(plan) {
2953
+ const parts = plan.perAgent.map((entry) => `${entry.agentName}: ${entry.count}`);
2954
+ if (plan.sharedCount > 0) {
2955
+ parts.push(`shared: ${plan.sharedCount}`);
2956
+ }
2957
+ return parts.join(", ");
2958
+ }
2959
+ function parseEnvAssignment(line) {
2960
+ if (!line.includes("=")) {
2961
+ return null;
2962
+ }
2963
+ const [key, ...valueParts] = line.split("=");
2964
+ return {
2965
+ key: key.trim(),
2966
+ value: valueParts.join("=").trim()
2967
+ };
2968
+ }
2969
+ function formatStatusValue(key, value) {
2970
+ switch (key) {
2971
+ case "AGENTAPPROVE_TOKEN":
2972
+ return value.slice(0, 15) + "...";
2973
+ case "AGENTAPPROVE_RETENTION_DAYS":
2974
+ return `${value} days`;
2975
+ case "AGENTAPPROVE_CONFIG_SET_AT": {
2976
+ const timestamp = Number.parseInt(value, 10);
2977
+ if (Number.isFinite(timestamp)) {
2978
+ return new Date(timestamp * 1000).toLocaleString();
2979
+ }
2980
+ return value;
2981
+ }
2982
+ case "AGENTAPPROVE_DEBUG_LOG":
2983
+ case "AGENTAPPROVE_E2E_ENABLED":
2984
+ return value === "true" ? "Enabled" : "Disabled";
2985
+ case "AGENTAPPROVE_E2E_MODE":
2986
+ return value.charAt(0).toUpperCase() + value.slice(1);
2987
+ case "AGENTAPPROVE_FAIL_BEHAVIOR":
2988
+ case "AGENTAPPROVE_PRIVACY":
2989
+ return value.charAt(0).toUpperCase() + value.slice(1);
2990
+ default:
2991
+ return value;
2992
+ }
2993
+ }
2994
+ function formatHookStatusLabel(name) {
2995
+ const explicitLabel = HOOK_STATUS_LABELS[name];
2996
+ if (explicitLabel) {
2997
+ return explicitLabel;
2998
+ }
2999
+ return name.replace(/\*/g, "events").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (letter) => letter.toUpperCase());
3000
+ }
2694
3001
  function getAgentApproveDir() {
2695
3002
  return join(homedir(), ".agentapprove");
2696
3003
  }
3004
+ function uniqueRemovalTargets(targets) {
3005
+ const seen = new Set;
3006
+ const unique = [];
3007
+ for (const target of targets) {
3008
+ const key = `${target.kind}:${target.actualPath}`;
3009
+ if (seen.has(key)) {
3010
+ continue;
3011
+ }
3012
+ seen.add(key);
3013
+ unique.push(target);
3014
+ }
3015
+ return unique;
3016
+ }
3017
+ function buildUninstallPlan() {
3018
+ const configuredAgents = detectInstalledAgents();
3019
+ const pluginArtifactTargets = uniqueRemovalTargets([
3020
+ ...getOpenClawPluginTargets(AGENTS.openclaw.configPath),
3021
+ ...getOpenCodePluginTargets(getOpenCodeConfigDir())
3022
+ ]);
3023
+ return {
3024
+ configuredAgents,
3025
+ pluginArtifactTargets,
3026
+ backupTargets: uniqueRemovalTargets(collectBackupTargets(Object.values(AGENTS).map((agent) => agent.configPath))),
3027
+ managedState: getManagedStateTargets(getAgentApproveDir())
3028
+ };
3029
+ }
3030
+ function hasUninstallWork(mode, plan) {
3031
+ if (plan.configuredAgents.length > 0 || plan.pluginArtifactTargets.length > 0) {
3032
+ return true;
3033
+ }
3034
+ if (mode === "purge") {
3035
+ return plan.backupTargets.length > 0 || plan.managedState.reversible.length > 0 || plan.managedState.crypto.length > 0;
3036
+ }
3037
+ return false;
3038
+ }
3039
+ function printRemovalTargets(title, targets) {
3040
+ if (targets.length === 0) {
3041
+ return;
3042
+ }
3043
+ console.log(source_default.dim(` ${title}`));
3044
+ for (const target of targets) {
3045
+ console.log(source_default.dim(` - ${target.displayPath}`));
3046
+ }
3047
+ }
3048
+ async function confirmUninstallPlan(mode, plan) {
3049
+ console.log();
3050
+ console.log(source_default.cyan(` ${mode === "purge" ? "Purge" : "Uninstall"} plan`));
3051
+ if (plan.configuredAgents.length > 0) {
3052
+ console.log(source_default.dim(` Agent configs: ${plan.configuredAgents.join(", ")}`));
3053
+ }
3054
+ printRemovalTargets("Plugin files to remove:", plan.pluginArtifactTargets);
3055
+ if (mode === "purge") {
3056
+ printRemovalTargets("Local Agent Approve files to remove:", plan.managedState.reversible);
3057
+ printRemovalTargets("Backups to remove:", plan.backupTargets);
3058
+ printRemovalTargets("Irreversible E2E material to remove:", plan.managedState.crypto);
3059
+ } else {
3060
+ console.log(source_default.dim(" Local Agent Approve state kept:"));
3061
+ console.log(source_default.dim(" - ~/.agentapprove/env and env.disabled"));
3062
+ console.log(source_default.dim(" - ~/.agentapprove/hooks, install.sh, and hook-debug.log"));
3063
+ console.log(source_default.dim(" - E2E keys and rotation metadata"));
3064
+ console.log(source_default.dim(" - OS credential-store token and config backups"));
3065
+ }
3066
+ console.log();
3067
+ const proceed = await ce({
3068
+ 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."
3069
+ });
3070
+ if (lD(proceed) || !proceed) {
3071
+ ge("Cancelled.");
3072
+ return false;
3073
+ }
3074
+ if (mode === "purge" && plan.managedState.crypto.length > 0) {
3075
+ const firstKeyConfirm = await ce({
3076
+ message: "Delete E2E keys and rotation metadata? You may lose access to encrypted local history on this machine."
3077
+ });
3078
+ if (lD(firstKeyConfirm) || !firstKeyConfirm) {
3079
+ v2.warn("Purge cancelled. Run `npx agentapprove uninstall` if you want to keep local keys and config.");
3080
+ return false;
3081
+ }
3082
+ const secondKeyConfirm = await ce({
3083
+ message: "Final confirmation: permanently delete the E2E keys and rotation metadata?"
3084
+ });
3085
+ if (lD(secondKeyConfirm) || !shouldDeleteCryptoMaterial(true, firstKeyConfirm === true, secondKeyConfirm === true)) {
3086
+ v2.warn("Purge cancelled. Run `npx agentapprove uninstall` if you want to keep local keys and config.");
3087
+ return false;
3088
+ }
3089
+ }
3090
+ return true;
3091
+ }
3092
+ function pruneEmptyDir(path) {
3093
+ if (!existsSync2(path)) {
3094
+ return;
3095
+ }
3096
+ try {
3097
+ if (readdirSync2(path).length === 0) {
3098
+ rmSync2(path, { recursive: false, force: true });
3099
+ }
3100
+ } catch {}
3101
+ }
3102
+ function finalizeAgentApproveDirPurge(agentApproveDir) {
3103
+ if (!existsSync2(agentApproveDir)) {
3104
+ return;
3105
+ }
3106
+ try {
3107
+ const stat = lstatSync(agentApproveDir);
3108
+ if (stat.isSymbolicLink()) {
3109
+ const actualDir = realpathSync2(agentApproveDir);
3110
+ if (existsSync2(actualDir) && readdirSync2(actualDir).length === 0) {
3111
+ rmSync2(actualDir, { recursive: false, force: true });
3112
+ }
3113
+ rmSync2(agentApproveDir, { recursive: false, force: true });
3114
+ return;
3115
+ }
3116
+ } catch {}
3117
+ pruneEmptyDir(agentApproveDir);
3118
+ }
2697
3119
  function readExistingConfig() {
2698
3120
  const envPath = join(getAgentApproveDir(), "env");
2699
- if (!existsSync(envPath)) {
3121
+ if (!existsSync2(envPath)) {
2700
3122
  return null;
2701
3123
  }
2702
3124
  try {
@@ -2704,10 +3126,12 @@ function readExistingConfig() {
2704
3126
  const config = {};
2705
3127
  for (const line of content.split(`
2706
3128
  `)) {
2707
- if (line.startsWith("#") || !line.includes("="))
3129
+ if (line.startsWith("#"))
3130
+ continue;
3131
+ const assignment = parseEnvAssignment(line);
3132
+ if (!assignment)
2708
3133
  continue;
2709
- const [key, ...valueParts] = line.split("=");
2710
- const value = valueParts.join("=").trim();
3134
+ const { key, value } = assignment;
2711
3135
  switch (key.trim()) {
2712
3136
  case "AGENTAPPROVE_API":
2713
3137
  config.apiUrl = value;
@@ -2727,6 +3151,9 @@ function readExistingConfig() {
2727
3151
  case "AGENTAPPROVE_E2E_MODE":
2728
3152
  config.e2eMode = value;
2729
3153
  break;
3154
+ case "AGENTAPPROVE_E2E_ENABLED":
3155
+ config.e2eEnabled = value === "true";
3156
+ break;
2730
3157
  case "AGENTAPPROVE_FAIL_BEHAVIOR":
2731
3158
  config.failBehavior = value;
2732
3159
  break;
@@ -2739,11 +3166,11 @@ function readExistingConfig() {
2739
3166
  }
2740
3167
  function discoverE2EKeys() {
2741
3168
  const dir = getAgentApproveDir();
2742
- if (!existsSync(dir))
3169
+ if (!existsSync2(dir))
2743
3170
  return [];
2744
3171
  const keys = [];
2745
3172
  const currentPath = join(dir, "e2e-key");
2746
- if (existsSync(currentPath)) {
3173
+ if (existsSync2(currentPath)) {
2747
3174
  try {
2748
3175
  const hex = readFileSync(currentPath, "utf-8").trim();
2749
3176
  if (hex.length === 64) {
@@ -2753,7 +3180,7 @@ function discoverE2EKeys() {
2753
3180
  } catch {}
2754
3181
  }
2755
3182
  try {
2756
- const files = readdirSync(dir).filter((f2) => f2.startsWith("e2e-key.") && f2.endsWith(".bak"));
3183
+ const files = readdirSync2(dir).filter((f2) => f2.startsWith("e2e-key.") && f2.endsWith(".bak"));
2757
3184
  for (const file of files) {
2758
3185
  const fullPath = join(dir, file);
2759
3186
  try {
@@ -2769,7 +3196,7 @@ function discoverE2EKeys() {
2769
3196
  }
2770
3197
  function readRotationConfig() {
2771
3198
  const configPath = join(getAgentApproveDir(), "e2e-rotation.json");
2772
- if (!existsSync(configPath))
3199
+ if (!existsSync2(configPath))
2773
3200
  return null;
2774
3201
  try {
2775
3202
  return JSON.parse(readFileSync(configPath, "utf-8"));
@@ -2783,11 +3210,11 @@ function writeRotationConfig(config) {
2783
3210
  }
2784
3211
  function ensureAgentApproveDir() {
2785
3212
  const dir = getAgentApproveDir();
2786
- if (!existsSync(dir)) {
3213
+ if (!existsSync2(dir)) {
2787
3214
  mkdirSync(dir, { recursive: true, mode: 448 });
2788
3215
  }
2789
3216
  const hooksDir = join(dir, "hooks");
2790
- if (!existsSync(hooksDir)) {
3217
+ if (!existsSync2(hooksDir)) {
2791
3218
  mkdirSync(hooksDir, { recursive: true, mode: 448 });
2792
3219
  }
2793
3220
  }
@@ -2796,14 +3223,14 @@ function migrateE2ERootKey() {
2796
3223
  const keyPath = join(dir, "e2e-key");
2797
3224
  const rootKeyPath = join(dir, "e2e-root-key");
2798
3225
  const rotationPath = join(dir, "e2e-rotation.json");
2799
- if (!existsSync(keyPath) || existsSync(rootKeyPath))
3226
+ if (!existsSync2(keyPath) || existsSync2(rootKeyPath))
2800
3227
  return;
2801
3228
  try {
2802
3229
  const keyHex = readFileSync(keyPath, "utf-8").trim();
2803
3230
  if (keyHex.length !== 64)
2804
3231
  return;
2805
3232
  writeFileSync(rootKeyPath, keyHex, { mode: 384 });
2806
- if (!existsSync(rotationPath)) {
3233
+ if (!existsSync2(rotationPath)) {
2807
3234
  const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
2808
3235
  writeRotationConfig({
2809
3236
  rootKeyId: keyId,
@@ -2815,7 +3242,7 @@ function migrateE2ERootKey() {
2815
3242
  } catch {}
2816
3243
  }
2817
3244
  function backupConfig(configPath) {
2818
- if (!existsSync(configPath)) {
3245
+ if (!existsSync2(configPath)) {
2819
3246
  return null;
2820
3247
  }
2821
3248
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
@@ -2824,7 +3251,7 @@ function backupConfig(configPath) {
2824
3251
  return backupPath;
2825
3252
  }
2826
3253
  function readJsonConfig(configPath) {
2827
- if (!existsSync(configPath)) {
3254
+ if (!existsSync2(configPath)) {
2828
3255
  return {};
2829
3256
  }
2830
3257
  try {
@@ -2835,21 +3262,21 @@ function readJsonConfig(configPath) {
2835
3262
  }
2836
3263
  }
2837
3264
  function writeJsonConfig(configPath, config) {
2838
- const dir = dirname(configPath);
2839
- if (!existsSync(dir)) {
3265
+ const dir = dirname2(configPath);
3266
+ if (!existsSync2(dir)) {
2840
3267
  mkdirSync(dir, { recursive: true, mode: 448 });
2841
3268
  }
2842
3269
  writeFileSync(configPath, JSON.stringify(config, null, 2) + `
2843
3270
  `);
2844
3271
  }
2845
3272
  function ensureCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.toml")) {
2846
- const dir = dirname(configPath);
2847
- if (!existsSync(dir)) {
3273
+ const dir = dirname2(configPath);
3274
+ if (!existsSync2(dir)) {
2848
3275
  mkdirSync(dir, { recursive: true, mode: 448 });
2849
3276
  }
2850
3277
  const sectionHeader = "[features]";
2851
3278
  const desiredLine = "codex_hooks = true";
2852
- if (!existsSync(configPath)) {
3279
+ if (!existsSync2(configPath)) {
2853
3280
  writeFileSync(configPath, `${sectionHeader}
2854
3281
  ${desiredLine}
2855
3282
  `, { mode: 384 });
@@ -2909,7 +3336,7 @@ ${desiredLine}
2909
3336
  return { updated: true, backupPath };
2910
3337
  }
2911
3338
  function disableCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.toml")) {
2912
- if (!existsSync(configPath)) {
3339
+ if (!existsSync2(configPath)) {
2913
3340
  return { updated: false, backupPath: null };
2914
3341
  }
2915
3342
  const original = readFileSync(configPath, "utf-8");
@@ -3197,7 +3624,7 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3197
3624
  }
3198
3625
  hooks.internal.enabled = true;
3199
3626
  writeJsonConfig(agent.configPath, config);
3200
- installedHooks.push("openclaw");
3627
+ installedHooks.push(installResult.label);
3201
3628
  return { success: true, backupPath, hooks: installedHooks };
3202
3629
  }
3203
3630
  if (agentId === "opencode") {
@@ -3212,13 +3639,13 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3212
3639
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
3213
3640
  try {
3214
3641
  let pkgJson = {};
3215
- if (existsSync(opencodePkgPath)) {
3642
+ if (existsSync2(opencodePkgPath)) {
3216
3643
  pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
3217
3644
  }
3218
3645
  if (!pkgJson.dependencies) {
3219
3646
  pkgJson.dependencies = {};
3220
3647
  }
3221
- pkgJson.dependencies["@agentapprove/opencode"] = "latest";
3648
+ pkgJson.dependencies["@agentapprove/opencode"] = OPENCODE_PLUGIN_VERSION;
3222
3649
  writeFileSync(opencodePkgPath, JSON.stringify(pkgJson, null, 2) + `
3223
3650
  `);
3224
3651
  } catch (err) {
@@ -3226,7 +3653,7 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3226
3653
  console.warn(`Warning: Could not update package.json: ${err.message}`);
3227
3654
  }
3228
3655
  }
3229
- installedHooks.push("agentapprove");
3656
+ installedHooks.push("Agent Approve plugin");
3230
3657
  return { success: true, backupPath, hooks: installedHooks };
3231
3658
  }
3232
3659
  const hooksToInstall = mode === "observe" ? agent.hooks.filter((h2) => !h2.isApprovalHook) : agent.hooks;
@@ -3653,64 +4080,10 @@ codex_hooks = true`;
3653
4080
  }
3654
4081
  return "";
3655
4082
  }
3656
- var HOOK_FILES = [
3657
- "common.sh",
3658
- "claude-pre-tool.sh",
3659
- "claude-post-tool.sh",
3660
- "claude-post-tool-failure.sh",
3661
- "claude-notification.sh",
3662
- "claude-user-prompt.sh",
3663
- "claude-prompt.sh",
3664
- "claude-session-start.sh",
3665
- "claude-session-end.sh",
3666
- "claude-stop.sh",
3667
- "notify-hook.sh",
3668
- "completion-hook.sh",
3669
- "cursor-session-start.sh",
3670
- "cursor-session-end.sh",
3671
- "cursor-approval.sh",
3672
- "cursor-mcp-approval.sh",
3673
- "cursor-pre-tool.sh",
3674
- "cursor-shell-complete.sh",
3675
- "cursor-mcp-complete.sh",
3676
- "cursor-post-tool.sh",
3677
- "cursor-start.sh",
3678
- "cursor-subagent-start.sh",
3679
- "cursor-subagent-stop.sh",
3680
- "cursor-precompact.sh",
3681
- "cursor-stop.sh",
3682
- "cursor-thought.sh",
3683
- "cursor-response.sh",
3684
- "gemini-before-tool.sh",
3685
- "gemini-after-tool.sh",
3686
- "gemini-before-agent.sh",
3687
- "gemini-after-agent.sh",
3688
- "gemini-before-model.sh",
3689
- "gemini-after-model.sh",
3690
- "gemini-notification.sh",
3691
- "gemini-session-start.sh",
3692
- "gemini-session-end.sh",
3693
- "gemini-stop.sh",
3694
- "codex-session-start.sh",
3695
- "codex-pre-tool.sh",
3696
- "codex-post-tool.sh",
3697
- "codex-user-prompt.sh",
3698
- "codex-stop.sh",
3699
- "github-session-start.sh",
3700
- "github-session-end.sh",
3701
- "github-user-prompt.sh",
3702
- "github-pre-tool.sh",
3703
- "github-post-tool.sh",
3704
- "github-error.sh",
3705
- "github-stop.sh",
3706
- "github-subagent-start.sh",
3707
- "github-subagent-stop.sh",
3708
- "github-precompact.sh"
3709
- ];
3710
- async function copyHookScripts(hooksDir, token) {
4083
+ async function copyHookScripts(hooksDir, token, files) {
3711
4084
  let downloaded = 0;
3712
4085
  const failed = [];
3713
- for (const file of HOOK_FILES) {
4086
+ for (const file of files) {
3714
4087
  try {
3715
4088
  const response = await fetch(`${API_URL}/${API_VERSION}/hooks/${file}?format=raw`, {
3716
4089
  headers: {
@@ -3737,13 +4110,43 @@ async function copyHookScripts(hooksDir, token) {
3737
4110
  }
3738
4111
  return { downloaded, failed };
3739
4112
  }
4113
+ function readOpenClawInstalledVersion() {
4114
+ const packagePath = join(homedir(), ".openclaw", "extensions", "openclaw", "package.json");
4115
+ if (!existsSync2(packagePath)) {
4116
+ return null;
4117
+ }
4118
+ try {
4119
+ const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
4120
+ return typeof pkg.version === "string" ? pkg.version : null;
4121
+ } catch {
4122
+ return null;
4123
+ }
4124
+ }
3740
4125
  function installOpenClawPluginViaCli() {
3741
4126
  try {
3742
4127
  execSync(`openclaw plugins install ${OPENCLAW_PLUGIN_SPEC}`, { stdio: "pipe" });
3743
- return { success: true };
4128
+ const installedVersion = readOpenClawInstalledVersion();
4129
+ if (!installedVersion) {
4130
+ return {
4131
+ success: true,
4132
+ label: "Agent Approve plugin (version not verified)"
4133
+ };
4134
+ }
4135
+ if (installedVersion !== OPENCLAW_PLUGIN_VERSION) {
4136
+ return {
4137
+ success: false,
4138
+ error: `OpenClaw installed ${installedVersion}, expected ${OPENCLAW_PLUGIN_VERSION}. Re-run after updating the published installer package.`,
4139
+ label: "Agent Approve plugin"
4140
+ };
4141
+ }
4142
+ return {
4143
+ success: true,
4144
+ version: installedVersion,
4145
+ label: `Agent Approve plugin v${installedVersion}`
4146
+ };
3744
4147
  } catch (err) {
3745
4148
  const message = err instanceof Error ? err.message : "unknown error";
3746
- return { success: false, error: message };
4149
+ return { success: false, error: message, label: "Agent Approve plugin" };
3747
4150
  }
3748
4151
  }
3749
4152
  var SYSTEM_DEPS = [
@@ -3968,7 +4371,7 @@ async function installCommand() {
3968
4371
  const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
3969
4372
  const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
3970
4373
  let e2eLine = "";
3971
- if (existsSync(e2eKeyPath2)) {
4374
+ if (existsSync2(e2eKeyPath2)) {
3972
4375
  const keyHex = readFileSync(e2eKeyPath2, "utf-8").trim();
3973
4376
  const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
3974
4377
  e2eLine = `
@@ -3978,7 +4381,7 @@ E2E Key: ${keyId}`;
3978
4381
  Privacy: ${existingConfig.privacy || "unknown"}${e2eLine}`, "Existing configuration found");
3979
4382
  } else {
3980
4383
  me(`Approve AI agent actions from your iPhone or Apple Watch.
3981
- Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and GitHub Copilot CLI.`, "About");
4384
+ Installs hooks and plugins for OpenClaw, OpenAI Codex, Claude Code, Cursor, Gemini CLI, and more.`, "About");
3982
4385
  }
3983
4386
  const installedAgents = detectInstalledAgents();
3984
4387
  const agentOptions = Object.entries(AGENTS).map(([id, agent]) => ({
@@ -4057,7 +4460,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4057
4460
  }
4058
4461
  connectionOptions.push({ value: "qr", label: "Scan QR code", hint: hasExistingToken ? undefined : "Recommended" });
4059
4462
  const connectionMethod = await le({
4060
- message: "Connect to iOS app (required for hook download)",
4463
+ message: "Connect to iOS app (required for setup and any hook downloads)",
4061
4464
  options: connectionOptions
4062
4465
  });
4063
4466
  if (lD(connectionMethod)) {
@@ -4072,7 +4475,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4072
4475
  if (connectionMethod === "existing") {
4073
4476
  token = existingConfig.token;
4074
4477
  apiUrl = existingConfig.apiUrl || API_URL;
4075
- const e2eKeyExists = existsSync(join(getAgentApproveDir(), "e2e-key"));
4478
+ const e2eKeyExists = existsSync2(join(getAgentApproveDir(), "e2e-key"));
4076
4479
  useE2E = e2eKeyExists;
4077
4480
  v2.success("Using existing token");
4078
4481
  } else if (connectionMethod === "qr") {
@@ -4082,7 +4485,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4082
4485
  const existingKeyPath = join(agentApproveDir, "e2e-key");
4083
4486
  let e2eUserKey = null;
4084
4487
  if (useE2E) {
4085
- if (existsSync(existingKeyPath)) {
4488
+ if (existsSync2(existingKeyPath)) {
4086
4489
  const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
4087
4490
  const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
4088
4491
  v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
@@ -4150,7 +4553,7 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
4150
4553
  });
4151
4554
  await new Promise((resolve) => setTimeout(resolve, 10));
4152
4555
  me(qrDisplay + `
4153
- Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4556
+ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
4154
4557
  const pairingSpinner = _2();
4155
4558
  pairingSpinner.start("Waiting for iOS app...");
4156
4559
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -4220,17 +4623,23 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4220
4623
  failBehavior,
4221
4624
  configSetAt
4222
4625
  }).catch(() => {});
4223
- const downloadSpinner = _2();
4224
- downloadSpinner.start("Downloading hook scripts");
4225
- const downloadResult = await copyHookScripts(hooksDir, token);
4226
- if (downloadResult.failed.length > 0) {
4227
- downloadSpinner.stop(`Downloaded ${downloadResult.downloaded} hooks, ${downloadResult.failed.length} failed`);
4228
- v2.warn(`Failed to download: ${downloadResult.failed.join(", ")}`);
4626
+ const hookDownloadPlan = buildHookDownloadPlan(selectedAgents);
4627
+ if (hookDownloadPlan.files.length > 0) {
4628
+ const downloadSpinner = _2();
4629
+ downloadSpinner.start("Downloading hook scripts");
4630
+ const downloadResult = await copyHookScripts(hooksDir, token, hookDownloadPlan.files);
4631
+ const summary = formatHookDownloadSummary(hookDownloadPlan);
4632
+ if (downloadResult.failed.length > 0) {
4633
+ downloadSpinner.stop(`Downloaded ${downloadResult.downloaded} of ${hookDownloadPlan.files.length} hook files (${summary})`);
4634
+ v2.warn(`Failed to download: ${downloadResult.failed.join(", ")}`);
4635
+ } else {
4636
+ downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files: ${summary})`);
4637
+ }
4229
4638
  } else {
4230
- downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files)`);
4639
+ v2.success("No hook scripts needed for the selected agents");
4231
4640
  }
4232
4641
  const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
4233
- const hasE2EKey = existsSync(e2eKeyPath);
4642
+ const hasE2EKey = existsSync2(e2eKeyPath);
4234
4643
  let installMode = "approval";
4235
4644
  if (hasE2EKey) {
4236
4645
  const modeChoice = await le({
@@ -4259,7 +4668,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4259
4668
  v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
4260
4669
  }
4261
4670
  const envPath = join(getAgentApproveDir(), "env");
4262
- if (existsSync(envPath)) {
4671
+ if (existsSync2(envPath)) {
4263
4672
  let envContent = readFileSync(envPath, "utf-8");
4264
4673
  if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
4265
4674
  envContent = envContent.replace(/AGENTAPPROVE_E2E_MODE=\w+/, `AGENTAPPROVE_E2E_MODE=${installMode}`);
@@ -4360,7 +4769,8 @@ Backups will be created with timestamp`, "Files to be modified");
4360
4769
  npx agentapprove pair Link a new iOS device
4361
4770
  npx agentapprove status Show current configuration
4362
4771
  npx agentapprove disable Temporarily disable hooks
4363
- npx agentapprove uninstall Remove all hooks and config
4772
+ npx agentapprove uninstall Remove hooks/plugins and keep local state
4773
+ npx agentapprove purge Remove hooks/plugins and delete local state
4364
4774
  npx agentapprove restore Restore original configs`, "Useful commands");
4365
4775
  ge(`${source_default.green("Agent Approve is ready!")} ${source_default.dim("Learn more at")} ${source_default.cyan("agentapprove.com")}`);
4366
4776
  }
@@ -4369,7 +4779,7 @@ async function statusCommand() {
4369
4779
  Agent Approve Status
4370
4780
  `));
4371
4781
  const envPath = join(getAgentApproveDir(), "env");
4372
- if (!existsSync(envPath)) {
4782
+ if (!existsSync2(envPath)) {
4373
4783
  console.log(source_default.yellow(" Not configured. Run `npx agentapprove` to set up.\n"));
4374
4784
  return;
4375
4785
  }
@@ -4378,38 +4788,42 @@ async function statusCommand() {
4378
4788
  `);
4379
4789
  for (const line of lines) {
4380
4790
  if (line.startsWith("AGENTAPPROVE_")) {
4381
- const [key, value] = line.split("=");
4382
- const displayKey = key.replace("AGENTAPPROVE_", "").toLowerCase();
4383
- const displayValue = key.includes("TOKEN") ? value.slice(0, 15) + "..." : value;
4791
+ const assignment = parseEnvAssignment(line);
4792
+ if (!assignment) {
4793
+ continue;
4794
+ }
4795
+ const { key, value } = assignment;
4796
+ const displayKey = STATUS_FIELD_LABELS[key] || key.replace("AGENTAPPROVE_", "");
4797
+ const displayValue = formatStatusValue(key, value);
4384
4798
  console.log(` ${source_default.dim(displayKey + ":")} ${displayValue}`);
4385
4799
  }
4386
4800
  }
4387
4801
  const e2eKeyFile = join(getAgentApproveDir(), "e2e-key");
4388
4802
  const e2eServerKeyFile = join(getAgentApproveDir(), "e2e-server-key");
4389
- if (existsSync(e2eKeyFile)) {
4803
+ if (existsSync2(e2eKeyFile)) {
4390
4804
  console.log(` ${source_default.green("✓")} E2E user key: installed`);
4391
4805
  }
4392
- if (existsSync(e2eServerKeyFile)) {
4806
+ if (existsSync2(e2eServerKeyFile)) {
4393
4807
  console.log(` ${source_default.green("✓")} E2E server key: installed`);
4394
4808
  }
4395
- if (!existsSync(e2eKeyFile) && !existsSync(e2eServerKeyFile)) {
4809
+ if (!existsSync2(e2eKeyFile) && !existsSync2(e2eServerKeyFile)) {
4396
4810
  console.log(` ${source_default.dim(" E2E encryption: not configured")}`);
4397
4811
  }
4398
4812
  console.log();
4399
4813
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4400
- if (existsSync(agent.configPath)) {
4814
+ if (existsSync2(agent.configPath)) {
4401
4815
  const config = readJsonConfig(agent.configPath);
4402
4816
  if (agentId === "opencode") {
4403
4817
  const pluginConfig = config.plugin;
4404
4818
  if (Array.isArray(pluginConfig) && pluginConfig.some((entry) => typeof entry === "string" && entry.includes("@agentapprove/opencode"))) {
4405
- console.log(` ${source_default.green("✓")} ${agent.name}: agentapprove plugin`);
4819
+ console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve plugin`);
4406
4820
  }
4407
4821
  continue;
4408
4822
  }
4409
4823
  if (agentId === "openclaw") {
4410
4824
  const entries = config.plugins?.entries;
4411
4825
  if (entries?.openclaw) {
4412
- console.log(` ${source_default.green("✓")} ${agent.name}: openclaw plugin`);
4826
+ console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve plugin`);
4413
4827
  }
4414
4828
  continue;
4415
4829
  }
@@ -4423,7 +4837,7 @@ async function statusCommand() {
4423
4837
  return str.includes("agentapprove");
4424
4838
  });
4425
4839
  if (installedHooks.length > 0) {
4426
- console.log(` ${source_default.green("✓")} ${agent.name}: ${installedHooks.map((h2) => h2.name).join(", ")}`);
4840
+ console.log(` ${source_default.green("✓")} ${agent.name}: ${installedHooks.map((h2) => formatHookStatusLabel(h2.name)).join(", ")}`);
4427
4841
  }
4428
4842
  }
4429
4843
  }
@@ -4433,8 +4847,8 @@ async function statusCommand() {
4433
4847
  async function disableCommand() {
4434
4848
  const envPath = join(getAgentApproveDir(), "env");
4435
4849
  const disabledPath = join(getAgentApproveDir(), "env.disabled");
4436
- if (!existsSync(envPath)) {
4437
- if (existsSync(disabledPath)) {
4850
+ if (!existsSync2(envPath)) {
4851
+ if (existsSync2(disabledPath)) {
4438
4852
  console.log(source_default.yellow(`
4439
4853
  Already disabled.
4440
4854
  `));
@@ -4453,13 +4867,13 @@ async function disableCommand() {
4453
4867
  async function enableCommand() {
4454
4868
  const envPath = join(getAgentApproveDir(), "env");
4455
4869
  const disabledPath = join(getAgentApproveDir(), "env.disabled");
4456
- if (existsSync(envPath)) {
4870
+ if (existsSync2(envPath)) {
4457
4871
  console.log(source_default.yellow(`
4458
4872
  Already enabled.
4459
4873
  `));
4460
4874
  return;
4461
4875
  }
4462
- if (!existsSync(disabledPath)) {
4876
+ if (!existsSync2(disabledPath)) {
4463
4877
  console.log(source_default.yellow("\n Not configured. Run `npx agentapprove` to set up.\n"));
4464
4878
  return;
4465
4879
  }
@@ -4469,12 +4883,22 @@ async function enableCommand() {
4469
4883
  ✓ Agent Approve enabled.
4470
4884
  `));
4471
4885
  }
4472
- async function uninstallCommand() {
4886
+ async function performUninstall(mode) {
4887
+ const plan = buildUninstallPlan();
4888
+ if (!hasUninstallWork(mode, plan)) {
4889
+ console.log(source_default.yellow(`
4890
+ Nothing to ${mode}. Run \`npx agentapprove install\` to set up Agent Approve.
4891
+ `));
4892
+ return;
4893
+ }
4894
+ if (!await confirmUninstallPlan(mode, plan)) {
4895
+ return;
4896
+ }
4473
4897
  console.log(source_default.cyan(`
4474
- Uninstalling Agent Approve hooks...
4898
+ ${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
4475
4899
  `));
4476
4900
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4477
- if (!existsSync(agent.configPath))
4901
+ if (!existsSync2(agent.configPath))
4478
4902
  continue;
4479
4903
  const config = readJsonConfig(agent.configPath);
4480
4904
  const hooksConfig = config[agent.hooksKey] ?? {};
@@ -4494,7 +4918,7 @@ async function uninstallCommand() {
4494
4918
  }
4495
4919
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
4496
4920
  try {
4497
- if (existsSync(opencodePkgPath)) {
4921
+ if (existsSync2(opencodePkgPath)) {
4498
4922
  const pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
4499
4923
  const deps = pkgJson.dependencies;
4500
4924
  if (deps && deps["@agentapprove/opencode"]) {
@@ -4508,13 +4932,15 @@ async function uninstallCommand() {
4508
4932
  const pluginsConfig = hooksConfig;
4509
4933
  const entries = pluginsConfig.entries;
4510
4934
  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;
4935
+ for (const key of ["openclaw", "agentapprove"]) {
4936
+ if (entries && entries[key]) {
4937
+ delete entries[key];
4938
+ modified = true;
4939
+ }
4940
+ if (installs && installs[key]) {
4941
+ delete installs[key];
4942
+ modified = true;
4943
+ }
4518
4944
  }
4519
4945
  } else {
4520
4946
  for (const hook of agent.hooks) {
@@ -4539,8 +4965,8 @@ async function uninstallCommand() {
4539
4965
  modified = true;
4540
4966
  }
4541
4967
  }
4542
- if (hooksConfig["Prompt"]) {
4543
- delete hooksConfig["Prompt"];
4968
+ if (hooksConfig.Prompt) {
4969
+ delete hooksConfig.Prompt;
4544
4970
  modified = true;
4545
4971
  }
4546
4972
  }
@@ -4555,50 +4981,72 @@ async function uninstallCommand() {
4555
4981
  }
4556
4982
  }
4557
4983
  }
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
4984
  const removedVSCode = removeFromVSCodeHookLocations();
4568
4985
  if (removedVSCode.length > 0) {
4569
4986
  for (const path of removedVSCode) {
4570
4987
  console.log(` ${source_default.green("✓")} Removed hook path from ${path}`);
4571
4988
  }
4572
4989
  }
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)}`);
4990
+ for (const target of plan.pluginArtifactTargets) {
4991
+ if (!removeRemovalTarget(target)) {
4992
+ continue;
4993
+ }
4994
+ console.log(` ${source_default.green("✓")} Removed plugin files at ${target.displayPath}`);
4995
+ if (target.actualPath.endsWith("/node_modules/@agentapprove/opencode")) {
4996
+ pruneEmptyDir(dirname2(target.actualPath));
4997
+ pruneEmptyDir(dirname2(dirname2(target.actualPath)));
4998
+ }
4999
+ if (target.actualPath.includes("/extensions/")) {
5000
+ pruneEmptyDir(dirname2(target.actualPath));
4584
5001
  }
4585
5002
  }
5003
+ if (mode === "purge") {
5004
+ for (const target of plan.backupTargets) {
5005
+ if (removeRemovalTarget(target)) {
5006
+ console.log(` ${source_default.green("✓")} Removed backup ${target.displayPath}`);
5007
+ }
5008
+ }
5009
+ for (const target of plan.managedState.reversible) {
5010
+ if (removeRemovalTarget(target)) {
5011
+ console.log(` ${source_default.green("✓")} Removed local file ${target.displayPath}`);
5012
+ }
5013
+ }
5014
+ for (const target of plan.managedState.crypto) {
5015
+ if (removeRemovalTarget(target)) {
5016
+ console.log(` ${source_default.green("✓")} Removed E2E material ${target.displayPath}`);
5017
+ }
5018
+ }
5019
+ if (deleteTokenFromKeychain()) {
5020
+ console.log(` ${source_default.green("✓")} Removed token from credential store`);
5021
+ }
5022
+ finalizeAgentApproveDirPurge(getAgentApproveDir());
5023
+ }
4586
5024
  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
5025
+ Agent Approve ${mode === "purge" ? "purged" : "uninstalled"}.`));
5026
+ if (mode === "purge") {
5027
+ console.log(source_default.dim(` Local Agent Approve state and backups were removed.
4590
5028
  `));
5029
+ return;
5030
+ }
5031
+ console.log(source_default.dim(" Local Agent Approve config, logs, hooks, backups, and E2E keys were kept."));
5032
+ console.log(source_default.dim(" Run `npx agentapprove purge` if you want to remove all local Agent Approve traces.\n"));
5033
+ }
5034
+ async function uninstallCommand() {
5035
+ await performUninstall("uninstall");
5036
+ }
5037
+ async function purgeCommand() {
5038
+ await performUninstall("purge");
4591
5039
  }
4592
5040
  async function restoreCommand() {
4593
5041
  console.log(source_default.cyan(`
4594
5042
  Restoring original configurations...
4595
5043
  `));
4596
- const { readdirSync: readdirSync2, copyFileSync: copyFileSync2 } = await import("fs");
5044
+ const { readdirSync: readdirSync3, copyFileSync: copyFileSync2 } = await import("fs");
4597
5045
  for (const [agentId, agent] of Object.entries(AGENTS)) {
4598
- const configDir = dirname(agent.configPath);
4599
- if (!existsSync(configDir))
5046
+ const configDir = dirname2(agent.configPath);
5047
+ if (!existsSync2(configDir))
4600
5048
  continue;
4601
- const files = readdirSync2(configDir);
5049
+ const files = readdirSync3(configDir);
4602
5050
  const backups = files.filter((f2) => f2.startsWith(agent.configPath.split("/").pop()) && f2.includes(".backup.")).sort().reverse();
4603
5051
  if (backups.length > 0) {
4604
5052
  const latestBackup = join(configDir, backups[0]);
@@ -4610,7 +5058,7 @@ async function restoreCommand() {
4610
5058
  }
4611
5059
  async function initRepoCommand() {
4612
5060
  const cliHooksPath = join(getAgentApproveDir(), "copilot-cli-hooks.json");
4613
- if (!existsSync(cliHooksPath)) {
5061
+ if (!existsSync2(cliHooksPath)) {
4614
5062
  console.log(source_default.yellow(`
4615
5063
  GitHub Copilot CLI hooks not found. Run the installer first with GitHub Copilot CLI selected.`));
4616
5064
  console.log(source_default.dim(` npx agentapprove install
@@ -4619,12 +5067,12 @@ async function initRepoCommand() {
4619
5067
  }
4620
5068
  let repoRoot = process.cwd();
4621
5069
  let found = false;
4622
- while (repoRoot !== dirname(repoRoot)) {
4623
- if (existsSync(join(repoRoot, ".git"))) {
5070
+ while (repoRoot !== dirname2(repoRoot)) {
5071
+ if (existsSync2(join(repoRoot, ".git"))) {
4624
5072
  found = true;
4625
5073
  break;
4626
5074
  }
4627
- repoRoot = dirname(repoRoot);
5075
+ repoRoot = dirname2(repoRoot);
4628
5076
  }
4629
5077
  if (!found) {
4630
5078
  console.log(source_default.yellow(`
@@ -4637,7 +5085,7 @@ async function initRepoCommand() {
4637
5085
  console.log(source_default.cyan(`
4638
5086
  Installing GitHub Copilot CLI hooks to ${targetFile}
4639
5087
  `));
4640
- if (existsSync(targetFile)) {
5088
+ if (existsSync2(targetFile)) {
4641
5089
  console.log(source_default.yellow(" agentapprove.json already exists in .github/hooks/"));
4642
5090
  const overwrite = await ce({
4643
5091
  message: "Overwrite existing file?"
@@ -4648,7 +5096,7 @@ async function initRepoCommand() {
4648
5096
  process.exit(0);
4649
5097
  }
4650
5098
  }
4651
- if (!existsSync(targetDir)) {
5099
+ if (!existsSync2(targetDir)) {
4652
5100
  mkdirSync(targetDir, { recursive: true });
4653
5101
  }
4654
5102
  copyFileSync(cliHooksPath, targetFile);
@@ -4666,13 +5114,14 @@ ${source_default.yellow("Usage:")}
4666
5114
 
4667
5115
  ${source_default.yellow("Commands:")}
4668
5116
  ${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)
5117
+ ${source_default.green("pair")} Link a new iOS device or repair E2E pairing
5118
+ ${source_default.green("refresh")} Generate a new token and reuse current pairing context
4671
5119
  ${source_default.green("init-repo")} Add GitHub Copilot CLI hooks to current repo (.github/hooks/)
4672
5120
  ${source_default.green("status")} Show current configuration and installed hooks
4673
5121
  ${source_default.green("disable")} Temporarily disable hooks
4674
5122
  ${source_default.green("enable")} Re-enable hooks after disabling
4675
- ${source_default.green("uninstall")} Remove all hooks from agent configs
5123
+ ${source_default.green("uninstall")} ${INSTALLER_COMMAND_DESCRIPTIONS.uninstall}
5124
+ ${source_default.green("purge")} ${INSTALLER_COMMAND_DESCRIPTIONS.purge}
4676
5125
  ${source_default.green("restore")} Restore original configs from backups
4677
5126
  ${source_default.green("help")} Show this help message
4678
5127
 
@@ -4700,18 +5149,39 @@ async function refreshCommand() {
4700
5149
  but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
4701
5150
  let token = null;
4702
5151
  let apiUrl = API_URL;
4703
- const session = await createPairingSession();
5152
+ const installedAgents = detectInstalledAgents();
5153
+ const e2eEnabled = existingConfig.e2eEnabled !== false;
5154
+ const e2eKeyPath = join(getAgentApproveDir(), "e2e-key");
5155
+ let e2eUserKey = null;
5156
+ let e2eKeyId;
5157
+ if (e2eEnabled && existsSync2(e2eKeyPath)) {
5158
+ const existingKey = readFileSync(e2eKeyPath, "utf-8").trim();
5159
+ if (existingKey.length === 64) {
5160
+ e2eUserKey = existingKey;
5161
+ e2eKeyId = createHash("sha256").update(Buffer.from(existingKey, "hex")).digest("hex").slice(0, 8);
5162
+ 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.`);
5163
+ }
5164
+ } else {
5165
+ v2.info('Refresh updates the token only. Use "npx agentapprove pair" if you need to repair or change E2E pairing.');
5166
+ }
5167
+ const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
4704
5168
  if (!session || session.error) {
4705
5169
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
4706
5170
  process.exit(1);
4707
5171
  }
5172
+ const machineHost = hostname();
5173
+ let qrUrl = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
5174
+ if (e2eUserKey) {
5175
+ const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
5176
+ qrUrl += `&e2eKey=${e2eKeyBase64url}`;
5177
+ }
4708
5178
  let qrDisplay = "";
4709
- import_qrcode_terminal.default.generate(session.qrUrl, { small: true }, (qr) => {
5179
+ import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
4710
5180
  qrDisplay = qr;
4711
5181
  });
4712
5182
  await new Promise((resolve) => setTimeout(resolve, 10));
4713
5183
  me(qrDisplay + `
4714
- Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
5184
+ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
4715
5185
  const pairingSpinner = _2();
4716
5186
  pairingSpinner.start("Waiting for iOS app...");
4717
5187
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -4741,10 +5211,16 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
4741
5211
  privacy,
4742
5212
  retentionDays,
4743
5213
  debugLog: existingConfig.debugLog,
5214
+ e2eMode: existingConfig.e2eMode,
5215
+ e2eEnabled,
4744
5216
  failBehavior,
4745
5217
  configSetAt
4746
5218
  });
4747
5219
  await storeTokenInKeychain(token);
5220
+ if (e2eEnabled && result.e2eServerKey) {
5221
+ const serverKeyPath = join(getAgentApproveDir(), "e2e-server-key");
5222
+ writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
5223
+ }
4748
5224
  pushConfigToCloud({
4749
5225
  apiUrl,
4750
5226
  token,
@@ -4869,7 +5345,7 @@ async function pairCommand() {
4869
5345
  const noteLabel = e2eKeyId ? `Session: ${session.sessionCode}
4870
5346
  Key ID: ${e2eKeyId}` : `Session: ${session.sessionCode}`;
4871
5347
  me(qrDisplay + `
4872
- ${noteLabel}`, "Scan with Agent Approve iOS app");
5348
+ ${noteLabel}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
4873
5349
  const pairingSpinner = _2();
4874
5350
  pairingSpinner.start("Waiting for iOS app...");
4875
5351
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -4890,7 +5366,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4890
5366
  pairingSpinner.stop(`Paired! ${result.email ? `Account: ${result.email}` : ""}`);
4891
5367
  if (e2eUserKey && e2eKeyId) {
4892
5368
  const rootKeyPath = join(getAgentApproveDir(), "e2e-root-key");
4893
- if (!existsSync(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
5369
+ if (!existsSync2(rootKeyPath) || readFileSync(rootKeyPath, "utf-8").trim() !== e2eUserKey) {
4894
5370
  writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
4895
5371
  }
4896
5372
  if (!readRotationConfig()) {
@@ -4914,12 +5390,14 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4914
5390
  privacy: existingConfig?.privacy || result.privacy || "full",
4915
5391
  retentionDays: existingConfig?.retentionDays ?? 30,
4916
5392
  debugLog: existingConfig?.debugLog,
5393
+ e2eMode: existingConfig?.e2eMode,
5394
+ e2eEnabled: !!e2eUserKey,
4917
5395
  failBehavior: existingConfig?.failBehavior || "ask",
4918
5396
  configSetAt
4919
5397
  });
4920
5398
  await storeTokenInKeychain(result.token);
4921
5399
  const hooksDir = join(getAgentApproveDir(), "hooks");
4922
- if (existsSync(hooksDir)) {
5400
+ if (existsSync2(hooksDir)) {
4923
5401
  const updateSpinner = _2();
4924
5402
  updateSpinner.start("Updating hook scripts with new token");
4925
5403
  try {
@@ -4941,7 +5419,7 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
4941
5419
  }
4942
5420
  async function updateHookScriptsWithToken(token, apiUrl) {
4943
5421
  const hooksDir = join(getAgentApproveDir(), "hooks");
4944
- if (!existsSync(hooksDir)) {
5422
+ if (!existsSync2(hooksDir)) {
4945
5423
  throw new Error("Hooks directory not found");
4946
5424
  }
4947
5425
  const hookFiles = [
@@ -4964,7 +5442,7 @@ async function updateHookScriptsWithToken(token, apiUrl) {
4964
5442
  ];
4965
5443
  for (const file of hookFiles) {
4966
5444
  const filePath = join(hooksDir, file);
4967
- if (existsSync(filePath)) {
5445
+ if (existsSync2(filePath)) {
4968
5446
  let content = readFileSync(filePath, "utf-8");
4969
5447
  content = content.replace(/export AGENTAPPROVE_TOKEN=".*"/, `export AGENTAPPROVE_TOKEN="${token}"`);
4970
5448
  content = content.replace(/export AGENTAPPROVE_API=".*"/, `export AGENTAPPROVE_API="${apiUrl}"`);
@@ -4996,6 +5474,9 @@ async function main() {
4996
5474
  case "uninstall":
4997
5475
  await uninstallCommand();
4998
5476
  break;
5477
+ case "purge":
5478
+ await purgeCommand();
5479
+ break;
4999
5480
  case "restore":
5000
5481
  await restoreCommand();
5001
5482
  break;