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.
- package/dist/cli.js +668 -187
- 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.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 (!
|
|
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.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 (
|
|
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 &&
|
|
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 }) =>
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
2890
|
+
if (existsSync2(join(homedir(), ".copilot"))) {
|
|
2671
2891
|
installed.push(id);
|
|
2672
2892
|
}
|
|
2673
2893
|
} else if (id === "openclaw") {
|
|
2674
|
-
if (
|
|
2894
|
+
if (existsSync2(join(homedir(), ".openclaw"))) {
|
|
2675
2895
|
installed.push(id);
|
|
2676
2896
|
}
|
|
2677
2897
|
} else if (id === "codex") {
|
|
2678
|
-
if (
|
|
2898
|
+
if (existsSync2(join(homedir(), ".codex"))) {
|
|
2679
2899
|
installed.push(id);
|
|
2680
2900
|
}
|
|
2681
2901
|
} else if (id === "opencode") {
|
|
2682
|
-
if (
|
|
2902
|
+
if (existsSync2(getOpenCodeConfigDir())) {
|
|
2683
2903
|
installed.push(id);
|
|
2684
2904
|
}
|
|
2685
2905
|
} else {
|
|
2686
|
-
const configDir =
|
|
2687
|
-
if (
|
|
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 (!
|
|
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("#")
|
|
3129
|
+
if (line.startsWith("#"))
|
|
3130
|
+
continue;
|
|
3131
|
+
const assignment = parseEnvAssignment(line);
|
|
3132
|
+
if (!assignment)
|
|
2708
3133
|
continue;
|
|
2709
|
-
const
|
|
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 (!
|
|
3169
|
+
if (!existsSync2(dir))
|
|
2743
3170
|
return [];
|
|
2744
3171
|
const keys = [];
|
|
2745
3172
|
const currentPath = join(dir, "e2e-key");
|
|
2746
|
-
if (
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
3213
|
+
if (!existsSync2(dir)) {
|
|
2787
3214
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2788
3215
|
}
|
|
2789
3216
|
const hooksDir = join(dir, "hooks");
|
|
2790
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
2839
|
-
if (!
|
|
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 =
|
|
2847
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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 (
|
|
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"] =
|
|
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("
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
4382
|
-
|
|
4383
|
-
|
|
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 (
|
|
4803
|
+
if (existsSync2(e2eKeyFile)) {
|
|
4390
4804
|
console.log(` ${source_default.green("✓")} E2E user key: installed`);
|
|
4391
4805
|
}
|
|
4392
|
-
if (
|
|
4806
|
+
if (existsSync2(e2eServerKeyFile)) {
|
|
4393
4807
|
console.log(` ${source_default.green("✓")} E2E server key: installed`);
|
|
4394
4808
|
}
|
|
4395
|
-
if (!
|
|
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 (
|
|
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}:
|
|
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}:
|
|
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 (!
|
|
4437
|
-
if (
|
|
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 (
|
|
4870
|
+
if (existsSync2(envPath)) {
|
|
4457
4871
|
console.log(source_default.yellow(`
|
|
4458
4872
|
Already enabled.
|
|
4459
4873
|
`));
|
|
4460
4874
|
return;
|
|
4461
4875
|
}
|
|
4462
|
-
if (!
|
|
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
|
|
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
|
|
4898
|
+
${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
|
|
4475
4899
|
`));
|
|
4476
4900
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4477
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
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
|
|
4543
|
-
delete hooksConfig
|
|
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
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
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
|
-
|
|
4589
|
-
|
|
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:
|
|
5044
|
+
const { readdirSync: readdirSync3, copyFileSync: copyFileSync2 } = await import("fs");
|
|
4597
5045
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
4598
|
-
const configDir =
|
|
4599
|
-
if (!
|
|
5046
|
+
const configDir = dirname2(agent.configPath);
|
|
5047
|
+
if (!existsSync2(configDir))
|
|
4600
5048
|
continue;
|
|
4601
|
-
const files =
|
|
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 (!
|
|
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 !==
|
|
4623
|
-
if (
|
|
5070
|
+
while (repoRoot !== dirname2(repoRoot)) {
|
|
5071
|
+
if (existsSync2(join(repoRoot, ".git"))) {
|
|
4624
5072
|
found = true;
|
|
4625
5073
|
break;
|
|
4626
5074
|
}
|
|
4627
|
-
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 (
|
|
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 (!
|
|
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
|
|
4670
|
-
${source_default.green("refresh")} Generate a new token
|
|
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")}
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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;
|