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.
- package/dist/cli.js +479 -110
- 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
|
|
2332
|
+
import { join, dirname as dirname2 } from "path";
|
|
2333
2333
|
import { execSync, spawnSync } from "child_process";
|
|
2334
2334
|
import { randomBytes, createHash } from "crypto";
|
|
2335
|
-
|
|
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 (!
|
|
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.
|
|
2384
|
-
var OPENCLAW_PLUGIN_VERSION = "0.2.
|
|
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 (
|
|
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 &&
|
|
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 }) =>
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
2843
|
+
if (existsSync2(join(homedir(), ".copilot"))) {
|
|
2671
2844
|
installed.push(id);
|
|
2672
2845
|
}
|
|
2673
2846
|
} else if (id === "openclaw") {
|
|
2674
|
-
if (
|
|
2847
|
+
if (existsSync2(join(homedir(), ".openclaw"))) {
|
|
2675
2848
|
installed.push(id);
|
|
2676
2849
|
}
|
|
2677
2850
|
} else if (id === "codex") {
|
|
2678
|
-
if (
|
|
2851
|
+
if (existsSync2(join(homedir(), ".codex"))) {
|
|
2679
2852
|
installed.push(id);
|
|
2680
2853
|
}
|
|
2681
2854
|
} else if (id === "opencode") {
|
|
2682
|
-
if (
|
|
2855
|
+
if (existsSync2(getOpenCodeConfigDir())) {
|
|
2683
2856
|
installed.push(id);
|
|
2684
2857
|
}
|
|
2685
2858
|
} else {
|
|
2686
|
-
const configDir =
|
|
2687
|
-
if (
|
|
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 (!
|
|
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 (!
|
|
3033
|
+
if (!existsSync2(dir))
|
|
2743
3034
|
return [];
|
|
2744
3035
|
const keys = [];
|
|
2745
3036
|
const currentPath = join(dir, "e2e-key");
|
|
2746
|
-
if (
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
3077
|
+
if (!existsSync2(dir)) {
|
|
2787
3078
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2788
3079
|
}
|
|
2789
3080
|
const hooksDir = join(dir, "hooks");
|
|
2790
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
2839
|
-
if (!
|
|
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 =
|
|
2847
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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.
|
|
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("
|
|
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 (
|
|
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"] =
|
|
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?.
|
|
3481
|
-
entries.
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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
|
|
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 (!
|
|
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 (
|
|
4681
|
+
if (existsSync2(e2eKeyFile)) {
|
|
4386
4682
|
console.log(` ${source_default.green("✓")} E2E user key: installed`);
|
|
4387
4683
|
}
|
|
4388
|
-
if (
|
|
4684
|
+
if (existsSync2(e2eServerKeyFile)) {
|
|
4389
4685
|
console.log(` ${source_default.green("✓")} E2E server key: installed`);
|
|
4390
4686
|
}
|
|
4391
|
-
if (!
|
|
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 (
|
|
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?.
|
|
4408
|
-
console.log(` ${source_default.green("✓")} ${agent.name}:
|
|
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 (!
|
|
4433
|
-
if (
|
|
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 (
|
|
4748
|
+
if (existsSync2(envPath)) {
|
|
4453
4749
|
console.log(source_default.yellow(`
|
|
4454
4750
|
Already enabled.
|
|
4455
4751
|
`));
|
|
4456
4752
|
return;
|
|
4457
4753
|
}
|
|
4458
|
-
if (!
|
|
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
|
|
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
|
|
4776
|
+
${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
|
|
4471
4777
|
`));
|
|
4472
4778
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4473
|
-
if (!
|
|
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 (
|
|
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
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
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
|
|
4533
|
-
delete hooksConfig
|
|
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
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
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
|
-
|
|
4579
|
-
|
|
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:
|
|
4922
|
+
const { readdirSync: readdirSync3, copyFileSync: copyFileSync2 } = await import("fs");
|
|
4587
4923
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4588
|
-
const configDir =
|
|
4589
|
-
if (!
|
|
4924
|
+
const configDir = dirname2(agent.configPath);
|
|
4925
|
+
if (!existsSync2(configDir))
|
|
4590
4926
|
continue;
|
|
4591
|
-
const files =
|
|
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 (!
|
|
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 !==
|
|
4613
|
-
if (
|
|
4948
|
+
while (repoRoot !== dirname2(repoRoot)) {
|
|
4949
|
+
if (existsSync2(join(repoRoot, ".git"))) {
|
|
4614
4950
|
found = true;
|
|
4615
4951
|
break;
|
|
4616
4952
|
}
|
|
4617
|
-
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 (
|
|
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 (!
|
|
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
|
|
4660
|
-
${source_default.green("refresh")} Generate a new token
|
|
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")}
|
|
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
|
|
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(
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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;
|