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.
- package/dist/cli.js +662 -258
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1821,20 +1821,6 @@ function ye() {
|
|
|
1821
1821
|
const s = ["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");
|
|
1822
1822
|
return new RegExp(s, "g");
|
|
1823
1823
|
}
|
|
1824
|
-
var ve = async (s, n) => {
|
|
1825
|
-
const t = {}, i = Object.keys(s);
|
|
1826
|
-
for (const r2 of i) {
|
|
1827
|
-
const o = s[r2], c = await o({ results: t })?.catch((l2) => {
|
|
1828
|
-
throw l2;
|
|
1829
|
-
});
|
|
1830
|
-
if (typeof n?.onCancel == "function" && lD(c)) {
|
|
1831
|
-
t[r2] = "canceled", n.onCancel({ results: t });
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
t[r2] = c;
|
|
1835
|
-
}
|
|
1836
|
-
return t;
|
|
1837
|
-
};
|
|
1838
1824
|
|
|
1839
1825
|
// ../../node_modules/.bun/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
1840
1826
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -2504,8 +2490,130 @@ function shouldDeleteCryptoMaterial(purgeConfirmed, firstKeyConfirmation, second
|
|
|
2504
2490
|
return purgeConfirmed && firstKeyConfirmation && secondKeyConfirmation;
|
|
2505
2491
|
}
|
|
2506
2492
|
|
|
2493
|
+
// src/pairing-artifact.ts
|
|
2494
|
+
var PAIRING_ARTIFACT_PREFIX = "AAP1.";
|
|
2495
|
+
function encodePairingArtifact(artifact) {
|
|
2496
|
+
const payload = {
|
|
2497
|
+
s: artifact.sessionCode,
|
|
2498
|
+
p: artifact.config.privacyTier,
|
|
2499
|
+
r: artifact.config.retentionDays,
|
|
2500
|
+
f: artifact.config.failBehavior,
|
|
2501
|
+
c: artifact.config.configSetAt,
|
|
2502
|
+
e: artifact.config.e2eEnabled ? 1 : 0
|
|
2503
|
+
};
|
|
2504
|
+
if (artifact.hostname) {
|
|
2505
|
+
payload.h = artifact.hostname;
|
|
2506
|
+
}
|
|
2507
|
+
if (artifact.e2eUserKey) {
|
|
2508
|
+
payload.k = Buffer.from(artifact.e2eUserKey, "hex").toString("base64url");
|
|
2509
|
+
}
|
|
2510
|
+
return `${PAIRING_ARTIFACT_PREFIX}${Buffer.from(JSON.stringify(payload)).toString("base64url")}`;
|
|
2511
|
+
}
|
|
2512
|
+
function buildPairingUrl(pairingArtifact) {
|
|
2513
|
+
return `agentapprove://pair?code=${encodeURIComponent(pairingArtifact)}`;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/install-flow.ts
|
|
2517
|
+
var VALID_RETENTION_DAYS = [1, 7, 30, 90, 365];
|
|
2518
|
+
function normalizePrivacy(value) {
|
|
2519
|
+
if (value === "minimal" || value === "summary" || value === "full") {
|
|
2520
|
+
return value;
|
|
2521
|
+
}
|
|
2522
|
+
return "full";
|
|
2523
|
+
}
|
|
2524
|
+
function normalizeRetentionDays(value) {
|
|
2525
|
+
if (value && VALID_RETENTION_DAYS.includes(value)) {
|
|
2526
|
+
return value;
|
|
2527
|
+
}
|
|
2528
|
+
return 30;
|
|
2529
|
+
}
|
|
2530
|
+
function normalizeFailBehavior(value) {
|
|
2531
|
+
if (value === "allow" || value === "deny" || value === "ask") {
|
|
2532
|
+
return value;
|
|
2533
|
+
}
|
|
2534
|
+
return "ask";
|
|
2535
|
+
}
|
|
2536
|
+
function normalizeInstallMode(value) {
|
|
2537
|
+
if (value === "observe" || value === "approval") {
|
|
2538
|
+
return value;
|
|
2539
|
+
}
|
|
2540
|
+
return "approval";
|
|
2541
|
+
}
|
|
2542
|
+
function getSetupProfileOptions(hasExistingConfig) {
|
|
2543
|
+
if (hasExistingConfig) {
|
|
2544
|
+
return [
|
|
2545
|
+
{
|
|
2546
|
+
value: "existing-config",
|
|
2547
|
+
label: "Existing config",
|
|
2548
|
+
hint: "Reuse the current local settings already saved on this computer."
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
value: "customize",
|
|
2552
|
+
label: "Customize options",
|
|
2553
|
+
hint: "Choose your own mode, fallback behavior, privacy, E2E, retention, and debug logging before pairing."
|
|
2554
|
+
}
|
|
2555
|
+
];
|
|
2556
|
+
}
|
|
2557
|
+
return [
|
|
2558
|
+
{
|
|
2559
|
+
value: "recommended",
|
|
2560
|
+
label: "Recommended setup",
|
|
2561
|
+
hint: "Use Approval mode, E2E on, fallback Ask, Full privacy, 30-day retention, and debug logging off."
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
value: "customize",
|
|
2565
|
+
label: "Customize options",
|
|
2566
|
+
hint: "Choose your own mode, fallback behavior, privacy, E2E, retention, and debug logging before pairing."
|
|
2567
|
+
}
|
|
2568
|
+
];
|
|
2569
|
+
}
|
|
2570
|
+
function getRecommendedInstallConfig() {
|
|
2571
|
+
return {
|
|
2572
|
+
privacy: "full",
|
|
2573
|
+
retentionDays: 30,
|
|
2574
|
+
failBehavior: "ask",
|
|
2575
|
+
debugLog: false,
|
|
2576
|
+
installMode: "approval",
|
|
2577
|
+
e2eEnabled: true
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
function getExistingInstallConfig(existingConfig) {
|
|
2581
|
+
return {
|
|
2582
|
+
privacy: normalizePrivacy(existingConfig?.privacy),
|
|
2583
|
+
retentionDays: normalizeRetentionDays(existingConfig?.retentionDays),
|
|
2584
|
+
failBehavior: normalizeFailBehavior(existingConfig?.failBehavior),
|
|
2585
|
+
debugLog: existingConfig?.debugLog ?? false,
|
|
2586
|
+
installMode: normalizeInstallMode(existingConfig?.e2eMode),
|
|
2587
|
+
e2eEnabled: existingConfig?.e2eEnabled !== false
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
function getInitialInstallConfig(choice, existingConfig) {
|
|
2591
|
+
if (choice === "recommended") {
|
|
2592
|
+
return getRecommendedInstallConfig();
|
|
2593
|
+
}
|
|
2594
|
+
return getExistingInstallConfig(existingConfig);
|
|
2595
|
+
}
|
|
2596
|
+
function getExistingKeyActionOptions(e2eRequired) {
|
|
2597
|
+
const options = [
|
|
2598
|
+
{ value: "reuse", label: "Reuse existing key", hint: "recommended - keeps old events decryptable" },
|
|
2599
|
+
{ value: "backup", label: "Back up old key, then generate new" },
|
|
2600
|
+
{ value: "discard", label: "Discard old key and generate new" }
|
|
2601
|
+
];
|
|
2602
|
+
if (!e2eRequired) {
|
|
2603
|
+
options.push({
|
|
2604
|
+
value: "disable",
|
|
2605
|
+
label: "Disable E2E encryption",
|
|
2606
|
+
hint: "events visible on web dashboard"
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
return options;
|
|
2610
|
+
}
|
|
2611
|
+
function shouldCreateFreshPairing(connectionMethod) {
|
|
2612
|
+
return connectionMethod === "qr" || connectionMethod === "copy";
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2507
2615
|
// src/cli.ts
|
|
2508
|
-
var VERSION = "0.1.
|
|
2616
|
+
var VERSION = "0.1.13";
|
|
2509
2617
|
function getApiUrl() {
|
|
2510
2618
|
return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
|
|
2511
2619
|
}
|
|
@@ -2553,9 +2661,12 @@ function getCommand() {
|
|
|
2553
2661
|
}
|
|
2554
2662
|
return filtered[0] || "install";
|
|
2555
2663
|
}
|
|
2556
|
-
var OPENCODE_PLUGIN_VERSION = "0.1.
|
|
2557
|
-
var OPENCLAW_PLUGIN_VERSION = "0.2.
|
|
2664
|
+
var OPENCODE_PLUGIN_VERSION = "0.1.9";
|
|
2665
|
+
var OPENCLAW_PLUGIN_VERSION = "0.2.8";
|
|
2558
2666
|
var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
|
|
2667
|
+
var PI_PLUGIN_VERSION = "0.1.0";
|
|
2668
|
+
var PI_PLUGIN_SPEC = `npm:@agentapprove/pi@${PI_PLUGIN_VERSION}`;
|
|
2669
|
+
var PI_STATUS_TIMEOUT_MS = 5000;
|
|
2559
2670
|
var AGENTS = {
|
|
2560
2671
|
"claude-code": {
|
|
2561
2672
|
name: "Claude Code",
|
|
@@ -2665,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
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
|
-
{
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
|
|
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: "
|
|
4439
|
-
{ value: "
|
|
4774
|
+
{ value: "full", label: "Full", hint: "Complete command details stored in event history" },
|
|
4775
|
+
{ value: "summary", label: "Summary", hint: "Truncated to 50 chars in event history" },
|
|
4776
|
+
{ value: "minimal", label: "Minimal", hint: "Tool name only in event history, most private" }
|
|
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
|
-
|
|
4449
|
-
const
|
|
4783
|
+
finalPrivacy = privacyChoice;
|
|
4784
|
+
}
|
|
4785
|
+
const skipE2E = hasFlag2("--no-e2e");
|
|
4786
|
+
const e2eRequired = installMode === "observe";
|
|
4787
|
+
if (e2eRequired) {
|
|
4788
|
+
if (skipE2E) {
|
|
4789
|
+
v2.warn("Observe Mode requires E2E encryption; ignoring --no-e2e.");
|
|
4790
|
+
}
|
|
4791
|
+
useE2E = true;
|
|
4792
|
+
} else if (skipE2E) {
|
|
4793
|
+
useE2E = false;
|
|
4794
|
+
}
|
|
4450
4795
|
ensureAgentApproveDir();
|
|
4451
|
-
const
|
|
4452
|
-
const
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
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
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
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(
|
|
5019
|
+
const e2eKeyPath = join(agentApproveDir, "e2e-key");
|
|
4642
5020
|
const hasE2EKey = existsSync2(e2eKeyPath);
|
|
4643
|
-
let installMode = "approval";
|
|
4644
5021
|
if (hasE2EKey) {
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
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
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
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.
|
|
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",
|