agentapprove 0.1.15 → 0.1.21
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 +386 -61
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2630,8 +2630,204 @@ function shouldCreateFreshPairing(connectionMethod) {
|
|
|
2630
2630
|
return connectionMethod === "qr" || connectionMethod === "copy";
|
|
2631
2631
|
}
|
|
2632
2632
|
|
|
2633
|
+
// src/install-validation.ts
|
|
2634
|
+
async function safeReadText(response) {
|
|
2635
|
+
try {
|
|
2636
|
+
return await response.text();
|
|
2637
|
+
} catch {
|
|
2638
|
+
return "";
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
function parseErrorBody(body) {
|
|
2642
|
+
if (!body)
|
|
2643
|
+
return null;
|
|
2644
|
+
try {
|
|
2645
|
+
const parsed = JSON.parse(body);
|
|
2646
|
+
if (parsed && typeof parsed === "object") {
|
|
2647
|
+
return parsed;
|
|
2648
|
+
}
|
|
2649
|
+
} catch {}
|
|
2650
|
+
return null;
|
|
2651
|
+
}
|
|
2652
|
+
async function validateExistingToken(token, apiUrl, apiVersion, options = {}) {
|
|
2653
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
2654
|
+
const filename = options.preflightFilename || "common.sh";
|
|
2655
|
+
const url = `${apiUrl}/${apiVersion}/hooks/${filename}?format=raw`;
|
|
2656
|
+
let response;
|
|
2657
|
+
try {
|
|
2658
|
+
response = await fetchImpl(url, {
|
|
2659
|
+
headers: {
|
|
2660
|
+
Authorization: `Bearer ${token}`
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
const message = err instanceof Error ? err.message : "network error";
|
|
2665
|
+
return {
|
|
2666
|
+
kind: "network_error",
|
|
2667
|
+
summary: "cannot reach Agent Approve from this network",
|
|
2668
|
+
serverMessage: message
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
const status = response.status;
|
|
2672
|
+
if (response.ok) {
|
|
2673
|
+
const body2 = await safeReadText(response);
|
|
2674
|
+
return {
|
|
2675
|
+
kind: "valid",
|
|
2676
|
+
summary: "verified - token is valid",
|
|
2677
|
+
cachedContent: body2,
|
|
2678
|
+
status
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
const body = await safeReadText(response);
|
|
2682
|
+
const parsed = parseErrorBody(body);
|
|
2683
|
+
const code = parsed?.code;
|
|
2684
|
+
const serverMessage = parsed?.error;
|
|
2685
|
+
if (status === 401 && code === "TOKEN_EXPIRED") {
|
|
2686
|
+
return {
|
|
2687
|
+
kind: "expired",
|
|
2688
|
+
summary: "expired (refresh required)",
|
|
2689
|
+
serverMessage,
|
|
2690
|
+
status
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
if (status === 401 && code === "INVALID_TOKEN") {
|
|
2694
|
+
return {
|
|
2695
|
+
kind: "invalid",
|
|
2696
|
+
summary: "invalid (re-pair required)",
|
|
2697
|
+
serverMessage,
|
|
2698
|
+
status
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
if (status === 403 && code === "AUTH_SCOPE_DENIED") {
|
|
2702
|
+
return {
|
|
2703
|
+
kind: "scope_denied",
|
|
2704
|
+
summary: "scope denied (re-pair required)",
|
|
2705
|
+
serverMessage,
|
|
2706
|
+
status
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
if (status === 503) {
|
|
2710
|
+
return {
|
|
2711
|
+
kind: "unavailable",
|
|
2712
|
+
summary: "validation temporarily unavailable",
|
|
2713
|
+
serverMessage,
|
|
2714
|
+
status
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
if (status === 403) {
|
|
2718
|
+
return {
|
|
2719
|
+
kind: "network_blocked",
|
|
2720
|
+
summary: "cannot reach service from this network",
|
|
2721
|
+
serverMessage,
|
|
2722
|
+
status
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
if (status === 401) {
|
|
2726
|
+
return {
|
|
2727
|
+
kind: "invalid",
|
|
2728
|
+
summary: "invalid (re-pair required)",
|
|
2729
|
+
serverMessage,
|
|
2730
|
+
status
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
return {
|
|
2734
|
+
kind: "unavailable",
|
|
2735
|
+
summary: "validation temporarily unavailable",
|
|
2736
|
+
serverMessage,
|
|
2737
|
+
status
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
function classifyHookDownloadFailure(status, body) {
|
|
2741
|
+
const parsed = parseErrorBody(body);
|
|
2742
|
+
const code = parsed?.code;
|
|
2743
|
+
if (status === 401 && code === "TOKEN_EXPIRED")
|
|
2744
|
+
return "token_expired";
|
|
2745
|
+
if (status === 401 && code === "INVALID_TOKEN")
|
|
2746
|
+
return "token_invalid";
|
|
2747
|
+
if (status === 403 && code === "AUTH_SCOPE_DENIED")
|
|
2748
|
+
return "scope_denied";
|
|
2749
|
+
if (status === 503)
|
|
2750
|
+
return "validation_unavailable";
|
|
2751
|
+
if (status === 403)
|
|
2752
|
+
return "network_blocked";
|
|
2753
|
+
if (status === 401)
|
|
2754
|
+
return "token_invalid";
|
|
2755
|
+
return "recoverable";
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// src/copy-hook-scripts.ts
|
|
2759
|
+
import { writeFileSync as fsWriteFileSync } from "fs";
|
|
2760
|
+
import { join as pathJoin } from "path";
|
|
2761
|
+
async function copyHookScripts(hooksDir, token, files, options) {
|
|
2762
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
2763
|
+
const writeFile = options.writeFileSync || fsWriteFileSync;
|
|
2764
|
+
const join = options.joinPath || pathJoin;
|
|
2765
|
+
let downloaded = 0;
|
|
2766
|
+
const failed = [];
|
|
2767
|
+
for (const file of files) {
|
|
2768
|
+
try {
|
|
2769
|
+
const response = await fetchImpl(`${options.apiUrl}/${options.apiVersion}/hooks/${file}?format=raw`, {
|
|
2770
|
+
headers: {
|
|
2771
|
+
Authorization: `Bearer ${token}`
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
if (response.ok) {
|
|
2775
|
+
const content = await response.text();
|
|
2776
|
+
const isShellScript = content.startsWith("#!/") || content.startsWith("# ");
|
|
2777
|
+
const isJsBundle = file.endsWith(".js") && (content.startsWith("//") || content.startsWith("import ") || content.startsWith("var ") || content.startsWith("const ") || content.startsWith("export "));
|
|
2778
|
+
if (isShellScript || isJsBundle) {
|
|
2779
|
+
const filePath = join(hooksDir, file);
|
|
2780
|
+
writeFile(filePath, content, { mode: isShellScript ? 493 : 420 });
|
|
2781
|
+
downloaded++;
|
|
2782
|
+
} else {
|
|
2783
|
+
failed.push(`${file} (invalid content)`);
|
|
2784
|
+
}
|
|
2785
|
+
} else {
|
|
2786
|
+
const body = await response.text().catch(() => "");
|
|
2787
|
+
const failureKind = classifyHookDownloadFailure(response.status, body);
|
|
2788
|
+
if (failureKind !== "recoverable") {
|
|
2789
|
+
return {
|
|
2790
|
+
downloaded,
|
|
2791
|
+
failed,
|
|
2792
|
+
terminalFailure: {
|
|
2793
|
+
kind: failureKind,
|
|
2794
|
+
file,
|
|
2795
|
+
status: response.status
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
failed.push(`${file} (${response.status})`);
|
|
2800
|
+
}
|
|
2801
|
+
} catch (err) {
|
|
2802
|
+
failed.push(`${file} (${err instanceof Error ? err.message : "network error"})`);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
return { downloaded, failed };
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// src/pairing-api-url.ts
|
|
2809
|
+
function looksLikeHostedAgentApproveApi(baseUrl) {
|
|
2810
|
+
try {
|
|
2811
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
2812
|
+
return host === "agentapprove.com" || host.endsWith(".agentapprove.com");
|
|
2813
|
+
} catch {
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
function resolvePairingApiBaseUrl(options) {
|
|
2818
|
+
const fromEnv = options.agentapproveApiEnv?.trim();
|
|
2819
|
+
if (fromEnv) {
|
|
2820
|
+
return fromEnv;
|
|
2821
|
+
}
|
|
2822
|
+
const fromFile = options.savedApiUrl?.trim();
|
|
2823
|
+
if (fromFile) {
|
|
2824
|
+
return fromFile;
|
|
2825
|
+
}
|
|
2826
|
+
return options.processDefaultApiUrl;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2633
2829
|
// src/cli.ts
|
|
2634
|
-
var VERSION = "0.1.
|
|
2830
|
+
var VERSION = "0.1.21";
|
|
2635
2831
|
function getApiUrl() {
|
|
2636
2832
|
return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
|
|
2637
2833
|
}
|
|
@@ -2703,11 +2899,11 @@ function getCommand() {
|
|
|
2703
2899
|
}
|
|
2704
2900
|
return filtered[0] || "install";
|
|
2705
2901
|
}
|
|
2706
|
-
var OPENCODE_PLUGIN_VERSION = "0.1.
|
|
2902
|
+
var OPENCODE_PLUGIN_VERSION = "0.1.18";
|
|
2707
2903
|
var OPENCODE_PLUGIN_SPEC = `@agentapprove/opencode@${OPENCODE_PLUGIN_VERSION}`;
|
|
2708
|
-
var OPENCLAW_PLUGIN_VERSION = "0.2.
|
|
2904
|
+
var OPENCLAW_PLUGIN_VERSION = "0.2.11";
|
|
2709
2905
|
var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
|
|
2710
|
-
var PI_PLUGIN_VERSION = "0.1.
|
|
2906
|
+
var PI_PLUGIN_VERSION = "0.1.6";
|
|
2711
2907
|
var PI_PLUGIN_SPEC = `npm:@agentapprove/pi@${PI_PLUGIN_VERSION}`;
|
|
2712
2908
|
var PI_STATUS_TIMEOUT_MS = 5000;
|
|
2713
2909
|
var AGENTS = {
|
|
@@ -3590,10 +3786,10 @@ function disableCodexFeatureFlag(configPath = join(homedir(), ".codex", "config.
|
|
|
3590
3786
|
writeFileSync(configPath, updated, { mode: 384 });
|
|
3591
3787
|
return { updated: true, backupPath };
|
|
3592
3788
|
}
|
|
3593
|
-
async function createPairingSession(configuredAgents, e2eKeyId) {
|
|
3789
|
+
async function createPairingSession(configuredAgents, e2eKeyId, apiBaseUrl = API_URL) {
|
|
3594
3790
|
try {
|
|
3595
3791
|
const machineHostname = hostname();
|
|
3596
|
-
const response = await fetch(`${
|
|
3792
|
+
const response = await fetch(`${apiBaseUrl}/${API_VERSION}/pair`, {
|
|
3597
3793
|
method: "POST",
|
|
3598
3794
|
headers: { "Content-Type": "application/json" },
|
|
3599
3795
|
body: JSON.stringify({
|
|
@@ -3612,9 +3808,9 @@ async function createPairingSession(configuredAgents, e2eKeyId) {
|
|
|
3612
3808
|
return { sessionCode: "", qrUrl: "", expiresIn: 0, error: String(err) };
|
|
3613
3809
|
}
|
|
3614
3810
|
}
|
|
3615
|
-
async function pollPairingSession(sessionCode) {
|
|
3811
|
+
async function pollPairingSession(sessionCode, apiBaseUrl = API_URL) {
|
|
3616
3812
|
try {
|
|
3617
|
-
const response = await fetch(`${
|
|
3813
|
+
const response = await fetch(`${apiBaseUrl}/${API_VERSION}/pair/${sessionCode}`);
|
|
3618
3814
|
return await response.json();
|
|
3619
3815
|
} catch {
|
|
3620
3816
|
return { status: "error" };
|
|
@@ -3623,7 +3819,7 @@ async function pollPairingSession(sessionCode) {
|
|
|
3623
3819
|
function sleep(ms) {
|
|
3624
3820
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3625
3821
|
}
|
|
3626
|
-
async function waitForPairing(sessionCode, onProgress, onCancel) {
|
|
3822
|
+
async function waitForPairing(sessionCode, onProgress, onCancel, apiBaseUrl = API_URL) {
|
|
3627
3823
|
let cancelled = false;
|
|
3628
3824
|
const handleKeypress = (key) => {
|
|
3629
3825
|
const char = key.toString();
|
|
@@ -3650,14 +3846,14 @@ async function waitForPairing(sessionCode, onProgress, onCancel) {
|
|
|
3650
3846
|
cleanup();
|
|
3651
3847
|
return "cancelled";
|
|
3652
3848
|
}
|
|
3653
|
-
const result = await pollPairingSession(sessionCode);
|
|
3849
|
+
const result = await pollPairingSession(sessionCode, apiBaseUrl);
|
|
3654
3850
|
if (result.status === "completed" && result.token) {
|
|
3655
3851
|
cleanup();
|
|
3656
3852
|
return {
|
|
3657
3853
|
token: result.token,
|
|
3658
3854
|
privacy: result.privacy || "full",
|
|
3659
3855
|
email: result.email || "",
|
|
3660
|
-
apiUrl: result.apiUrl ||
|
|
3856
|
+
apiUrl: result.apiUrl || apiBaseUrl,
|
|
3661
3857
|
e2eServerKey: result.e2eServerKey
|
|
3662
3858
|
};
|
|
3663
3859
|
}
|
|
@@ -4428,35 +4624,27 @@ codex_hooks = true`;
|
|
|
4428
4624
|
}
|
|
4429
4625
|
return "";
|
|
4430
4626
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
} else {
|
|
4453
|
-
failed.push(`${file} (${response.status})`);
|
|
4454
|
-
}
|
|
4455
|
-
} catch (err) {
|
|
4456
|
-
failed.push(`${file} (${err instanceof Error ? err.message : "network error"})`);
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
return { downloaded, failed };
|
|
4627
|
+
function describeDownloadTerminalFailure(kind) {
|
|
4628
|
+
switch (kind) {
|
|
4629
|
+
case "token_expired":
|
|
4630
|
+
return "Hook download stopped: your saved token has expired. Run `npx agentapprove refresh` to re-pair, or run the installer again to choose Refresh / Re-pair.";
|
|
4631
|
+
case "token_invalid":
|
|
4632
|
+
return "Hook download stopped: your saved token is no longer valid. Run `npx agentapprove pair` to pair this computer again.";
|
|
4633
|
+
case "scope_denied":
|
|
4634
|
+
return "Hook download stopped: your saved token cannot download hooks. Run `npx agentapprove pair` to pair this computer again.";
|
|
4635
|
+
case "validation_unavailable":
|
|
4636
|
+
return "Hook download stopped: Agent Approve could not validate your token right now. Please try again in a few minutes.";
|
|
4637
|
+
case "network_blocked":
|
|
4638
|
+
return "Hook download stopped: Service temporarily unavailable. Try again later.";
|
|
4639
|
+
case "recoverable":
|
|
4640
|
+
return "Hook download stopped: encountered an unexpected response.";
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
async function copyHookScripts2(hooksDir, token, files, options = {}) {
|
|
4644
|
+
return copyHookScripts(hooksDir, token, files, {
|
|
4645
|
+
apiUrl: options.apiUrl || API_URL,
|
|
4646
|
+
apiVersion: API_VERSION
|
|
4647
|
+
});
|
|
4460
4648
|
}
|
|
4461
4649
|
function readOpenClawInstalledVersion() {
|
|
4462
4650
|
const packagePath = join(homedir(), ".openclaw", "extensions", "openclaw", "package.json");
|
|
@@ -4810,16 +4998,107 @@ async function checkSystemDependencies() {
|
|
|
4810
4998
|
}
|
|
4811
4999
|
return true;
|
|
4812
5000
|
}
|
|
5001
|
+
async function runExistingTokenPreflight(token, apiUrlForPreflight) {
|
|
5002
|
+
const preflightSpinner = _2();
|
|
5003
|
+
preflightSpinner.start("Checking saved token");
|
|
5004
|
+
let result = await validateExistingToken(token, apiUrlForPreflight, API_VERSION);
|
|
5005
|
+
if (result.kind === "unavailable" || result.kind === "network_error") {
|
|
5006
|
+
const message = result.kind === "unavailable" ? "Token validation is temporarily unavailable." : "Could not reach Agent Approve from this network.";
|
|
5007
|
+
preflightSpinner.stop(message);
|
|
5008
|
+
const retryChoice = await le({
|
|
5009
|
+
message: "How would you like to continue?",
|
|
5010
|
+
options: [
|
|
5011
|
+
{ value: "retry", label: "Retry" },
|
|
5012
|
+
{ value: "cancel", label: "Cancel" }
|
|
5013
|
+
]
|
|
5014
|
+
});
|
|
5015
|
+
if (lD(retryChoice) || retryChoice === "cancel") {
|
|
5016
|
+
he("Installation cancelled");
|
|
5017
|
+
process.exit(0);
|
|
5018
|
+
}
|
|
5019
|
+
preflightSpinner.start("Checking saved token");
|
|
5020
|
+
result = await validateExistingToken(token, apiUrlForPreflight, API_VERSION);
|
|
5021
|
+
}
|
|
5022
|
+
switch (result.kind) {
|
|
5023
|
+
case "valid":
|
|
5024
|
+
preflightSpinner.stop("Saved token verified");
|
|
5025
|
+
break;
|
|
5026
|
+
case "expired":
|
|
5027
|
+
preflightSpinner.stop("Saved token has expired");
|
|
5028
|
+
break;
|
|
5029
|
+
case "invalid":
|
|
5030
|
+
case "scope_denied":
|
|
5031
|
+
preflightSpinner.stop("Saved token is no longer valid");
|
|
5032
|
+
break;
|
|
5033
|
+
case "network_blocked":
|
|
5034
|
+
preflightSpinner.stop("Service temporarily unavailable");
|
|
5035
|
+
break;
|
|
5036
|
+
default:
|
|
5037
|
+
preflightSpinner.stop("Saved token check finished");
|
|
5038
|
+
}
|
|
5039
|
+
return result;
|
|
5040
|
+
}
|
|
5041
|
+
function describeExistingTokenStatus(preflight) {
|
|
5042
|
+
if (!preflight) {
|
|
5043
|
+
return "already linked to your Agent Approve account";
|
|
5044
|
+
}
|
|
5045
|
+
switch (preflight.kind) {
|
|
5046
|
+
case "valid":
|
|
5047
|
+
return "verified - linked to your Agent Approve account";
|
|
5048
|
+
case "expired":
|
|
5049
|
+
return "expired (refresh required)";
|
|
5050
|
+
case "invalid":
|
|
5051
|
+
return "invalid (re-pair required)";
|
|
5052
|
+
case "scope_denied":
|
|
5053
|
+
return "scope denied (re-pair required)";
|
|
5054
|
+
case "unavailable":
|
|
5055
|
+
return "validation temporarily unavailable";
|
|
5056
|
+
case "network_blocked":
|
|
5057
|
+
return "cannot reach service from this network";
|
|
5058
|
+
case "network_error":
|
|
5059
|
+
return "cannot reach Agent Approve from this network";
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
async function promptTokenRecoveryAction(preflight) {
|
|
5063
|
+
if (preflight.kind === "expired") {
|
|
5064
|
+
const choice2 = await le({
|
|
5065
|
+
message: "Your saved token has expired. How would you like to continue?",
|
|
5066
|
+
options: [
|
|
5067
|
+
{ value: "refresh", label: "Refresh token", hint: "Keeps the existing encryption key and current settings" },
|
|
5068
|
+
{ value: "repair", label: "Pair this computer again", hint: "You can rotate the encryption key" },
|
|
5069
|
+
{ value: "cancel", label: "Cancel", hint: "Exit the installer" }
|
|
5070
|
+
]
|
|
5071
|
+
});
|
|
5072
|
+
if (lD(choice2))
|
|
5073
|
+
return "cancel";
|
|
5074
|
+
return choice2;
|
|
5075
|
+
}
|
|
5076
|
+
const choice = await le({
|
|
5077
|
+
message: "Your saved token is no longer valid. How would you like to continue?",
|
|
5078
|
+
options: [
|
|
5079
|
+
{ value: "repair", label: "Pair this computer again" },
|
|
5080
|
+
{ value: "cancel", label: "Cancel", hint: "Exit the installer" }
|
|
5081
|
+
]
|
|
5082
|
+
});
|
|
5083
|
+
if (lD(choice))
|
|
5084
|
+
return "cancel";
|
|
5085
|
+
return choice;
|
|
5086
|
+
}
|
|
4813
5087
|
async function installCommand() {
|
|
4814
5088
|
console.clear();
|
|
4815
5089
|
pe(source_default.bgCyan.black(" Agent Approve ") + source_default.gray(` Hooks Installer v${VERSION}`));
|
|
4816
5090
|
migrateE2ERootKey();
|
|
4817
|
-
const isCustomApi = !API_URL.includes("agentapprove.com");
|
|
4818
|
-
if (isCustomApi) {
|
|
4819
|
-
v2.warn(`Using custom API: ${API_URL}`);
|
|
4820
|
-
}
|
|
4821
5091
|
await checkSystemDependencies();
|
|
4822
5092
|
const existingConfig = readExistingConfig();
|
|
5093
|
+
const pairingApiBase = resolvePairingApiBaseUrl({
|
|
5094
|
+
agentapproveApiEnv: process.env.AGENTAPPROVE_API,
|
|
5095
|
+
savedApiUrl: existingConfig?.apiUrl,
|
|
5096
|
+
processDefaultApiUrl: API_URL
|
|
5097
|
+
});
|
|
5098
|
+
const isCustomApi = !looksLikeHostedAgentApproveApi(pairingApiBase);
|
|
5099
|
+
if (isCustomApi) {
|
|
5100
|
+
v2.warn(`Using custom API: ${pairingApiBase}`);
|
|
5101
|
+
}
|
|
4823
5102
|
const hasExistingToken = !!(existingConfig?.token && existingConfig.token.length > 10);
|
|
4824
5103
|
let existingTokenPreview = "Not set";
|
|
4825
5104
|
let existingKeyId = null;
|
|
@@ -4831,6 +5110,12 @@ async function installCommand() {
|
|
|
4831
5110
|
existingKeyId = createHash("sha256").update(Buffer.from(keyHex, "hex")).digest("hex").slice(0, 8);
|
|
4832
5111
|
}
|
|
4833
5112
|
}
|
|
5113
|
+
const existingTokenPreflight = hasExistingToken ? await runExistingTokenPreflight(existingConfig.token, pairingApiBase) : null;
|
|
5114
|
+
if (existingTokenPreflight?.kind === "network_blocked") {
|
|
5115
|
+
v2.error("Service temporarily unavailable. Try again later.");
|
|
5116
|
+
he("Installation cancelled");
|
|
5117
|
+
process.exit(1);
|
|
5118
|
+
}
|
|
4834
5119
|
me(`Approve AI agent actions from your iPhone or Apple Watch.
|
|
4835
5120
|
Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw, and more.`, "About");
|
|
4836
5121
|
const installedAgents = detectInstalledAgents();
|
|
@@ -4850,7 +5135,7 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
4850
5135
|
process.exit(0);
|
|
4851
5136
|
}
|
|
4852
5137
|
const setupProfileSummary = existingConfig ? formatSetupProfileBlock("Existing config", getInitialInstallConfig("existing-config", existingConfig), [
|
|
4853
|
-
`Connection token: ${existingTokenPreview} -
|
|
5138
|
+
`Connection token: ${existingTokenPreview} - ${describeExistingTokenStatus(existingTokenPreflight)}.`,
|
|
4854
5139
|
...existingKeyId ? [`Encryption key ID: ${existingKeyId} - already installed for this computer.`] : []
|
|
4855
5140
|
]) : formatSetupProfileBlock("Recommended setup", getInitialInstallConfig("recommended", existingConfig));
|
|
4856
5141
|
me(setupProfileSummary, "Setup profiles");
|
|
@@ -4862,13 +5147,23 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
4862
5147
|
he("Installation cancelled");
|
|
4863
5148
|
process.exit(0);
|
|
4864
5149
|
}
|
|
5150
|
+
let pendingRecoveryAction = null;
|
|
5151
|
+
if (hasExistingToken && existingTokenPreflight && (existingTokenPreflight.kind === "expired" || existingTokenPreflight.kind === "invalid" || existingTokenPreflight.kind === "scope_denied")) {
|
|
5152
|
+
pendingRecoveryAction = await promptTokenRecoveryAction(existingTokenPreflight);
|
|
5153
|
+
if (pendingRecoveryAction === "cancel") {
|
|
5154
|
+
he("Installation cancelled");
|
|
5155
|
+
process.exit(0);
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
const requiresFreshPair = pendingRecoveryAction !== null;
|
|
5159
|
+
const forceKeyReuseForRecovery = pendingRecoveryAction === "refresh";
|
|
4865
5160
|
ensureAgentApproveDir();
|
|
4866
5161
|
const hooksDir = join(getAgentApproveDir(), "hooks");
|
|
4867
5162
|
const selectedInstallConfig = getInitialInstallConfig(setupProfile, existingConfig);
|
|
4868
5163
|
let token = null;
|
|
4869
5164
|
let finalPrivacy = selectedInstallConfig.privacy;
|
|
4870
5165
|
let email = "";
|
|
4871
|
-
let apiUrl =
|
|
5166
|
+
let apiUrl = pairingApiBase;
|
|
4872
5167
|
let debugLog = selectedInstallConfig.debugLog;
|
|
4873
5168
|
let retentionDays = selectedInstallConfig.retentionDays;
|
|
4874
5169
|
let failBehavior = selectedInstallConfig.failBehavior;
|
|
@@ -4956,7 +5251,8 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
4956
5251
|
if (existsSync2(existingKeyPath)) {
|
|
4957
5252
|
const oldKeyHex = readFileSync(existingKeyPath, "utf-8").trim();
|
|
4958
5253
|
const oldKeyId = createHash("sha256").update(Buffer.from(oldKeyHex, "hex")).digest("hex").slice(0, 8);
|
|
4959
|
-
|
|
5254
|
+
const autoReuseKey = forceKeyReuseForRecovery || setupProfile === "existing-config" && !requiresFreshPair;
|
|
5255
|
+
if (autoReuseKey) {
|
|
4960
5256
|
e2eUserKey = oldKeyHex;
|
|
4961
5257
|
} else {
|
|
4962
5258
|
v2.info(`Existing E2E key found (Key ID: ${oldKeyId})`);
|
|
@@ -5014,9 +5310,10 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5014
5310
|
}
|
|
5015
5311
|
debugLog = debugLogChoice;
|
|
5016
5312
|
}
|
|
5017
|
-
const
|
|
5313
|
+
const canReuseExistingToken = hasExistingToken && !requiresFreshPair;
|
|
5314
|
+
const silentlyReuseExistingToken = setupProfile === "existing-config" && canReuseExistingToken;
|
|
5018
5315
|
const connectionOptions = [];
|
|
5019
|
-
if (
|
|
5316
|
+
if (canReuseExistingToken) {
|
|
5020
5317
|
const tokenPreview = existingConfig.token.slice(0, 15) + "...";
|
|
5021
5318
|
connectionOptions.push({
|
|
5022
5319
|
value: "existing",
|
|
@@ -5024,10 +5321,20 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5024
5321
|
hint: tokenPreview
|
|
5025
5322
|
});
|
|
5026
5323
|
}
|
|
5027
|
-
connectionOptions.push({ value: "qr", label: "Scan QR code", hint:
|
|
5324
|
+
connectionOptions.push({ value: "qr", label: "Scan QR code", hint: canReuseExistingToken ? undefined : "Recommended" }, { value: "copy", label: "Copy and paste code", hint: "No camera" });
|
|
5028
5325
|
let connectionMethod;
|
|
5029
5326
|
if (silentlyReuseExistingToken) {
|
|
5030
5327
|
connectionMethod = "existing";
|
|
5328
|
+
} else if (requiresFreshPair) {
|
|
5329
|
+
const connectionChoice = await le({
|
|
5330
|
+
message: pendingRecoveryAction === "refresh" ? "How would you like to refresh the token?" : "How would you like to pair this computer?",
|
|
5331
|
+
options: connectionOptions
|
|
5332
|
+
});
|
|
5333
|
+
if (lD(connectionChoice)) {
|
|
5334
|
+
he("Installation cancelled");
|
|
5335
|
+
process.exit(0);
|
|
5336
|
+
}
|
|
5337
|
+
connectionMethod = connectionChoice;
|
|
5031
5338
|
} else {
|
|
5032
5339
|
const connectionChoice = await le({
|
|
5033
5340
|
message: "Connect to iOS app (required for setup and any hook downloads)",
|
|
@@ -5041,7 +5348,7 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5041
5348
|
}
|
|
5042
5349
|
if (connectionMethod === "existing") {
|
|
5043
5350
|
token = existingConfig.token;
|
|
5044
|
-
apiUrl =
|
|
5351
|
+
apiUrl = pairingApiBase;
|
|
5045
5352
|
if (setupProfile === "existing-config" && existingConfig?.configSetAt) {
|
|
5046
5353
|
configSetAt = existingConfig.configSetAt;
|
|
5047
5354
|
}
|
|
@@ -5067,7 +5374,7 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5067
5374
|
configSetAt,
|
|
5068
5375
|
e2eEnabled: useE2E
|
|
5069
5376
|
};
|
|
5070
|
-
const session = await createPairingSession(selectedAgents, e2eKeyId);
|
|
5377
|
+
const session = await createPairingSession(selectedAgents, e2eKeyId, pairingApiBase);
|
|
5071
5378
|
if (!session || session.error) {
|
|
5072
5379
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
5073
5380
|
he("Cannot continue without token");
|
|
@@ -5093,7 +5400,7 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5093
5400
|
const minutes = Math.floor(expiresIn / 60);
|
|
5094
5401
|
const seconds = expiresIn % 60;
|
|
5095
5402
|
pairingSpinner.message(`Waiting for iOS app... ${minutes}:${seconds.toString().padStart(2, "0")}`);
|
|
5096
|
-
}, () => {});
|
|
5403
|
+
}, () => {}, pairingApiBase);
|
|
5097
5404
|
if (result === "cancelled") {
|
|
5098
5405
|
pairingSpinner.stop("Cancelled");
|
|
5099
5406
|
he("Installation cancelled");
|
|
@@ -5102,6 +5409,7 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5102
5409
|
token = result.token;
|
|
5103
5410
|
finalPrivacy = result.privacy;
|
|
5104
5411
|
email = result.email;
|
|
5412
|
+
apiUrl = result.apiUrl || pairingApiBase;
|
|
5105
5413
|
if (e2eUserKey && e2eKeyId) {
|
|
5106
5414
|
const rootKeyPath = join(agentApproveDir, "e2e-root-key");
|
|
5107
5415
|
writeFileSync(rootKeyPath, e2eUserKey, { mode: 384 });
|
|
@@ -5152,9 +5460,15 @@ Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw
|
|
|
5152
5460
|
if (hookDownloadPlan.files.length > 0) {
|
|
5153
5461
|
const downloadSpinner = _2();
|
|
5154
5462
|
downloadSpinner.start("Downloading hook scripts");
|
|
5155
|
-
const downloadResult = await
|
|
5463
|
+
const downloadResult = await copyHookScripts2(hooksDir, token, hookDownloadPlan.files, { apiUrl });
|
|
5156
5464
|
const summary = formatHookDownloadSummary(hookDownloadPlan);
|
|
5157
|
-
if (downloadResult.
|
|
5465
|
+
if (downloadResult.terminalFailure) {
|
|
5466
|
+
downloadSpinner.stop(`Downloaded ${downloadResult.downloaded} of ${hookDownloadPlan.files.length} hook files (${summary})`);
|
|
5467
|
+
const message = describeDownloadTerminalFailure(downloadResult.terminalFailure.kind);
|
|
5468
|
+
v2.error(message);
|
|
5469
|
+
he("Hook download stopped before completion.");
|
|
5470
|
+
process.exit(1);
|
|
5471
|
+
} else if (downloadResult.failed.length > 0) {
|
|
5158
5472
|
downloadSpinner.stop(`Downloaded ${downloadResult.downloaded} of ${hookDownloadPlan.files.length} hook files (${summary})`);
|
|
5159
5473
|
v2.warn(`Failed to download: ${downloadResult.failed.join(", ")}`);
|
|
5160
5474
|
} else {
|
|
@@ -5697,10 +6011,15 @@ async function refreshCommand() {
|
|
|
5697
6011
|
v2.error('No existing configuration found. Run "npx agentapprove install" first.');
|
|
5698
6012
|
process.exit(1);
|
|
5699
6013
|
}
|
|
6014
|
+
const pairingApiBase = resolvePairingApiBaseUrl({
|
|
6015
|
+
agentapproveApiEnv: process.env.AGENTAPPROVE_API,
|
|
6016
|
+
savedApiUrl: existingConfig?.apiUrl,
|
|
6017
|
+
processDefaultApiUrl: API_URL
|
|
6018
|
+
});
|
|
5700
6019
|
me(`Your API token has expired (30 days). Tokens extend automatically on use,
|
|
5701
6020
|
but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
|
|
5702
6021
|
let token = null;
|
|
5703
|
-
let apiUrl =
|
|
6022
|
+
let apiUrl = pairingApiBase;
|
|
5704
6023
|
const installedAgents = detectInstalledAgents();
|
|
5705
6024
|
const e2eEnabled = existingConfig.e2eEnabled !== false;
|
|
5706
6025
|
const e2eKeyPath = join(getAgentApproveDir(), "e2e-key");
|
|
@@ -5727,7 +6046,7 @@ but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
|
|
|
5727
6046
|
configSetAt,
|
|
5728
6047
|
e2eEnabled
|
|
5729
6048
|
};
|
|
5730
|
-
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
|
|
6049
|
+
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId, pairingApiBase);
|
|
5731
6050
|
if (!session || session.error) {
|
|
5732
6051
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
5733
6052
|
process.exit(1);
|
|
@@ -5753,13 +6072,14 @@ but if unused for 30 days they expire. Get a new one below.`, "Token Expired");
|
|
|
5753
6072
|
const minutes = Math.floor(expiresIn / 60);
|
|
5754
6073
|
const seconds = expiresIn % 60;
|
|
5755
6074
|
pairingSpinner.message(`Waiting for iOS app... ${minutes}:${seconds.toString().padStart(2, "0")}`);
|
|
5756
|
-
}, () => {});
|
|
6075
|
+
}, () => {}, pairingApiBase);
|
|
5757
6076
|
if (result === "cancelled") {
|
|
5758
6077
|
pairingSpinner.stop("Cancelled");
|
|
5759
6078
|
he("Refresh cancelled");
|
|
5760
6079
|
process.exit(0);
|
|
5761
6080
|
} else if (result) {
|
|
5762
6081
|
token = result.token;
|
|
6082
|
+
apiUrl = result.apiUrl || pairingApiBase;
|
|
5763
6083
|
pairingSpinner.stop(`Connected! ${result.email ? `Account: ${result.email}` : ""}`);
|
|
5764
6084
|
} else {
|
|
5765
6085
|
pairingSpinner.stop("Session expired");
|
|
@@ -5797,6 +6117,11 @@ async function pairCommand() {
|
|
|
5797
6117
|
console.clear();
|
|
5798
6118
|
pe(source_default.bgCyan.black(" Agent Approve ") + source_default.gray(" Device Pairing"));
|
|
5799
6119
|
const existingConfig = readExistingConfig();
|
|
6120
|
+
const pairingApiBase = resolvePairingApiBaseUrl({
|
|
6121
|
+
agentapproveApiEnv: process.env.AGENTAPPROVE_API,
|
|
6122
|
+
savedApiUrl: existingConfig?.apiUrl,
|
|
6123
|
+
processDefaultApiUrl: API_URL
|
|
6124
|
+
});
|
|
5800
6125
|
const keys = discoverE2EKeys();
|
|
5801
6126
|
if (keys.length === 0 && !existingConfig) {
|
|
5802
6127
|
v2.error("No E2E keys or configuration found.");
|
|
@@ -5887,7 +6212,7 @@ async function pairCommand() {
|
|
|
5887
6212
|
configSetAt: pairingConfigSetAt,
|
|
5888
6213
|
e2eEnabled: !!e2eUserKey
|
|
5889
6214
|
};
|
|
5890
|
-
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId);
|
|
6215
|
+
const session = await createPairingSession(installedAgents.length > 0 ? installedAgents : undefined, e2eKeyId, pairingApiBase);
|
|
5891
6216
|
if (!session || session.error) {
|
|
5892
6217
|
v2.error(`Failed to create pairing session: ${session?.error || "Unknown error"}`);
|
|
5893
6218
|
process.exit(1);
|
|
@@ -5913,7 +6238,7 @@ async function pairCommand() {
|
|
|
5913
6238
|
const minutes = Math.floor(expiresIn / 60);
|
|
5914
6239
|
const seconds = expiresIn % 60;
|
|
5915
6240
|
pairingSpinner.message(`Waiting for iOS app... ${minutes}:${seconds.toString().padStart(2, "0")}`);
|
|
5916
|
-
}, () => {});
|
|
6241
|
+
}, () => {}, pairingApiBase);
|
|
5917
6242
|
if (result === "cancelled") {
|
|
5918
6243
|
pairingSpinner.stop("Cancelled");
|
|
5919
6244
|
he("Pairing cancelled");
|
|
@@ -5943,7 +6268,7 @@ async function pairCommand() {
|
|
|
5943
6268
|
writeFileSync(serverKeyPath, result.e2eServerKey, { mode: 384 });
|
|
5944
6269
|
}
|
|
5945
6270
|
}
|
|
5946
|
-
const apiUrl =
|
|
6271
|
+
const apiUrl = result.apiUrl || pairingApiBase;
|
|
5947
6272
|
const configSetAt = Math.floor(Date.now() / 1000);
|
|
5948
6273
|
saveEnvConfig({
|
|
5949
6274
|
apiUrl,
|