agentapprove 0.1.21 → 0.1.22
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 +593 -14
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2313,9 +2313,9 @@ var source_default = chalk;
|
|
|
2313
2313
|
|
|
2314
2314
|
// src/cli.ts
|
|
2315
2315
|
var import_qrcode_terminal = __toESM(require_main(), 1);
|
|
2316
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, renameSync, readdirSync as readdirSync2, statSync, lstatSync, realpathSync as realpathSync2, rmSync as rmSync2 } from "fs";
|
|
2317
|
-
import { homedir, hostname, platform } from "os";
|
|
2318
|
-
import { join, dirname as dirname2 } from "path";
|
|
2316
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, readdirSync as readdirSync2, statSync, lstatSync, realpathSync as realpathSync2, rmSync as rmSync2 } from "fs";
|
|
2317
|
+
import { homedir, hostname, platform, tmpdir } from "os";
|
|
2318
|
+
import { join, dirname as dirname2, sep as sep2 } from "path";
|
|
2319
2319
|
import { execSync, spawnSync } from "child_process";
|
|
2320
2320
|
import { randomBytes, createHash } from "crypto";
|
|
2321
2321
|
|
|
@@ -2827,7 +2827,7 @@ function resolvePairingApiBaseUrl(options) {
|
|
|
2827
2827
|
}
|
|
2828
2828
|
|
|
2829
2829
|
// src/cli.ts
|
|
2830
|
-
var VERSION = "0.1.
|
|
2830
|
+
var VERSION = "0.1.22";
|
|
2831
2831
|
function getApiUrl() {
|
|
2832
2832
|
return process.env.AGENTAPPROVE_API || "https://api.agentapprove.com";
|
|
2833
2833
|
}
|
|
@@ -2903,9 +2903,11 @@ var OPENCODE_PLUGIN_VERSION = "0.1.18";
|
|
|
2903
2903
|
var OPENCODE_PLUGIN_SPEC = `@agentapprove/opencode@${OPENCODE_PLUGIN_VERSION}`;
|
|
2904
2904
|
var OPENCLAW_PLUGIN_VERSION = "0.2.11";
|
|
2905
2905
|
var OPENCLAW_PLUGIN_SPEC = `@agentapprove/openclaw@${OPENCLAW_PLUGIN_VERSION}`;
|
|
2906
|
-
var PI_PLUGIN_VERSION = "0.1.
|
|
2906
|
+
var PI_PLUGIN_VERSION = "0.1.7";
|
|
2907
2907
|
var PI_PLUGIN_SPEC = `npm:@agentapprove/pi@${PI_PLUGIN_VERSION}`;
|
|
2908
2908
|
var PI_STATUS_TIMEOUT_MS = 5000;
|
|
2909
|
+
var HERMES_PLUGIN_VERSION = "0.1.0";
|
|
2910
|
+
var HERMES_PIP_DEPS = ["cryptography>=44.0,<46", "httpx>=0.27,<0.29"];
|
|
2909
2911
|
var AGENTS = {
|
|
2910
2912
|
"claude-code": {
|
|
2911
2913
|
name: "Claude Code",
|
|
@@ -2944,6 +2946,23 @@ var AGENTS = {
|
|
|
2944
2946
|
{ name: "afterAgentResponse", file: "cursor-response.sh", description: "Agent response" }
|
|
2945
2947
|
]
|
|
2946
2948
|
},
|
|
2949
|
+
windsurf: {
|
|
2950
|
+
name: "Windsurf",
|
|
2951
|
+
configPath: join(homedir(), ".codeium", "windsurf", "hooks.json"),
|
|
2952
|
+
hooksKey: "hooks",
|
|
2953
|
+
hooks: [
|
|
2954
|
+
{ name: "pre_user_prompt", file: "windsurf-hook.sh", description: "User prompt (logging only)" },
|
|
2955
|
+
{ name: "pre_read_code", file: "windsurf-hook.sh", description: "File read approval", isApprovalHook: true },
|
|
2956
|
+
{ name: "post_read_code", file: "windsurf-hook.sh", description: "File read completion logging" },
|
|
2957
|
+
{ name: "pre_write_code", file: "windsurf-hook.sh", description: "File write approval", isApprovalHook: true },
|
|
2958
|
+
{ name: "post_write_code", file: "windsurf-hook.sh", description: "File write completion logging" },
|
|
2959
|
+
{ name: "pre_run_command", file: "windsurf-hook.sh", description: "Shell command approval", isApprovalHook: true },
|
|
2960
|
+
{ name: "post_run_command", file: "windsurf-hook.sh", description: "Shell command completion logging" },
|
|
2961
|
+
{ name: "pre_mcp_tool_use", file: "windsurf-hook.sh", description: "MCP tool approval", isApprovalHook: true },
|
|
2962
|
+
{ name: "post_mcp_tool_use", file: "windsurf-hook.sh", description: "MCP tool completion logging" },
|
|
2963
|
+
{ name: "post_cascade_response", file: "windsurf-hook.sh", description: "Cascade response (logging)" }
|
|
2964
|
+
]
|
|
2965
|
+
},
|
|
2947
2966
|
"gemini-cli": {
|
|
2948
2967
|
name: "Gemini CLI",
|
|
2949
2968
|
configPath: join(homedir(), ".gemini", "settings.json"),
|
|
@@ -3023,6 +3042,27 @@ var AGENTS = {
|
|
|
3023
3042
|
hooks: [
|
|
3024
3043
|
{ name: "agentapprove", file: "@agentapprove/pi", description: "Tool approval + event monitoring", isApprovalHook: true, isPlugin: true }
|
|
3025
3044
|
]
|
|
3045
|
+
},
|
|
3046
|
+
hermes: {
|
|
3047
|
+
name: "Hermes",
|
|
3048
|
+
configPath: join(homedir(), ".hermes", "config.yaml"),
|
|
3049
|
+
hooksKey: "plugins",
|
|
3050
|
+
hooks: [
|
|
3051
|
+
{ name: "agentapprove", file: "agentapprove", description: "Tool approval + event monitoring + follow-up input", isApprovalHook: true, isPlugin: true }
|
|
3052
|
+
]
|
|
3053
|
+
},
|
|
3054
|
+
openhands: {
|
|
3055
|
+
name: "OpenHands",
|
|
3056
|
+
configPath: join(homedir(), ".openhands", "hooks.json"),
|
|
3057
|
+
hooksKey: "",
|
|
3058
|
+
hooks: [
|
|
3059
|
+
{ name: "session_start", file: "openhands-hook.sh", description: "Session started" },
|
|
3060
|
+
{ name: "user_prompt_submit", file: "openhands-hook.sh", description: "User prompt (logging only)" },
|
|
3061
|
+
{ name: "pre_tool_use", file: "openhands-hook.sh", description: "Tool approval", isApprovalHook: true, timeout: 300 },
|
|
3062
|
+
{ name: "post_tool_use", file: "openhands-hook.sh", description: "Tool completion logging" },
|
|
3063
|
+
{ name: "stop", file: "openhands-hook.sh", description: "Agent stopped (iOS input)", isApprovalHook: true, timeout: 600 },
|
|
3064
|
+
{ name: "session_end", file: "openhands-hook.sh", description: "Session ended" }
|
|
3065
|
+
]
|
|
3026
3066
|
}
|
|
3027
3067
|
};
|
|
3028
3068
|
var SHARED_HOOK_FILES = ["common.sh"];
|
|
@@ -3070,7 +3110,24 @@ var HOOK_STATUS_LABELS = {
|
|
|
3070
3110
|
userPromptSubmitted: "Prompt submitted",
|
|
3071
3111
|
errorOccurred: "Error",
|
|
3072
3112
|
SubagentStart: "Subagent start",
|
|
3073
|
-
SubagentStop: "Subagent stop"
|
|
3113
|
+
SubagentStop: "Subagent stop",
|
|
3114
|
+
pre_user_prompt: "User prompt",
|
|
3115
|
+
pre_read_code: "File read approval",
|
|
3116
|
+
post_read_code: "File read completed",
|
|
3117
|
+
pre_write_code: "File write approval",
|
|
3118
|
+
post_write_code: "File write completed",
|
|
3119
|
+
pre_run_command: "Shell approval",
|
|
3120
|
+
post_run_command: "Shell completed",
|
|
3121
|
+
pre_mcp_tool_use: "MCP approval",
|
|
3122
|
+
post_mcp_tool_use: "MCP completed",
|
|
3123
|
+
post_cascade_response: "Cascade response",
|
|
3124
|
+
pre_tool_call: "Tool approval",
|
|
3125
|
+
post_tool_call: "Tool completed",
|
|
3126
|
+
pre_llm_call: "Prompt submitted",
|
|
3127
|
+
post_llm_call: "Stop",
|
|
3128
|
+
on_session_start: "Session start",
|
|
3129
|
+
on_session_end: "Session end",
|
|
3130
|
+
subagent_stop: "Subagent stop"
|
|
3074
3131
|
};
|
|
3075
3132
|
function findGitBash() {
|
|
3076
3133
|
if (!isWindows())
|
|
@@ -3269,6 +3326,15 @@ function detectInstalledAgents() {
|
|
|
3269
3326
|
installed.push(id);
|
|
3270
3327
|
} catch {}
|
|
3271
3328
|
}
|
|
3329
|
+
} else if (id === "hermes") {
|
|
3330
|
+
if (existsSync2(join(homedir(), ".hermes"))) {
|
|
3331
|
+
installed.push(id);
|
|
3332
|
+
} else {
|
|
3333
|
+
try {
|
|
3334
|
+
execSync("hermes --version", { stdio: "ignore" });
|
|
3335
|
+
installed.push(id);
|
|
3336
|
+
} catch {}
|
|
3337
|
+
}
|
|
3272
3338
|
} else {
|
|
3273
3339
|
const configDir = dirname2(agent.configPath);
|
|
3274
3340
|
if (existsSync2(configDir)) {
|
|
@@ -4107,12 +4173,27 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
4107
4173
|
}
|
|
4108
4174
|
return { success: true, backupPath: null, hooks: [installResult.label] };
|
|
4109
4175
|
}
|
|
4176
|
+
if (agentId === "hermes") {
|
|
4177
|
+
if (mode === "observe") {
|
|
4178
|
+
const disableResult = disableHermesPluginViaCli();
|
|
4179
|
+
if (!disableResult.success) {
|
|
4180
|
+
return { success: false, backupPath: null, hooks: [], error: disableResult.error };
|
|
4181
|
+
}
|
|
4182
|
+
return { success: true, backupPath: null, hooks: [] };
|
|
4183
|
+
}
|
|
4184
|
+
const installResult = await installHermesPluginViaCli();
|
|
4185
|
+
if (!installResult.success) {
|
|
4186
|
+
return { success: false, backupPath: null, hooks: [], error: installResult.error };
|
|
4187
|
+
}
|
|
4188
|
+
return { success: true, backupPath: null, hooks: [installResult.label] };
|
|
4189
|
+
}
|
|
4110
4190
|
const backupPath = backupConfig(agent.configPath);
|
|
4111
4191
|
const config = readJsonConfig(agent.configPath);
|
|
4112
|
-
|
|
4192
|
+
const useTopLevelHooks = agent.hooksKey === "";
|
|
4193
|
+
if (!useTopLevelHooks && !config[agent.hooksKey]) {
|
|
4113
4194
|
config[agent.hooksKey] = {};
|
|
4114
4195
|
}
|
|
4115
|
-
const hooksConfig = config[agent.hooksKey];
|
|
4196
|
+
const hooksConfig = useTopLevelHooks ? config : config[agent.hooksKey];
|
|
4116
4197
|
const installedHooks = [];
|
|
4117
4198
|
if (agentId === "openclaw") {
|
|
4118
4199
|
const installResult = installOpenClawPluginViaCli();
|
|
@@ -4292,6 +4373,19 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
4292
4373
|
}
|
|
4293
4374
|
}
|
|
4294
4375
|
installedHooks.push(hook.name);
|
|
4376
|
+
} else if (agentId === "windsurf") {
|
|
4377
|
+
const existing = hooksConfig[hook.name];
|
|
4378
|
+
const existingArray = Array.isArray(existing) ? existing : existing && typeof existing === "object" ? [existing] : typeof existing === "string" ? [{ command: existing }] : [];
|
|
4379
|
+
const cleanedArray = existingArray.filter((h2) => {
|
|
4380
|
+
const command = h2.command || h2.powershell || "";
|
|
4381
|
+
return !command.includes("agentapprove");
|
|
4382
|
+
});
|
|
4383
|
+
cleanedArray.push({
|
|
4384
|
+
command: hookCommand,
|
|
4385
|
+
show_output: false
|
|
4386
|
+
});
|
|
4387
|
+
hooksConfig[hook.name] = cleanedArray;
|
|
4388
|
+
installedHooks.push(hook.name);
|
|
4295
4389
|
} else if (agentId === "gemini-cli") {
|
|
4296
4390
|
if (!hooksConfig[hook.name]) {
|
|
4297
4391
|
hooksConfig[hook.name] = [];
|
|
@@ -4340,6 +4434,41 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
4340
4434
|
cleanedArray.push(hookEntry);
|
|
4341
4435
|
hooksConfig[hook.name] = cleanedArray;
|
|
4342
4436
|
installedHooks.push(hook.name);
|
|
4437
|
+
} else if (agentId === "openhands") {
|
|
4438
|
+
if (!hooksConfig[hook.name]) {
|
|
4439
|
+
hooksConfig[hook.name] = [];
|
|
4440
|
+
}
|
|
4441
|
+
const hookArray = hooksConfig[hook.name];
|
|
4442
|
+
const hookTimeout = hook.timeout;
|
|
4443
|
+
const isApproval = hook.isApprovalHook;
|
|
4444
|
+
const existingIdx = hookArray.findIndex((h2) => h2.hooks?.some((hookScript) => {
|
|
4445
|
+
if (typeof hookScript === "string")
|
|
4446
|
+
return hookScript.includes("agentapprove");
|
|
4447
|
+
if (typeof hookScript === "object" && hookScript.command)
|
|
4448
|
+
return hookScript.command.includes("agentapprove");
|
|
4449
|
+
return false;
|
|
4450
|
+
}));
|
|
4451
|
+
const hookObject = {
|
|
4452
|
+
type: "command",
|
|
4453
|
+
command: hookCommand
|
|
4454
|
+
};
|
|
4455
|
+
if (isApproval === true || hookTimeout !== undefined) {
|
|
4456
|
+
hookObject.timeout = hookTimeout ?? 300;
|
|
4457
|
+
hookObject.async = false;
|
|
4458
|
+
} else {
|
|
4459
|
+
hookObject.timeout = 30;
|
|
4460
|
+
hookObject.async = true;
|
|
4461
|
+
}
|
|
4462
|
+
const hookEntry = {
|
|
4463
|
+
matcher: "*",
|
|
4464
|
+
hooks: [hookObject]
|
|
4465
|
+
};
|
|
4466
|
+
if (existingIdx >= 0) {
|
|
4467
|
+
hookArray[existingIdx] = hookEntry;
|
|
4468
|
+
} else {
|
|
4469
|
+
hookArray.push(hookEntry);
|
|
4470
|
+
}
|
|
4471
|
+
installedHooks.push(hook.name);
|
|
4343
4472
|
} else if (agentId === "copilot-cli") {
|
|
4344
4473
|
if (typeof config["version"] !== "number") {
|
|
4345
4474
|
config["version"] = 1;
|
|
@@ -4372,7 +4501,7 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
4372
4501
|
const approvalHooks = agent.hooks.filter((h2) => h2.isApprovalHook);
|
|
4373
4502
|
for (const hook of approvalHooks) {
|
|
4374
4503
|
if (hooksConfig[hook.name]) {
|
|
4375
|
-
if (agentId === "claude-code" || agentId === "gemini-cli" || agentId === "codex") {
|
|
4504
|
+
if (agentId === "claude-code" || agentId === "gemini-cli" || agentId === "codex" || agentId === "openhands") {
|
|
4376
4505
|
const hookArray = hooksConfig[hook.name];
|
|
4377
4506
|
if (Array.isArray(hookArray)) {
|
|
4378
4507
|
const cleaned = hookArray.filter((h2) => {
|
|
@@ -4433,6 +4562,31 @@ async function installHooksForAgent(agentId, hooksDir, mode = "approval") {
|
|
|
4433
4562
|
hooksConfig[hook.name] = cleaned;
|
|
4434
4563
|
}
|
|
4435
4564
|
}
|
|
4565
|
+
} else if (agentId === "windsurf") {
|
|
4566
|
+
const existing = hooksConfig[hook.name];
|
|
4567
|
+
if (Array.isArray(existing)) {
|
|
4568
|
+
const cleaned = existing.filter((h2) => {
|
|
4569
|
+
if (typeof h2 === "object" && h2 !== null) {
|
|
4570
|
+
const cmd = h2.command || h2.powershell || "";
|
|
4571
|
+
return !cmd.includes("agentapprove");
|
|
4572
|
+
}
|
|
4573
|
+
if (typeof h2 === "string")
|
|
4574
|
+
return !h2.includes("agentapprove");
|
|
4575
|
+
return true;
|
|
4576
|
+
});
|
|
4577
|
+
if (cleaned.length === 0) {
|
|
4578
|
+
delete hooksConfig[hook.name];
|
|
4579
|
+
} else {
|
|
4580
|
+
hooksConfig[hook.name] = cleaned;
|
|
4581
|
+
}
|
|
4582
|
+
} else if (typeof existing === "object" && existing !== null) {
|
|
4583
|
+
const cmd = existing.command || existing.powershell || "";
|
|
4584
|
+
if (cmd.includes("agentapprove")) {
|
|
4585
|
+
delete hooksConfig[hook.name];
|
|
4586
|
+
}
|
|
4587
|
+
} else if (typeof existing === "string" && existing.includes("agentapprove")) {
|
|
4588
|
+
delete hooksConfig[hook.name];
|
|
4589
|
+
}
|
|
4436
4590
|
} else if (agentId === "openclaw") {
|
|
4437
4591
|
const pluginsObj = hooksConfig;
|
|
4438
4592
|
const entries = pluginsObj?.entries;
|
|
@@ -4510,6 +4664,14 @@ codex_hooks = true`;
|
|
|
4510
4664
|
hooksObj[hook.name] = [entry];
|
|
4511
4665
|
}
|
|
4512
4666
|
return JSON.stringify({ version: 1, hooks: hooksObj }, null, 2);
|
|
4667
|
+
} else if (agentId === "windsurf") {
|
|
4668
|
+
const hooksObj = {};
|
|
4669
|
+
for (const hook of agent.hooks) {
|
|
4670
|
+
const hookPath = join(hooksDir, hook.file);
|
|
4671
|
+
const hookCmd = buildHookCommand(hookPath, agentId);
|
|
4672
|
+
hooksObj[hook.name] = [{ command: hookCmd, show_output: false }];
|
|
4673
|
+
}
|
|
4674
|
+
return JSON.stringify({ hooks: hooksObj }, null, 2);
|
|
4513
4675
|
} else if (agentId === "gemini-cli") {
|
|
4514
4676
|
const hooksObj = {};
|
|
4515
4677
|
for (const hook of agent.hooks) {
|
|
@@ -4557,6 +4719,23 @@ codex_hooks = true`;
|
|
|
4557
4719
|
hooksObj[hook.name] = [entry];
|
|
4558
4720
|
}
|
|
4559
4721
|
return JSON.stringify({ version: 1, hooks: hooksObj }, null, 2);
|
|
4722
|
+
} else if (agentId === "openhands") {
|
|
4723
|
+
const hooksObj = {};
|
|
4724
|
+
for (const hook of agent.hooks) {
|
|
4725
|
+
const hookPath = join(hooksDir, hook.file);
|
|
4726
|
+
const hookCmd = buildHookCommand(hookPath, agentId);
|
|
4727
|
+
const hookTimeout = hook.timeout;
|
|
4728
|
+
const isApproval = hook.isApprovalHook;
|
|
4729
|
+
const blocking = isApproval === true || hookTimeout !== undefined;
|
|
4730
|
+
const hookObject = {
|
|
4731
|
+
type: "command",
|
|
4732
|
+
command: hookCmd,
|
|
4733
|
+
timeout: blocking ? hookTimeout ?? 300 : 30,
|
|
4734
|
+
async: !blocking
|
|
4735
|
+
};
|
|
4736
|
+
hooksObj[hook.name] = [{ matcher: "*", hooks: [hookObject] }];
|
|
4737
|
+
}
|
|
4738
|
+
return JSON.stringify(hooksObj, null, 2);
|
|
4560
4739
|
} else if (agentId === "openclaw") {
|
|
4561
4740
|
const configJson = JSON.stringify({
|
|
4562
4741
|
plugins: {
|
|
@@ -4620,6 +4799,25 @@ codex_hooks = true`;
|
|
|
4620
4799
|
"",
|
|
4621
4800
|
"Restart Pi to activate the extension."
|
|
4622
4801
|
].join(`
|
|
4802
|
+
`);
|
|
4803
|
+
} else if (agentId === "hermes") {
|
|
4804
|
+
return [
|
|
4805
|
+
"Install the Agent Approve plugin for Hermes:",
|
|
4806
|
+
"",
|
|
4807
|
+
" npx agentapprove # automatic: download + verify + extract + enable",
|
|
4808
|
+
"",
|
|
4809
|
+
`Manual: download ${HERMES_BUNDLE_FILENAME} from your Agent Approve API,`,
|
|
4810
|
+
`verify SHA-256 matches ${HERMES_BUNDLE_SHA256.slice(0, 16)}…, extract into`,
|
|
4811
|
+
`~/.hermes/plugins/agentapprove/, write the SHA-256 to a .bundle-hash sidecar`,
|
|
4812
|
+
"in that directory, then:",
|
|
4813
|
+
"",
|
|
4814
|
+
" pip install --user cryptography httpx",
|
|
4815
|
+
" hermes plugins enable agentapprove",
|
|
4816
|
+
"",
|
|
4817
|
+
"The plugin reads your existing Agent Approve config from ~/.agentapprove/env.",
|
|
4818
|
+
"",
|
|
4819
|
+
"Hermes loads the plugin on the next session — no restart needed."
|
|
4820
|
+
].join(`
|
|
4623
4821
|
`);
|
|
4624
4822
|
}
|
|
4625
4823
|
return "";
|
|
@@ -4791,6 +4989,346 @@ function removePiPluginViaCli() {
|
|
|
4791
4989
|
};
|
|
4792
4990
|
}
|
|
4793
4991
|
}
|
|
4992
|
+
var HERMES_BUNDLE_SHA256 = "2572cec0a2d8467e9ffc2000e5c52cf652265d20638244a8a70e2e3a6c9d2734";
|
|
4993
|
+
var HERMES_BUNDLE_FILENAME = `agentapprove-hermes-${HERMES_PLUGIN_VERSION}.tar.gz`;
|
|
4994
|
+
var HERMES_PLUGIN_DIR = join(homedir(), ".hermes", "plugins", "agentapprove");
|
|
4995
|
+
function findPython() {
|
|
4996
|
+
for (const candidate of ["python3.12", "python3.11", "python3.10", "python3"]) {
|
|
4997
|
+
try {
|
|
4998
|
+
const out = execSync(`${candidate} -c "import sys; print('%d.%d' % sys.version_info[:2])"`, {
|
|
4999
|
+
encoding: "utf-8",
|
|
5000
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
5001
|
+
}).trim();
|
|
5002
|
+
const [major, minor] = out.split(".").map(Number);
|
|
5003
|
+
if (major === 3 && minor >= 10) {
|
|
5004
|
+
return { command: candidate, version: out };
|
|
5005
|
+
}
|
|
5006
|
+
} catch {
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
return null;
|
|
5011
|
+
}
|
|
5012
|
+
function isSafeApiUrl(candidate) {
|
|
5013
|
+
try {
|
|
5014
|
+
const parsed = new URL(candidate);
|
|
5015
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
5016
|
+
return false;
|
|
5017
|
+
if (!parsed.hostname)
|
|
5018
|
+
return false;
|
|
5019
|
+
return true;
|
|
5020
|
+
} catch {
|
|
5021
|
+
return false;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
function isSafeApiVersion(candidate) {
|
|
5025
|
+
return /^[a-z0-9-]+$/i.test(candidate);
|
|
5026
|
+
}
|
|
5027
|
+
function readInstallerConfig() {
|
|
5028
|
+
const envPath = join(getAgentApproveDir(), "env");
|
|
5029
|
+
let apiUrl = API_URL;
|
|
5030
|
+
let apiVersion = "v001";
|
|
5031
|
+
let token = "";
|
|
5032
|
+
if (existsSync2(envPath)) {
|
|
5033
|
+
try {
|
|
5034
|
+
const content = readFileSync(envPath, "utf-8");
|
|
5035
|
+
for (const line of content.split(`
|
|
5036
|
+
`)) {
|
|
5037
|
+
if (line.startsWith("#"))
|
|
5038
|
+
continue;
|
|
5039
|
+
const assignment = parseEnvAssignment(line);
|
|
5040
|
+
if (!assignment)
|
|
5041
|
+
continue;
|
|
5042
|
+
if (assignment.key === "AGENTAPPROVE_TOKEN") {
|
|
5043
|
+
token = assignment.value;
|
|
5044
|
+
} else if (assignment.key === "AGENTAPPROVE_API") {
|
|
5045
|
+
const normalized = assignment.value.replace(/\/+$/, "");
|
|
5046
|
+
if (isSafeApiUrl(normalized)) {
|
|
5047
|
+
apiUrl = normalized;
|
|
5048
|
+
}
|
|
5049
|
+
} else if (assignment.key === "AGENTAPPROVE_API_VERSION") {
|
|
5050
|
+
const normalized = assignment.value.replace(/^\/+|\/+$/g, "");
|
|
5051
|
+
if (isSafeApiVersion(normalized)) {
|
|
5052
|
+
apiVersion = normalized;
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
} catch {}
|
|
5057
|
+
}
|
|
5058
|
+
return { apiUrl: apiUrl.replace(/\/+$/, ""), apiVersion, token };
|
|
5059
|
+
}
|
|
5060
|
+
var HERMES_BUNDLE_MAX_BYTES = 10 * 1024 * 1024;
|
|
5061
|
+
async function downloadHermesBundle(apiUrl, apiVersion, token) {
|
|
5062
|
+
if (!isSafeApiUrl(apiUrl) || !isSafeApiVersion(apiVersion)) {
|
|
5063
|
+
return { ok: false, error: `Refusing to download bundle: invalid API URL or version in ~/.agentapprove/env` };
|
|
5064
|
+
}
|
|
5065
|
+
let url;
|
|
5066
|
+
try {
|
|
5067
|
+
url = new URL(`${apiVersion}/hooks/${encodeURIComponent(HERMES_BUNDLE_FILENAME)}`, apiUrl.endsWith("/") ? apiUrl : apiUrl + "/");
|
|
5068
|
+
} catch (err) {
|
|
5069
|
+
return { ok: false, error: `Could not build download URL: ${err instanceof Error ? err.message : String(err)}` };
|
|
5070
|
+
}
|
|
5071
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
5072
|
+
return { ok: false, error: `Refusing to download bundle: non-http(s) URL ${url.protocol}` };
|
|
5073
|
+
}
|
|
5074
|
+
let response;
|
|
5075
|
+
try {
|
|
5076
|
+
response = await fetch(url.toString(), {
|
|
5077
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
5078
|
+
});
|
|
5079
|
+
} catch (err) {
|
|
5080
|
+
return { ok: false, error: `Network error downloading ${HERMES_BUNDLE_FILENAME}: ${err instanceof Error ? err.message : String(err)}` };
|
|
5081
|
+
}
|
|
5082
|
+
if (!response.ok) {
|
|
5083
|
+
return { ok: false, error: `Bundle download returned ${response.status} from ${url}` };
|
|
5084
|
+
}
|
|
5085
|
+
const declared = parseInt(response.headers.get("content-length") || "", 10);
|
|
5086
|
+
if (Number.isFinite(declared) && declared > HERMES_BUNDLE_MAX_BYTES) {
|
|
5087
|
+
return { ok: false, error: `Bundle exceeds ${HERMES_BUNDLE_MAX_BYTES} byte limit (server reported ${declared}). Refusing to download.` };
|
|
5088
|
+
}
|
|
5089
|
+
try {
|
|
5090
|
+
const buffer = await response.arrayBuffer();
|
|
5091
|
+
if (buffer.byteLength > HERMES_BUNDLE_MAX_BYTES) {
|
|
5092
|
+
return { ok: false, error: `Bundle body exceeded ${HERMES_BUNDLE_MAX_BYTES} byte limit (got ${buffer.byteLength}).` };
|
|
5093
|
+
}
|
|
5094
|
+
return { ok: true, bytes: new Uint8Array(buffer) };
|
|
5095
|
+
} catch (err) {
|
|
5096
|
+
return { ok: false, error: `Failed to read bundle body: ${err instanceof Error ? err.message : String(err)}` };
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
function verifyBundleHash(bytes) {
|
|
5100
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
5101
|
+
}
|
|
5102
|
+
function validateExtractedTree(root) {
|
|
5103
|
+
const realRoot = realpathSync2(root);
|
|
5104
|
+
const stack = [root];
|
|
5105
|
+
while (stack.length > 0) {
|
|
5106
|
+
const dir = stack.pop();
|
|
5107
|
+
let entries;
|
|
5108
|
+
try {
|
|
5109
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
5110
|
+
} catch (err) {
|
|
5111
|
+
return { ok: false, error: `Cannot read staged dir ${dir}: ${err instanceof Error ? err.message : String(err)}` };
|
|
5112
|
+
}
|
|
5113
|
+
for (const entry of entries) {
|
|
5114
|
+
const fullPath = join(dir, String(entry.name));
|
|
5115
|
+
let resolved;
|
|
5116
|
+
try {
|
|
5117
|
+
resolved = realpathSync2(fullPath);
|
|
5118
|
+
} catch (err) {
|
|
5119
|
+
return { ok: false, error: `Cannot resolve ${fullPath}: ${err instanceof Error ? err.message : String(err)}` };
|
|
5120
|
+
}
|
|
5121
|
+
if (resolved !== realRoot && !resolved.startsWith(realRoot + sep2)) {
|
|
5122
|
+
return { ok: false, error: `Bundle entry escapes staging dir: ${String(entry.name)} → ${resolved}` };
|
|
5123
|
+
}
|
|
5124
|
+
if (!entry.isFile() && !entry.isDirectory()) {
|
|
5125
|
+
return { ok: false, error: `Bundle contains non-regular entry: ${String(entry.name)} (${entry.isSymbolicLink() ? "symlink" : "other"})` };
|
|
5126
|
+
}
|
|
5127
|
+
if (entry.isDirectory()) {
|
|
5128
|
+
stack.push(fullPath);
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
return { ok: true };
|
|
5133
|
+
}
|
|
5134
|
+
function hermesDepsAlreadyInstalled(pythonCommand) {
|
|
5135
|
+
try {
|
|
5136
|
+
execSync(`${pythonCommand} -c "import cryptography, httpx"`, {
|
|
5137
|
+
stdio: "pipe",
|
|
5138
|
+
timeout: 5000
|
|
5139
|
+
});
|
|
5140
|
+
return true;
|
|
5141
|
+
} catch {
|
|
5142
|
+
return false;
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
async function installHermesPluginViaCli() {
|
|
5146
|
+
try {
|
|
5147
|
+
execSync("hermes --version", { stdio: "pipe" });
|
|
5148
|
+
} catch {
|
|
5149
|
+
return {
|
|
5150
|
+
success: false,
|
|
5151
|
+
label: "Agent Approve plugin",
|
|
5152
|
+
error: "Hermes CLI not found on PATH. Install Hermes from https://hermes-agent.nousresearch.com first, then re-run npx agentapprove."
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
const python = findPython();
|
|
5156
|
+
if (!python) {
|
|
5157
|
+
return {
|
|
5158
|
+
success: false,
|
|
5159
|
+
label: "Agent Approve plugin",
|
|
5160
|
+
error: "Python 3.10+ not found on PATH. Install Python 3.10 or newer (e.g. `brew install python@3.12`) and re-run npx agentapprove."
|
|
5161
|
+
};
|
|
5162
|
+
}
|
|
5163
|
+
const cfg = readInstallerConfig();
|
|
5164
|
+
if (!cfg.token) {
|
|
5165
|
+
return {
|
|
5166
|
+
success: false,
|
|
5167
|
+
label: "Agent Approve plugin",
|
|
5168
|
+
error: "Missing API token. Pair your phone first by running `npx agentapprove` and completing the QR scan."
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
const download = await downloadHermesBundle(cfg.apiUrl, cfg.apiVersion, cfg.token);
|
|
5172
|
+
if (!download.ok) {
|
|
5173
|
+
return { success: false, label: "Agent Approve plugin", error: download.error };
|
|
5174
|
+
}
|
|
5175
|
+
const actualHash = verifyBundleHash(download.bytes);
|
|
5176
|
+
if (actualHash !== HERMES_BUNDLE_SHA256) {
|
|
5177
|
+
return {
|
|
5178
|
+
success: false,
|
|
5179
|
+
label: "Agent Approve plugin",
|
|
5180
|
+
error: `Bundle integrity check failed: expected ${HERMES_BUNDLE_SHA256.slice(0, 16)}…, got ${actualHash.slice(0, 16)}…. Refusing to install.`
|
|
5181
|
+
};
|
|
5182
|
+
}
|
|
5183
|
+
let tmpDir;
|
|
5184
|
+
try {
|
|
5185
|
+
tmpDir = mkdtempSync(join(tmpdir(), "aa-hermes-"));
|
|
5186
|
+
} catch (err) {
|
|
5187
|
+
return {
|
|
5188
|
+
success: false,
|
|
5189
|
+
label: "Agent Approve plugin",
|
|
5190
|
+
error: `Could not create temp directory for bundle staging: ${err instanceof Error ? err.message : String(err)}`
|
|
5191
|
+
};
|
|
5192
|
+
}
|
|
5193
|
+
const tarballPath = join(tmpDir, HERMES_BUNDLE_FILENAME);
|
|
5194
|
+
let backupPath = null;
|
|
5195
|
+
try {
|
|
5196
|
+
writeFileSync(tarballPath, download.bytes);
|
|
5197
|
+
try {
|
|
5198
|
+
execSync(`tar -xzf "${tarballPath}" -C "${tmpDir}" --no-same-owner --no-same-permissions`, { stdio: "pipe" });
|
|
5199
|
+
} catch (err) {
|
|
5200
|
+
return {
|
|
5201
|
+
success: false,
|
|
5202
|
+
label: "Agent Approve plugin",
|
|
5203
|
+
error: `tar extract failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
const stagedDir = join(tmpDir, "agentapprove");
|
|
5207
|
+
if (!existsSync2(stagedDir)) {
|
|
5208
|
+
return {
|
|
5209
|
+
success: false,
|
|
5210
|
+
label: "Agent Approve plugin",
|
|
5211
|
+
error: "Bundle did not contain expected `agentapprove/` directory."
|
|
5212
|
+
};
|
|
5213
|
+
}
|
|
5214
|
+
const validation = validateExtractedTree(stagedDir);
|
|
5215
|
+
if (!validation.ok) {
|
|
5216
|
+
return {
|
|
5217
|
+
success: false,
|
|
5218
|
+
label: "Agent Approve plugin",
|
|
5219
|
+
error: `Bundle integrity violation: ${validation.error}`
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
mkdirSync(dirname2(HERMES_PLUGIN_DIR), { recursive: true });
|
|
5223
|
+
if (existsSync2(HERMES_PLUGIN_DIR)) {
|
|
5224
|
+
backupPath = `${HERMES_PLUGIN_DIR}.bak-${Date.now()}`;
|
|
5225
|
+
renameSync(HERMES_PLUGIN_DIR, backupPath);
|
|
5226
|
+
}
|
|
5227
|
+
try {
|
|
5228
|
+
renameSync(stagedDir, HERMES_PLUGIN_DIR);
|
|
5229
|
+
} catch (err) {
|
|
5230
|
+
if (backupPath) {
|
|
5231
|
+
try {
|
|
5232
|
+
renameSync(backupPath, HERMES_PLUGIN_DIR);
|
|
5233
|
+
backupPath = null;
|
|
5234
|
+
} catch {}
|
|
5235
|
+
}
|
|
5236
|
+
const restoreNote = backupPath ? ` Your previous install was preserved at ${backupPath} — recover with: mv "${backupPath}" "${HERMES_PLUGIN_DIR}"` : "";
|
|
5237
|
+
return {
|
|
5238
|
+
success: false,
|
|
5239
|
+
label: "Agent Approve plugin",
|
|
5240
|
+
error: `Could not install bundle to ${HERMES_PLUGIN_DIR}: ${err instanceof Error ? err.message : String(err)}.${restoreNote}`
|
|
5241
|
+
};
|
|
5242
|
+
}
|
|
5243
|
+
writeFileSync(join(HERMES_PLUGIN_DIR, ".bundle-hash"), HERMES_BUNDLE_SHA256, { mode: 420 });
|
|
5244
|
+
if (backupPath) {
|
|
5245
|
+
rmSync2(backupPath, { recursive: true, force: true });
|
|
5246
|
+
backupPath = null;
|
|
5247
|
+
}
|
|
5248
|
+
} catch (err) {
|
|
5249
|
+
if (backupPath) {
|
|
5250
|
+
try {
|
|
5251
|
+
rmSync2(HERMES_PLUGIN_DIR, { recursive: true, force: true });
|
|
5252
|
+
renameSync(backupPath, HERMES_PLUGIN_DIR);
|
|
5253
|
+
backupPath = null;
|
|
5254
|
+
} catch {}
|
|
5255
|
+
}
|
|
5256
|
+
const restoreNote = backupPath ? ` Your previous install was preserved at ${backupPath} — recover with: mv "${backupPath}" "${HERMES_PLUGIN_DIR}"` : "";
|
|
5257
|
+
return {
|
|
5258
|
+
success: false,
|
|
5259
|
+
label: "Agent Approve plugin",
|
|
5260
|
+
error: `Bundle install failed: ${err instanceof Error ? err.message : String(err)}.${restoreNote}`
|
|
5261
|
+
};
|
|
5262
|
+
} finally {
|
|
5263
|
+
try {
|
|
5264
|
+
rmSync2(tmpDir, { recursive: true, force: true });
|
|
5265
|
+
} catch {}
|
|
5266
|
+
if (backupPath && existsSync2(HERMES_PLUGIN_DIR)) {
|
|
5267
|
+
try {
|
|
5268
|
+
rmSync2(backupPath, { recursive: true, force: true });
|
|
5269
|
+
} catch {}
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
if (!hermesDepsAlreadyInstalled(python.command)) {
|
|
5273
|
+
const depsArg = HERMES_PIP_DEPS.map((d2) => `"${d2}"`).join(" ");
|
|
5274
|
+
try {
|
|
5275
|
+
execSync(`${python.command} -m pip install --user --quiet ${depsArg}`, { stdio: "pipe" });
|
|
5276
|
+
} catch (err) {
|
|
5277
|
+
return {
|
|
5278
|
+
success: false,
|
|
5279
|
+
label: "Agent Approve plugin",
|
|
5280
|
+
error: `Installed bundle but could not install Python runtime deps: ${err instanceof Error ? err.message : String(err)}. ` + `Try manually: ${python.command} -m pip install --user ${HERMES_PIP_DEPS.join(" ")}`
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
try {
|
|
5285
|
+
execSync("hermes plugins enable agentapprove", { stdio: "pipe" });
|
|
5286
|
+
} catch (err) {
|
|
5287
|
+
return {
|
|
5288
|
+
success: false,
|
|
5289
|
+
label: "Agent Approve plugin",
|
|
5290
|
+
error: `Installed bundle but could not enable plugin in Hermes: ${err instanceof Error ? err.message : String(err)}. ` + `Try manually: hermes plugins enable agentapprove`
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
return { success: true, label: `Agent Approve plugin ${HERMES_PLUGIN_VERSION}` };
|
|
5294
|
+
}
|
|
5295
|
+
function isHermesDisableNoOp(err) {
|
|
5296
|
+
if (!err || typeof err !== "object")
|
|
5297
|
+
return false;
|
|
5298
|
+
const e2 = err;
|
|
5299
|
+
const stderr = Buffer.isBuffer(e2.stderr) ? e2.stderr.toString() : String(e2.stderr || "");
|
|
5300
|
+
const stdout = Buffer.isBuffer(e2.stdout) ? e2.stdout.toString() : String(e2.stdout || "");
|
|
5301
|
+
const haystack = `${e2.message || ""}
|
|
5302
|
+
${stderr}
|
|
5303
|
+
${stdout}`.toLowerCase();
|
|
5304
|
+
return haystack.includes("not enabled") || haystack.includes("not installed") || haystack.includes("is not enabled") || haystack.includes("plugin agentapprove not found") || haystack.includes("plugin 'agentapprove' not found");
|
|
5305
|
+
}
|
|
5306
|
+
function disableHermesPluginViaCli() {
|
|
5307
|
+
try {
|
|
5308
|
+
execSync("hermes plugins disable agentapprove", { stdio: "pipe" });
|
|
5309
|
+
return { success: true };
|
|
5310
|
+
} catch (err) {
|
|
5311
|
+
if (isHermesDisableNoOp(err)) {
|
|
5312
|
+
return { success: true, alreadyDisabled: true };
|
|
5313
|
+
}
|
|
5314
|
+
return {
|
|
5315
|
+
success: false,
|
|
5316
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5317
|
+
};
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
function removeHermesPlugin() {
|
|
5321
|
+
const disabled = disableHermesPluginViaCli();
|
|
5322
|
+
try {
|
|
5323
|
+
rmSync2(HERMES_PLUGIN_DIR, { recursive: true, force: true });
|
|
5324
|
+
return disabled.success ? { success: true } : { success: false, error: `disable failed: ${disabled.error || "unknown"}` };
|
|
5325
|
+
} catch (err) {
|
|
5326
|
+
return {
|
|
5327
|
+
success: false,
|
|
5328
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5329
|
+
};
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
4794
5332
|
var SYSTEM_DEPS = [
|
|
4795
5333
|
{
|
|
4796
5334
|
name: "curl",
|
|
@@ -5117,7 +5655,7 @@ async function installCommand() {
|
|
|
5117
5655
|
process.exit(1);
|
|
5118
5656
|
}
|
|
5119
5657
|
me(`Approve AI agent actions from your iPhone or Apple Watch.
|
|
5120
|
-
Installs hooks and extensions for Cursor, Claude, Gemini, Pi, OpenCode, OpenClaw, and more.`, "About");
|
|
5658
|
+
Installs hooks and extensions for Cursor, Windsurf, Claude, Gemini, Pi, OpenCode, OpenClaw, and more.`, "About");
|
|
5121
5659
|
const installedAgents = detectInstalledAgents();
|
|
5122
5660
|
const agentOptions = Object.entries(AGENTS).map(([id, agent]) => ({
|
|
5123
5661
|
value: id,
|
|
@@ -5531,6 +6069,9 @@ AGENTAPPROVE_E2E_MODE=${installMode}
|
|
|
5531
6069
|
if (agentId === "pi") {
|
|
5532
6070
|
filesToModify.push("Pi package registry (via `pi install`)");
|
|
5533
6071
|
}
|
|
6072
|
+
if (agentId === "hermes") {
|
|
6073
|
+
filesToModify.push(`Hermes plugin directory (download + verify ${HERMES_BUNDLE_FILENAME} → extract to ~/.hermes/plugins/agentapprove/ → \`hermes plugins enable agentapprove\`)`);
|
|
6074
|
+
}
|
|
5534
6075
|
if (agentId === "vscode-agent") {
|
|
5535
6076
|
const vsCodeVariants = findInstalledVSCodeVariants();
|
|
5536
6077
|
for (const { path: settingsPath, variant } of vsCodeVariants) {
|
|
@@ -5552,7 +6093,7 @@ Backups will be created with timestamp`, "Files to be modified");
|
|
|
5552
6093
|
for (const agentId of selectedAgents) {
|
|
5553
6094
|
const agent = AGENTS[agentId];
|
|
5554
6095
|
const spinner = _2();
|
|
5555
|
-
const spinnerMsg = agentId === "pi" ? `Installing ${agent.name} extension` : agentId === "openclaw" ? `Installing ${agent.name} plugin` : `Configuring ${agent.name}`;
|
|
6096
|
+
const spinnerMsg = agentId === "pi" ? `Installing ${agent.name} extension` : agentId === "openclaw" || agentId === "hermes" ? `Installing ${agent.name} plugin` : `Configuring ${agent.name}`;
|
|
5556
6097
|
spinner.start(spinnerMsg);
|
|
5557
6098
|
const result = await installHooksForAgent(agentId, hooksDir, installMode);
|
|
5558
6099
|
if (result.success) {
|
|
@@ -5593,6 +6134,17 @@ Backups will be created with timestamp`, "Files to be modified");
|
|
|
5593
6134
|
v2.warn(`Could not install ${PI_PLUGIN_SPEC} via Pi.
|
|
5594
6135
|
` + ` Error: ${result.error || "unknown"}
|
|
5595
6136
|
` + ` Install manually: pi install ${PI_PLUGIN_SPEC}
|
|
6137
|
+
` + ` Then re-run: npx agentapprove`);
|
|
6138
|
+
} else if (agentId === "hermes") {
|
|
6139
|
+
v2.warn(`Could not install Agent Approve plugin for Hermes.
|
|
6140
|
+
` + ` Error: ${result.error || "unknown"}
|
|
6141
|
+
` + ` Install manually:
|
|
6142
|
+
` + ` 1. Download ${HERMES_BUNDLE_FILENAME} from your Agent Approve API
|
|
6143
|
+
` + ` 2. Verify SHA-256 matches ${HERMES_BUNDLE_SHA256.slice(0, 16)}…
|
|
6144
|
+
` + ` 3. Extract into ~/.hermes/plugins/agentapprove/
|
|
6145
|
+
` + ` 4. Write the SHA-256 to ~/.hermes/plugins/agentapprove/.bundle-hash
|
|
6146
|
+
` + ` 5. pip install --user cryptography httpx
|
|
6147
|
+
` + ` 6. hermes plugins enable agentapprove
|
|
5596
6148
|
` + ` Then re-run: npx agentapprove`);
|
|
5597
6149
|
}
|
|
5598
6150
|
}
|
|
@@ -5668,6 +6220,22 @@ async function statusCommand() {
|
|
|
5668
6220
|
} catch {}
|
|
5669
6221
|
continue;
|
|
5670
6222
|
}
|
|
6223
|
+
if (agentId === "hermes") {
|
|
6224
|
+
try {
|
|
6225
|
+
const listOutput = execSync("hermes plugins list", {
|
|
6226
|
+
encoding: "utf-8",
|
|
6227
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
6228
|
+
timeout: PI_STATUS_TIMEOUT_MS
|
|
6229
|
+
});
|
|
6230
|
+
const enabled = /^.*agentapprove.*enabled/im.test(listOutput);
|
|
6231
|
+
if (enabled) {
|
|
6232
|
+
console.log(` ${source_default.green("✓")} ${agent.name}: Agent Approve plugin`);
|
|
6233
|
+
} else if (listOutput.includes("agentapprove")) {
|
|
6234
|
+
console.log(` ${source_default.yellow("⚠")} ${agent.name}: Agent Approve plugin installed but disabled (run: hermes plugins enable agentapprove)`);
|
|
6235
|
+
}
|
|
6236
|
+
} catch {}
|
|
6237
|
+
continue;
|
|
6238
|
+
}
|
|
5671
6239
|
if (existsSync2(agent.configPath)) {
|
|
5672
6240
|
const config = readJsonConfig(agent.configPath);
|
|
5673
6241
|
if (agentId === "opencode") {
|
|
@@ -5684,7 +6252,7 @@ async function statusCommand() {
|
|
|
5684
6252
|
}
|
|
5685
6253
|
continue;
|
|
5686
6254
|
}
|
|
5687
|
-
const hooksConfig = config[agent.hooksKey];
|
|
6255
|
+
const hooksConfig = agent.hooksKey === "" ? config : config[agent.hooksKey];
|
|
5688
6256
|
if (hooksConfig) {
|
|
5689
6257
|
const installedHooks = agent.hooks.filter((h2) => {
|
|
5690
6258
|
const hookEntry = hooksConfig[h2.name];
|
|
@@ -5764,10 +6332,19 @@ async function performUninstall(mode) {
|
|
|
5764
6332
|
}
|
|
5765
6333
|
continue;
|
|
5766
6334
|
}
|
|
6335
|
+
if (agentId === "hermes") {
|
|
6336
|
+
const result = removeHermesPlugin();
|
|
6337
|
+
if (result.success) {
|
|
6338
|
+
console.log(` ${source_default.green("✓")} Hermes plugin disabled and removed`);
|
|
6339
|
+
} else {
|
|
6340
|
+
console.log(` ${source_default.yellow("⚠")} Hermes plugin removal partial (${result.error || "unknown error"})`);
|
|
6341
|
+
}
|
|
6342
|
+
continue;
|
|
6343
|
+
}
|
|
5767
6344
|
if (!existsSync2(agent.configPath))
|
|
5768
6345
|
continue;
|
|
5769
6346
|
const config = readJsonConfig(agent.configPath);
|
|
5770
|
-
const hooksConfig = config[agent.hooksKey] ?? {};
|
|
6347
|
+
const hooksConfig = agent.hooksKey === "" ? config : config[agent.hooksKey] ?? {};
|
|
5771
6348
|
let modified = false;
|
|
5772
6349
|
if (agentId === "opencode") {
|
|
5773
6350
|
const pluginArray = config.plugin;
|
|
@@ -6317,7 +6894,9 @@ async function updateHookScriptsWithToken(token, apiUrl) {
|
|
|
6317
6894
|
"cursor-thought.sh",
|
|
6318
6895
|
"cursor-response.sh",
|
|
6319
6896
|
"gemini-approval.sh",
|
|
6320
|
-
"gemini-complete.sh"
|
|
6897
|
+
"gemini-complete.sh",
|
|
6898
|
+
"windsurf-hook.sh",
|
|
6899
|
+
"openhands-hook.sh"
|
|
6321
6900
|
];
|
|
6322
6901
|
for (const file of hookFiles) {
|
|
6323
6902
|
const filePath = join(hooksDir, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentapprove",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Approve AI agent actions from your iPhone or Apple Watch",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"claude",
|
|
22
22
|
"claude-code",
|
|
23
23
|
"cursor",
|
|
24
|
+
"windsurf",
|
|
24
25
|
"gemini",
|
|
25
26
|
"openclaw",
|
|
26
27
|
"opencode",
|