agentapprove 0.1.11 → 0.1.13
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 +860 -334
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1821,20 +1821,6 @@ function ye() {
|
|
|
1821
1821
|
const s = ["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");
|
|
1822
1822
|
return new RegExp(s, "g");
|
|
1823
1823
|
}
|
|
1824
|
-
var ve = async (s, n) => {
|
|
1825
|
-
const t = {}, i = Object.keys(s);
|
|
1826
|
-
for (const r2 of i) {
|
|
1827
|
-
const o = s[r2], c = await o({ results: t })?.catch((l2) => {
|
|
1828
|
-
throw l2;
|
|
1829
|
-
});
|
|
1830
|
-
if (typeof n?.onCancel == "function" && lD(c)) {
|
|
1831
|
-
t[r2] = "canceled", n.onCancel({ results: t });
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
t[r2] = c;
|
|
1835
|
-
}
|
|
1836
|
-
return t;
|
|
1837
|
-
};
|
|
1838
1824
|
|
|
1839
1825
|
// ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
1840
1826
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -2504,8 +2490,130 @@ function shouldDeleteCryptoMaterial(purgeConfirmed, firstKeyConfirmation, second
|
|
|
2504
2490
|
return purgeConfirmed && firstKeyConfirmation && secondKeyConfirmation;
|
|
2505
2491
|
}
|
|
2506
2492
|
|
|
2493
|
+
// src/pairing-artifact.ts
|
|
2494
|
+
var PAIRING_ARTIFACT_PREFIX = "AAP1.";
|
|
2495
|
+
function encodePairingArtifact(artifact) {
|
|
2496
|
+
const payload = {
|
|
2497
|
+
s: artifact.sessionCode,
|
|
2498
|
+
p: artifact.config.privacyTier,
|
|
2499
|
+
r: artifact.config.retentionDays,
|
|
2500
|
+
f: artifact.config.failBehavior,
|
|
2501
|
+
c: artifact.config.configSetAt,
|
|
2502
|
+
e: artifact.config.e2eEnabled ? 1 : 0
|
|
2503
|
+
};
|
|
2504
|
+
if (artifact.hostname) {
|
|
2505
|
+
payload.h = artifact.hostname;
|
|
2506
|
+
}
|
|
2507
|
+
if (artifact.e2eUserKey) {
|
|
2508
|
+
payload.k = Buffer.from(artifact.e2eUserKey, "hex").toString("base64url");
|
|
2509
|
+
}
|
|
2510
|
+
return `${PAIRING_ARTIFACT_PREFIX}${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
|
|
2511
|
+
}
|
|
2512
|
+
function buildPairingUrl(pairingArtifact) {
|
|
2513
|
+
return `agentapprove://pair?code=${encodeURIComponent(pairingArtifact)}`;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/install-flow.ts
|
|
2517
|
+
var VALID_RETENTION_DAYS = [1, 7, 30, 90, 365];
|
|
2518
|
+
function normalizePrivacy(value) {
|
|
2519
|
+
if (value === "minimal" || value === "summary" || value === "full") {
|
|
2520
|
+
return value;
|
|
2521
|
+
}
|
|
2522
|
+
return "full";
|
|
2523
|
+
}
|
|
2524
|
+
function normalizeRetentionDays(value) {
|
|
2525
|
+
if (value && VALID_RETENTION_DAYS.includes(value)) {
|
|
2526
|
+
return value;
|
|
2527
|
+
}
|
|
2528
|
+
return 30;
|
|
2529
|
+
}
|
|
2530
|
+
function normalizeFailBehavior(value) {
|
|
2531
|
+
if (value === "allow" || value === "deny" || value === "ask") {
|
|
2532
|
+
return value;
|
|
2533
|
+
}
|
|
2534
|
+
return "ask";
|
|
2535
|
+
}
|
|
2536
|
+
function normalizeInstallMode(value) {
|
|
2537
|
+
if (value === "observe" || value === "approval") {
|
|
2538
|
+
return value;
|
|
2539
|
+
}
|
|
2540
|
+
return "approval";
|
|
2541
|
+
}
|
|
2542
|
+
function getSetupProfileOptions(hasExistingConfig) {
|
|
2543
|
+
if (hasExistingConfig) {
|
|
2544
|
+
return [
|
|
2545
|
+
{
|
|
2546
|
+
value: "existing-config",
|
|
2547
|
+
label: "Existing config",
|
|
2548
|
+
hint: "Reuse the current local settings already saved on this computer."
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
value: "customize",
|
|
2552
|
+
label: "Customize options",
|
|
2553
|
+
hint: "Choose your own mode, fallback behavior, privacy, E2E, retention, and debug logging before pairing."
|
|
2554
|
+
}
|
|
2555
|
+
];
|
|
2556
|
+
}
|
|
2557
|
+
return [
|
|
2558
|
+
{
|
|
2559
|
+
value: "recommended",
|
|
2560
|
+
label: "Recommended setup",
|
|
2561
|
+
hint: "Use Approval mode, E2E on, fallback Ask, Full privacy, 30-day retention, and debug logging off."
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
value: "customize",
|
|
2565
|
+
label: "Customize options",
|
|
2566
|
+
hint: "Choose your own mode, fallback behavior, privacy, E2E, retention, and debug logging before pairing."
|
|
2567
|
+
}
|
|
2568
|
+
];
|
|
2569
|
+
}
|
|
2570
|
+
function getRecommendedInstallConfig() {
|
|
2571
|
+
return {
|
|
2572
|
+
privacy: "full",
|
|
2573
|
+
retentionDays: 30,
|
|
2574
|
+
failBehavior: "ask",
|
|
2575
|
+
debugLog: false,
|
|
2576
|
+
installMode: "approval",
|
|
2577
|
+
e2eEnabled: true
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
function getExistingInstallConfig(existingConfig) {
|
|
2581
|
+
return {
|
|
2582
|
+
privacy: normalizePrivacy(existingConfig?.privacy),
|
|
2583
|
+
retentionDays: normalizeRetentionDays(existingConfig?.retentionDays),
|
|
2584
|
+
failBehavior: normalizeFailBehavior(existingConfig?.failBehavior),
|
|
2585
|
+
debugLog: existingConfig?.debugLog ?? false,
|
|
2586
|
+
installMode: normalizeInstallMode(existingConfig?.e2eMode),
|
|
2587
|
+
e2eEnabled: existingConfig?.e2eEnabled !== false
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
function getInitialInstallConfig(choice, existingConfig) {
|
|
2591
|
+
if (choice === "recommended") {
|
|
2592
|
+
return getRecommendedInstallConfig();
|
|
2593
|
+
}
|
|
2594
|
+
return getExistingInstallConfig(existingConfig);
|
|
2595
|
+
}
|
|
2596
|
+
function getExistingKeyActionOptions(e2eRequired) {
|
|
2597
|
+
const options = [
|
|
2598
|
+
{ value: "reuse", label: "Reuse existing key", hint: "recommended - keeps old events decryptable" },
|
|
2599
|
+
{ value: "backup", label: "Back up old key, then generate new" },
|
|
2600
|
+
{ value: "discard", label: "Discard old key and generate new" }
|
|
2601
|
+
];
|
|
2602
|
+
if (!e2eRequired) {
|
|
2603
|
+
options.push({
|
|
2604
|
+
value: "disable",
|
|
2605
|
+
label: "Disable E2E encryption",
|
|
2606
|
+
hint: "events visible on web dashboard"
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
return options;
|
|
2610
|
+
}
|
|
2611
|
+
function shouldCreateFreshPairing(connectionMethod) {
|
|
2612
|
+
return connectionMethod === "qr" || connectionMethod === "copy";
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2507
2615
|
// src/cli.ts
|
|
2508
|
-
var VERSION = "0.1.
|
|
2616
|
+
var VERSION = "0.1.13";
|
|
2509
2617
|
function getApiUrl() {
|
|
2510
2618
|
return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
|
|
2511
2619
|
}
|
|
@@ -2553,9 +2661,12 @@ function getCommand() {
|
|
|
2553
2661
|
}
|
|
2554
2662
|
return filtered[0] || "install";
|
|
2555
2663
|
}
|
|
2556
|
-
var OPENCODE_PLUGIN_VERSION = "0.1.
|
|
2557
|
-
var OPENCLAW_PLUGIN_VERSION = "0.2.
|
|
2664
|
+
var OPENCODE_PLUGIN_VERSION = "0.1.9";
|
|
2665
|
+
var OPENCLAW_PLUGIN_VERSION = "0.2.8";
|
|
2558
2666
|
var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
|
|
2667
|
+
var PI_PLUGIN_VERSION = "0.1.0";
|
|
2668
|
+
var PI_PLUGIN_SPEC = `npm:@agentapprove/pi@${PI_PLUGIN_VERSION}`;
|
|
2669
|
+
var PI_STATUS_TIMEOUT_MS = 5000;
|
|
2559
2670
|
var AGENTS = {
|
|
2560
2671
|
"claude-code": {
|
|
2561
2672
|
name: "Claude Code",
|
|
@@ -2665,8 +2776,63 @@ var AGENTS = {
|
|
|
2665
2776
|
hooks: [
|
|
2666
2777
|
{ name: "agentapprove", file: "@agentapprove/opencode", description: "Tool approval + event monitoring", isApprovalHook: true, isPlugin: true }
|
|
2667
2778
|
]
|
|
2779
|
+
},
|
|
2780
|
+
pi: {
|
|
2781
|
+
name: "Pi",
|
|
2782
|
+
configPath: join(homedir(), ".pi", "agent", "settings.json"),
|
|
2783
|
+
hooksKey: "packages",
|
|
2784
|
+
hooks: [
|
|
2785
|
+
{ name: "agentapprove", file: "@agentapprove/pi", description: "Tool approval + event monitoring", isApprovalHook: true, isPlugin: true }
|
|
2786
|
+
]
|
|
2668
2787
|
}
|
|
2669
2788
|
};
|
|
2789
|
+
var SHARED_HOOK_FILES = ["common.sh"];
|
|
2790
|
+
var STATUS_FIELD_LABELS = {
|
|
2791
|
+
AGENTAPPROVE_API: "API URL",
|
|
2792
|
+
AGENTAPPROVE_TOKEN: "Token",
|
|
2793
|
+
AGENTAPPROVE_PRIVACY: "Privacy",
|
|
2794
|
+
AGENTAPPROVE_RETENTION_DAYS: "Retention",
|
|
2795
|
+
AGENTAPPROVE_CONFIG_SET_AT: "Configured at",
|
|
2796
|
+
AGENTAPPROVE_DEBUG_LOG: "Debug logging",
|
|
2797
|
+
AGENTAPPROVE_E2E_MODE: "Security mode",
|
|
2798
|
+
AGENTAPPROVE_E2E_ENABLED: "E2E encryption",
|
|
2799
|
+
AGENTAPPROVE_FAIL_BEHAVIOR: "If unreachable"
|
|
2800
|
+
};
|
|
2801
|
+
var HOOK_STATUS_LABELS = {
|
|
2802
|
+
PreToolUse: "Tool approval",
|
|
2803
|
+
PostToolUse: "Tool completed",
|
|
2804
|
+
PostToolUseFailure: "Tool failed",
|
|
2805
|
+
PermissionRequest: "Permission request",
|
|
2806
|
+
UserPromptSubmit: "Prompt submitted",
|
|
2807
|
+
SessionStart: "Session start",
|
|
2808
|
+
SessionEnd: "Session end",
|
|
2809
|
+
Stop: "Stop",
|
|
2810
|
+
sessionStart: "Session start",
|
|
2811
|
+
sessionEnd: "Session end",
|
|
2812
|
+
beforeShellExecution: "Shell approval",
|
|
2813
|
+
beforeMCPExecution: "MCP approval",
|
|
2814
|
+
preToolUse: "Tool approval",
|
|
2815
|
+
afterShellExecution: "Shell completed",
|
|
2816
|
+
afterMCPExecution: "MCP completed",
|
|
2817
|
+
postToolUse: "Tool completed",
|
|
2818
|
+
beforeSubmitPrompt: "Prompt submitted",
|
|
2819
|
+
subagentStart: "Subagent start",
|
|
2820
|
+
subagentStop: "Subagent stop",
|
|
2821
|
+
preCompact: "Pre-compact",
|
|
2822
|
+
afterAgentThought: "Agent thinking",
|
|
2823
|
+
afterAgentResponse: "Agent response",
|
|
2824
|
+
BeforeTool: "Tool approval",
|
|
2825
|
+
AfterTool: "Tool completed",
|
|
2826
|
+
BeforeAgent: "Prompt submitted",
|
|
2827
|
+
AfterAgent: "Stop",
|
|
2828
|
+
BeforeModel: "Model request",
|
|
2829
|
+
AfterModel: "Model response",
|
|
2830
|
+
Notification: "Notification",
|
|
2831
|
+
userPromptSubmitted: "Prompt submitted",
|
|
2832
|
+
errorOccurred: "Error",
|
|
2833
|
+
SubagentStart: "Subagent start",
|
|
2834
|
+
SubagentStop: "Subagent stop"
|
|
2835
|
+
};
|
|
2670
2836
|
function findGitBash() {
|
|
2671
2837
|
if (!isWindows())
|
|
2672
2838
|
return null;
|
|
@@ -2855,6 +3021,15 @@ function detectInstalledAgents() {
|
|
|
2855
3021
|
if (existsSync2(getOpenCodeConfigDir())) {
|
|
2856
3022
|
installed.push(id);
|
|
2857
3023
|
}
|
|
3024
|
+
} else if (id === "pi") {
|
|
3025
|
+
if (existsSync2(join(homedir(), ".pi", "agent"))) {
|
|
3026
|
+
installed.push(id);
|
|
3027
|
+
} else {
|
|
3028
|
+
try {
|
|
3029
|
+
execSync("pi --version", { stdio: "ignore" });
|
|
3030
|
+
installed.push(id);
|
|
3031
|
+
} catch {}
|
|
3032
|
+
}
|
|
2858
3033
|
} else {
|
|
2859
3034
|
const configDir = dirname2(agent.configPath);
|
|
2860
3035
|
if (existsSync2(configDir)) {
|
|
@@ -2864,6 +3039,155 @@ function detectInstalledAgents() {
|
|
|
2864
3039
|
}
|
|
2865
3040
|
return installed;
|
|
2866
3041
|
}
|
|
3042
|
+
function getDownloadableHookFilesForAgent(agentId) {
|
|
3043
|
+
const agent = AGENTS[agentId];
|
|
3044
|
+
if (!agent)
|
|
3045
|
+
return [];
|
|
3046
|
+
return agent.hooks.filter((hook) => !hook.isPlugin).map((hook) => hook.file);
|
|
3047
|
+
}
|
|
3048
|
+
function buildHookDownloadPlan(agentIds) {
|
|
3049
|
+
const files = new Set;
|
|
3050
|
+
const perAgent = [];
|
|
3051
|
+
for (const agentId of agentIds) {
|
|
3052
|
+
const agent = AGENTS[agentId];
|
|
3053
|
+
if (!agent)
|
|
3054
|
+
continue;
|
|
3055
|
+
const agentFiles = [...new Set(getDownloadableHookFilesForAgent(agentId))];
|
|
3056
|
+
if (agentFiles.length === 0) {
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
perAgent.push({
|
|
3060
|
+
agentId,
|
|
3061
|
+
agentName: agent.name,
|
|
3062
|
+
count: agentFiles.length
|
|
3063
|
+
});
|
|
3064
|
+
for (const file of agentFiles) {
|
|
3065
|
+
files.add(file);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
const sharedCount = perAgent.length > 0 ? SHARED_HOOK_FILES.length : 0;
|
|
3069
|
+
if (sharedCount > 0) {
|
|
3070
|
+
for (const file of SHARED_HOOK_FILES) {
|
|
3071
|
+
files.add(file);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
return {
|
|
3075
|
+
files: [...files],
|
|
3076
|
+
perAgent,
|
|
3077
|
+
sharedCount
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
function formatHookDownloadSummary(plan) {
|
|
3081
|
+
const parts = plan.perAgent.map((entry) => `${entry.agentName}: ${entry.count}`);
|
|
3082
|
+
if (plan.sharedCount > 0) {
|
|
3083
|
+
parts.push(`shared: ${plan.sharedCount}`);
|
|
3084
|
+
}
|
|
3085
|
+
return parts.join(", ");
|
|
3086
|
+
}
|
|
3087
|
+
function parseEnvAssignment(line) {
|
|
3088
|
+
if (!line.includes("=")) {
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
const [key, ...valueParts] = line.split("=");
|
|
3092
|
+
return {
|
|
3093
|
+
key: key.trim(),
|
|
3094
|
+
value: valueParts.join("=").trim()
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
function formatStatusValue(key, value) {
|
|
3098
|
+
switch (key) {
|
|
3099
|
+
case "AGENTAPPROVE_TOKEN":
|
|
3100
|
+
return value.slice(0, 15) + "...";
|
|
3101
|
+
case "AGENTAPPROVE_RETENTION_DAYS":
|
|
3102
|
+
return `${value} days`;
|
|
3103
|
+
case "AGENTAPPROVE_CONFIG_SET_AT": {
|
|
3104
|
+
const timestamp = Number.parseInt(value, 10);
|
|
3105
|
+
if (Number.isFinite(timestamp)) {
|
|
3106
|
+
return new Date(timestamp * 1000).toLocaleString();
|
|
3107
|
+
}
|
|
3108
|
+
return value;
|
|
3109
|
+
}
|
|
3110
|
+
case "AGENTAPPROVE_DEBUG_LOG":
|
|
3111
|
+
case "AGENTAPPROVE_E2E_ENABLED":
|
|
3112
|
+
return value === "true" ? "Enabled" : "Disabled";
|
|
3113
|
+
case "AGENTAPPROVE_E2E_MODE":
|
|
3114
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3115
|
+
case "AGENTAPPROVE_FAIL_BEHAVIOR":
|
|
3116
|
+
case "AGENTAPPROVE_PRIVACY":
|
|
3117
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3118
|
+
default:
|
|
3119
|
+
return value;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
function formatSetupValue(value) {
|
|
3123
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3124
|
+
}
|
|
3125
|
+
function describePrivacy(value) {
|
|
3126
|
+
switch (value) {
|
|
3127
|
+
case "minimal":
|
|
3128
|
+
return "Tool name only is stored in event history.";
|
|
3129
|
+
case "summary":
|
|
3130
|
+
return "Command details are truncated to 50 characters in event history.";
|
|
3131
|
+
case "full":
|
|
3132
|
+
default:
|
|
3133
|
+
return "Complete command details are stored in event history.";
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
function describeRetentionDays(days) {
|
|
3137
|
+
return `Delete events older than ${days} ${days === 1 ? "day" : "days"}.`;
|
|
3138
|
+
}
|
|
3139
|
+
function describeFailBehavior(value) {
|
|
3140
|
+
switch (value) {
|
|
3141
|
+
case "allow":
|
|
3142
|
+
return "Allow commands to proceed without approval.";
|
|
3143
|
+
case "deny":
|
|
3144
|
+
return "Block all commands until the service is restored.";
|
|
3145
|
+
case "ask":
|
|
3146
|
+
default:
|
|
3147
|
+
return "Fall back to the agent's built-in approval dialog.";
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
function describeInstallMode(value) {
|
|
3151
|
+
switch (value) {
|
|
3152
|
+
case "observe":
|
|
3153
|
+
return "Agent runs freely and all events are end-to-end encrypted.";
|
|
3154
|
+
case "approval":
|
|
3155
|
+
default:
|
|
3156
|
+
return "Commands are sent via an encrypted channel for policy evaluation.";
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
function describeE2EEnabled(enabled) {
|
|
3160
|
+
if (enabled) {
|
|
3161
|
+
return "Event content is encrypted between this computer and your devices.";
|
|
3162
|
+
}
|
|
3163
|
+
return "Event content can be viewed on the web dashboard.";
|
|
3164
|
+
}
|
|
3165
|
+
function describeDebugLogging(enabled) {
|
|
3166
|
+
if (enabled) {
|
|
3167
|
+
return "Write hook logs to ~/.agentapprove/hook-debug.log.";
|
|
3168
|
+
}
|
|
3169
|
+
return "Do not write hook debug logs unless you turn them on later.";
|
|
3170
|
+
}
|
|
3171
|
+
function formatSetupProfileBlock(title, config, extraLines = []) {
|
|
3172
|
+
return [
|
|
3173
|
+
title,
|
|
3174
|
+
...extraLines.map((line) => ` ${line}`),
|
|
3175
|
+
` Security mode: ${formatSetupValue(config.installMode)} - ${describeInstallMode(config.installMode)}`,
|
|
3176
|
+
` If Agent Approve is unreachable: ${formatSetupValue(config.failBehavior)} - ${describeFailBehavior(config.failBehavior)}`,
|
|
3177
|
+
` Privacy tier: ${formatSetupValue(config.privacy)} - ${describePrivacy(config.privacy)}`,
|
|
3178
|
+
` End-to-end encryption: ${config.e2eEnabled ? "Enabled" : "Disabled"} - ${describeE2EEnabled(config.e2eEnabled)}`,
|
|
3179
|
+
` Data retention: ${config.retentionDays} days - ${describeRetentionDays(config.retentionDays)}`,
|
|
3180
|
+
` Debug logging: ${config.debugLog ? "Enabled" : "Disabled"} - ${describeDebugLogging(config.debugLog)}`
|
|
3181
|
+
].join(`
|
|
3182
|
+
`);
|
|
3183
|
+
}
|
|
3184
|
+
function formatHookStatusLabel(name) {
|
|
3185
|
+
const explicitLabel = HOOK_STATUS_LABELS[name];
|
|
3186
|
+
if (explicitLabel) {
|
|
3187
|
+
return explicitLabel;
|
|
3188
|
+
}
|
|
3189
|
+
return name.replace(/\*/g, "events").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (letter) => letter.toUpperCase());
|
|
3190
|
+
}
|
|
2867
3191
|
function getAgentApproveDir() {
|
|
2868
3192
|
return join(homedir(), ".agentapprove");
|
|
2869
3193
|
}
|
|
@@ -2992,10 +3316,12 @@ function readExistingConfig() {
|
|
|
2992
3316
|
const config = {};
|
|
2993
3317
|
for (const line of content.split(`
|
|
2994
3318
|
`)) {
|
|
2995
|
-
if (line.startsWith("#")
|
|
3319
|
+
if (line.startsWith("#"))
|
|
3320
|
+
continue;
|
|
3321
|
+
const assignment = parseEnvAssignment(line);
|
|
3322
|
+
if (!assignment)
|
|
2996
3323
|
continue;
|
|
2997
|
-
const
|
|
2998
|
-
const value = valueParts.join("=").trim();
|
|
3324
|
+
const { key, value } = assignment;
|
|
2999
3325
|
switch (key.trim()) {
|
|
3000
3326
|
case "AGENTAPPROVE_API":
|
|
3001
3327
|
config.apiUrl = value;
|
|
@@ -3297,6 +3623,101 @@ async function waitForPairing(sessionCode, onProgress, onCancel) {
|
|
|
3297
3623
|
cleanup();
|
|
3298
3624
|
return null;
|
|
3299
3625
|
}
|
|
3626
|
+
function buildPairingPresentation(params) {
|
|
3627
|
+
const pairingArtifact = encodePairingArtifact({
|
|
3628
|
+
sessionCode: params.sessionCode,
|
|
3629
|
+
hostname: params.hostname,
|
|
3630
|
+
config: params.config,
|
|
3631
|
+
e2eUserKey: params.e2eUserKey ?? undefined
|
|
3632
|
+
});
|
|
3633
|
+
return {
|
|
3634
|
+
pairingArtifact,
|
|
3635
|
+
pairingUrl: buildPairingUrl(pairingArtifact),
|
|
3636
|
+
manualCode: pairingArtifact
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
function copyToClipboard(text) {
|
|
3640
|
+
let cmd;
|
|
3641
|
+
let args = [];
|
|
3642
|
+
switch (process.platform) {
|
|
3643
|
+
case "darwin":
|
|
3644
|
+
cmd = "pbcopy";
|
|
3645
|
+
break;
|
|
3646
|
+
case "win32":
|
|
3647
|
+
cmd = "clip";
|
|
3648
|
+
break;
|
|
3649
|
+
default:
|
|
3650
|
+
for (const candidate of [
|
|
3651
|
+
{ cmd: "wl-copy", args: [] },
|
|
3652
|
+
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
3653
|
+
{ cmd: "xsel", args: ["--clipboard", "--input"] }
|
|
3654
|
+
]) {
|
|
3655
|
+
const result2 = spawnSync(candidate.cmd, candidate.args, {
|
|
3656
|
+
input: text,
|
|
3657
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
3658
|
+
});
|
|
3659
|
+
if (result2.status === 0 && !result2.error)
|
|
3660
|
+
return true;
|
|
3661
|
+
}
|
|
3662
|
+
return false;
|
|
3663
|
+
}
|
|
3664
|
+
const result = spawnSync(cmd, args, {
|
|
3665
|
+
input: text,
|
|
3666
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
3667
|
+
});
|
|
3668
|
+
return result.status === 0 && !result.error;
|
|
3669
|
+
}
|
|
3670
|
+
async function promptPairingMethod() {
|
|
3671
|
+
const choice = await le({
|
|
3672
|
+
message: "How will you connect to the iOS app?",
|
|
3673
|
+
options: [
|
|
3674
|
+
{ value: "qr", label: "Scan QR code", hint: "Recommended" },
|
|
3675
|
+
{ value: "copy", label: "Copy and paste code", hint: "No camera" }
|
|
3676
|
+
],
|
|
3677
|
+
initialValue: "qr"
|
|
3678
|
+
});
|
|
3679
|
+
if (lD(choice)) {
|
|
3680
|
+
he("Cancelled");
|
|
3681
|
+
process.exit(0);
|
|
3682
|
+
}
|
|
3683
|
+
return choice;
|
|
3684
|
+
}
|
|
3685
|
+
async function showPairingPresentation(params) {
|
|
3686
|
+
const detailLines = [`Session: ${params.sessionCode}`];
|
|
3687
|
+
if (params.keyId) {
|
|
3688
|
+
detailLines.push(`Key ID: ${params.keyId}`);
|
|
3689
|
+
}
|
|
3690
|
+
if (params.method === "qr") {
|
|
3691
|
+
let qrDisplay = "";
|
|
3692
|
+
import_qrcode_terminal.default.generate(params.pairingUrl, { small: true }, (qr) => {
|
|
3693
|
+
qrDisplay = qr;
|
|
3694
|
+
});
|
|
3695
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
3696
|
+
process.stdout.write(`
|
|
3697
|
+
`);
|
|
3698
|
+
process.stdout.write(` Scan with the Agent Approve iOS app:
|
|
3699
|
+
|
|
3700
|
+
`);
|
|
3701
|
+
process.stdout.write(qrDisplay);
|
|
3702
|
+
process.stdout.write(`
|
|
3703
|
+
`);
|
|
3704
|
+
me(detailLines.join(`
|
|
3705
|
+
`), "Pairing details");
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
me(detailLines.join(`
|
|
3709
|
+
`), "Pairing details");
|
|
3710
|
+
const copied = copyToClipboard(params.manualCode);
|
|
3711
|
+
if (copied) {
|
|
3712
|
+
v2.success("Paste code copied to clipboard. In the iOS app, choose the option to enter a code manually and paste.");
|
|
3713
|
+
} else {
|
|
3714
|
+
v2.warn("Could not access the system clipboard. Copy the code below manually instead.");
|
|
3715
|
+
}
|
|
3716
|
+
process.stdout.write(`
|
|
3717
|
+
` + params.manualCode + `
|
|
3718
|
+
|
|
3719
|
+
`);
|
|
3720
|
+
}
|
|
3300
3721
|
function saveEnvConfig(config) {
|
|
3301
3722
|
const envPath = join(getAgentApproveDir(), "env");
|
|
3302
3723
|
const configSetAt = config.configSetAt || Math.floor(Date.now() / 1000);
|
|
@@ -3353,7 +3774,7 @@ function isCredentialManagerAvailable() {
|
|
|
3353
3774
|
return false;
|
|
3354
3775
|
}
|
|
3355
3776
|
}
|
|
3356
|
-
async function storeTokenInKeychain(token) {
|
|
3777
|
+
async function storeTokenInKeychain(token, options) {
|
|
3357
3778
|
if (isKeychainAvailable()) {
|
|
3358
3779
|
try {
|
|
3359
3780
|
spawnSync("security", [
|
|
@@ -3376,7 +3797,9 @@ async function storeTokenInKeychain(token) {
|
|
|
3376
3797
|
if (addResult.status !== 0 || addResult.error) {
|
|
3377
3798
|
throw addResult.error || new Error("security add-generic-password failed");
|
|
3378
3799
|
}
|
|
3379
|
-
|
|
3800
|
+
if (!options?.quiet) {
|
|
3801
|
+
v2.success("Token stored in macOS Keychain");
|
|
3802
|
+
}
|
|
3380
3803
|
return true;
|
|
3381
3804
|
} catch (err) {
|
|
3382
3805
|
v2.warn("Could not store token in Keychain (file fallback will be used)");
|
|
@@ -3394,7 +3817,9 @@ async function storeTokenInKeychain(token) {
|
|
|
3394
3817
|
if (addResult.status !== 0 || addResult.error) {
|
|
3395
3818
|
throw addResult.error || new Error("cmdkey add credential failed");
|
|
3396
3819
|
}
|
|
3397
|
-
|
|
3820
|
+
if (!options?.quiet) {
|
|
3821
|
+
v2.success("Token stored in Windows Credential Manager");
|
|
3822
|
+
}
|
|
3398
3823
|
return true;
|
|
3399
3824
|
} catch (err) {
|
|
3400
3825
|
v2.warn("Could not store token in Credential Manager (file fallback will be used)");
|
|
@@ -3424,31 +3849,18 @@ function deleteTokenFromKeychain() {
|
|
|
3424
3849
|
}
|
|
3425
3850
|
return false;
|
|
3426
3851
|
}
|
|
3427
|
-
async function pushConfigToCloud(config) {
|
|
3428
|
-
try {
|
|
3429
|
-
const response = await fetch(`${config.apiUrl}/${API_VERSION}/config`, {
|
|
3430
|
-
method: "PUT",
|
|
3431
|
-
headers: {
|
|
3432
|
-
"Content-Type": "application/json",
|
|
3433
|
-
Authorization: `Bearer ${config.token}`
|
|
3434
|
-
},
|
|
3435
|
-
body: JSON.stringify({
|
|
3436
|
-
privacyTier: config.privacy,
|
|
3437
|
-
retentionDays: config.retentionDays,
|
|
3438
|
-
failBehavior: config.failBehavior,
|
|
3439
|
-
configSetAt: config.configSetAt
|
|
3440
|
-
})
|
|
3441
|
-
});
|
|
3442
|
-
return response.ok;
|
|
3443
|
-
} catch (error) {
|
|
3444
|
-
return false;
|
|
3445
|
-
}
|
|
3446
|
-
}
|
|
3447
3852
|
async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
3448
3853
|
const agent = AGENTS[agentId];
|
|
3449
3854
|
if (!agent) {
|
|
3450
3855
|
return { success: false, backupPath: null, hooks: [] };
|
|
3451
3856
|
}
|
|
3857
|
+
if (agentId === "pi") {
|
|
3858
|
+
const installResult = installPiPluginViaCli();
|
|
3859
|
+
if (!installResult.success) {
|
|
3860
|
+
return { success: false, backupPath: null, hooks: [], error: installResult.error };
|
|
3861
|
+
}
|
|
3862
|
+
return { success: true, backupPath: null, hooks: [installResult.label] };
|
|
3863
|
+
}
|
|
3452
3864
|
const backupPath = backupConfig(agent.configPath);
|
|
3453
3865
|
const config = readJsonConfig(agent.configPath);
|
|
3454
3866
|
if (!config[agent.hooksKey]) {
|
|
@@ -3488,7 +3900,7 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
3488
3900
|
}
|
|
3489
3901
|
hooks.internal.enabled = true;
|
|
3490
3902
|
writeJsonConfig(agent.configPath, config);
|
|
3491
|
-
installedHooks.push(
|
|
3903
|
+
installedHooks.push(installResult.label);
|
|
3492
3904
|
return { success: true, backupPath, hooks: installedHooks };
|
|
3493
3905
|
}
|
|
3494
3906
|
if (agentId === "opencode") {
|
|
@@ -3517,7 +3929,7 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
3517
3929
|
console.warn(`Warning: Could not update package.json: ${err.message}`);
|
|
3518
3930
|
}
|
|
3519
3931
|
}
|
|
3520
|
-
installedHooks.push("
|
|
3932
|
+
installedHooks.push("Agent Approve plugin");
|
|
3521
3933
|
return { success: true, backupPath, hooks: installedHooks };
|
|
3522
3934
|
}
|
|
3523
3935
|
const hooksToInstall = mode === "observe" ? agent.hooks.filter((h2) => !h2.isApprovalHook) : agent.hooks;
|
|
@@ -3940,68 +4352,25 @@ codex_hooks = true`;
|
|
|
3940
4352
|
"",
|
|
3941
4353
|
"OpenCode will auto-install the plugin on next start."
|
|
3942
4354
|
].join(`
|
|
4355
|
+
`);
|
|
4356
|
+
} else if (agentId === "pi") {
|
|
4357
|
+
return [
|
|
4358
|
+
"Install the Agent Approve extension for Pi:",
|
|
4359
|
+
"",
|
|
4360
|
+
` pi install ${PI_PLUGIN_SPEC}`,
|
|
4361
|
+
"",
|
|
4362
|
+
"The extension reads your existing Agent Approve config from ~/.agentapprove/env.",
|
|
4363
|
+
"",
|
|
4364
|
+
"Restart Pi to activate the extension."
|
|
4365
|
+
].join(`
|
|
3943
4366
|
`);
|
|
3944
4367
|
}
|
|
3945
4368
|
return "";
|
|
3946
4369
|
}
|
|
3947
|
-
|
|
3948
|
-
"common.sh",
|
|
3949
|
-
"claude-pre-tool.sh",
|
|
3950
|
-
"claude-post-tool.sh",
|
|
3951
|
-
"claude-post-tool-failure.sh",
|
|
3952
|
-
"claude-notification.sh",
|
|
3953
|
-
"claude-user-prompt.sh",
|
|
3954
|
-
"claude-prompt.sh",
|
|
3955
|
-
"claude-session-start.sh",
|
|
3956
|
-
"claude-session-end.sh",
|
|
3957
|
-
"claude-stop.sh",
|
|
3958
|
-
"notify-hook.sh",
|
|
3959
|
-
"completion-hook.sh",
|
|
3960
|
-
"cursor-session-start.sh",
|
|
3961
|
-
"cursor-session-end.sh",
|
|
3962
|
-
"cursor-approval.sh",
|
|
3963
|
-
"cursor-mcp-approval.sh",
|
|
3964
|
-
"cursor-pre-tool.sh",
|
|
3965
|
-
"cursor-shell-complete.sh",
|
|
3966
|
-
"cursor-mcp-complete.sh",
|
|
3967
|
-
"cursor-post-tool.sh",
|
|
3968
|
-
"cursor-start.sh",
|
|
3969
|
-
"cursor-subagent-start.sh",
|
|
3970
|
-
"cursor-subagent-stop.sh",
|
|
3971
|
-
"cursor-precompact.sh",
|
|
3972
|
-
"cursor-stop.sh",
|
|
3973
|
-
"cursor-thought.sh",
|
|
3974
|
-
"cursor-response.sh",
|
|
3975
|
-
"gemini-before-tool.sh",
|
|
3976
|
-
"gemini-after-tool.sh",
|
|
3977
|
-
"gemini-before-agent.sh",
|
|
3978
|
-
"gemini-after-agent.sh",
|
|
3979
|
-
"gemini-before-model.sh",
|
|
3980
|
-
"gemini-after-model.sh",
|
|
3981
|
-
"gemini-notification.sh",
|
|
3982
|
-
"gemini-session-start.sh",
|
|
3983
|
-
"gemini-session-end.sh",
|
|
3984
|
-
"gemini-stop.sh",
|
|
3985
|
-
"codex-session-start.sh",
|
|
3986
|
-
"codex-pre-tool.sh",
|
|
3987
|
-
"codex-post-tool.sh",
|
|
3988
|
-
"codex-user-prompt.sh",
|
|
3989
|
-
"codex-stop.sh",
|
|
3990
|
-
"github-session-start.sh",
|
|
3991
|
-
"github-session-end.sh",
|
|
3992
|
-
"github-user-prompt.sh",
|
|
3993
|
-
"github-pre-tool.sh",
|
|
3994
|
-
"github-post-tool.sh",
|
|
3995
|
-
"github-error.sh",
|
|
3996
|
-
"github-stop.sh",
|
|
3997
|
-
"github-subagent-start.sh",
|
|
3998
|
-
"github-subagent-stop.sh",
|
|
3999
|
-
"github-precompact.sh"
|
|
4000
|
-
];
|
|
4001
|
-
async function copyHookScripts(hooksDir, token) {
|
|
4370
|
+
async function copyHookScripts(hooksDir, token, files) {
|
|
4002
4371
|
let downloaded = 0;
|
|
4003
4372
|
const failed = [];
|
|
4004
|
-
for (const file of
|
|
4373
|
+
for (const file of files) {
|
|
4005
4374
|
try {
|
|
4006
4375
|
const response = await fetch(`${API_URL}/${API_VERSION}/hooks/${file}?format=raw`, {
|
|
4007
4376
|
headers: {
|
|
@@ -4028,13 +4397,66 @@ async function copyHookScripts(hooksDir, token) {
|
|
|
4028
4397
|
}
|
|
4029
4398
|
return { downloaded, failed };
|
|
4030
4399
|
}
|
|
4400
|
+
function readOpenClawInstalledVersion() {
|
|
4401
|
+
const packagePath = join(homedir(), ".openclaw", "extensions", "openclaw", "package.json");
|
|
4402
|
+
if (!existsSync2(packagePath)) {
|
|
4403
|
+
return null;
|
|
4404
|
+
}
|
|
4405
|
+
try {
|
|
4406
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
4407
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
4408
|
+
} catch {
|
|
4409
|
+
return null;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4031
4412
|
function installOpenClawPluginViaCli() {
|
|
4032
4413
|
try {
|
|
4033
4414
|
execSync(`openclaw plugins install ${OPENCLAW_PLUGIN_SPEC}`, { stdio: "pipe" });
|
|
4034
|
-
|
|
4415
|
+
const installedVersion = readOpenClawInstalledVersion();
|
|
4416
|
+
if (!installedVersion) {
|
|
4417
|
+
return {
|
|
4418
|
+
success: true,
|
|
4419
|
+
label: "Agent Approve plugin (version not verified)"
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
if (installedVersion !== OPENCLAW_PLUGIN_VERSION) {
|
|
4423
|
+
return {
|
|
4424
|
+
success: false,
|
|
4425
|
+
error: `OpenClaw installed ${installedVersion}, expected ${OPENCLAW_PLUGIN_VERSION}. Re-run after updating the published installer package.`,
|
|
4426
|
+
label: "Agent Approve plugin"
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
return {
|
|
4430
|
+
success: true,
|
|
4431
|
+
version: installedVersion,
|
|
4432
|
+
label: `Agent Approve plugin v${installedVersion}`
|
|
4433
|
+
};
|
|
4035
4434
|
} catch (err) {
|
|
4036
4435
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
4037
|
-
return { success: false, error: message };
|
|
4436
|
+
return { success: false, error: message, label: "Agent Approve plugin" };
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
function installPiPluginViaCli() {
|
|
4440
|
+
try {
|
|
4441
|
+
execSync(`pi install ${PI_PLUGIN_SPEC}`, { stdio: "pipe" });
|
|
4442
|
+
return { success: true, label: `Agent Approve extension ${PI_PLUGIN_VERSION}` };
|
|
4443
|
+
} catch (err) {
|
|
4444
|
+
return {
|
|
4445
|
+
success: false,
|
|
4446
|
+
label: "Agent Approve extension",
|
|
4447
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
function removePiPluginViaCli() {
|
|
4452
|
+
try {
|
|
4453
|
+
execSync("pi remove npm:@agentapprove/pi", { stdio: "pipe" });
|
|
4454
|
+
return { success: true };
|
|
4455
|
+
} catch (err) {
|
|
4456
|
+
return {
|
|
4457
|
+
success: false,
|
|
4458
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4459
|
+
};
|
|
4038
4460
|
}
|
|
4039
4461
|
}
|
|
4040
4462
|
var SYSTEM_DEPS = [
|
|
@@ -4255,136 +4677,148 @@ async function installCommand() {
|
|
|
4255
4677
|
await checkSystemDependencies();
|
|
4256
4678
|
const existingConfig = readExistingConfig();
|
|
4257
4679
|
const hasExistingToken = !!(existingConfig?.token && existingConfig.token.length > 10);
|
|
4680
|
+
let existingTokenPreview = "Not set";
|
|
4681
|
+
let existingKeyId = null;
|
|
4258
4682
|
if (existingConfig) {
|
|
4259
|
-
|
|
4683
|
+
existingTokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "Not set";
|
|
4260
4684
|
const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
|
|
4261
|
-
let e2eLine = "";
|
|
4262
4685
|
if (existsSync2(e2eKeyPath2)) {
|
|
4263
4686
|
const keyHex = readFileSync(e2eKeyPath2, "utf-8").trim();
|
|
4264
|
-
|
|
4265
|
-
e2eLine = `
|
|
4266
|
-
E2E Key: ${keyId}`;
|
|
4687
|
+
existingKeyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
|
|
4267
4688
|
}
|
|
4268
|
-
me(`Token: ${tokenPreview}
|
|
4269
|
-
Privacy: ${existingConfig.privacy || "unknown"}${e2eLine}`, "Existing configuration found");
|
|
4270
|
-
} else {
|
|
4271
|
-
me(`Approve AI agent actions from your iPhone or Apple Watch.
|
|
4272
|
-
Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and GitHub Copilot CLI.`, "About");
|
|
4273
4689
|
}
|
|
4690
|
+
me(`Approve AI agent actions from your iPhone or Apple Watch.
|
|
4691
|
+
Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw, and more.`, "About");
|
|
4274
4692
|
const installedAgents = detectInstalledAgents();
|
|
4275
4693
|
const agentOptions = Object.entries(AGENTS).map(([id, agent]) => ({
|
|
4276
4694
|
value: id,
|
|
4277
4695
|
label: agent.name,
|
|
4278
4696
|
hint: installedAgents.includes(id) ? "Detected" : "Not found"
|
|
4279
4697
|
}));
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4698
|
+
const selectedAgents = await $e({
|
|
4699
|
+
message: "Select agents to configure",
|
|
4700
|
+
options: agentOptions,
|
|
4701
|
+
initialValues: installedAgents,
|
|
4702
|
+
required: true
|
|
4703
|
+
});
|
|
4704
|
+
if (lD(selectedAgents)) {
|
|
4705
|
+
he("Installation cancelled");
|
|
4706
|
+
process.exit(0);
|
|
4707
|
+
}
|
|
4708
|
+
const setupProfileSummary = existingConfig ? formatSetupProfileBlock("Existing config", getInitialInstallConfig("existing-config", existingConfig), [
|
|
4709
|
+
`Connection token: ${existingTokenPreview} - already linked to your Agent Approve account.`,
|
|
4710
|
+
...existingKeyId ? [`Encryption key ID: ${existingKeyId} - already installed for this computer.`] : []
|
|
4711
|
+
]) : formatSetupProfileBlock("Recommended setup", getInitialInstallConfig("recommended", existingConfig));
|
|
4712
|
+
me(setupProfileSummary, "Setup profiles");
|
|
4713
|
+
const setupProfile = await le({
|
|
4714
|
+
message: "How would you like to set up Agent Approve?",
|
|
4715
|
+
options: getSetupProfileOptions(Boolean(existingConfig))
|
|
4716
|
+
});
|
|
4717
|
+
if (lD(setupProfile)) {
|
|
4718
|
+
he("Installation cancelled");
|
|
4719
|
+
process.exit(0);
|
|
4720
|
+
}
|
|
4721
|
+
ensureAgentApproveDir();
|
|
4722
|
+
const hooksDir = join(getAgentApproveDir(), "hooks");
|
|
4723
|
+
const selectedInstallConfig = getInitialInstallConfig(setupProfile, existingConfig);
|
|
4724
|
+
let token = null;
|
|
4725
|
+
let finalPrivacy = selectedInstallConfig.privacy;
|
|
4726
|
+
let email = "";
|
|
4727
|
+
let apiUrl = API_URL;
|
|
4728
|
+
let debugLog = selectedInstallConfig.debugLog;
|
|
4729
|
+
let retentionDays = selectedInstallConfig.retentionDays;
|
|
4730
|
+
let failBehavior = selectedInstallConfig.failBehavior;
|
|
4731
|
+
let installMode = selectedInstallConfig.installMode;
|
|
4732
|
+
let useE2E = selectedInstallConfig.e2eEnabled;
|
|
4733
|
+
let configSetAt = Math.floor(Date.now() / 1000);
|
|
4734
|
+
if (setupProfile === "customize") {
|
|
4735
|
+
const modeChoice = await le({
|
|
4736
|
+
message: "Choose your security mode:",
|
|
4737
|
+
initialValue: installMode,
|
|
4302
4738
|
options: [
|
|
4303
|
-
{
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4739
|
+
{
|
|
4740
|
+
value: "approval",
|
|
4741
|
+
label: "Approval Mode (recommended)",
|
|
4742
|
+
hint: "Agent asks permission before running commands. Server evaluates policies via encrypted channel."
|
|
4743
|
+
},
|
|
4744
|
+
{
|
|
4745
|
+
value: "observe",
|
|
4746
|
+
label: "Observe Mode (full E2E)",
|
|
4747
|
+
hint: "Agent runs freely. All events are end-to-end encrypted. No server policy evaluation."
|
|
4748
|
+
}
|
|
4308
4749
|
]
|
|
4309
|
-
})
|
|
4310
|
-
|
|
4750
|
+
});
|
|
4751
|
+
if (lD(modeChoice)) {
|
|
4752
|
+
he("Installation cancelled");
|
|
4753
|
+
process.exit(0);
|
|
4754
|
+
}
|
|
4755
|
+
installMode = modeChoice;
|
|
4756
|
+
const failBehaviorChoice = await le({
|
|
4311
4757
|
message: "If Agent Approve is unreachable, hooks should:",
|
|
4312
|
-
initialValue:
|
|
4758
|
+
initialValue: failBehavior,
|
|
4313
4759
|
options: [
|
|
4314
4760
|
{ value: "ask", label: "Ask", hint: "Fall back to the agent's built-in approval dialog (recommended)" },
|
|
4315
4761
|
{ value: "deny", label: "Deny", hint: "Block all commands until service is restored" },
|
|
4316
4762
|
{ value: "allow", label: "Allow", hint: "Allow all commands to proceed without approval" }
|
|
4317
4763
|
]
|
|
4318
|
-
})
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
}
|
|
4323
|
-
|
|
4324
|
-
|
|
4764
|
+
});
|
|
4765
|
+
if (lD(failBehaviorChoice)) {
|
|
4766
|
+
he("Installation cancelled");
|
|
4767
|
+
process.exit(0);
|
|
4768
|
+
}
|
|
4769
|
+
failBehavior = failBehaviorChoice;
|
|
4770
|
+
const privacyChoice = await le({
|
|
4771
|
+
message: "Privacy tier - controls what data is stored in event history",
|
|
4772
|
+
initialValue: finalPrivacy,
|
|
4325
4773
|
options: [
|
|
4326
|
-
{ value: "
|
|
4327
|
-
{ value: "
|
|
4774
|
+
{ value: "full", label: "Full", hint: "Complete command details stored in event history" },
|
|
4775
|
+
{ value: "summary", label: "Summary", hint: "Truncated to 50 chars in event history" },
|
|
4776
|
+
{ value: "minimal", label: "Minimal", hint: "Tool name only in event history, most private" }
|
|
4328
4777
|
]
|
|
4329
|
-
})
|
|
4330
|
-
|
|
4331
|
-
onCancel: () => {
|
|
4778
|
+
});
|
|
4779
|
+
if (lD(privacyChoice)) {
|
|
4332
4780
|
he("Installation cancelled");
|
|
4333
4781
|
process.exit(0);
|
|
4334
4782
|
}
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
const
|
|
4783
|
+
finalPrivacy = privacyChoice;
|
|
4784
|
+
}
|
|
4785
|
+
const skipE2E = hasFlag2("--no-e2e");
|
|
4786
|
+
const e2eRequired = installMode === "observe";
|
|
4787
|
+
if (e2eRequired) {
|
|
4788
|
+
if (skipE2E) {
|
|
4789
|
+
v2.warn("Observe Mode requires E2E encryption; ignoring --no-e2e.");
|
|
4790
|
+
}
|
|
4791
|
+
useE2E = true;
|
|
4792
|
+
} else if (skipE2E) {
|
|
4793
|
+
useE2E = false;
|
|
4794
|
+
}
|
|
4338
4795
|
ensureAgentApproveDir();
|
|
4339
|
-
const
|
|
4340
|
-
const
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4796
|
+
const agentApproveDir = getAgentApproveDir();
|
|
4797
|
+
const existingKeyPath = join(agentApproveDir, "e2e-key");
|
|
4798
|
+
let e2eUserKey = null;
|
|
4799
|
+
let backupOldKeyTo = null;
|
|
4800
|
+
if (setupProfile === "customize" && !e2eRequired && !skipE2E) {
|
|
4801
|
+
const e2eChoice = await ce({
|
|
4802
|
+
message: "Enable end-to-end encryption?",
|
|
4803
|
+
initialValue: useE2E
|
|
4347
4804
|
});
|
|
4805
|
+
if (lD(e2eChoice)) {
|
|
4806
|
+
he("Installation cancelled");
|
|
4807
|
+
process.exit(0);
|
|
4808
|
+
}
|
|
4809
|
+
useE2E = e2eChoice;
|
|
4348
4810
|
}
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
process.exit(0);
|
|
4357
|
-
}
|
|
4358
|
-
let token = null;
|
|
4359
|
-
let finalPrivacy = privacy;
|
|
4360
|
-
let email = "";
|
|
4361
|
-
let apiUrl = API_URL;
|
|
4362
|
-
let useE2E = true;
|
|
4363
|
-
if (connectionMethod === "existing") {
|
|
4364
|
-
token = existingConfig.token;
|
|
4365
|
-
apiUrl = existingConfig.apiUrl || API_URL;
|
|
4366
|
-
const e2eKeyExists = existsSync2(join(getAgentApproveDir(), "e2e-key"));
|
|
4367
|
-
useE2E = e2eKeyExists;
|
|
4368
|
-
v2.success("Using existing token");
|
|
4369
|
-
} else if (connectionMethod === "qr") {
|
|
4370
|
-
const skipE2E = hasFlag2("--no-e2e");
|
|
4371
|
-
useE2E = !skipE2E;
|
|
4372
|
-
const agentApproveDir = getAgentApproveDir();
|
|
4373
|
-
const existingKeyPath = join(agentApproveDir, "e2e-key");
|
|
4374
|
-
let e2eUserKey = null;
|
|
4375
|
-
if (useE2E) {
|
|
4376
|
-
if (existsSync2(existingKeyPath)) {
|
|
4377
|
-
const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
|
|
4378
|
-
const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
|
|
4811
|
+
if (useE2E) {
|
|
4812
|
+
if (existsSync2(existingKeyPath)) {
|
|
4813
|
+
const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
|
|
4814
|
+
const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
|
|
4815
|
+
if (setupProfile === "existing-config") {
|
|
4816
|
+
e2eUserKey = oldKeyHex;
|
|
4817
|
+
} else {
|
|
4379
4818
|
v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
|
|
4380
4819
|
const keyAction = await le({
|
|
4381
4820
|
message: "Reuse existing encryption key or generate a new one?",
|
|
4382
|
-
options:
|
|
4383
|
-
{ value: "reuse", label: "Reuse existing key", hint: "recommended — keeps old events decryptable" },
|
|
4384
|
-
{ value: "backup", label: "Back up old key, then generate new" },
|
|
4385
|
-
{ value: "discard", label: "Discard old key and generate new" },
|
|
4386
|
-
{ value: "disable", label: "Disable E2E encryption", hint: "events visible on web dashboard" }
|
|
4387
|
-
]
|
|
4821
|
+
options: getExistingKeyActionOptions(e2eRequired)
|
|
4388
4822
|
});
|
|
4389
4823
|
if (lD(keyAction)) {
|
|
4390
4824
|
he("Installation cancelled");
|
|
@@ -4396,33 +4830,96 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
|
|
|
4396
4830
|
e2eUserKey = oldKeyHex;
|
|
4397
4831
|
} else {
|
|
4398
4832
|
if (keyAction === "backup") {
|
|
4399
|
-
|
|
4400
|
-
renameSync(existingKeyPath, backupPath);
|
|
4401
|
-
v2.success(`Old key backed up to ${backupPath}`);
|
|
4402
|
-
}
|
|
4403
|
-
e2eUserKey = randomBytes(32).toString("hex");
|
|
4404
|
-
}
|
|
4405
|
-
} else {
|
|
4406
|
-
if (!skipE2E) {
|
|
4407
|
-
const e2eChoice = await ce({
|
|
4408
|
-
message: "Enable end-to-end encryption?",
|
|
4409
|
-
initialValue: true
|
|
4410
|
-
});
|
|
4411
|
-
if (lD(e2eChoice)) {
|
|
4412
|
-
he("Installation cancelled");
|
|
4413
|
-
process.exit(0);
|
|
4833
|
+
backupOldKeyTo = join(agentApproveDir, `e2e-key.${oldKeyId}.bak`);
|
|
4414
4834
|
}
|
|
4415
|
-
useE2E = e2eChoice;
|
|
4416
|
-
}
|
|
4417
|
-
if (useE2E) {
|
|
4418
4835
|
e2eUserKey = randomBytes(32).toString("hex");
|
|
4419
4836
|
}
|
|
4420
4837
|
}
|
|
4838
|
+
} else {
|
|
4839
|
+
e2eUserKey = randomBytes(32).toString("hex");
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
if (!useE2E) {
|
|
4843
|
+
v2.info("E2E encryption disabled — event content will be visible on the web dashboard");
|
|
4844
|
+
}
|
|
4845
|
+
const e2eKeyId = e2eUserKey ? createHash("sha256").update(Buffer.from(e2eUserKey, "hex")).digest("hex").slice(0, 8) : undefined;
|
|
4846
|
+
if (setupProfile === "customize") {
|
|
4847
|
+
const retentionChoice = await le({
|
|
4848
|
+
message: "Data retention - how long to keep event history",
|
|
4849
|
+
initialValue: `${retentionDays}`,
|
|
4850
|
+
options: [
|
|
4851
|
+
{ value: "365", label: "1 Year", hint: "Delete events older than 1 year" },
|
|
4852
|
+
{ value: "90", label: "90 Days", hint: "Delete events older than 90 days" },
|
|
4853
|
+
{ value: "30", label: "30 Days", hint: "Delete events older than 30 days (recommended)" },
|
|
4854
|
+
{ value: "7", label: "1 Week", hint: "Delete events older than 7 days" },
|
|
4855
|
+
{ value: "1", label: "1 Day", hint: "Delete events older than 1 day" }
|
|
4856
|
+
]
|
|
4857
|
+
});
|
|
4858
|
+
if (lD(retentionChoice)) {
|
|
4859
|
+
he("Installation cancelled");
|
|
4860
|
+
process.exit(0);
|
|
4861
|
+
}
|
|
4862
|
+
retentionDays = parseInt(retentionChoice, 10) || 30;
|
|
4863
|
+
const debugLogChoice = await ce({
|
|
4864
|
+
message: "Enable debug logging? (writes to ~/.agentapprove/hook-debug.log)",
|
|
4865
|
+
initialValue: debugLog
|
|
4866
|
+
});
|
|
4867
|
+
if (lD(debugLogChoice)) {
|
|
4868
|
+
he("Installation cancelled");
|
|
4869
|
+
process.exit(0);
|
|
4421
4870
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4871
|
+
debugLog = debugLogChoice;
|
|
4872
|
+
}
|
|
4873
|
+
const silentlyReuseExistingToken = setupProfile === "existing-config" && hasExistingToken;
|
|
4874
|
+
const connectionOptions = [];
|
|
4875
|
+
if (hasExistingToken) {
|
|
4876
|
+
const tokenPreview = existingConfig.token.slice(0, 15) + "...";
|
|
4877
|
+
connectionOptions.push({
|
|
4878
|
+
value: "existing",
|
|
4879
|
+
label: "Use existing token",
|
|
4880
|
+
hint: tokenPreview
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4883
|
+
connectionOptions.push({ value: "qr", label: "Scan QR code", hint: hasExistingToken ? undefined : "Recommended" }, { value: "copy", label: "Copy and paste code", hint: "No camera" });
|
|
4884
|
+
let connectionMethod;
|
|
4885
|
+
if (silentlyReuseExistingToken) {
|
|
4886
|
+
connectionMethod = "existing";
|
|
4887
|
+
} else {
|
|
4888
|
+
const connectionChoice = await le({
|
|
4889
|
+
message: "Connect to iOS app (required for setup and any hook downloads)",
|
|
4890
|
+
options: connectionOptions
|
|
4891
|
+
});
|
|
4892
|
+
if (lD(connectionChoice)) {
|
|
4893
|
+
he("Installation cancelled");
|
|
4894
|
+
process.exit(0);
|
|
4424
4895
|
}
|
|
4425
|
-
|
|
4896
|
+
connectionMethod = connectionChoice;
|
|
4897
|
+
}
|
|
4898
|
+
if (connectionMethod === "existing") {
|
|
4899
|
+
token = existingConfig.token;
|
|
4900
|
+
apiUrl = existingConfig.apiUrl || API_URL;
|
|
4901
|
+
const e2eKeyExists = existsSync2(existingKeyPath);
|
|
4902
|
+
useE2E = e2eKeyExists;
|
|
4903
|
+
e2eUserKey = null;
|
|
4904
|
+
backupOldKeyTo = null;
|
|
4905
|
+
if (!silentlyReuseExistingToken) {
|
|
4906
|
+
v2.success("Using existing token");
|
|
4907
|
+
}
|
|
4908
|
+
} else if (shouldCreateFreshPairing(connectionMethod)) {
|
|
4909
|
+
const pairingMethod = connectionMethod;
|
|
4910
|
+
if (backupOldKeyTo) {
|
|
4911
|
+
renameSync(existingKeyPath, backupOldKeyTo);
|
|
4912
|
+
v2.success(`Old key backed up to ${backupOldKeyTo}`);
|
|
4913
|
+
backupOldKeyTo = null;
|
|
4914
|
+
}
|
|
4915
|
+
configSetAt = Math.floor(Date.now() / 1000);
|
|
4916
|
+
const pairingConfig = {
|
|
4917
|
+
privacyTier: finalPrivacy,
|
|
4918
|
+
retentionDays,
|
|
4919
|
+
failBehavior,
|
|
4920
|
+
configSetAt,
|
|
4921
|
+
e2eEnabled: useE2E
|
|
4922
|
+
};
|
|
4426
4923
|
const session = await createPairingSession(selectedAgents, e2eKeyId);
|
|
4427
4924
|
if (!session || session.error) {
|
|
4428
4925
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
@@ -4430,18 +4927,19 @@ Installs hooks for Claude Code, Cursor, Gemini CLI, VS Code GitHub Copilot, and
|
|
|
4430
4927
|
process.exit(1);
|
|
4431
4928
|
} else {
|
|
4432
4929
|
const machineHost = hostname();
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4930
|
+
const pairingPresentation = buildPairingPresentation({
|
|
4931
|
+
sessionCode: session.sessionCode,
|
|
4932
|
+
hostname: machineHost,
|
|
4933
|
+
config: pairingConfig,
|
|
4934
|
+
e2eUserKey
|
|
4935
|
+
});
|
|
4936
|
+
await showPairingPresentation({
|
|
4937
|
+
sessionCode: session.sessionCode,
|
|
4938
|
+
pairingUrl: pairingPresentation.pairingUrl,
|
|
4939
|
+
manualCode: pairingPresentation.manualCode,
|
|
4940
|
+
keyId: e2eKeyId,
|
|
4941
|
+
method: pairingMethod
|
|
4441
4942
|
});
|
|
4442
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
4443
|
-
me(qrDisplay + `
|
|
4444
|
-
Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
4445
4943
|
const pairingSpinner = _2();
|
|
4446
4944
|
pairingSpinner.start("Waiting for iOS app...");
|
|
4447
4945
|
const result = await waitForPairing(session.sessionCode, (expiresIn) => {
|
|
@@ -4458,10 +4956,9 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4458
4956
|
finalPrivacy = result.privacy;
|
|
4459
4957
|
email = result.email;
|
|
4460
4958
|
if (e2eUserKey && e2eKeyId) {
|
|
4461
|
-
const
|
|
4462
|
-
const rootKeyPath = join(agentApproveDir2, "e2e-root-key");
|
|
4959
|
+
const rootKeyPath = join(agentApproveDir, "e2e-root-key");
|
|
4463
4960
|
writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
|
|
4464
|
-
const userKeyPath = join(
|
|
4961
|
+
const userKeyPath = join(agentApproveDir, "e2e-key");
|
|
4465
4962
|
writeFileSync(userKeyPath, e2eUserKey, { mode: 384 });
|
|
4466
4963
|
writeRotationConfig({
|
|
4467
4964
|
rootKeyId: e2eKeyId,
|
|
@@ -4470,7 +4967,7 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4470
4967
|
startedAt: new Date().toISOString()
|
|
4471
4968
|
});
|
|
4472
4969
|
if (result.e2eServerKey) {
|
|
4473
|
-
const serverKeyPath = join(
|
|
4970
|
+
const serverKeyPath = join(agentApproveDir, "e2e-server-key");
|
|
4474
4971
|
writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
|
|
4475
4972
|
}
|
|
4476
4973
|
}
|
|
@@ -4490,7 +4987,6 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4490
4987
|
he("Cannot continue without token");
|
|
4491
4988
|
process.exit(1);
|
|
4492
4989
|
}
|
|
4493
|
-
const configSetAt = Math.floor(Date.now() / 1000);
|
|
4494
4990
|
saveEnvConfig({
|
|
4495
4991
|
apiUrl,
|
|
4496
4992
|
token,
|
|
@@ -4501,55 +4997,36 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
4501
4997
|
e2eEnabled: useE2E,
|
|
4502
4998
|
failBehavior
|
|
4503
4999
|
});
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
5000
|
+
if (!silentlyReuseExistingToken) {
|
|
5001
|
+
v2.success("Configuration saved to ~/.agentapprove/env");
|
|
5002
|
+
}
|
|
5003
|
+
await storeTokenInKeychain(token, { quiet: silentlyReuseExistingToken });
|
|
5004
|
+
const hookDownloadPlan = buildHookDownloadPlan(selectedAgents);
|
|
5005
|
+
if (hookDownloadPlan.files.length > 0) {
|
|
5006
|
+
const downloadSpinner = _2();
|
|
5007
|
+
downloadSpinner.start("Downloading hook scripts");
|
|
5008
|
+
const downloadResult = await copyHookScripts(hooksDir, token, hookDownloadPlan.files);
|
|
5009
|
+
const summary = formatHookDownloadSummary(hookDownloadPlan);
|
|
5010
|
+
if (downloadResult.failed.length > 0) {
|
|
5011
|
+
downloadSpinner.stop(`Downloaded ${downloadResult.downloaded} of ${hookDownloadPlan.files.length} hook files (${summary})`);
|
|
5012
|
+
v2.warn(`Failed to download: ${downloadResult.failed.join(", ")}`);
|
|
5013
|
+
} else {
|
|
5014
|
+
downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files: ${summary})`);
|
|
5015
|
+
}
|
|
4520
5016
|
} else {
|
|
4521
|
-
|
|
5017
|
+
v2.success("No hook scripts needed for the selected agents");
|
|
4522
5018
|
}
|
|
4523
|
-
const e2eKeyPath = join(
|
|
5019
|
+
const e2eKeyPath = join(agentApproveDir, "e2e-key");
|
|
4524
5020
|
const hasE2EKey = existsSync2(e2eKeyPath);
|
|
4525
|
-
let installMode = "approval";
|
|
4526
5021
|
if (hasE2EKey) {
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
hint: "Agent asks permission before running commands. Server evaluates policies via encrypted channel."
|
|
4534
|
-
},
|
|
4535
|
-
{
|
|
4536
|
-
value: "observe",
|
|
4537
|
-
label: "Observe Mode (full E2E)",
|
|
4538
|
-
hint: "Agent runs freely. All events are end-to-end encrypted. No server policy evaluation."
|
|
4539
|
-
}
|
|
4540
|
-
]
|
|
4541
|
-
});
|
|
4542
|
-
if (lD(modeChoice)) {
|
|
4543
|
-
he("Installation cancelled");
|
|
4544
|
-
process.exit(0);
|
|
4545
|
-
}
|
|
4546
|
-
installMode = modeChoice;
|
|
4547
|
-
if (installMode === "observe") {
|
|
4548
|
-
v2.info("Observe mode: All events are E2E encrypted. No approval hooks will be installed.");
|
|
4549
|
-
} else {
|
|
4550
|
-
v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
|
|
5022
|
+
if (!silentlyReuseExistingToken) {
|
|
5023
|
+
if (installMode === "observe") {
|
|
5024
|
+
v2.info("Observe mode: All events are E2E encrypted. No approval hooks will be installed.");
|
|
5025
|
+
} else {
|
|
5026
|
+
v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
|
|
5027
|
+
}
|
|
4551
5028
|
}
|
|
4552
|
-
const envPath = join(
|
|
5029
|
+
const envPath = join(agentApproveDir, "env");
|
|
4553
5030
|
if (existsSync2(envPath)) {
|
|
4554
5031
|
let envContent = readFileSync(envPath, "utf-8");
|
|
4555
5032
|
if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
|
|
@@ -4563,17 +5040,33 @@ AGENTAPPROVE_E2E_MODE=${installMode}
|
|
|
4563
5040
|
writeFileSync(envPath, envContent, { mode: 384 });
|
|
4564
5041
|
}
|
|
4565
5042
|
}
|
|
5043
|
+
const installMethod = await le({
|
|
5044
|
+
message: "How would you like to install hooks?",
|
|
5045
|
+
options: [
|
|
5046
|
+
{ value: "auto", label: "Automatic", hint: "Back up configs and install hooks for me" },
|
|
5047
|
+
{ value: "manual", label: "Manual", hint: "Show me what to add (I'll do it myself)" }
|
|
5048
|
+
]
|
|
5049
|
+
});
|
|
5050
|
+
if (lD(installMethod)) {
|
|
5051
|
+
he("Installation cancelled");
|
|
5052
|
+
process.exit(0);
|
|
5053
|
+
}
|
|
4566
5054
|
if (installMethod === "auto") {
|
|
4567
5055
|
const filesToModify = [];
|
|
4568
5056
|
for (const agentId of selectedAgents) {
|
|
4569
5057
|
const agent = AGENTS[agentId];
|
|
4570
|
-
|
|
5058
|
+
if (agentId !== "pi") {
|
|
5059
|
+
filesToModify.push(agent.configPath);
|
|
5060
|
+
}
|
|
4571
5061
|
if (agentId === "codex") {
|
|
4572
5062
|
filesToModify.push(join(homedir(), ".codex", "config.toml"));
|
|
4573
5063
|
}
|
|
4574
5064
|
if (agentId === "opencode") {
|
|
4575
5065
|
filesToModify.push(join(getOpenCodeConfigDir(), "package.json"));
|
|
4576
5066
|
}
|
|
5067
|
+
if (agentId === "pi") {
|
|
5068
|
+
filesToModify.push("Pi package registry (via `pi install`)");
|
|
5069
|
+
}
|
|
4577
5070
|
if (agentId === "vscode-agent") {
|
|
4578
5071
|
const vsCodeVariants = findInstalledVSCodeVariants();
|
|
4579
5072
|
for (const { path: settingsPath, variant } of vsCodeVariants) {
|
|
@@ -4595,7 +5088,7 @@ Backups will be created with timestamp`, "Files to be modified");
|
|
|
4595
5088
|
for (const agentId of selectedAgents) {
|
|
4596
5089
|
const agent = AGENTS[agentId];
|
|
4597
5090
|
const spinner = _2();
|
|
4598
|
-
const spinnerMsg = agentId === "openclaw" ? `Installing ${agent.name} plugin` : `Configuring ${agent.name}`;
|
|
5091
|
+
const spinnerMsg = agentId === "pi" ? `Installing ${agent.name} extension` : agentId === "openclaw" ? `Installing ${agent.name} plugin` : `Configuring ${agent.name}`;
|
|
4599
5092
|
spinner.start(spinnerMsg);
|
|
4600
5093
|
const result = await installHooksForAgent(agentId, hooksDir, installMode);
|
|
4601
5094
|
if (result.success) {
|
|
@@ -4631,6 +5124,11 @@ Backups will be created with timestamp`, "Files to be modified");
|
|
|
4631
5124
|
v2.warn(`Could not install ${OPENCLAW_PLUGIN_SPEC} via OpenClaw CLI.
|
|
4632
5125
|
` + ` Error: ${result.error || "unknown"}
|
|
4633
5126
|
` + ` Install manually: openclaw plugins install ${OPENCLAW_PLUGIN_SPEC}
|
|
5127
|
+
` + ` Then re-run: npx agentapprove`);
|
|
5128
|
+
} else if (agentId === "pi") {
|
|
5129
|
+
v2.warn(`Could not install ${PI_PLUGIN_SPEC} via Pi.
|
|
5130
|
+
` + ` Error: ${result.error || "unknown"}
|
|
5131
|
+
` + ` Install manually: pi install ${PI_PLUGIN_SPEC}
|
|
4634
5132
|
` + ` Then re-run: npx agentapprove`);
|
|
4635
5133
|
}
|
|
4636
5134
|
}
|
|
@@ -4670,9 +5168,13 @@ async function statusCommand() {
|
|
|
4670
5168
|
`);
|
|
4671
5169
|
for (const line of lines) {
|
|
4672
5170
|
if (line.startsWith("AGENTAPPROVE_")) {
|
|
4673
|
-
const
|
|
4674
|
-
|
|
4675
|
-
|
|
5171
|
+
const assignment = parseEnvAssignment(line);
|
|
5172
|
+
if (!assignment) {
|
|
5173
|
+
continue;
|
|
5174
|
+
}
|
|
5175
|
+
const { key, value } = assignment;
|
|
5176
|
+
const displayKey = STATUS_FIELD_LABELS[key] || key.replace("AGENTAPPROVE_", "");
|
|
5177
|
+
const displayValue = formatStatusValue(key, value);
|
|
4676
5178
|
console.log(` ${source_default.dim(displayKey + ":")} ${displayValue}`);
|
|
4677
5179
|
}
|
|
4678
5180
|
}
|
|
@@ -4689,19 +5191,32 @@ async function statusCommand() {
|
|
|
4689
5191
|
}
|
|
4690
5192
|
console.log();
|
|
4691
5193
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
5194
|
+
if (agentId === "pi") {
|
|
5195
|
+
try {
|
|
5196
|
+
const listOutput = execSync("pi list", {
|
|
5197
|
+
encoding: "utf-8",
|
|
5198
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
5199
|
+
timeout: PI_STATUS_TIMEOUT_MS
|
|
5200
|
+
});
|
|
5201
|
+
if (listOutput.includes("@agentapprove/pi")) {
|
|
5202
|
+
console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve extension`);
|
|
5203
|
+
}
|
|
5204
|
+
} catch {}
|
|
5205
|
+
continue;
|
|
5206
|
+
}
|
|
4692
5207
|
if (existsSync2(agent.configPath)) {
|
|
4693
5208
|
const config = readJsonConfig(agent.configPath);
|
|
4694
5209
|
if (agentId === "opencode") {
|
|
4695
5210
|
const pluginConfig = config.plugin;
|
|
4696
5211
|
if (Array.isArray(pluginConfig) && pluginConfig.some((entry) => typeof entry === "string" && entry.includes("@agentapprove/opencode"))) {
|
|
4697
|
-
console.log(` ${source_default.green("✓")} ${agent.name}:
|
|
5212
|
+
console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve plugin`);
|
|
4698
5213
|
}
|
|
4699
5214
|
continue;
|
|
4700
5215
|
}
|
|
4701
5216
|
if (agentId === "openclaw") {
|
|
4702
5217
|
const entries = config.plugins?.entries;
|
|
4703
5218
|
if (entries?.openclaw) {
|
|
4704
|
-
console.log(` ${source_default.green("✓")} ${agent.name}:
|
|
5219
|
+
console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve plugin`);
|
|
4705
5220
|
}
|
|
4706
5221
|
continue;
|
|
4707
5222
|
}
|
|
@@ -4715,7 +5230,7 @@ async function statusCommand() {
|
|
|
4715
5230
|
return str.includes("agentapprove");
|
|
4716
5231
|
});
|
|
4717
5232
|
if (installedHooks.length > 0) {
|
|
4718
|
-
console.log(` ${source_default.green("✓")} ${agent.name}: ${installedHooks.map((h2) => h2.name).join(", ")}`);
|
|
5233
|
+
console.log(` ${source_default.green("✓")} ${agent.name}: ${installedHooks.map((h2) => formatHookStatusLabel(h2.name)).join(", ")}`);
|
|
4719
5234
|
}
|
|
4720
5235
|
}
|
|
4721
5236
|
}
|
|
@@ -4776,6 +5291,15 @@ async function performUninstall(mode) {
|
|
|
4776
5291
|
${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
|
|
4777
5292
|
`));
|
|
4778
5293
|
for (const [agentId, agent] of Object.entries(AGENTS)) {
|
|
5294
|
+
if (agentId === "pi") {
|
|
5295
|
+
const result = removePiPluginViaCli();
|
|
5296
|
+
if (result.success) {
|
|
5297
|
+
console.log(` ${source_default.green("✓")} Pi extension removed`);
|
|
5298
|
+
} else if (existsSync2(agent.configPath)) {
|
|
5299
|
+
console.log(` ${source_default.yellow("⚠")} Pi extension removal skipped or failed (${result.error || "Pi CLI unavailable"})`);
|
|
5300
|
+
}
|
|
5301
|
+
continue;
|
|
5302
|
+
}
|
|
4779
5303
|
if (!existsSync2(agent.configPath))
|
|
4780
5304
|
continue;
|
|
4781
5305
|
const config = readJsonConfig(agent.configPath);
|
|
@@ -5042,24 +5566,37 @@ but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
|
|
|
5042
5566
|
} else {
|
|
5043
5567
|
v2.info('Refresh updates the token only. Use "npx agentapprove pair" if you need to repair or change E2E pairing.');
|
|
5044
5568
|
}
|
|
5569
|
+
const configSetAt = Math.floor(Date.now() / 1000);
|
|
5570
|
+
const privacy = existingConfig.privacy || "full";
|
|
5571
|
+
const retentionDays = existingConfig.retentionDays ?? 30;
|
|
5572
|
+
const failBehavior = existingConfig.failBehavior || "ask";
|
|
5573
|
+
const pairingConfig = {
|
|
5574
|
+
privacyTier: privacy,
|
|
5575
|
+
retentionDays,
|
|
5576
|
+
failBehavior,
|
|
5577
|
+
configSetAt,
|
|
5578
|
+
e2eEnabled
|
|
5579
|
+
};
|
|
5045
5580
|
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
|
|
5046
5581
|
if (!session || session.error) {
|
|
5047
5582
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
5048
5583
|
process.exit(1);
|
|
5049
5584
|
}
|
|
5585
|
+
const pairingMethod = await promptPairingMethod();
|
|
5050
5586
|
const machineHost = hostname();
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5587
|
+
const pairingPresentation = buildPairingPresentation({
|
|
5588
|
+
sessionCode: session.sessionCode,
|
|
5589
|
+
hostname: machineHost,
|
|
5590
|
+
config: pairingConfig,
|
|
5591
|
+
e2eUserKey
|
|
5592
|
+
});
|
|
5593
|
+
await showPairingPresentation({
|
|
5594
|
+
sessionCode: session.sessionCode,
|
|
5595
|
+
pairingUrl: pairingPresentation.pairingUrl,
|
|
5596
|
+
manualCode: pairingPresentation.manualCode,
|
|
5597
|
+
keyId: e2eKeyId,
|
|
5598
|
+
method: pairingMethod
|
|
5059
5599
|
});
|
|
5060
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5061
|
-
me(qrDisplay + `
|
|
5062
|
-
Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
5063
5600
|
const pairingSpinner = _2();
|
|
5064
5601
|
pairingSpinner.start("Waiting for iOS app...");
|
|
5065
5602
|
const result = await waitForPairing(session.sessionCode, (expiresIn) => {
|
|
@@ -5079,10 +5616,6 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
5079
5616
|
v2.error('Session expired. Run "npx agentapprove refresh" to try again.');
|
|
5080
5617
|
process.exit(1);
|
|
5081
5618
|
}
|
|
5082
|
-
const configSetAt = Math.floor(Date.now() / 1000);
|
|
5083
|
-
const privacy = existingConfig.privacy || "full";
|
|
5084
|
-
const retentionDays = existingConfig.retentionDays ?? 30;
|
|
5085
|
-
const failBehavior = existingConfig.failBehavior || "ask";
|
|
5086
5619
|
saveEnvConfig({
|
|
5087
5620
|
apiUrl,
|
|
5088
5621
|
token,
|
|
@@ -5099,14 +5632,6 @@ Session: ${session.sessionCode}`, "Scan with Agent Approve iOS app");
|
|
|
5099
5632
|
const serverKeyPath = join(getAgentApproveDir(), "e2e-server-key");
|
|
5100
5633
|
writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
|
|
5101
5634
|
}
|
|
5102
|
-
pushConfigToCloud({
|
|
5103
|
-
apiUrl,
|
|
5104
|
-
token,
|
|
5105
|
-
privacy,
|
|
5106
|
-
retentionDays,
|
|
5107
|
-
failBehavior,
|
|
5108
|
-
configSetAt
|
|
5109
|
-
}).catch(() => {});
|
|
5110
5635
|
const updateSpinner = _2();
|
|
5111
5636
|
updateSpinner.start("Updating hook scripts with new token");
|
|
5112
5637
|
try {
|
|
@@ -5204,26 +5729,34 @@ async function pairCommand() {
|
|
|
5204
5729
|
updateEnvValue("AGENTAPPROVE_E2E_ENABLED", "true");
|
|
5205
5730
|
}
|
|
5206
5731
|
const installedAgents = detectInstalledAgents();
|
|
5732
|
+
const pairingConfigSetAt = Math.floor(Date.now() / 1000);
|
|
5733
|
+
const pairingConfig = {
|
|
5734
|
+
privacyTier: existingConfig?.privacy || "full",
|
|
5735
|
+
retentionDays: existingConfig?.retentionDays ?? 30,
|
|
5736
|
+
failBehavior: existingConfig?.failBehavior || "ask",
|
|
5737
|
+
configSetAt: pairingConfigSetAt,
|
|
5738
|
+
e2eEnabled: !!e2eUserKey
|
|
5739
|
+
};
|
|
5207
5740
|
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
|
|
5208
5741
|
if (!session || session.error) {
|
|
5209
5742
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
5210
5743
|
process.exit(1);
|
|
5211
5744
|
}
|
|
5745
|
+
const pairingMethod = await promptPairingMethod();
|
|
5212
5746
|
const machineHost = hostname();
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5747
|
+
const pairingPresentation = buildPairingPresentation({
|
|
5748
|
+
sessionCode: session.sessionCode,
|
|
5749
|
+
hostname: machineHost,
|
|
5750
|
+
config: pairingConfig,
|
|
5751
|
+
e2eUserKey
|
|
5752
|
+
});
|
|
5753
|
+
await showPairingPresentation({
|
|
5754
|
+
sessionCode: session.sessionCode,
|
|
5755
|
+
pairingUrl: pairingPresentation.pairingUrl,
|
|
5756
|
+
manualCode: pairingPresentation.manualCode,
|
|
5757
|
+
keyId: e2eKeyId,
|
|
5758
|
+
method: pairingMethod
|
|
5221
5759
|
});
|
|
5222
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
5223
|
-
const noteLabel = e2eKeyId ? `Session: ${session.sessionCode}
|
|
5224
|
-
Key ID: ${e2eKeyId}` : `Session: ${session.sessionCode}`;
|
|
5225
|
-
me(qrDisplay + `
|
|
5226
|
-
${noteLabel}`, "Scan with Agent Approve iOS app");
|
|
5227
5760
|
const pairingSpinner = _2();
|
|
5228
5761
|
pairingSpinner.start("Waiting for iOS app...");
|
|
5229
5762
|
const result = await waitForPairing(session.sessionCode, (expiresIn) => {
|
|
@@ -5286,13 +5819,6 @@ ${noteLabel}`, "Scan with Agent Approve iOS app");
|
|
|
5286
5819
|
v2.warn('You may need to run "npx agentapprove install" to reconfigure hooks.');
|
|
5287
5820
|
}
|
|
5288
5821
|
}
|
|
5289
|
-
pushConfigToCloud({
|
|
5290
|
-
apiUrl,
|
|
5291
|
-
token: result.token,
|
|
5292
|
-
privacy: existingConfig?.privacy || result.privacy || "full",
|
|
5293
|
-
retentionDays: existingConfig?.retentionDays ?? 30,
|
|
5294
|
-
configSetAt
|
|
5295
|
-
}).catch(() => {});
|
|
5296
5822
|
ge(source_default.green(`Device paired with key ${e2eKeyId}`));
|
|
5297
5823
|
}
|
|
5298
5824
|
async function updateHookScriptsWithToken(token, apiUrl) {
|