agentapprove 0.1.12 → 0.1.15

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 +850 -296
  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;
@@ -2444,10 +2430,28 @@ function getOpenClawPluginTargets(openclawConfigPath) {
2444
2430
  legacyPath ? createRemovalTarget(legacyPath, "plugin_artifact", true) : null
2445
2431
  ].filter((target) => target !== null);
2446
2432
  }
2447
- function getOpenCodePluginTargets(opencodeConfigDir) {
2433
+ function getOpenCodePluginTargets(opencodeConfigDir, opencodeCacheDir) {
2448
2434
  const pluginPath = safeJoinWithinBase(opencodeConfigDir, "node_modules", "@agentapprove", "opencode");
2435
+ const cacheNodeModulesPath = opencodeCacheDir ? safeJoinWithinBase(opencodeCacheDir, "node_modules", "@agentapprove", "opencode") : null;
2436
+ const packageCacheTargets = [];
2437
+ if (opencodeCacheDir) {
2438
+ const scopedPackageCacheDir = safeJoinWithinBase(opencodeCacheDir, "packages", "@agentapprove");
2439
+ if (scopedPackageCacheDir && existsSync(scopedPackageCacheDir)) {
2440
+ for (const entry of readdirSync(scopedPackageCacheDir)) {
2441
+ if (!entry.startsWith("opencode@")) {
2442
+ continue;
2443
+ }
2444
+ const packageCachePath = safeJoinWithinBase(scopedPackageCacheDir, entry);
2445
+ if (packageCachePath) {
2446
+ packageCacheTargets.push(createRemovalTarget(packageCachePath, "plugin_artifact", true));
2447
+ }
2448
+ }
2449
+ }
2450
+ }
2449
2451
  return [
2450
- pluginPath ? createRemovalTarget(pluginPath, "plugin_artifact", true) : null
2452
+ pluginPath ? createRemovalTarget(pluginPath, "plugin_artifact", true) : null,
2453
+ cacheNodeModulesPath ? createRemovalTarget(cacheNodeModulesPath, "plugin_artifact", true) : null,
2454
+ ...packageCacheTargets
2451
2455
  ].filter((target) => target !== null);
2452
2456
  }
2453
2457
  function collectBackupTargets(configPaths) {
@@ -2504,8 +2508,130 @@ function shouldDeleteCryptoMaterial(purgeConfirmed, firstKeyConfirmation, second
2504
2508
  return purgeConfirmed && firstKeyConfirmation && secondKeyConfirmation;
2505
2509
  }
2506
2510
 
2511
+ // src/pairing-artifact.ts
2512
+ var PAIRING_ARTIFACT_PREFIX = "AAP1.";
2513
+ function encodePairingArtifact(artifact) {
2514
+ const payload = {
2515
+ s: artifact.sessionCode,
2516
+ p: artifact.config.privacyTier,
2517
+ r: artifact.config.retentionDays,
2518
+ f: artifact.config.failBehavior,
2519
+ c: artifact.config.configSetAt,
2520
+ e: artifact.config.e2eEnabled ? 1 : 0
2521
+ };
2522
+ if (artifact.hostname) {
2523
+ payload.h = artifact.hostname;
2524
+ }
2525
+ if (artifact.e2eUserKey) {
2526
+ payload.k = Buffer.from(artifact.e2eUserKey, "hex").toString("base64url");
2527
+ }
2528
+ return `${PAIRING_ARTIFACT_PREFIX}${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
2529
+ }
2530
+ function buildPairingUrl(pairingArtifact) {
2531
+ return `agentapprove://pair?code=${encodeURIComponent(pairingArtifact)}`;
2532
+ }
2533
+
2534
+ // src/install-flow.ts
2535
+ var VALID_RETENTION_DAYS = [1, 7, 30, 90, 365];
2536
+ function normalizePrivacy(value) {
2537
+ if (value === "minimal" || value === "summary" || value === "full") {
2538
+ return value;
2539
+ }
2540
+ return "full";
2541
+ }
2542
+ function normalizeRetentionDays(value) {
2543
+ if (value && VALID_RETENTION_DAYS.includes(value)) {
2544
+ return value;
2545
+ }
2546
+ return 30;
2547
+ }
2548
+ function normalizeFailBehavior(value) {
2549
+ if (value === "allow" || value === "deny" || value === "ask") {
2550
+ return value;
2551
+ }
2552
+ return "ask";
2553
+ }
2554
+ function normalizeInstallMode(value) {
2555
+ if (value === "observe" || value === "approval") {
2556
+ return value;
2557
+ }
2558
+ return "approval";
2559
+ }
2560
+ function getSetupProfileOptions(hasExistingConfig) {
2561
+ if (hasExistingConfig) {
2562
+ return [
2563
+ {
2564
+ value: "existing-config",
2565
+ label: "Existing config",
2566
+ hint: "Reuse the current local settings already saved on this computer."
2567
+ },
2568
+ {
2569
+ value: "customize",
2570
+ label: "Customize options",
2571
+ hint: "Choose your own mode, fallback behavior, privacy, E2E, retention, and debug logging before pairing."
2572
+ }
2573
+ ];
2574
+ }
2575
+ return [
2576
+ {
2577
+ value: "recommended",
2578
+ label: "Recommended setup",
2579
+ hint: "Use Approval mode, E2E on, fallback Ask, Full privacy, 30-day retention, and debug logging off."
2580
+ },
2581
+ {
2582
+ value: "customize",
2583
+ label: "Customize options",
2584
+ hint: "Choose your own mode, fallback behavior, privacy, E2E, retention, and debug logging before pairing."
2585
+ }
2586
+ ];
2587
+ }
2588
+ function getRecommendedInstallConfig() {
2589
+ return {
2590
+ privacy: "full",
2591
+ retentionDays: 30,
2592
+ failBehavior: "ask",
2593
+ debugLog: false,
2594
+ installMode: "approval",
2595
+ e2eEnabled: true
2596
+ };
2597
+ }
2598
+ function getExistingInstallConfig(existingConfig) {
2599
+ return {
2600
+ privacy: normalizePrivacy(existingConfig?.privacy),
2601
+ retentionDays: normalizeRetentionDays(existingConfig?.retentionDays),
2602
+ failBehavior: normalizeFailBehavior(existingConfig?.failBehavior),
2603
+ debugLog: existingConfig?.debugLog ?? false,
2604
+ installMode: normalizeInstallMode(existingConfig?.e2eMode),
2605
+ e2eEnabled: existingConfig?.e2eEnabled !== false
2606
+ };
2607
+ }
2608
+ function getInitialInstallConfig(choice, existingConfig) {
2609
+ if (choice === "recommended") {
2610
+ return getRecommendedInstallConfig();
2611
+ }
2612
+ return getExistingInstallConfig(existingConfig);
2613
+ }
2614
+ function getExistingKeyActionOptions(e2eRequired) {
2615
+ const options = [
2616
+ { value: "reuse", label: "Reuse existing key", hint: "recommended - keeps old events decryptable" },
2617
+ { value: "backup", label: "Back up old key, then generate new" },
2618
+ { value: "discard", label: "Discard old key and generate new" }
2619
+ ];
2620
+ if (!e2eRequired) {
2621
+ options.push({
2622
+ value: "disable",
2623
+ label: "Disable E2E encryption",
2624
+ hint: "events visible on web dashboard"
2625
+ });
2626
+ }
2627
+ return options;
2628
+ }
2629
+ function shouldCreateFreshPairing(connectionMethod) {
2630
+ return connectionMethod === "qr" || connectionMethod === "copy";
2631
+ }
2632
+
2507
2633
  // src/cli.ts
2508
- var VERSION = "0.1.12";
2634
+ var VERSION = "0.1.15";
2509
2635
  function getApiUrl() {
2510
2636
  return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
2511
2637
  }
@@ -2518,6 +2644,12 @@ function getOpenCodeConfigDir() {
2518
2644
  function getOpenCodeConfigPath() {
2519
2645
  return join(getOpenCodeConfigDir(), "opencode.json");
2520
2646
  }
2647
+ function getXdgCacheHome() {
2648
+ return process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
2649
+ }
2650
+ function getOpenCodeCacheDir() {
2651
+ return join(getXdgCacheHome(), "opencode");
2652
+ }
2521
2653
  var API_URL = getApiUrl();
2522
2654
  var API_VERSION = process.env.AGENTAPPROVE_API_VERSION || "v001";
2523
2655
  function hasFlag2(flag) {
@@ -2525,18 +2657,36 @@ function hasFlag2(flag) {
2525
2657
  }
2526
2658
  function updateEnvValue(key, value) {
2527
2659
  const envPath = join(getAgentApproveDir(), "env");
2528
- if (!existsSync2(envPath))
2529
- return;
2530
- let content = readFileSync(envPath, "utf-8");
2531
- const pattern = new RegExp(`^${key}=.*$`, "m");
2532
- if (pattern.test(content)) {
2533
- content = content.replace(pattern, `${key}=${value}`);
2534
- } else {
2535
- content = content.trimEnd() + `
2536
- ${key}=${value}
2537
- `;
2660
+ let content;
2661
+ try {
2662
+ content = readFileSync(envPath, "utf-8");
2663
+ } catch (err) {
2664
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
2665
+ return;
2666
+ }
2667
+ throw err;
2538
2668
  }
2539
- writeFileSync(envPath, content, { mode: 384 });
2669
+ const lines = content.endsWith(`
2670
+ `) ? content.slice(0, -1).split(`
2671
+ `) : content.split(`
2672
+ `);
2673
+ let seenKey = false;
2674
+ const updatedLines = lines.flatMap((line) => {
2675
+ const trimmed = line.replace(/^export\s+/, "").trim();
2676
+ const eqIdx = trimmed.indexOf("=");
2677
+ if (eqIdx <= 0 || trimmed.slice(0, eqIdx) !== key)
2678
+ return [line];
2679
+ if (seenKey)
2680
+ return [];
2681
+ seenKey = true;
2682
+ return [`${key}=${value}`];
2683
+ });
2684
+ if (!seenKey) {
2685
+ updatedLines.push(`${key}=${value}`);
2686
+ }
2687
+ writeFileSync(envPath, `${updatedLines.join(`
2688
+ `)}
2689
+ `, { mode: 384 });
2540
2690
  }
2541
2691
  function getCommand() {
2542
2692
  const args = process.argv.slice(2);
@@ -2553,9 +2703,13 @@ function getCommand() {
2553
2703
  }
2554
2704
  return filtered[0] || "install";
2555
2705
  }
2556
- var OPENCODE_PLUGIN_VERSION = "0.1.8";
2557
- var OPENCLAW_PLUGIN_VERSION = "0.2.7";
2706
+ var OPENCODE_PLUGIN_VERSION = "0.1.12";
2707
+ var OPENCODE_PLUGIN_SPEC = `@agentapprove/opencode@${OPENCODE_PLUGIN_VERSION}`;
2708
+ var OPENCLAW_PLUGIN_VERSION = "0.2.10";
2558
2709
  var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
2710
+ var PI_PLUGIN_VERSION = "0.1.2";
2711
+ var PI_PLUGIN_SPEC = `npm:@agentapprove/pi@${PI_PLUGIN_VERSION}`;
2712
+ var PI_STATUS_TIMEOUT_MS = 5000;
2559
2713
  var AGENTS = {
2560
2714
  "claude-code": {
2561
2715
  name: "Claude Code",
@@ -2665,6 +2819,14 @@ var AGENTS = {
2665
2819
  hooks: [
2666
2820
  { name: "agentapprove", file: "@agentapprove/opencode", description: "Tool approval + event monitoring", isApprovalHook: true, isPlugin: true }
2667
2821
  ]
2822
+ },
2823
+ pi: {
2824
+ name: "Pi",
2825
+ configPath: join(homedir(), ".pi", "agent", "settings.json"),
2826
+ hooksKey: "packages",
2827
+ hooks: [
2828
+ { name: "agentapprove", file: "@agentapprove/pi", description: "Tool approval + event monitoring", isApprovalHook: true, isPlugin: true }
2829
+ ]
2668
2830
  }
2669
2831
  };
2670
2832
  var SHARED_HOOK_FILES = ["common.sh"];
@@ -2902,6 +3064,15 @@ function detectInstalledAgents() {
2902
3064
  if (existsSync2(getOpenCodeConfigDir())) {
2903
3065
  installed.push(id);
2904
3066
  }
3067
+ } else if (id === "pi") {
3068
+ if (existsSync2(join(homedir(), ".pi", "agent"))) {
3069
+ installed.push(id);
3070
+ } else {
3071
+ try {
3072
+ execSync("pi --version", { stdio: "ignore" });
3073
+ installed.push(id);
3074
+ } catch {}
3075
+ }
2905
3076
  } else {
2906
3077
  const configDir = dirname2(agent.configPath);
2907
3078
  if (existsSync2(configDir)) {
@@ -2991,6 +3162,68 @@ function formatStatusValue(key, value) {
2991
3162
  return value;
2992
3163
  }
2993
3164
  }
3165
+ function formatSetupValue(value) {
3166
+ return value.charAt(0).toUpperCase() + value.slice(1);
3167
+ }
3168
+ function describePrivacy(value) {
3169
+ switch (value) {
3170
+ case "minimal":
3171
+ return "Tool name only is stored in event history.";
3172
+ case "summary":
3173
+ return "Command details are truncated to 50 characters in event history.";
3174
+ case "full":
3175
+ default:
3176
+ return "Complete command details are stored in event history.";
3177
+ }
3178
+ }
3179
+ function describeRetentionDays(days) {
3180
+ return `Delete events older than ${days} ${days === 1 ? "day" : "days"}.`;
3181
+ }
3182
+ function describeFailBehavior(value) {
3183
+ switch (value) {
3184
+ case "allow":
3185
+ return "Allow commands to proceed without approval.";
3186
+ case "deny":
3187
+ return "Block all commands until the service is restored.";
3188
+ case "ask":
3189
+ default:
3190
+ return "Fall back to the agent's built-in approval dialog.";
3191
+ }
3192
+ }
3193
+ function describeInstallMode(value) {
3194
+ switch (value) {
3195
+ case "observe":
3196
+ return "Agent runs freely and all events are end-to-end encrypted.";
3197
+ case "approval":
3198
+ default:
3199
+ return "Commands are sent via an encrypted channel for policy evaluation.";
3200
+ }
3201
+ }
3202
+ function describeE2EEnabled(enabled) {
3203
+ if (enabled) {
3204
+ return "Event content is encrypted between this computer and your devices.";
3205
+ }
3206
+ return "Event content can be viewed on the web dashboard.";
3207
+ }
3208
+ function describeDebugLogging(enabled) {
3209
+ if (enabled) {
3210
+ return "Write hook logs to ~/.agentapprove/hook-debug.log.";
3211
+ }
3212
+ return "Do not write hook debug logs unless you turn them on later.";
3213
+ }
3214
+ function formatSetupProfileBlock(title, config, extraLines = []) {
3215
+ return [
3216
+ title,
3217
+ ...extraLines.map((line) => ` ${line}`),
3218
+ ` Security mode: ${formatSetupValue(config.installMode)} - ${describeInstallMode(config.installMode)}`,
3219
+ ` If Agent Approve is unreachable: ${formatSetupValue(config.failBehavior)} - ${describeFailBehavior(config.failBehavior)}`,
3220
+ ` Privacy tier: ${formatSetupValue(config.privacy)} - ${describePrivacy(config.privacy)}`,
3221
+ ` End-to-end encryption: ${config.e2eEnabled ? "Enabled" : "Disabled"} - ${describeE2EEnabled(config.e2eEnabled)}`,
3222
+ ` Data retention: ${config.retentionDays} days - ${describeRetentionDays(config.retentionDays)}`,
3223
+ ` Debug logging: ${config.debugLog ? "Enabled" : "Disabled"} - ${describeDebugLogging(config.debugLog)}`
3224
+ ].join(`
3225
+ `);
3226
+ }
2994
3227
  function formatHookStatusLabel(name) {
2995
3228
  const explicitLabel = HOOK_STATUS_LABELS[name];
2996
3229
  if (explicitLabel) {
@@ -3018,7 +3251,7 @@ function buildUninstallPlan() {
3018
3251
  const configuredAgents = detectInstalledAgents();
3019
3252
  const pluginArtifactTargets = uniqueRemovalTargets([
3020
3253
  ...getOpenClawPluginTargets(AGENTS.openclaw.configPath),
3021
- ...getOpenCodePluginTargets(getOpenCodeConfigDir())
3254
+ ...getOpenCodePluginTargets(getOpenCodeConfigDir(), getOpenCodeCacheDir())
3022
3255
  ]);
3023
3256
  return {
3024
3257
  configuredAgents,
@@ -3145,6 +3378,13 @@ function readExistingConfig() {
3145
3378
  case "AGENTAPPROVE_RETENTION_DAYS":
3146
3379
  config.retentionDays = value === "forever" ? 365 : parseInt(value, 10);
3147
3380
  break;
3381
+ case "AGENTAPPROVE_CONFIG_SET_AT": {
3382
+ const parsed = parseInt(value, 10);
3383
+ if (Number.isFinite(parsed) && parsed > 0) {
3384
+ config.configSetAt = parsed;
3385
+ }
3386
+ break;
3387
+ }
3148
3388
  case "AGENTAPPROVE_DEBUG_LOG":
3149
3389
  config.debugLog = value === "true";
3150
3390
  break;
@@ -3433,6 +3673,101 @@ async function waitForPairing(sessionCode, onProgress, onCancel) {
3433
3673
  cleanup();
3434
3674
  return null;
3435
3675
  }
3676
+ function buildPairingPresentation(params) {
3677
+ const pairingArtifact = encodePairingArtifact({
3678
+ sessionCode: params.sessionCode,
3679
+ hostname: params.hostname,
3680
+ config: params.config,
3681
+ e2eUserKey: params.e2eUserKey ?? undefined
3682
+ });
3683
+ return {
3684
+ pairingArtifact,
3685
+ pairingUrl: buildPairingUrl(pairingArtifact),
3686
+ manualCode: pairingArtifact
3687
+ };
3688
+ }
3689
+ function copyToClipboard(text) {
3690
+ let cmd;
3691
+ let args = [];
3692
+ switch (process.platform) {
3693
+ case "darwin":
3694
+ cmd = "pbcopy";
3695
+ break;
3696
+ case "win32":
3697
+ cmd = "clip";
3698
+ break;
3699
+ default:
3700
+ for (const candidate of [
3701
+ { cmd: "wl-copy", args: [] },
3702
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
3703
+ { cmd: "xsel", args: ["--clipboard", "--input"] }
3704
+ ]) {
3705
+ const result2 = spawnSync(candidate.cmd, candidate.args, {
3706
+ input: text,
3707
+ stdio: ["pipe", "ignore", "ignore"]
3708
+ });
3709
+ if (result2.status === 0 && !result2.error)
3710
+ return true;
3711
+ }
3712
+ return false;
3713
+ }
3714
+ const result = spawnSync(cmd, args, {
3715
+ input: text,
3716
+ stdio: ["pipe", "ignore", "ignore"]
3717
+ });
3718
+ return result.status === 0 && !result.error;
3719
+ }
3720
+ async function promptPairingMethod() {
3721
+ const choice = await le({
3722
+ message: "How will you connect to the iOS app?",
3723
+ options: [
3724
+ { value: "qr", label: "Scan QR code", hint: "Recommended" },
3725
+ { value: "copy", label: "Copy and paste code", hint: "No camera" }
3726
+ ],
3727
+ initialValue: "qr"
3728
+ });
3729
+ if (lD(choice)) {
3730
+ he("Cancelled");
3731
+ process.exit(0);
3732
+ }
3733
+ return choice;
3734
+ }
3735
+ async function showPairingPresentation(params) {
3736
+ const detailLines = [`Session: ${params.sessionCode}`];
3737
+ if (params.keyId) {
3738
+ detailLines.push(`Key ID: ${params.keyId}`);
3739
+ }
3740
+ if (params.method === "qr") {
3741
+ let qrDisplay = "";
3742
+ import_qrcode_terminal.default.generate(params.pairingUrl, { small: true }, (qr) => {
3743
+ qrDisplay = qr;
3744
+ });
3745
+ await new Promise((resolve) => setTimeout(resolve, 10));
3746
+ process.stdout.write(`
3747
+ `);
3748
+ process.stdout.write(` Scan with the Agent Approve iOS app:
3749
+
3750
+ `);
3751
+ process.stdout.write(qrDisplay);
3752
+ process.stdout.write(`
3753
+ `);
3754
+ me(detailLines.join(`
3755
+ `), "Pairing details");
3756
+ return;
3757
+ }
3758
+ me(detailLines.join(`
3759
+ `), "Pairing details");
3760
+ const copied = copyToClipboard(params.manualCode);
3761
+ if (copied) {
3762
+ v2.success("Paste code copied to clipboard. In the iOS app, choose the option to enter a code manually and paste.");
3763
+ } else {
3764
+ v2.warn("Could not access the system clipboard. Copy the code below manually instead.");
3765
+ }
3766
+ process.stdout.write(`
3767
+ ` + params.manualCode + `
3768
+
3769
+ `);
3770
+ }
3436
3771
  function saveEnvConfig(config) {
3437
3772
  const envPath = join(getAgentApproveDir(), "env");
3438
3773
  const configSetAt = config.configSetAt || Math.floor(Date.now() / 1000);
@@ -3489,7 +3824,7 @@ function isCredentialManagerAvailable() {
3489
3824
  return false;
3490
3825
  }
3491
3826
  }
3492
- async function storeTokenInKeychain(token) {
3827
+ async function storeTokenInKeychain(token, options) {
3493
3828
  if (isKeychainAvailable()) {
3494
3829
  try {
3495
3830
  spawnSync("security", [
@@ -3512,7 +3847,9 @@ async function storeTokenInKeychain(token) {
3512
3847
  if (addResult.status !== 0 || addResult.error) {
3513
3848
  throw addResult.error || new Error("security add-generic-password failed");
3514
3849
  }
3515
- v2.success("Token stored in macOS Keychain");
3850
+ if (!options?.quiet) {
3851
+ v2.success("Token stored in macOS Keychain");
3852
+ }
3516
3853
  return true;
3517
3854
  } catch (err) {
3518
3855
  v2.warn("Could not store token in Keychain (file fallback will be used)");
@@ -3530,7 +3867,9 @@ async function storeTokenInKeychain(token) {
3530
3867
  if (addResult.status !== 0 || addResult.error) {
3531
3868
  throw addResult.error || new Error("cmdkey add credential failed");
3532
3869
  }
3533
- v2.success("Token stored in Windows Credential Manager");
3870
+ if (!options?.quiet) {
3871
+ v2.success("Token stored in Windows Credential Manager");
3872
+ }
3534
3873
  return true;
3535
3874
  } catch (err) {
3536
3875
  v2.warn("Could not store token in Credential Manager (file fallback will be used)");
@@ -3560,31 +3899,18 @@ function deleteTokenFromKeychain() {
3560
3899
  }
3561
3900
  return false;
3562
3901
  }
3563
- async function pushConfigToCloud(config) {
3564
- try {
3565
- const response = await fetch(`${config.apiUrl}/${API_VERSION}/config`, {
3566
- method: "PUT",
3567
- headers: {
3568
- "Content-Type": "application/json",
3569
- Authorization: `Bearer ${config.token}`
3570
- },
3571
- body: JSON.stringify({
3572
- privacyTier: config.privacy,
3573
- retentionDays: config.retentionDays,
3574
- failBehavior: config.failBehavior,
3575
- configSetAt: config.configSetAt
3576
- })
3577
- });
3578
- return response.ok;
3579
- } catch (error) {
3580
- return false;
3581
- }
3582
- }
3583
3902
  async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3584
3903
  const agent = AGENTS[agentId];
3585
3904
  if (!agent) {
3586
3905
  return { success: false, backupPath: null, hooks: [] };
3587
3906
  }
3907
+ if (agentId === "pi") {
3908
+ const installResult = installPiPluginViaCli();
3909
+ if (!installResult.success) {
3910
+ return { success: false, backupPath: null, hooks: [], error: installResult.error };
3911
+ }
3912
+ return { success: true, backupPath: null, hooks: [installResult.label] };
3913
+ }
3588
3914
  const backupPath = backupConfig(agent.configPath);
3589
3915
  const config = readJsonConfig(agent.configPath);
3590
3916
  if (!config[agent.hooksKey]) {
@@ -3628,32 +3954,41 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3628
3954
  return { success: true, backupPath, hooks: installedHooks };
3629
3955
  }
3630
3956
  if (agentId === "opencode") {
3631
- if (!Array.isArray(config.plugin)) {
3632
- config.plugin = [];
3957
+ const installResult = installOpenCodePluginViaCli();
3958
+ if (!installResult.success) {
3959
+ return { success: false, backupPath, hooks: [], error: installResult.error };
3633
3960
  }
3634
- const pluginArray = config.plugin;
3635
- if (!pluginArray.includes("@agentapprove/opencode")) {
3636
- pluginArray.push("@agentapprove/opencode");
3961
+ const opencodeConfig = readJsonConfig(agent.configPath);
3962
+ if (!Array.isArray(opencodeConfig.plugin)) {
3963
+ opencodeConfig.plugin = [];
3637
3964
  }
3638
- writeJsonConfig(agent.configPath, config);
3965
+ const pluginArray = opencodeConfig.plugin.filter((entry) => typeof entry === "string").filter((entry) => entry !== "@agentapprove/opencode" && !entry.startsWith("@agentapprove/opencode@"));
3966
+ if (!pluginArray.includes(OPENCODE_PLUGIN_SPEC)) {
3967
+ pluginArray.push(OPENCODE_PLUGIN_SPEC);
3968
+ }
3969
+ opencodeConfig.plugin = pluginArray;
3970
+ writeJsonConfig(agent.configPath, opencodeConfig);
3639
3971
  const opencodePkgPath = join(getOpenCodeConfigDir(), "package.json");
3640
3972
  try {
3641
- let pkgJson = {};
3642
- if (existsSync2(opencodePkgPath)) {
3643
- pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
3644
- }
3645
- if (!pkgJson.dependencies) {
3646
- pkgJson.dependencies = {};
3647
- }
3648
- pkgJson.dependencies["@agentapprove/opencode"] = OPENCODE_PLUGIN_VERSION;
3649
- writeFileSync(opencodePkgPath, JSON.stringify(pkgJson, null, 2) + `
3973
+ const pkgJson = JSON.parse(readFileSync(opencodePkgPath, "utf-8"));
3974
+ const deps = pkgJson.dependencies;
3975
+ if (deps?.["@agentapprove/opencode"]) {
3976
+ delete deps["@agentapprove/opencode"];
3977
+ if (Object.keys(deps).length === 0) {
3978
+ delete pkgJson.dependencies;
3979
+ }
3980
+ writeFileSync(opencodePkgPath, JSON.stringify(pkgJson, null, 2) + `
3650
3981
  `);
3982
+ }
3651
3983
  } catch (err) {
3984
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
3985
+ return { success: true, backupPath, hooks: [...installedHooks, installResult.label] };
3986
+ }
3652
3987
  if (err instanceof Error) {
3653
3988
  console.warn(`Warning: Could not update package.json: ${err.message}`);
3654
3989
  }
3655
3990
  }
3656
- installedHooks.push("Agent Approve plugin");
3991
+ installedHooks.push(installResult.label);
3657
3992
  return { success: true, backupPath, hooks: installedHooks };
3658
3993
  }
3659
3994
  const hooksToInstall = mode === "observe" ? agent.hooks.filter((h2) => !h2.isApprovalHook) : agent.hooks;
@@ -4062,19 +4397,32 @@ codex_hooks = true`;
4062
4397
  `);
4063
4398
  } else if (agentId === "opencode") {
4064
4399
  const configJson = JSON.stringify({
4065
- plugin: ["@agentapprove/opencode"]
4400
+ plugin: [OPENCODE_PLUGIN_SPEC]
4066
4401
  }, null, 2);
4067
4402
  const opencodeConfigPath = process.env.XDG_CONFIG_HOME ? `${process.env.XDG_CONFIG_HOME}/opencode/opencode.json` : "~/.config/opencode/opencode.json";
4068
4403
  return [
4069
- `Add the Agent Approve plugin to your OpenCode config (${opencodeConfigPath}):`,
4404
+ "Install the Agent Approve plugin for OpenCode:",
4405
+ "",
4406
+ ` opencode plugin ${OPENCODE_PLUGIN_SPEC} --global --force`,
4407
+ "",
4408
+ `This command installs the npm plugin and updates your global OpenCode config (${opencodeConfigPath}).`,
4409
+ "",
4410
+ "If you need to edit the config manually, ensure it contains:",
4070
4411
  "",
4071
4412
  configJson,
4072
4413
  "",
4073
- "Then add the dependency to your OpenCode package.json (same directory):",
4414
+ "Restart OpenCode to activate the plugin."
4415
+ ].join(`
4416
+ `);
4417
+ } else if (agentId === "pi") {
4418
+ return [
4419
+ "Install the Agent Approve extension for Pi:",
4420
+ "",
4421
+ ` pi install ${PI_PLUGIN_SPEC}`,
4074
4422
  "",
4075
- ` { "dependencies": { "@agentapprove/opencode": "${OPENCODE_PLUGIN_VERSION}" } }`,
4423
+ "The extension reads your existing Agent Approve config from ~/.agentapprove/env.",
4076
4424
  "",
4077
- "OpenCode will auto-install the plugin on next start."
4425
+ "Restart Pi to activate the extension."
4078
4426
  ].join(`
4079
4427
  `);
4080
4428
  }
@@ -4149,6 +4497,112 @@ function installOpenClawPluginViaCli() {
4149
4497
  return { success: false, error: message, label: "Agent Approve plugin" };
4150
4498
  }
4151
4499
  }
4500
+ function readJsonPackageVersion(packagePath) {
4501
+ if (!existsSync2(packagePath)) {
4502
+ return null;
4503
+ }
4504
+ try {
4505
+ const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
4506
+ return typeof pkg.version === "string" ? pkg.version : null;
4507
+ } catch {
4508
+ return null;
4509
+ }
4510
+ }
4511
+ function readOpenCodeInstalledVersion() {
4512
+ const cacheDir = getOpenCodeCacheDir();
4513
+ const candidates = [
4514
+ join(cacheDir, "packages", "@agentapprove", `opencode@${OPENCODE_PLUGIN_VERSION}`, "node_modules", "@agentapprove", "opencode", "package.json"),
4515
+ join(cacheDir, "packages", "@agentapprove", "opencode@latest", "node_modules", "@agentapprove", "opencode", "package.json"),
4516
+ join(cacheDir, "node_modules", "@agentapprove", "opencode", "package.json"),
4517
+ join(getOpenCodeConfigDir(), "node_modules", "@agentapprove", "opencode", "package.json")
4518
+ ];
4519
+ for (const candidate of candidates) {
4520
+ const version = readJsonPackageVersion(candidate);
4521
+ if (version === OPENCODE_PLUGIN_VERSION) {
4522
+ return version;
4523
+ }
4524
+ }
4525
+ for (const candidate of candidates) {
4526
+ const version = readJsonPackageVersion(candidate);
4527
+ if (version) {
4528
+ return version;
4529
+ }
4530
+ }
4531
+ return null;
4532
+ }
4533
+ function installOpenCodePluginViaCli() {
4534
+ try {
4535
+ const result = spawnSync("opencode", ["plugin", OPENCODE_PLUGIN_SPEC, "--global", "--force"], {
4536
+ encoding: "utf8",
4537
+ stdio: ["ignore", "pipe", "pipe"]
4538
+ });
4539
+ if (result.error) {
4540
+ return {
4541
+ success: false,
4542
+ label: "Agent Approve plugin",
4543
+ error: `Could not run opencode plugin installer: ${result.error.message}`
4544
+ };
4545
+ }
4546
+ if (result.status !== 0) {
4547
+ const detail = [result.stdout, result.stderr].filter(Boolean).join(`
4548
+ `).trim();
4549
+ return {
4550
+ success: false,
4551
+ label: "Agent Approve plugin",
4552
+ error: detail || `opencode plugin installer exited with code ${result.status}`
4553
+ };
4554
+ }
4555
+ const installedVersion = readOpenCodeInstalledVersion();
4556
+ if (!installedVersion) {
4557
+ return {
4558
+ success: false,
4559
+ label: "Agent Approve plugin",
4560
+ error: `OpenCode installed ${OPENCODE_PLUGIN_SPEC}, but the installer could not verify the package version. Re-run manually with: opencode plugin ${OPENCODE_PLUGIN_SPEC} --global --force`
4561
+ };
4562
+ }
4563
+ if (installedVersion !== OPENCODE_PLUGIN_VERSION) {
4564
+ return {
4565
+ success: false,
4566
+ label: "Agent Approve plugin",
4567
+ error: `OpenCode installed ${installedVersion}, expected ${OPENCODE_PLUGIN_VERSION}. Re-run after the npm registry exposes the latest package.`
4568
+ };
4569
+ }
4570
+ return {
4571
+ success: true,
4572
+ version: installedVersion,
4573
+ label: `Agent Approve plugin v${installedVersion}`
4574
+ };
4575
+ } catch (err) {
4576
+ return {
4577
+ success: false,
4578
+ label: "Agent Approve plugin",
4579
+ error: err instanceof Error ? err.message : String(err)
4580
+ };
4581
+ }
4582
+ }
4583
+ function installPiPluginViaCli() {
4584
+ try {
4585
+ execSync(`pi install ${PI_PLUGIN_SPEC}`, { stdio: "pipe" });
4586
+ return { success: true, label: `Agent Approve extension ${PI_PLUGIN_VERSION}` };
4587
+ } catch (err) {
4588
+ return {
4589
+ success: false,
4590
+ label: "Agent Approve extension",
4591
+ error: err instanceof Error ? err.message : String(err)
4592
+ };
4593
+ }
4594
+ }
4595
+ function removePiPluginViaCli() {
4596
+ try {
4597
+ execSync("pi remove npm:@agentapprove/pi", { stdio: "pipe" });
4598
+ return { success: true };
4599
+ } catch (err) {
4600
+ return {
4601
+ success: false,
4602
+ error: err instanceof Error ? err.message : String(err)
4603
+ };
4604
+ }
4605
+ }
4152
4606
  var SYSTEM_DEPS = [
4153
4607
  {
4154
4608
  name: "curl",
@@ -4367,136 +4821,148 @@ async function installCommand() {
4367
4821
  await checkSystemDependencies();
4368
4822
  const existingConfig = readExistingConfig();
4369
4823
  const hasExistingToken = !!(existingConfig?.token && existingConfig.token.length > 10);
4824
+ let existingTokenPreview = "Not set";
4825
+ let existingKeyId = null;
4370
4826
  if (existingConfig) {
4371
- const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
4827
+ existingTokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "Not set";
4372
4828
  const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
4373
- let e2eLine = "";
4374
4829
  if (existsSync2(e2eKeyPath2)) {
4375
4830
  const keyHex = readFileSync(e2eKeyPath2, "utf-8").trim();
4376
- const keyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
4377
- e2eLine = `
4378
- E2E Key: ${keyId}`;
4831
+ existingKeyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
4379
4832
  }
4380
- me(`Token: ${tokenPreview}
4381
- Privacy: ${existingConfig.privacy || "unknown"}${e2eLine}`, "Existing configuration found");
4382
- } else {
4383
- me(`Approve AI agent actions from your iPhone or Apple Watch.
4384
- Installs hooks and plugins for OpenClaw, OpenAI Codex, Claude Code, Cursor, Gemini CLI, and more.`, "About");
4385
4833
  }
4834
+ me(`Approve AI agent actions from your iPhone or Apple Watch.
4835
+ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw, and more.`, "About");
4386
4836
  const installedAgents = detectInstalledAgents();
4387
4837
  const agentOptions = Object.entries(AGENTS).map(([id, agent]) => ({
4388
4838
  value: id,
4389
4839
  label: agent.name,
4390
4840
  hint: installedAgents.includes(id) ? "Detected" : "Not found"
4391
4841
  }));
4392
- const defaultPrivacy = existingConfig?.privacy || "full";
4393
- const defaultDebugLog = existingConfig?.debugLog ?? false;
4394
- const defaultFailBehavior = existingConfig?.failBehavior || "ask";
4395
- const setup = await ve({
4396
- agents: () => $e({
4397
- message: "Select agents to configure",
4398
- options: agentOptions,
4399
- initialValues: installedAgents,
4400
- required: true
4401
- }),
4402
- privacy: () => le({
4403
- message: "Privacy tier - controls what data is stored in event logs",
4404
- initialValue: defaultPrivacy,
4405
- options: [
4406
- { value: "full", label: "Full", hint: "Complete command details stored in logs" },
4407
- { value: "summary", label: "Summary", hint: "Truncated to 50 chars in logs" },
4408
- { value: "minimal", label: "Minimal", hint: "Tool name only in logs, most private" }
4409
- ]
4410
- }),
4411
- retention: () => le({
4412
- message: "Data retention - how long to keep event history",
4413
- initialValue: "30",
4842
+ const selectedAgents = await $e({
4843
+ message: "Select agents to configure",
4844
+ options: agentOptions,
4845
+ initialValues: installedAgents,
4846
+ required: true
4847
+ });
4848
+ if (lD(selectedAgents)) {
4849
+ he("Installation cancelled");
4850
+ process.exit(0);
4851
+ }
4852
+ const setupProfileSummary = existingConfig ? formatSetupProfileBlock("Existing config", getInitialInstallConfig("existing-config", existingConfig), [
4853
+ `Connection token: ${existingTokenPreview} - already linked to your Agent Approve account.`,
4854
+ ...existingKeyId ? [`Encryption key ID: ${existingKeyId} - already installed for this computer.`] : []
4855
+ ]) : formatSetupProfileBlock("Recommended setup", getInitialInstallConfig("recommended", existingConfig));
4856
+ me(setupProfileSummary, "Setup profiles");
4857
+ const setupProfile = await le({
4858
+ message: "How would you like to set up Agent Approve?",
4859
+ options: getSetupProfileOptions(Boolean(existingConfig))
4860
+ });
4861
+ if (lD(setupProfile)) {
4862
+ he("Installation cancelled");
4863
+ process.exit(0);
4864
+ }
4865
+ ensureAgentApproveDir();
4866
+ const hooksDir = join(getAgentApproveDir(), "hooks");
4867
+ const selectedInstallConfig = getInitialInstallConfig(setupProfile, existingConfig);
4868
+ let token = null;
4869
+ let finalPrivacy = selectedInstallConfig.privacy;
4870
+ let email = "";
4871
+ let apiUrl = API_URL;
4872
+ let debugLog = selectedInstallConfig.debugLog;
4873
+ let retentionDays = selectedInstallConfig.retentionDays;
4874
+ let failBehavior = selectedInstallConfig.failBehavior;
4875
+ let installMode = selectedInstallConfig.installMode;
4876
+ let useE2E = selectedInstallConfig.e2eEnabled;
4877
+ let configSetAt = setupProfile === "existing-config" && existingConfig?.configSetAt ? existingConfig.configSetAt : Math.floor(Date.now() / 1000);
4878
+ if (setupProfile === "customize") {
4879
+ const modeChoice = await le({
4880
+ message: "Choose your security mode:",
4881
+ initialValue: installMode,
4414
4882
  options: [
4415
- { value: "365", label: "1 Year", hint: "Delete events older than 1 year" },
4416
- { value: "90", label: "90 Days", hint: "Delete events older than 90 days" },
4417
- { value: "30", label: "30 Days", hint: "Delete events older than 30 days (recommended)" },
4418
- { value: "7", label: "1 Week", hint: "Delete events older than 7 days" },
4419
- { value: "1", label: "1 Day", hint: "Delete events older than 1 day" }
4883
+ {
4884
+ value: "approval",
4885
+ label: "Approval Mode (recommended)",
4886
+ hint: "Agent asks permission before running commands. Server evaluates policies via encrypted channel."
4887
+ },
4888
+ {
4889
+ value: "observe",
4890
+ label: "Observe Mode (full E2E)",
4891
+ hint: "Agent runs freely. All events are end-to-end encrypted. No server policy evaluation."
4892
+ }
4420
4893
  ]
4421
- }),
4422
- failBehavior: () => le({
4894
+ });
4895
+ if (lD(modeChoice)) {
4896
+ he("Installation cancelled");
4897
+ process.exit(0);
4898
+ }
4899
+ installMode = modeChoice;
4900
+ const failBehaviorChoice = await le({
4423
4901
  message: "If Agent Approve is unreachable, hooks should:",
4424
- initialValue: defaultFailBehavior,
4902
+ initialValue: failBehavior,
4425
4903
  options: [
4426
4904
  { value: "ask", label: "Ask", hint: "Fall back to the agent's built-in approval dialog (recommended)" },
4427
4905
  { value: "deny", label: "Deny", hint: "Block all commands until service is restored" },
4428
4906
  { value: "allow", label: "Allow", hint: "Allow all commands to proceed without approval" }
4429
4907
  ]
4430
- }),
4431
- debugLog: () => ce({
4432
- message: "Enable debug logging? (writes to ~/.agentapprove/hook-debug.log)",
4433
- initialValue: defaultDebugLog
4434
- }),
4435
- installMethod: () => le({
4436
- message: "How would you like to install hooks?",
4908
+ });
4909
+ if (lD(failBehaviorChoice)) {
4910
+ he("Installation cancelled");
4911
+ process.exit(0);
4912
+ }
4913
+ failBehavior = failBehaviorChoice;
4914
+ const privacyChoice = await le({
4915
+ message: "Privacy tier - controls what data is stored in event history",
4916
+ initialValue: finalPrivacy,
4437
4917
  options: [
4438
- { value: "auto", label: "Automatic", hint: "Back up configs and install hooks for me" },
4439
- { value: "manual", label: "Manual", hint: "Show me what to add (I'll do it myself)" }
4918
+ { value: "full", label: "Full", hint: "Complete command details stored in event history" },
4919
+ { value: "summary", label: "Summary", hint: "Truncated to 50 chars in event history" },
4920
+ { value: "minimal", label: "Minimal", hint: "Tool name only in event history, most private" }
4440
4921
  ]
4441
- })
4442
- }, {
4443
- onCancel: () => {
4922
+ });
4923
+ if (lD(privacyChoice)) {
4444
4924
  he("Installation cancelled");
4445
4925
  process.exit(0);
4446
4926
  }
4447
- });
4448
- const { agents: selectedAgents, privacy, retention, failBehavior, debugLog, installMethod } = setup;
4449
- const retentionDays = parseInt(retention, 10) || 30;
4927
+ finalPrivacy = privacyChoice;
4928
+ }
4929
+ const skipE2E = hasFlag2("--no-e2e");
4930
+ const e2eRequired = installMode === "observe";
4931
+ if (e2eRequired) {
4932
+ if (skipE2E) {
4933
+ v2.warn("Observe Mode requires E2E encryption; ignoring --no-e2e.");
4934
+ }
4935
+ useE2E = true;
4936
+ } else if (skipE2E) {
4937
+ useE2E = false;
4938
+ }
4450
4939
  ensureAgentApproveDir();
4451
- const hooksDir = join(getAgentApproveDir(), "hooks");
4452
- const connectionOptions = [];
4453
- if (hasExistingToken) {
4454
- const tokenPreview = existingConfig.token.slice(0, 15) + "...";
4455
- connectionOptions.push({
4456
- value: "existing",
4457
- label: "Use existing token",
4458
- hint: tokenPreview
4940
+ const agentApproveDir = getAgentApproveDir();
4941
+ const existingKeyPath = join(agentApproveDir, "e2e-key");
4942
+ let e2eUserKey = null;
4943
+ let backupOldKeyTo = null;
4944
+ if (setupProfile === "customize" && !e2eRequired && !skipE2E) {
4945
+ const e2eChoice = await ce({
4946
+ message: "Enable end-to-end encryption?",
4947
+ initialValue: useE2E
4459
4948
  });
4949
+ if (lD(e2eChoice)) {
4950
+ he("Installation cancelled");
4951
+ process.exit(0);
4952
+ }
4953
+ useE2E = e2eChoice;
4460
4954
  }
4461
- connectionOptions.push({ value: "qr", label: "Scan QR code", hint: hasExistingToken ? undefined : "Recommended" });
4462
- const connectionMethod = await le({
4463
- message: "Connect to iOS app (required for setup and any hook downloads)",
4464
- options: connectionOptions
4465
- });
4466
- if (lD(connectionMethod)) {
4467
- he("Installation cancelled");
4468
- process.exit(0);
4469
- }
4470
- let token = null;
4471
- let finalPrivacy = privacy;
4472
- let email = "";
4473
- let apiUrl = API_URL;
4474
- let useE2E = true;
4475
- if (connectionMethod === "existing") {
4476
- token = existingConfig.token;
4477
- apiUrl = existingConfig.apiUrl || API_URL;
4478
- const e2eKeyExists = existsSync2(join(getAgentApproveDir(), "e2e-key"));
4479
- useE2E = e2eKeyExists;
4480
- v2.success("Using existing token");
4481
- } else if (connectionMethod === "qr") {
4482
- const skipE2E = hasFlag2("--no-e2e");
4483
- useE2E = !skipE2E;
4484
- const agentApproveDir = getAgentApproveDir();
4485
- const existingKeyPath = join(agentApproveDir, "e2e-key");
4486
- let e2eUserKey = null;
4487
- if (useE2E) {
4488
- if (existsSync2(existingKeyPath)) {
4489
- const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
4490
- const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
4955
+ if (useE2E) {
4956
+ if (existsSync2(existingKeyPath)) {
4957
+ const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
4958
+ const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
4959
+ if (setupProfile === "existing-config") {
4960
+ e2eUserKey = oldKeyHex;
4961
+ } else {
4491
4962
  v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
4492
4963
  const keyAction = await le({
4493
4964
  message: "Reuse existing encryption key or generate a new one?",
4494
- options: [
4495
- { value: "reuse", label: "Reuse existing key", hint: "recommended — keeps old events decryptable" },
4496
- { value: "backup", label: "Back up old key, then generate new" },
4497
- { value: "discard", label: "Discard old key and generate new" },
4498
- { value: "disable", label: "Disable E2E encryption", hint: "events visible on web dashboard" }
4499
- ]
4965
+ options: getExistingKeyActionOptions(e2eRequired)
4500
4966
  });
4501
4967
  if (lD(keyAction)) {
4502
4968
  he("Installation cancelled");
@@ -4508,33 +4974,99 @@ Installs hooks and plugins for OpenClaw, OpenAI Codex, Claude Code, Cursor, Gemi
4508
4974
  e2eUserKey = oldKeyHex;
4509
4975
  } else {
4510
4976
  if (keyAction === "backup") {
4511
- const backupPath = join(agentApproveDir, `e2e-key.${oldKeyId}.bak`);
4512
- renameSync(existingKeyPath, backupPath);
4513
- v2.success(`Old key backed up to ${backupPath}`);
4977
+ backupOldKeyTo = join(agentApproveDir, `e2e-key.${oldKeyId}.bak`);
4514
4978
  }
4515
4979
  e2eUserKey = randomBytes(32).toString("hex");
4516
4980
  }
4517
- } else {
4518
- if (!skipE2E) {
4519
- const e2eChoice = await ce({
4520
- message: "Enable end-to-end encryption?",
4521
- initialValue: true
4522
- });
4523
- if (lD(e2eChoice)) {
4524
- he("Installation cancelled");
4525
- process.exit(0);
4526
- }
4527
- useE2E = e2eChoice;
4528
- }
4529
- if (useE2E) {
4530
- e2eUserKey = randomBytes(32).toString("hex");
4531
- }
4532
4981
  }
4982
+ } else {
4983
+ e2eUserKey = randomBytes(32).toString("hex");
4984
+ }
4985
+ }
4986
+ if (!useE2E) {
4987
+ v2.info("E2E encryption disabled — event content will be visible on the web dashboard");
4988
+ }
4989
+ const e2eKeyId = e2eUserKey ? createHash("sha256").update(Buffer.from(e2eUserKey, "hex")).digest("hex").slice(0, 8) : undefined;
4990
+ if (setupProfile === "customize") {
4991
+ const retentionChoice = await le({
4992
+ message: "Data retention - how long to keep event history",
4993
+ initialValue: `${retentionDays}`,
4994
+ options: [
4995
+ { value: "365", label: "1 Year", hint: "Delete events older than 1 year" },
4996
+ { value: "90", label: "90 Days", hint: "Delete events older than 90 days" },
4997
+ { value: "30", label: "30 Days", hint: "Delete events older than 30 days (recommended)" },
4998
+ { value: "7", label: "1 Week", hint: "Delete events older than 7 days" },
4999
+ { value: "1", label: "1 Day", hint: "Delete events older than 1 day" }
5000
+ ]
5001
+ });
5002
+ if (lD(retentionChoice)) {
5003
+ he("Installation cancelled");
5004
+ process.exit(0);
5005
+ }
5006
+ retentionDays = parseInt(retentionChoice, 10) || 30;
5007
+ const debugLogChoice = await ce({
5008
+ message: "Enable debug logging? (writes to ~/.agentapprove/hook-debug.log)",
5009
+ initialValue: debugLog
5010
+ });
5011
+ if (lD(debugLogChoice)) {
5012
+ he("Installation cancelled");
5013
+ process.exit(0);
4533
5014
  }
4534
- if (!useE2E) {
4535
- v2.info("E2E encryption disabled — event content will be visible on the web dashboard");
5015
+ debugLog = debugLogChoice;
5016
+ }
5017
+ const silentlyReuseExistingToken = setupProfile === "existing-config" && hasExistingToken;
5018
+ const connectionOptions = [];
5019
+ if (hasExistingToken) {
5020
+ const tokenPreview = existingConfig.token.slice(0, 15) + "...";
5021
+ connectionOptions.push({
5022
+ value: "existing",
5023
+ label: "Use existing token",
5024
+ hint: tokenPreview
5025
+ });
5026
+ }
5027
+ connectionOptions.push({ value: "qr", label: "Scan QR code", hint: hasExistingToken ? undefined : "Recommended" }, { value: "copy", label: "Copy and paste code", hint: "No camera" });
5028
+ let connectionMethod;
5029
+ if (silentlyReuseExistingToken) {
5030
+ connectionMethod = "existing";
5031
+ } else {
5032
+ const connectionChoice = await le({
5033
+ message: "Connect to iOS app (required for setup and any hook downloads)",
5034
+ options: connectionOptions
5035
+ });
5036
+ if (lD(connectionChoice)) {
5037
+ he("Installation cancelled");
5038
+ process.exit(0);
4536
5039
  }
4537
- const e2eKeyId = e2eUserKey ? createHash("sha256").update(Buffer.from(e2eUserKey, "hex")).digest("hex").slice(0, 8) : undefined;
5040
+ connectionMethod = connectionChoice;
5041
+ }
5042
+ if (connectionMethod === "existing") {
5043
+ token = existingConfig.token;
5044
+ apiUrl = existingConfig.apiUrl || API_URL;
5045
+ if (setupProfile === "existing-config" && existingConfig?.configSetAt) {
5046
+ configSetAt = existingConfig.configSetAt;
5047
+ }
5048
+ const e2eKeyExists = existsSync2(existingKeyPath);
5049
+ useE2E = e2eKeyExists;
5050
+ e2eUserKey = null;
5051
+ backupOldKeyTo = null;
5052
+ if (!silentlyReuseExistingToken) {
5053
+ v2.success("Using existing token");
5054
+ }
5055
+ } else if (shouldCreateFreshPairing(connectionMethod)) {
5056
+ const pairingMethod = connectionMethod;
5057
+ if (backupOldKeyTo) {
5058
+ renameSync(existingKeyPath, backupOldKeyTo);
5059
+ v2.success(`Old key backed up to ${backupOldKeyTo}`);
5060
+ backupOldKeyTo = null;
5061
+ }
5062
+ configSetAt = Math.floor(Date.now() / 1000);
5063
+ const pairingConfig = {
5064
+ privacyTier: finalPrivacy,
5065
+ retentionDays,
5066
+ failBehavior,
5067
+ configSetAt,
5068
+ e2eEnabled: useE2E
5069
+ };
4538
5070
  const session = await createPairingSession(selectedAgents, e2eKeyId);
4539
5071
  if (!session || session.error) {
4540
5072
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
@@ -4542,18 +5074,19 @@ Installs hooks and plugins for OpenClaw, OpenAI Codex, Claude Code, Cursor, Gemi
4542
5074
  process.exit(1);
4543
5075
  } else {
4544
5076
  const machineHost = hostname();
4545
- let qrUrlWithE2e = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
4546
- if (e2eUserKey) {
4547
- const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
4548
- qrUrlWithE2e += `&e2eKey=${e2eKeyBase64url}`;
4549
- }
4550
- let qrDisplay = "";
4551
- import_qrcode_terminal.default.generate(qrUrlWithE2e, { small: true }, (qr) => {
4552
- qrDisplay = qr;
5077
+ const pairingPresentation = buildPairingPresentation({
5078
+ sessionCode: session.sessionCode,
5079
+ hostname: machineHost,
5080
+ config: pairingConfig,
5081
+ e2eUserKey
5082
+ });
5083
+ await showPairingPresentation({
5084
+ sessionCode: session.sessionCode,
5085
+ pairingUrl: pairingPresentation.pairingUrl,
5086
+ manualCode: pairingPresentation.manualCode,
5087
+ keyId: e2eKeyId,
5088
+ method: pairingMethod
4553
5089
  });
4554
- await new Promise((resolve) => setTimeout(resolve, 10));
4555
- me(qrDisplay + `
4556
- Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
4557
5090
  const pairingSpinner = _2();
4558
5091
  pairingSpinner.start("Waiting for iOS app...");
4559
5092
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -4570,10 +5103,9 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4570
5103
  finalPrivacy = result.privacy;
4571
5104
  email = result.email;
4572
5105
  if (e2eUserKey && e2eKeyId) {
4573
- const agentApproveDir2 = getAgentApproveDir();
4574
- const rootKeyPath = join(agentApproveDir2, "e2e-root-key");
5106
+ const rootKeyPath = join(agentApproveDir, "e2e-root-key");
4575
5107
  writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
4576
- const userKeyPath = join(agentApproveDir2, "e2e-key");
5108
+ const userKeyPath = join(agentApproveDir, "e2e-key");
4577
5109
  writeFileSync(userKeyPath, e2eUserKey, { mode: 384 });
4578
5110
  writeRotationConfig({
4579
5111
  rootKeyId: e2eKeyId,
@@ -4582,7 +5114,7 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4582
5114
  startedAt: new Date().toISOString()
4583
5115
  });
4584
5116
  if (result.e2eServerKey) {
4585
- const serverKeyPath = join(agentApproveDir2, "e2e-server-key");
5117
+ const serverKeyPath = join(agentApproveDir, "e2e-server-key");
4586
5118
  writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
4587
5119
  }
4588
5120
  }
@@ -4602,7 +5134,6 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4602
5134
  he("Cannot continue without token");
4603
5135
  process.exit(1);
4604
5136
  }
4605
- const configSetAt = Math.floor(Date.now() / 1000);
4606
5137
  saveEnvConfig({
4607
5138
  apiUrl,
4608
5139
  token,
@@ -4613,16 +5144,10 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4613
5144
  e2eEnabled: useE2E,
4614
5145
  failBehavior
4615
5146
  });
4616
- v2.success("Configuration saved to ~/.agentapprove/env");
4617
- await storeTokenInKeychain(token);
4618
- pushConfigToCloud({
4619
- apiUrl,
4620
- token,
4621
- privacy: finalPrivacy,
4622
- retentionDays,
4623
- failBehavior,
4624
- configSetAt
4625
- }).catch(() => {});
5147
+ if (!silentlyReuseExistingToken) {
5148
+ v2.success("Configuration saved to ~/.agentapprove/env");
5149
+ }
5150
+ await storeTokenInKeychain(token, { quiet: silentlyReuseExistingToken });
4626
5151
  const hookDownloadPlan = buildHookDownloadPlan(selectedAgents);
4627
5152
  if (hookDownloadPlan.files.length > 0) {
4628
5153
  const downloadSpinner = _2();
@@ -4638,36 +5163,17 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4638
5163
  } else {
4639
5164
  v2.success("No hook scripts needed for the selected agents");
4640
5165
  }
4641
- const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
5166
+ const e2eKeyPath = join(agentApproveDir, "e2e-key");
4642
5167
  const hasE2EKey = existsSync2(e2eKeyPath);
4643
- let installMode = "approval";
4644
5168
  if (hasE2EKey) {
4645
- const modeChoice = await le({
4646
- message: "Choose your security mode:",
4647
- options: [
4648
- {
4649
- value: "approval",
4650
- label: "Approval Mode (recommended)",
4651
- hint: "Agent asks permission before running commands. Server evaluates policies via encrypted channel."
4652
- },
4653
- {
4654
- value: "observe",
4655
- label: "Observe Mode (full E2E)",
4656
- hint: "Agent runs freely. All events are end-to-end encrypted. No server policy evaluation."
4657
- }
4658
- ]
4659
- });
4660
- if (lD(modeChoice)) {
4661
- he("Installation cancelled");
4662
- process.exit(0);
4663
- }
4664
- installMode = modeChoice;
4665
- if (installMode === "observe") {
4666
- v2.info("Observe mode: All events are E2E encrypted. No approval hooks will be installed.");
4667
- } else {
4668
- v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
5169
+ if (!silentlyReuseExistingToken) {
5170
+ if (installMode === "observe") {
5171
+ v2.info("Observe mode: All events are E2E encrypted. No approval hooks will be installed.");
5172
+ } else {
5173
+ v2.info("Approval mode: Commands are sent via encrypted channel for policy evaluation.");
5174
+ }
4669
5175
  }
4670
- const envPath = join(getAgentApproveDir(), "env");
5176
+ const envPath = join(agentApproveDir, "env");
4671
5177
  if (existsSync2(envPath)) {
4672
5178
  let envContent = readFileSync(envPath, "utf-8");
4673
5179
  if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
@@ -4681,16 +5187,35 @@ AGENTAPPROVE_E2E_MODE=${installMode}
4681
5187
  writeFileSync(envPath, envContent, { mode: 384 });
4682
5188
  }
4683
5189
  }
5190
+ const installMethod = await le({
5191
+ message: "How would you like to install hooks?",
5192
+ options: [
5193
+ { value: "auto", label: "Automatic", hint: "Back up configs and install hooks for me" },
5194
+ { value: "manual", label: "Manual", hint: "Show me what to add (I'll do it myself)" }
5195
+ ]
5196
+ });
5197
+ if (lD(installMethod)) {
5198
+ he("Installation cancelled");
5199
+ process.exit(0);
5200
+ }
4684
5201
  if (installMethod === "auto") {
4685
5202
  const filesToModify = [];
4686
5203
  for (const agentId of selectedAgents) {
4687
5204
  const agent = AGENTS[agentId];
4688
- filesToModify.push(agent.configPath);
5205
+ if (agentId !== "pi") {
5206
+ filesToModify.push(agent.configPath);
5207
+ }
4689
5208
  if (agentId === "codex") {
4690
5209
  filesToModify.push(join(homedir(), ".codex", "config.toml"));
4691
5210
  }
4692
5211
  if (agentId === "opencode") {
4693
- filesToModify.push(join(getOpenCodeConfigDir(), "package.json"));
5212
+ filesToModify.push(`OpenCode plugin cache (via \`opencode plugin ${OPENCODE_PLUGIN_SPEC} --global --force\`)`);
5213
+ }
5214
+ if (agentId === "openclaw") {
5215
+ filesToModify.push(`OpenClaw plugin registry (via \`openclaw plugins install ${OPENCLAW_PLUGIN_SPEC}\`)`);
5216
+ }
5217
+ if (agentId === "pi") {
5218
+ filesToModify.push("Pi package registry (via `pi install`)");
4694
5219
  }
4695
5220
  if (agentId === "vscode-agent") {
4696
5221
  const vsCodeVariants = findInstalledVSCodeVariants();
@@ -4713,14 +5238,14 @@ Backups will be created with timestamp`, "Files to be modified");
4713
5238
  for (const agentId of selectedAgents) {
4714
5239
  const agent = AGENTS[agentId];
4715
5240
  const spinner = _2();
4716
- const spinnerMsg = agentId === "openclaw" ? `Installing ${agent.name} plugin` : `Configuring ${agent.name}`;
5241
+ const spinnerMsg = agentId === "pi" ? `Installing ${agent.name} extension` : agentId === "openclaw" ? `Installing ${agent.name} plugin` : `Configuring ${agent.name}`;
4717
5242
  spinner.start(spinnerMsg);
4718
5243
  const result = await installHooksForAgent(agentId, hooksDir, installMode);
4719
5244
  if (result.success) {
4720
5245
  const installedHookNames = result.hooks.join(", ");
4721
5246
  const backupMsg = result.backupPath ? source_default.dim(` (backup created)`) : "";
4722
- const modeLabel = installMode === "observe" ? source_default.cyan(" [observe]") : source_default.green(" [approval]");
4723
- spinner.stop(`${agent.name}${modeLabel}: ${installedHookNames}${backupMsg}`);
5247
+ const agentName = source_default.cyan(agent.name);
5248
+ spinner.stop(`${agentName}: ${installedHookNames}${backupMsg}`);
4724
5249
  if (agentId === "vscode-agent") {
4725
5250
  const vsCodeSpinner = _2();
4726
5251
  vsCodeSpinner.start("Registering hook path in VS Code settings");
@@ -4749,6 +5274,11 @@ Backups will be created with timestamp`, "Files to be modified");
4749
5274
  v2.warn(`Could not install ${OPENCLAW_PLUGIN_SPEC} via OpenClaw CLI.
4750
5275
  ` + ` Error: ${result.error || "unknown"}
4751
5276
  ` + ` Install manually: openclaw plugins install ${OPENCLAW_PLUGIN_SPEC}
5277
+ ` + ` Then re-run: npx agentapprove`);
5278
+ } else if (agentId === "pi") {
5279
+ v2.warn(`Could not install ${PI_PLUGIN_SPEC} via Pi.
5280
+ ` + ` Error: ${result.error || "unknown"}
5281
+ ` + ` Install manually: pi install ${PI_PLUGIN_SPEC}
4752
5282
  ` + ` Then re-run: npx agentapprove`);
4753
5283
  }
4754
5284
  }
@@ -4811,6 +5341,19 @@ async function statusCommand() {
4811
5341
  }
4812
5342
  console.log();
4813
5343
  for (const [agentId, agent] of Object.entries(AGENTS)) {
5344
+ if (agentId === "pi") {
5345
+ try {
5346
+ const listOutput = execSync("pi list", {
5347
+ encoding: "utf-8",
5348
+ stdio: ["pipe", "pipe", "ignore"],
5349
+ timeout: PI_STATUS_TIMEOUT_MS
5350
+ });
5351
+ if (listOutput.includes("@agentapprove/pi")) {
5352
+ console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve extension`);
5353
+ }
5354
+ } catch {}
5355
+ continue;
5356
+ }
4814
5357
  if (existsSync2(agent.configPath)) {
4815
5358
  const config = readJsonConfig(agent.configPath);
4816
5359
  if (agentId === "opencode") {
@@ -4898,6 +5441,15 @@ async function performUninstall(mode) {
4898
5441
  ${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
4899
5442
  `));
4900
5443
  for (const [agentId, agent] of Object.entries(AGENTS)) {
5444
+ if (agentId === "pi") {
5445
+ const result = removePiPluginViaCli();
5446
+ if (result.success) {
5447
+ console.log(` ${source_default.green("✓")} Pi extension removed`);
5448
+ } else if (existsSync2(agent.configPath)) {
5449
+ console.log(` ${source_default.yellow("⚠")} Pi extension removal skipped or failed (${result.error || "Pi CLI unavailable"})`);
5450
+ }
5451
+ continue;
5452
+ }
4901
5453
  if (!existsSync2(agent.configPath))
4902
5454
  continue;
4903
5455
  const config = readJsonConfig(agent.configPath);
@@ -5164,24 +5716,37 @@ but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
5164
5716
  } else {
5165
5717
  v2.info('Refresh updates the token only. Use "npx agentapprove pair" if you need to repair or change E2E pairing.');
5166
5718
  }
5719
+ const configSetAt = existingConfig.configSetAt || Math.floor(Date.now() / 1000);
5720
+ const privacy = existingConfig.privacy || "full";
5721
+ const retentionDays = existingConfig.retentionDays ?? 30;
5722
+ const failBehavior = existingConfig.failBehavior || "ask";
5723
+ const pairingConfig = {
5724
+ privacyTier: privacy,
5725
+ retentionDays,
5726
+ failBehavior,
5727
+ configSetAt,
5728
+ e2eEnabled
5729
+ };
5167
5730
  const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
5168
5731
  if (!session || session.error) {
5169
5732
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
5170
5733
  process.exit(1);
5171
5734
  }
5735
+ const pairingMethod = await promptPairingMethod();
5172
5736
  const machineHost = hostname();
5173
- let qrUrl = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
5174
- if (e2eUserKey) {
5175
- const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
5176
- qrUrl += `&e2eKey=${e2eKeyBase64url}`;
5177
- }
5178
- let qrDisplay = "";
5179
- import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
5180
- qrDisplay = qr;
5737
+ const pairingPresentation = buildPairingPresentation({
5738
+ sessionCode: session.sessionCode,
5739
+ hostname: machineHost,
5740
+ config: pairingConfig,
5741
+ e2eUserKey
5742
+ });
5743
+ await showPairingPresentation({
5744
+ sessionCode: session.sessionCode,
5745
+ pairingUrl: pairingPresentation.pairingUrl,
5746
+ manualCode: pairingPresentation.manualCode,
5747
+ keyId: e2eKeyId,
5748
+ method: pairingMethod
5181
5749
  });
5182
- await new Promise((resolve) => setTimeout(resolve, 10));
5183
- me(qrDisplay + `
5184
- Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
5185
5750
  const pairingSpinner = _2();
5186
5751
  pairingSpinner.start("Waiting for iOS app...");
5187
5752
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -5201,10 +5766,6 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
5201
5766
  v2.error('Session expired. Run "npx agentapprove refresh" to try again.');
5202
5767
  process.exit(1);
5203
5768
  }
5204
- const configSetAt = Math.floor(Date.now() / 1000);
5205
- const privacy = existingConfig.privacy || "full";
5206
- const retentionDays = existingConfig.retentionDays ?? 30;
5207
- const failBehavior = existingConfig.failBehavior || "ask";
5208
5769
  saveEnvConfig({
5209
5770
  apiUrl,
5210
5771
  token,
@@ -5221,14 +5782,6 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
5221
5782
  const serverKeyPath = join(getAgentApproveDir(), "e2e-server-key");
5222
5783
  writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
5223
5784
  }
5224
- pushConfigToCloud({
5225
- apiUrl,
5226
- token,
5227
- privacy,
5228
- retentionDays,
5229
- failBehavior,
5230
- configSetAt
5231
- }).catch(() => {});
5232
5785
  const updateSpinner = _2();
5233
5786
  updateSpinner.start("Updating hook scripts with new token");
5234
5787
  try {
@@ -5326,26 +5879,34 @@ async function pairCommand() {
5326
5879
  updateEnvValue("AGENTAPPROVE_E2E_ENABLED", "true");
5327
5880
  }
5328
5881
  const installedAgents = detectInstalledAgents();
5882
+ const pairingConfigSetAt = Math.floor(Date.now() / 1000);
5883
+ const pairingConfig = {
5884
+ privacyTier: existingConfig?.privacy || "full",
5885
+ retentionDays: existingConfig?.retentionDays ?? 30,
5886
+ failBehavior: existingConfig?.failBehavior || "ask",
5887
+ configSetAt: pairingConfigSetAt,
5888
+ e2eEnabled: !!e2eUserKey
5889
+ };
5329
5890
  const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
5330
5891
  if (!session || session.error) {
5331
5892
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
5332
5893
  process.exit(1);
5333
5894
  }
5895
+ const pairingMethod = await promptPairingMethod();
5334
5896
  const machineHost = hostname();
5335
- let qrUrlWithE2e = `${session.qrUrl}&host=${encodeURIComponent(machineHost)}`;
5336
- if (e2eUserKey) {
5337
- const e2eKeyBase64url = Buffer.from(e2eUserKey, "hex").toString("base64url");
5338
- qrUrlWithE2e += `&e2eKey=${e2eKeyBase64url}`;
5339
- }
5340
- let qrDisplay = "";
5341
- import_qrcode_terminal.default.generate(qrUrlWithE2e, { small: true }, (qr) => {
5342
- qrDisplay = qr;
5897
+ const pairingPresentation = buildPairingPresentation({
5898
+ sessionCode: session.sessionCode,
5899
+ hostname: machineHost,
5900
+ config: pairingConfig,
5901
+ e2eUserKey
5902
+ });
5903
+ await showPairingPresentation({
5904
+ sessionCode: session.sessionCode,
5905
+ pairingUrl: pairingPresentation.pairingUrl,
5906
+ manualCode: pairingPresentation.manualCode,
5907
+ keyId: e2eKeyId,
5908
+ method: pairingMethod
5343
5909
  });
5344
- await new Promise((resolve) => setTimeout(resolve, 10));
5345
- const noteLabel = e2eKeyId ? `Session: ${session.sessionCode}
5346
- Key ID: ${e2eKeyId}` : `Session: ${session.sessionCode}`;
5347
- me(qrDisplay + `
5348
- ${noteLabel}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
5349
5910
  const pairingSpinner = _2();
5350
5911
  pairingSpinner.start("Waiting for iOS app...");
5351
5912
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -5408,13 +5969,6 @@ ${noteLabel}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
5408
5969
  v2.warn('You may need to run "npx agentapprove install" to reconfigure hooks.');
5409
5970
  }
5410
5971
  }
5411
- pushConfigToCloud({
5412
- apiUrl,
5413
- token: result.token,
5414
- privacy: existingConfig?.privacy || result.privacy || "full",
5415
- retentionDays: existingConfig?.retentionDays ?? 30,
5416
- configSetAt
5417
- }).catch(() => {});
5418
5972
  ge(source_default.green(`Device paired with key ${e2eKeyId}`));
5419
5973
  }
5420
5974
  async function updateHookScriptsWithToken(token, apiUrl) {