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