agentapprove 0.1.12 → 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 +662 -258
  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.12";
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.8";
2557
- var OPENCLAW_PLUGIN_VERSION = "0.2.7";
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,6 +2776,14 @@ 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
  };
2670
2789
  var SHARED_HOOK_FILES = ["common.sh"];
@@ -2902,6 +3021,15 @@ function detectInstalledAgents() {
2902
3021
  if (existsSync2(getOpenCodeConfigDir())) {
2903
3022
  installed.push(id);
2904
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
+ }
2905
3033
  } else {
2906
3034
  const configDir = dirname2(agent.configPath);
2907
3035
  if (existsSync2(configDir)) {
@@ -2991,6 +3119,68 @@ function formatStatusValue(key, value) {
2991
3119
  return value;
2992
3120
  }
2993
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
+ }
2994
3184
  function formatHookStatusLabel(name) {
2995
3185
  const explicitLabel = HOOK_STATUS_LABELS[name];
2996
3186
  if (explicitLabel) {
@@ -3433,6 +3623,101 @@ async function waitForPairing(sessionCode, onProgress, onCancel) {
3433
3623
  cleanup();
3434
3624
  return null;
3435
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
+ }
3436
3721
  function saveEnvConfig(config) {
3437
3722
  const envPath = join(getAgentApproveDir(), "env");
3438
3723
  const configSetAt = config.configSetAt || Math.floor(Date.now() / 1000);
@@ -3489,7 +3774,7 @@ function isCredentialManagerAvailable() {
3489
3774
  return false;
3490
3775
  }
3491
3776
  }
3492
- async function storeTokenInKeychain(token) {
3777
+ async function storeTokenInKeychain(token, options) {
3493
3778
  if (isKeychainAvailable()) {
3494
3779
  try {
3495
3780
  spawnSync("security", [
@@ -3512,7 +3797,9 @@ async function storeTokenInKeychain(token) {
3512
3797
  if (addResult.status !== 0 || addResult.error) {
3513
3798
  throw addResult.error || new Error("security add-generic-password failed");
3514
3799
  }
3515
- v2.success("Token stored in macOS Keychain");
3800
+ if (!options?.quiet) {
3801
+ v2.success("Token stored in macOS Keychain");
3802
+ }
3516
3803
  return true;
3517
3804
  } catch (err) {
3518
3805
  v2.warn("Could not store token in Keychain (file fallback will be used)");
@@ -3530,7 +3817,9 @@ async function storeTokenInKeychain(token) {
3530
3817
  if (addResult.status !== 0 || addResult.error) {
3531
3818
  throw addResult.error || new Error("cmdkey add credential failed");
3532
3819
  }
3533
- v2.success("Token stored in Windows Credential Manager");
3820
+ if (!options?.quiet) {
3821
+ v2.success("Token stored in Windows Credential Manager");
3822
+ }
3534
3823
  return true;
3535
3824
  } catch (err) {
3536
3825
  v2.warn("Could not store token in Credential Manager (file fallback will be used)");
@@ -3560,31 +3849,18 @@ function deleteTokenFromKeychain() {
3560
3849
  }
3561
3850
  return false;
3562
3851
  }
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
3852
  async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
3584
3853
  const agent = AGENTS[agentId];
3585
3854
  if (!agent) {
3586
3855
  return { success: false, backupPath: null, hooks: [] };
3587
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
+ }
3588
3864
  const backupPath = backupConfig(agent.configPath);
3589
3865
  const config = readJsonConfig(agent.configPath);
3590
3866
  if (!config[agent.hooksKey]) {
@@ -4076,6 +4352,17 @@ codex_hooks = true`;
4076
4352
  "",
4077
4353
  "OpenCode will auto-install the plugin on next start."
4078
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(`
4079
4366
  `);
4080
4367
  }
4081
4368
  return "";
@@ -4149,6 +4436,29 @@ function installOpenClawPluginViaCli() {
4149
4436
  return { success: false, error: message, label: "Agent Approve plugin" };
4150
4437
  }
4151
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
+ };
4460
+ }
4461
+ }
4152
4462
  var SYSTEM_DEPS = [
4153
4463
  {
4154
4464
  name: "curl",
@@ -4367,136 +4677,148 @@ async function installCommand() {
4367
4677
  await checkSystemDependencies();
4368
4678
  const existingConfig = readExistingConfig();
4369
4679
  const hasExistingToken = !!(existingConfig?.token && existingConfig.token.length > 10);
4680
+ let existingTokenPreview = "Not set";
4681
+ let existingKeyId = null;
4370
4682
  if (existingConfig) {
4371
- const tokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "not set";
4683
+ existingTokenPreview = hasExistingToken ? existingConfig.token.slice(0, 15) + "..." : "Not set";
4372
4684
  const e2eKeyPath2 = join(getAgentApproveDir(), "e2e-key");
4373
- let e2eLine = "";
4374
4685
  if (existsSync2(e2eKeyPath2)) {
4375
4686
  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}`;
4687
+ existingKeyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
4379
4688
  }
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
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");
4386
4692
  const installedAgents = detectInstalledAgents();
4387
4693
  const agentOptions = Object.entries(AGENTS).map(([id, agent]) => ({
4388
4694
  value: id,
4389
4695
  label: agent.name,
4390
4696
  hint: installedAgents.includes(id) ? "Detected" : "Not found"
4391
4697
  }));
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",
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,
4414
4738
  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" }
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
+ }
4420
4749
  ]
4421
- }),
4422
- failBehavior: () => le({
4750
+ });
4751
+ if (lD(modeChoice)) {
4752
+ he("Installation cancelled");
4753
+ process.exit(0);
4754
+ }
4755
+ installMode = modeChoice;
4756
+ const failBehaviorChoice = await le({
4423
4757
  message: "If Agent Approve is unreachable, hooks should:",
4424
- initialValue: defaultFailBehavior,
4758
+ initialValue: failBehavior,
4425
4759
  options: [
4426
4760
  { value: "ask", label: "Ask", hint: "Fall back to the agent's built-in approval dialog (recommended)" },
4427
4761
  { value: "deny", label: "Deny", hint: "Block all commands until service is restored" },
4428
4762
  { value: "allow", label: "Allow", hint: "Allow all commands to proceed without approval" }
4429
4763
  ]
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?",
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,
4437
4773
  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)" }
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" }
4440
4777
  ]
4441
- })
4442
- }, {
4443
- onCancel: () => {
4778
+ });
4779
+ if (lD(privacyChoice)) {
4444
4780
  he("Installation cancelled");
4445
4781
  process.exit(0);
4446
4782
  }
4447
- });
4448
- const { agents: selectedAgents, privacy, retention, failBehavior, debugLog, installMethod } = setup;
4449
- 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
+ }
4450
4795
  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
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
4459
4804
  });
4805
+ if (lD(e2eChoice)) {
4806
+ he("Installation cancelled");
4807
+ process.exit(0);
4808
+ }
4809
+ useE2E = e2eChoice;
4460
4810
  }
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);
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 {
4491
4818
  v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
4492
4819
  const keyAction = await le({
4493
4820
  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
- ]
4821
+ options: getExistingKeyActionOptions(e2eRequired)
4500
4822
  });
4501
4823
  if (lD(keyAction)) {
4502
4824
  he("Installation cancelled");
@@ -4508,33 +4830,96 @@ Installs hooks and plugins for OpenClaw, OpenAI Codex, Claude Code, Cursor, Gemi
4508
4830
  e2eUserKey = oldKeyHex;
4509
4831
  } else {
4510
4832
  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}`);
4833
+ backupOldKeyTo = join(agentApproveDir, `e2e-key.${oldKeyId}.bak`);
4514
4834
  }
4515
4835
  e2eUserKey = randomBytes(32).toString("hex");
4516
4836
  }
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
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);
4533
4870
  }
4534
- if (!useE2E) {
4535
- 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);
4536
4895
  }
4537
- 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
+ };
4538
4923
  const session = await createPairingSession(selectedAgents, e2eKeyId);
4539
4924
  if (!session || session.error) {
4540
4925
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
@@ -4542,18 +4927,19 @@ Installs hooks and plugins for OpenClaw, OpenAI Codex, Claude Code, Cursor, Gemi
4542
4927
  process.exit(1);
4543
4928
  } else {
4544
4929
  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;
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
4553
4942
  });
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
4943
  const pairingSpinner = _2();
4558
4944
  pairingSpinner.start("Waiting for iOS app...");
4559
4945
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -4570,10 +4956,9 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4570
4956
  finalPrivacy = result.privacy;
4571
4957
  email = result.email;
4572
4958
  if (e2eUserKey && e2eKeyId) {
4573
- const agentApproveDir2 = getAgentApproveDir();
4574
- const rootKeyPath = join(agentApproveDir2, "e2e-root-key");
4959
+ const rootKeyPath = join(agentApproveDir, "e2e-root-key");
4575
4960
  writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
4576
- const userKeyPath = join(agentApproveDir2, "e2e-key");
4961
+ const userKeyPath = join(agentApproveDir, "e2e-key");
4577
4962
  writeFileSync(userKeyPath, e2eUserKey, { mode: 384 });
4578
4963
  writeRotationConfig({
4579
4964
  rootKeyId: e2eKeyId,
@@ -4582,7 +4967,7 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4582
4967
  startedAt: new Date().toISOString()
4583
4968
  });
4584
4969
  if (result.e2eServerKey) {
4585
- const serverKeyPath = join(agentApproveDir2, "e2e-server-key");
4970
+ const serverKeyPath = join(agentApproveDir, "e2e-server-key");
4586
4971
  writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
4587
4972
  }
4588
4973
  }
@@ -4602,7 +4987,6 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4602
4987
  he("Cannot continue without token");
4603
4988
  process.exit(1);
4604
4989
  }
4605
- const configSetAt = Math.floor(Date.now() / 1000);
4606
4990
  saveEnvConfig({
4607
4991
  apiUrl,
4608
4992
  token,
@@ -4613,16 +4997,10 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4613
4997
  e2eEnabled: useE2E,
4614
4998
  failBehavior
4615
4999
  });
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(() => {});
5000
+ if (!silentlyReuseExistingToken) {
5001
+ v2.success("Configuration saved to ~/.agentapprove/env");
5002
+ }
5003
+ await storeTokenInKeychain(token, { quiet: silentlyReuseExistingToken });
4626
5004
  const hookDownloadPlan = buildHookDownloadPlan(selectedAgents);
4627
5005
  if (hookDownloadPlan.files.length > 0) {
4628
5006
  const downloadSpinner = _2();
@@ -4638,36 +5016,17 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
4638
5016
  } else {
4639
5017
  v2.success("No hook scripts needed for the selected agents");
4640
5018
  }
4641
- const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
5019
+ const e2eKeyPath = join(agentApproveDir, "e2e-key");
4642
5020
  const hasE2EKey = existsSync2(e2eKeyPath);
4643
- let installMode = "approval";
4644
5021
  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.");
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
+ }
4669
5028
  }
4670
- const envPath = join(getAgentApproveDir(), "env");
5029
+ const envPath = join(agentApproveDir, "env");
4671
5030
  if (existsSync2(envPath)) {
4672
5031
  let envContent = readFileSync(envPath, "utf-8");
4673
5032
  if (envContent.includes("AGENTAPPROVE_E2E_MODE=")) {
@@ -4681,17 +5040,33 @@ AGENTAPPROVE_E2E_MODE=${installMode}
4681
5040
  writeFileSync(envPath, envContent, { mode: 384 });
4682
5041
  }
4683
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
+ }
4684
5054
  if (installMethod === "auto") {
4685
5055
  const filesToModify = [];
4686
5056
  for (const agentId of selectedAgents) {
4687
5057
  const agent = AGENTS[agentId];
4688
- filesToModify.push(agent.configPath);
5058
+ if (agentId !== "pi") {
5059
+ filesToModify.push(agent.configPath);
5060
+ }
4689
5061
  if (agentId === "codex") {
4690
5062
  filesToModify.push(join(homedir(), ".codex", "config.toml"));
4691
5063
  }
4692
5064
  if (agentId === "opencode") {
4693
5065
  filesToModify.push(join(getOpenCodeConfigDir(), "package.json"));
4694
5066
  }
5067
+ if (agentId === "pi") {
5068
+ filesToModify.push("Pi package registry (via `pi install`)");
5069
+ }
4695
5070
  if (agentId === "vscode-agent") {
4696
5071
  const vsCodeVariants = findInstalledVSCodeVariants();
4697
5072
  for (const { path: settingsPath, variant } of vsCodeVariants) {
@@ -4713,7 +5088,7 @@ Backups will be created with timestamp`, "Files to be modified");
4713
5088
  for (const agentId of selectedAgents) {
4714
5089
  const agent = AGENTS[agentId];
4715
5090
  const spinner = _2();
4716
- 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}`;
4717
5092
  spinner.start(spinnerMsg);
4718
5093
  const result = await installHooksForAgent(agentId, hooksDir, installMode);
4719
5094
  if (result.success) {
@@ -4749,6 +5124,11 @@ Backups will be created with timestamp`, "Files to be modified");
4749
5124
  v2.warn(`Could not install ${OPENCLAW_PLUGIN_SPEC} via OpenClaw CLI.
4750
5125
  ` + ` Error: ${result.error || "unknown"}
4751
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}
4752
5132
  ` + ` Then re-run: npx agentapprove`);
4753
5133
  }
4754
5134
  }
@@ -4811,6 +5191,19 @@ async function statusCommand() {
4811
5191
  }
4812
5192
  console.log();
4813
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
+ }
4814
5207
  if (existsSync2(agent.configPath)) {
4815
5208
  const config = readJsonConfig(agent.configPath);
4816
5209
  if (agentId === "opencode") {
@@ -4898,6 +5291,15 @@ async function performUninstall(mode) {
4898
5291
  ${mode === "purge" ? "Purging" : "Uninstalling"} Agent Approve...
4899
5292
  `));
4900
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
+ }
4901
5303
  if (!existsSync2(agent.configPath))
4902
5304
  continue;
4903
5305
  const config = readJsonConfig(agent.configPath);
@@ -5164,24 +5566,37 @@ but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
5164
5566
  } else {
5165
5567
  v2.info('Refresh updates the token only. Use "npx agentapprove pair" if you need to repair or change E2E pairing.');
5166
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
+ };
5167
5580
  const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
5168
5581
  if (!session || session.error) {
5169
5582
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
5170
5583
  process.exit(1);
5171
5584
  }
5585
+ const pairingMethod = await promptPairingMethod();
5172
5586
  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;
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
5181
5599
  });
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
5600
  const pairingSpinner = _2();
5186
5601
  pairingSpinner.start("Waiting for iOS app...");
5187
5602
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -5201,10 +5616,6 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
5201
5616
  v2.error('Session expired. Run "npx agentapprove refresh" to try again.');
5202
5617
  process.exit(1);
5203
5618
  }
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
5619
  saveEnvConfig({
5209
5620
  apiUrl,
5210
5621
  token,
@@ -5221,14 +5632,6 @@ Session: ${session.sessionCode}`, "Scan in Agent Approve iOS app (Settings > Sca
5221
5632
  const serverKeyPath = join(getAgentApproveDir(), "e2e-server-key");
5222
5633
  writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
5223
5634
  }
5224
- pushConfigToCloud({
5225
- apiUrl,
5226
- token,
5227
- privacy,
5228
- retentionDays,
5229
- failBehavior,
5230
- configSetAt
5231
- }).catch(() => {});
5232
5635
  const updateSpinner = _2();
5233
5636
  updateSpinner.start("Updating hook scripts with new token");
5234
5637
  try {
@@ -5326,26 +5729,34 @@ async function pairCommand() {
5326
5729
  updateEnvValue("AGENTAPPROVE_E2E_ENABLED", "true");
5327
5730
  }
5328
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
+ };
5329
5740
  const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
5330
5741
  if (!session || session.error) {
5331
5742
  v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
5332
5743
  process.exit(1);
5333
5744
  }
5745
+ const pairingMethod = await promptPairingMethod();
5334
5746
  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;
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
5343
5759
  });
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
5760
  const pairingSpinner = _2();
5350
5761
  pairingSpinner.start("Waiting for iOS app...");
5351
5762
  const result = await waitForPairing(session.sessionCode, (expiresIn) => {
@@ -5408,13 +5819,6 @@ ${noteLabel}`, "Scan in Agent Approve iOS app (Settings > Scan QR Code)");
5408
5819
  v2.warn('You may need to run "npx agentapprove install" to reconfigure hooks.');
5409
5820
  }
5410
5821
  }
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
5822
  ge(source_default.green(`Device paired with key ${e2eKeyId}`));
5419
5823
  }
5420
5824
  async function updateHookScriptsWithToken(token, apiUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentapprove",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Approve AI agent actions from your iPhone or Apple Watch",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "gemini",
25
25
  "openclaw",
26
26
  "opencode",
27
+ "pi",
27
28
  "ai",
28
29
  "agent",
29
30
  "approval",