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.
Files changed (2) hide show
  1. package/dist/cli.js +860 -334
  2. 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.11";
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.7";
2557
- var OPENCLAW_PLUGIN_VERSION = "0.2.5";
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("#") || !line.includes("="))
3319
+ if (line.startsWith("#"))
3320
+ continue;
3321
+ const assignment = parseEnvAssignment(line);
3322
+ if (!assignment)
2996
3323
  continue;
2997
- const [key, ...valueParts] = line.split("=");
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
- v2.success("Token stored in macOS Keychain");
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
- v2.success("Token stored in Windows Credential Manager");
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("openclaw");
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("agentapprove");
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
- var HOOK_FILES = [
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 HOOK_FILES) {
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
- return { success: true };
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
- const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
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
- const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
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 defaultPrivacy = existingConfig?.privacy || "full";
4281
- const defaultDebugLog = existingConfig?.debugLog ?? false;
4282
- const defaultFailBehavior = existingConfig?.failBehavior || "ask";
4283
- const setup = await ve({
4284
- agents: () => $e({
4285
- message: "Select agents to configure",
4286
- options: agentOptions,
4287
- initialValues: installedAgents,
4288
- required: true
4289
- }),
4290
- privacy: () => le({
4291
- message: "Privacy tier - controls what data is stored in event logs",
4292
- initialValue: defaultPrivacy,
4293
- options: [
4294
- { value: "full", label: "Full", hint: "Complete command details stored in logs" },
4295
- { value: "summary", label: "Summary", hint: "Truncated to 50 chars in logs" },
4296
- { value: "minimal", label: "Minimal", hint: "Tool name only in logs, most private" }
4297
- ]
4298
- }),
4299
- retention: () => le({
4300
- message: "Data retention - how long to keep event history",
4301
- initialValue: "30",
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
- { value: "365", label: "1 Year", hint: "Delete events older than 1 year" },
4304
- { value: "90", label: "90 Days", hint: "Delete events older than 90 days" },
4305
- { value: "30", label: "30 Days", hint: "Delete events older than 30 days (recommended)" },
4306
- { value: "7", label: "1 Week", hint: "Delete events older than 7 days" },
4307
- { value: "1", label: "1 Day", hint: "Delete events older than 1 day" }
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
- failBehavior: () => le({
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: defaultFailBehavior,
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
- debugLog: () => ce({
4320
- message: "Enable debug logging? (writes to ~/.agentapprove/hook-debug.log)",
4321
- initialValue: defaultDebugLog
4322
- }),
4323
- installMethod: () => le({
4324
- message: "How would you like to install hooks?",
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: "auto", label: "Automatic", hint: "Back up configs and install hooks for me" },
4327
- { value: "manual", label: "Manual", hint: "Show me what to add (I'll do it myself)" }
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
- const { agents: selectedAgents, privacy, retention, failBehavior, debugLog, installMethod } = setup;
4337
- const retentionDays = parseInt(retention, 10) || 30;
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 hooksDir = join(getAgentApproveDir(), "hooks");
4340
- const connectionOptions = [];
4341
- if (hasExistingToken) {
4342
- const tokenPreview = existingConfig.token.slice(0, 15) + "...";
4343
- connectionOptions.push({
4344
- value: "existing",
4345
- label: "Use existing token",
4346
- hint: tokenPreview
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
- connectionOptions.push({ value: "qr", label: "Scan QR code", hint: hasExistingToken ? undefined : "Recommended" });
4350
- const connectionMethod = await le({
4351
- message: "Connect to iOS app (required for hook download)",
4352
- options: connectionOptions
4353
- });
4354
- if (lD(connectionMethod)) {
4355
- he("Installation cancelled");
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
- const backupPath = join(agentApproveDir, `e2e-key.${oldKeyId}.bak`);
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
- if (!useE2E) {
4423
- v2.info("E2E encryption disabled — event content will be visible on the web dashboard");
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
- const e2eKeyId = e2eUserKey ? createHash("sha256").update(Buffer.from(e2eUserKey, "hex")).digest("hex").slice(0, 8) : undefined;
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
- let qrUrlWithE2e = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
4434
- if (e2eUserKey) {
4435
- const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
4436
- qrUrlWithE2e += `&e2eKey=${e2eKeyBase64url}`;
4437
- }
4438
- let qrDisplay = "";
4439
- import_qrcode_terminal.default.generate(qrUrlWithE2e, { small: true }, (qr) => {
4440
- qrDisplay = qr;
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 agentApproveDir2 = getAgentApproveDir();
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(agentApproveDir2, "e2e-key");
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(agentApproveDir2, "e2e-server-key");
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
- v2.success("Configuration saved to ~/.agentapprove/env");
4505
- await storeTokenInKeychain(token);
4506
- pushConfigToCloud({
4507
- apiUrl,
4508
- token,
4509
- privacy: finalPrivacy,
4510
- retentionDays,
4511
- failBehavior,
4512
- configSetAt
4513
- }).catch(() => {});
4514
- const downloadSpinner = _2();
4515
- downloadSpinner.start("Downloading hook scripts");
4516
- const downloadResult = await copyHookScripts(hooksDir, token);
4517
- if (downloadResult.failed.length > 0) {
4518
- downloadSpinner.stop(`Downloaded ${downloadResult.downloaded} hooks, ${downloadResult.failed.length} failed`);
4519
- v2.warn(`Failed to download: ${downloadResult.failed.join(", ")}`);
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
- downloadSpinner.stop(`Hook scripts downloaded (${downloadResult.downloaded} files)`);
5017
+ v2.success("No hook scripts needed for the selected agents");
4522
5018
  }
4523
- const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
5019
+ const e2eKeyPath = join(agentApproveDir, "e2e-key");
4524
5020
  const hasE2EKey = existsSync2(e2eKeyPath);
4525
- let installMode = "approval";
4526
5021
  if (hasE2EKey) {
4527
- const modeChoice = await le({
4528
- message: "Choose your security mode:",
4529
- options: [
4530
- {
4531
- value: "approval",
4532
- label: "Approval Mode (recommended)",
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(getAgentApproveDir(), "env");
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
- filesToModify.push(agent.configPath);
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 [key, value] = line.split("=");
4674
- const displayKey = key.replace("AGENTAPPROVE_", "").toLowerCase();
4675
- const displayValue = key.includes("TOKEN") ? value.slice(0, 15) + "..." : value;
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}: agentapprove plugin`);
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}: openclaw plugin`);
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
- let qrUrl = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
5052
- if (e2eUserKey) {
5053
- const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
5054
- qrUrl += `&e2eKey=${e2eKeyBase64url}`;
5055
- }
5056
- let qrDisplay = "";
5057
- import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
5058
- qrDisplay = qr;
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
- let qrUrlWithE2e = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
5214
- if (e2eUserKey) {
5215
- const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
5216
- qrUrlWithE2e += `&e2eKey=${e2eKeyBase64url}`;
5217
- }
5218
- let qrDisplay = "";
5219
- import_qrcode_terminal.default.generate(qrUrlWithE2e, { small: true }, (qr) => {
5220
- qrDisplay = qr;
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) {