@pushary/agent-hooks 0.70.0 → 0.72.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.72.0
4
+
5
+ ### Setup tells you when Codex was already quarantined
6
+
7
+ 0.71.0 stopped setup from getting your Codex binary deleted. This handles the
8
+ machines where it already happened. What macOS leaves behind passes every check
9
+ setup made: the `codex` on your PATH is a small JavaScript launcher and it
10
+ survives, `which codex` still answers, only the binary it launches is gone. So
11
+ setup wired up an agent that could not start and reported success.
12
+
13
+ It now says so before writing anything, and only when it can prove it: a vendor
14
+ directory that exists and holds nothing the size of a native binary. A layout it
15
+ does not recognise stays quiet, because telling somebody their working install is
16
+ broken is the worse mistake. Reinstall with `npm install -g @openai/codex` or
17
+ `brew install --cask codex`.
18
+
19
+ ### Setup no longer runs the Hermes binary either
20
+
21
+ The same hazard, one agent over. When the config edit that enables the plugin
22
+ failed, setup fell back to running `hermes plugins enable pushary` — executing a
23
+ third-party agent binary, which is exactly what cost people their Codex install.
24
+ The fallback also did not work, because `hermes plugins enable` does not see a
25
+ pip install. It is gone, and a failed config edit now tells you the one line to
26
+ add by hand.
27
+
28
+ Executing an agent binary is now a build failure rather than a habit: every
29
+ source file in this package is scanned on every CI run, and the check knows the
30
+ difference between running `codex` and asking `which` where it is.
31
+
32
+ ## 0.71.0
33
+
34
+ ### Setting up Codex no longer gets Codex deleted by macOS
35
+
36
+ Setup ran `codex --version` to decide whether your Codex was new enough for
37
+ native hooks. On macOS, executing a binary hands it to XProtect, and current
38
+ definitions false-positive on the Codex CLI: macOS killed it and moved it to the
39
+ Trash. Reading a version number destroyed the tool it was asking, mid-setup, and
40
+ the only thing you saw was "codex was not opened because it contains malware".
41
+
42
+ Setup no longer runs Codex, or any other agent's binary, to learn something about
43
+ it. The version now comes from the package manifest next to the binary, which
44
+ covers npm, bun, pnpm and yarn installs, and from `brew list` for the Homebrew
45
+ cask, which ships no manifest. Same result, nothing launched.
46
+
3
47
  ## 0.70.0
4
48
 
5
49
  ### A key you name is the key you get
@@ -110,8 +110,8 @@ import {
110
110
  } from "../chunk-KZERVKTD.js";
111
111
 
112
112
  // bin/pushary-setup.ts
113
- import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync } from "fs";
114
- import { join, dirname, basename } from "path";
113
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync, realpathSync, readdirSync, statSync } from "fs";
114
+ import { join as join2, dirname as dirname2, basename } from "path";
115
115
  import { homedir, tmpdir } from "os";
116
116
  import { execSync as execSync2 } from "child_process";
117
117
  import { checkbox, input, confirm } from "@inquirer/prompts";
@@ -198,6 +198,89 @@ var describeClosing = (facts) => {
198
198
  };
199
199
  };
200
200
 
201
+ // src/setup/codex-version.ts
202
+ import { dirname, join } from "path";
203
+ var parseCodexVersion = (raw) => {
204
+ const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
205
+ if (!match) return null;
206
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
207
+ };
208
+ var compareCodexVersion = (a, b) => {
209
+ for (let i = 0; i < 3; i++) {
210
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
211
+ }
212
+ return 0;
213
+ };
214
+ var versionFromManifest = (probe, binPath) => {
215
+ let dir = dirname(binPath);
216
+ for (let depth = 0; depth < 4; depth++) {
217
+ const raw = probe.readFile(join(dir, "package.json"));
218
+ if (raw) {
219
+ try {
220
+ const { version } = JSON.parse(raw);
221
+ if (typeof version === "string") {
222
+ const parsed = parseCodexVersion(version);
223
+ if (parsed) return parsed;
224
+ }
225
+ } catch {
226
+ }
227
+ }
228
+ const parent = dirname(dir);
229
+ if (parent === dir) break;
230
+ dir = parent;
231
+ }
232
+ return null;
233
+ };
234
+ var readCodexVersion = (probe) => {
235
+ const onPath = probe.resolveOnPath();
236
+ if (onPath) {
237
+ const fromManifest = versionFromManifest(probe, probe.realpath(onPath) ?? onPath);
238
+ if (fromManifest) return fromManifest;
239
+ }
240
+ const brew = probe.brewVersions();
241
+ return brew ? parseCodexVersion(brew) : null;
242
+ };
243
+
244
+ // src/setup/codex-install.ts
245
+ var NATIVE_BINARY_MIN_BYTES = 20 * 1024 * 1024;
246
+ var packageRootFor = (probe, binPath) => {
247
+ let dir = probe.dirname(binPath);
248
+ for (let depth = 0; depth < 4; depth++) {
249
+ if (probe.exists(probe.join(dir, "package.json"))) return dir;
250
+ const parent = probe.dirname(dir);
251
+ if (parent === dir) return null;
252
+ dir = parent;
253
+ }
254
+ return null;
255
+ };
256
+ var holdsNativeBinary = (probe, dir, depth = 0) => {
257
+ if (depth > 4) return false;
258
+ const entries = probe.listDir(dir);
259
+ if (!entries) return false;
260
+ return entries.some((entry) => {
261
+ const path = probe.join(dir, entry);
262
+ const size = probe.fileSize(path);
263
+ if (size !== null) return size >= NATIVE_BINARY_MIN_BYTES;
264
+ return holdsNativeBinary(probe, path, depth + 1);
265
+ });
266
+ };
267
+ var platformPackages = (probe, packageRoot) => {
268
+ const scopes = [probe.join(packageRoot, "node_modules", "@openai"), probe.dirname(packageRoot)];
269
+ return scopes.flatMap(
270
+ (scope) => (probe.listDir(scope) ?? []).filter((entry) => entry.startsWith("codex-")).map((entry) => probe.join(scope, entry))
271
+ );
272
+ };
273
+ var checkCodexInstall = (probe) => {
274
+ const onPath = probe.resolveOnPath();
275
+ if (!onPath) return { kind: "unknown" };
276
+ const packageRoot = packageRootFor(probe, probe.realpath(onPath) ?? onPath);
277
+ if (!packageRoot) return { kind: "unknown" };
278
+ const vendors = platformPackages(probe, packageRoot).map((pkg) => probe.join(pkg, "vendor")).filter((vendor) => probe.exists(vendor));
279
+ if (vendors.length === 0) return { kind: "unknown" };
280
+ if (vendors.some((vendor) => holdsNativeBinary(probe, vendor))) return { kind: "ok" };
281
+ return { kind: "binary-missing", vendorDir: vendors[0] };
282
+ };
283
+
201
284
  // src/skills-cli.ts
202
285
  import { execSync } from "child_process";
203
286
  var installSkillViaSkillsCli = (agent) => {
@@ -435,7 +518,7 @@ var readAgentJson = (filePath) => {
435
518
  );
436
519
  };
437
520
  var writeJson = (filePath, data) => {
438
- const dir = dirname(filePath);
521
+ const dir = dirname2(filePath);
439
522
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
440
523
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
441
524
  };
@@ -514,23 +597,23 @@ var installGlobally2 = async () => {
514
597
  var _cachedSkillContent = null;
515
598
  var fetchSkillContent = async () => {
516
599
  if (_cachedSkillContent) return _cachedSkillContent;
517
- const __dirname = dirname(fileURLToPath(import.meta.url));
600
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
518
601
  const candidates = [
519
- join(__dirname, "..", "..", "data", "SKILL.md"),
520
- join(__dirname, "..", "data", "SKILL.md")
602
+ join2(__dirname, "..", "..", "data", "SKILL.md"),
603
+ join2(__dirname, "..", "data", "SKILL.md")
521
604
  ];
522
605
  const source = candidates.find((path) => existsSync(path));
523
606
  if (!source) throw new Error("packaged skill not found");
524
607
  _cachedSkillContent = readFileSync(source, "utf-8");
525
608
  return _cachedSkillContent;
526
609
  };
527
- var skillsCliInUse = () => isInstalled("skills") || existsSync(join(homedir(), ".agents", "skills"));
610
+ var skillsCliInUse = () => isInstalled("skills") || existsSync(join2(homedir(), ".agents", "skills"));
528
611
  var installSkill = async (agent, fallbackDir) => {
529
612
  await spinner("Installing Pushary skill", async () => {
530
613
  if (skillsCliInUse() && installSkillViaSkillsCli(agent)) return;
531
614
  const content = await fetchSkillContent();
532
615
  if (!existsSync(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
533
- writeFileSync(join(fallbackDir, "SKILL.md"), content, "utf-8");
616
+ writeFileSync(join2(fallbackDir, "SKILL.md"), content, "utf-8");
534
617
  });
535
618
  };
536
619
  var setupClaudeCode = async (apiKey) => {
@@ -577,8 +660,8 @@ var resolveHermesPython = () => {
577
660
  }
578
661
  } catch {
579
662
  }
580
- const venvRoot = join(homedir(), ".hermes", "hermes-agent", "venv");
581
- const candidates = IS_WINDOWS ? [join(venvRoot, "Scripts", "python.exe"), join(venvRoot, "Scripts", "python3.exe")] : [join(venvRoot, "bin", "python3"), join(venvRoot, "bin", "python")];
663
+ const venvRoot = join2(homedir(), ".hermes", "hermes-agent", "venv");
664
+ const candidates = IS_WINDOWS ? [join2(venvRoot, "Scripts", "python.exe"), join2(venvRoot, "Scripts", "python3.exe")] : [join2(venvRoot, "bin", "python3"), join2(venvRoot, "bin", "python")];
582
665
  return candidates.find(existsSync) ?? null;
583
666
  };
584
667
  var ensurePip = (python) => {
@@ -594,13 +677,13 @@ var ensurePip = (python) => {
594
677
  };
595
678
  var enablePusharyPlugin = (python) => {
596
679
  const snippet = 'from hermes_cli.config import load_config, save_config; c = load_config(); p = c.get("plugins") if isinstance(c.get("plugins"), dict) else {}; e = p.get("enabled") if isinstance(p.get("enabled"), list) else []; p["enabled"] = (e + ["pushary"]) if "pushary" not in e else e; c["plugins"] = p; a = c.get("agent") if isinstance(c.get("agent"), dict) else {}; d = a.get("disabled_toolsets") if isinstance(a.get("disabled_toolsets"), list) else []; a["disabled_toolsets"] = (d + ["clarify"]) if "clarify" not in d else d; c["agent"] = a; save_config(c)';
597
- const scriptPath = join(tmpdir(), `pushary-hermes-${process.pid}.py`);
680
+ const scriptPath = join2(tmpdir(), `pushary-hermes-${process.pid}.py`);
598
681
  try {
599
682
  writeFileSync(scriptPath, snippet.split("; ").join("\n"), "utf-8");
600
683
  execSync2(`"${python}" "${scriptPath}"`, { stdio: "pipe", timeout: 15e3 });
601
- return;
684
+ return true;
602
685
  } catch {
603
- execSync2("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
686
+ return false;
604
687
  } finally {
605
688
  try {
606
689
  rmSync(scriptPath, { force: true });
@@ -627,9 +710,14 @@ var setupHermes = async (_apiKey) => {
627
710
  ensurePip(python);
628
711
  execSync2(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
629
712
  });
713
+ let pluginEnabled = false;
630
714
  await spinner("Enabling plugin + routing questions to push", async () => {
631
- enablePusharyPlugin(python);
715
+ pluginEnabled = enablePusharyPlugin(python);
632
716
  });
717
+ if (!pluginEnabled) {
718
+ console.log(` ${yellow("!")} Installed the plugin, but could not edit Hermes' config.`);
719
+ noteManual('Add "pushary" to plugins.enabled in ~/.hermes/config.yaml, and "clarify" to agent.disabled_toolsets');
720
+ }
633
721
  console.log();
634
722
  console.log(` ${dim("What this configured:")}`);
635
723
  console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
@@ -638,27 +726,69 @@ var setupHermes = async (_apiKey) => {
638
726
  console.log(` ${dim("\u2022")} Permission gating: set ${bold("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
639
727
  console.log(` ${dim("To re-enable terminal prompts:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
640
728
  };
641
- var CODEX_HOOKS_JSON = join(CODEX_HOME, "hooks.json");
729
+ var CODEX_HOOKS_JSON = join2(CODEX_HOME, "hooks.json");
642
730
  var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
643
731
  var CODEX_TRUST_VERIFIED_MAX = [0, 142, 2];
644
- var parseCodexVersion = (raw) => {
645
- const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
646
- if (!match) return null;
647
- return [Number(match[1]), Number(match[2]), Number(match[3])];
648
- };
649
- var compareCodexVersion = (a, b) => {
650
- for (let i = 0; i < 3; i++) {
651
- if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
732
+ var codexOnPath = () => {
733
+ const whichCmd = IS_WINDOWS ? "where" : "which";
734
+ try {
735
+ const found = execSync2(`${whichCmd} codex`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
736
+ return found || null;
737
+ } catch {
738
+ return null;
652
739
  }
653
- return 0;
654
740
  };
655
- var readCodexVersion = () => {
741
+ var codexRealpath = (path) => {
656
742
  try {
657
- return parseCodexVersion(execSync2("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }));
743
+ return realpathSync(path);
658
744
  } catch {
659
745
  return null;
660
746
  }
661
747
  };
748
+ var detectCodexVersion = () => readCodexVersion({
749
+ resolveOnPath: codexOnPath,
750
+ realpath: codexRealpath,
751
+ readFile: (path) => {
752
+ try {
753
+ return readFileSync(path, "utf-8");
754
+ } catch {
755
+ return null;
756
+ }
757
+ },
758
+ brewVersions: () => {
759
+ try {
760
+ return execSync2("brew list --versions codex", {
761
+ encoding: "utf-8",
762
+ stdio: "pipe",
763
+ timeout: 1e4
764
+ }).trim() || null;
765
+ } catch {
766
+ return null;
767
+ }
768
+ }
769
+ });
770
+ var detectCodexInstall = () => checkCodexInstall({
771
+ resolveOnPath: codexOnPath,
772
+ realpath: codexRealpath,
773
+ exists: existsSync,
774
+ listDir: (path) => {
775
+ try {
776
+ return readdirSync(path);
777
+ } catch {
778
+ return null;
779
+ }
780
+ },
781
+ fileSize: (path) => {
782
+ try {
783
+ const stat = statSync(path);
784
+ return stat.isFile() ? stat.size : null;
785
+ } catch {
786
+ return null;
787
+ }
788
+ },
789
+ join: join2,
790
+ dirname: dirname2
791
+ });
662
792
  var codexSupportsHooks = (version) => version !== null && compareCodexVersion(version, CODEX_HOOKS_MIN_VERSION) >= 0;
663
793
  var codexTrustAutoSupported = (version) => version !== null && codexSupportsHooks(version) && compareCodexVersion(version, CODEX_TRUST_VERIFIED_MAX) <= 0;
664
794
  var removeCodexNotifyEntry = (codexConfig) => {
@@ -704,6 +834,16 @@ var setupCodex = async (apiKey) => {
704
834
  console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
705
835
  return "skipped";
706
836
  }
837
+ if (detectCodexInstall().kind === "binary-missing") {
838
+ console.log(` ${yellow("!")} Your Codex install is missing the binary it runs.`);
839
+ console.log(` ${dim("A Pushary setup before 0.71.0 asked Codex its version. On macOS that hands")}`);
840
+ console.log(` ${dim("the binary to XProtect, which quarantines the Codex CLI and moves it to the")}`);
841
+ console.log(` ${dim("Trash. Setup no longer runs it. Reinstall Codex to get it back:")}`);
842
+ console.log(` ${cyan("npm install -g @openai/codex")}`);
843
+ console.log(` ${dim("Configuring anyway, so it is ready when Codex is.")}`);
844
+ console.log();
845
+ noteManual("Reinstall Codex: a Pushary setup before 0.71.0 got its binary quarantined by macOS");
846
+ }
707
847
  await installGlobally2();
708
848
  const codexConfig = codexConfigToml();
709
849
  await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
@@ -711,7 +851,7 @@ var setupCodex = async (apiKey) => {
711
851
  addCodexMcpServer(config, apiKey);
712
852
  writeFileAtomic(codexConfig, stringifyTOML(config), KEY_FILE_MODE);
713
853
  });
714
- const codexVersion = readCodexVersion();
854
+ const codexVersion = detectCodexVersion();
715
855
  const hooksSupported = codexSupportsHooks(codexVersion);
716
856
  const trustAuto = codexTrustAutoSupported(codexVersion);
717
857
  let trusted = false;
@@ -739,9 +879,16 @@ var setupCodex = async (apiKey) => {
739
879
  removeCodexNotifyEntry(codexConfig);
740
880
  });
741
881
  } else {
742
- console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
743
- console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
744
- console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
882
+ if (codexVersion !== null) {
883
+ console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
884
+ console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
885
+ console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
886
+ } else {
887
+ console.log(` ${yellow("!")} Could not read your Codex version from its install, so hooks were skipped.`);
888
+ console.log(` ${dim("Deliberately not run: asking the binary its version is what macOS quarantines.")}`);
889
+ console.log(` ${dim(`Installing the notify handler, which works on every version. If Codex is ${CODEX_HOOKS_MIN_VERSION.join(".")}+,`)}`);
890
+ console.log(` ${dim("re-run setup after installing it through npm or Homebrew to get native hooks.")}`);
891
+ }
745
892
  await spinner("Adding notify handler for Codex events (deprecated)", async () => {
746
893
  addCodexNotifyEntry(codexConfig);
747
894
  });
@@ -779,17 +926,17 @@ var setupCodex = async (apiKey) => {
779
926
  }
780
927
  };
781
928
  var resolveBundledPlugin = () => {
782
- const dir = dirname(fileURLToPath(import.meta.url));
929
+ const dir = dirname2(fileURLToPath(import.meta.url));
783
930
  const candidates = [
784
- join(dir, "..", "..", "data", "cursor-plugin"),
785
- join(dir, "..", "data", "cursor-plugin"),
786
- join(dir, "..", "..", "..", "cursor-plugin"),
787
- join(dir, "..", "..", "cursor-plugin")
931
+ join2(dir, "..", "..", "data", "cursor-plugin"),
932
+ join2(dir, "..", "data", "cursor-plugin"),
933
+ join2(dir, "..", "..", "..", "cursor-plugin"),
934
+ join2(dir, "..", "..", "cursor-plugin")
788
935
  ];
789
- return candidates.find((p) => existsSync(join(p, ".cursor-plugin", "plugin.json"))) ?? null;
936
+ return candidates.find((p) => existsSync(join2(p, ".cursor-plugin", "plugin.json"))) ?? null;
790
937
  };
791
938
  var installCursorUserHooks = (gateScript) => {
792
- const template = readJson(join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
939
+ const template = readJson(join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
793
940
  if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
794
941
  const entry = { ...template, command: `node "${gateScript}"` };
795
942
  let userHooks = {};
@@ -811,7 +958,7 @@ var installCursorUserHooks = (gateScript) => {
811
958
  writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
812
959
  };
813
960
  var neutralizePluginGate = () => {
814
- const path = join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
961
+ const path = join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
815
962
  if (!existsSync(path)) return;
816
963
  const data = readJson(path);
817
964
  if (data.hooks && "beforeShellExecution" in data.hooks) {
@@ -826,7 +973,7 @@ var setupCursor = async (apiKey) => {
826
973
  const source = resolveBundledPlugin();
827
974
  if (!source) throw new Error("bundled Cursor plugin not found in this package");
828
975
  await spinner("Installing Pushary plugin", async () => {
829
- const staging = join(dirname(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
976
+ const staging = join2(dirname2(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
830
977
  const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
831
978
  rmSync(staging, { recursive: true, force: true });
832
979
  try {
@@ -834,7 +981,7 @@ var setupCursor = async (apiKey) => {
834
981
  recursive: true,
835
982
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
836
983
  });
837
- const staged = readJsonSafe(join(staging, ".cursor-plugin", "plugin.json"));
984
+ const staged = readJsonSafe(join2(staging, ".cursor-plugin", "plugin.json"));
838
985
  if (staged.kind !== "ok") {
839
986
  throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
840
987
  }
@@ -852,7 +999,7 @@ var setupCursor = async (apiKey) => {
852
999
  }
853
1000
  });
854
1001
  await spinner("Linking your API key", async () => {
855
- const mcpPath = join(CURSOR_PLUGIN_DIR, "mcp.json");
1002
+ const mcpPath = join2(CURSOR_PLUGIN_DIR, "mcp.json");
856
1003
  const mcp = readAgentJson(mcpPath);
857
1004
  const servers = mcp.mcpServers ?? {};
858
1005
  if (servers.pushary) {
@@ -862,7 +1009,7 @@ var setupCursor = async (apiKey) => {
862
1009
  }
863
1010
  });
864
1011
  await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
865
- installCursorUserHooks(join(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
1012
+ installCursorUserHooks(join2(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
866
1013
  neutralizePluginGate();
867
1014
  });
868
1015
  console.log();
@@ -874,23 +1021,23 @@ var setupCursor = async (apiKey) => {
874
1021
  noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
875
1022
  };
876
1023
  var resolveBundledVsCodePlugin = () => {
877
- const dir = dirname(fileURLToPath(import.meta.url));
1024
+ const dir = dirname2(fileURLToPath(import.meta.url));
878
1025
  const candidates = [
879
- join(dir, "..", "..", "data", "vscode-plugin"),
880
- join(dir, "..", "data", "vscode-plugin"),
881
- join(dir, "..", "..", "..", "vscode-plugin"),
882
- join(dir, "..", "..", "vscode-plugin")
1026
+ join2(dir, "..", "..", "data", "vscode-plugin"),
1027
+ join2(dir, "..", "data", "vscode-plugin"),
1028
+ join2(dir, "..", "..", "..", "vscode-plugin"),
1029
+ join2(dir, "..", "..", "vscode-plugin")
883
1030
  ];
884
- return candidates.find((p) => existsSync(join(p, ".claude-plugin", "plugin.json"))) ?? null;
1031
+ return candidates.find((p) => existsSync(join2(p, ".claude-plugin", "plugin.json"))) ?? null;
885
1032
  };
886
1033
  var pinVsCodeGatePath = (pluginDir) => {
887
- const hooksPath = join(pluginDir, "hooks", "hooks.json");
1034
+ const hooksPath = join2(pluginDir, "hooks", "hooks.json");
888
1035
  const data = readJson(hooksPath);
889
1036
  const entries = data.hooks?.PreToolUse;
890
1037
  if (!Array.isArray(entries) || entries.length === 0) {
891
1038
  throw new Error("bundled VS Code hooks.json is missing a PreToolUse entry");
892
1039
  }
893
- const gate = join(pluginDir, "scripts", "pushary-gate.mjs");
1040
+ const gate = join2(pluginDir, "scripts", "pushary-gate.mjs");
894
1041
  data.hooks.PreToolUse = entries.map((entry) => ({ ...entry, command: `node "${gate}"` }));
895
1042
  writeJson(hooksPath, data);
896
1043
  };
@@ -906,7 +1053,7 @@ var registerVsCodePlugin = (pluginDir) => {
906
1053
  continue;
907
1054
  }
908
1055
  if (current !== null) backupFile(settingsPath);
909
- mkdirSync(dirname(settingsPath), { recursive: true });
1056
+ mkdirSync(dirname2(settingsPath), { recursive: true });
910
1057
  writeFileAtomic(settingsPath, result.content);
911
1058
  written.push(settingsPath);
912
1059
  }
@@ -919,16 +1066,16 @@ var setupVsCode = async (apiKey) => {
919
1066
  const source = resolveBundledVsCodePlugin();
920
1067
  if (!source) throw new Error("bundled VS Code plugin not found in this package");
921
1068
  await spinner("Installing Pushary plugin", async () => {
922
- const staging = join(dirname(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
1069
+ const staging = join2(dirname2(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
923
1070
  const backup = `${VSCODE_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
924
- mkdirSync(dirname(VSCODE_PLUGIN_DIR), { recursive: true });
1071
+ mkdirSync(dirname2(VSCODE_PLUGIN_DIR), { recursive: true });
925
1072
  rmSync(staging, { recursive: true, force: true });
926
1073
  try {
927
1074
  cpSync(source, staging, {
928
1075
  recursive: true,
929
1076
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
930
1077
  });
931
- const staged = readJsonSafe(join(staging, ".claude-plugin", "plugin.json"));
1078
+ const staged = readJsonSafe(join2(staging, ".claude-plugin", "plugin.json"));
932
1079
  if (staged.kind !== "ok") {
933
1080
  throw new Error("staged VS Code plugin is missing or has an unreadable plugin.json");
934
1081
  }
@@ -946,7 +1093,7 @@ var setupVsCode = async (apiKey) => {
946
1093
  }
947
1094
  });
948
1095
  await spinner("Linking your API key", async () => {
949
- const mcpPath = join(VSCODE_PLUGIN_DIR, ".mcp.json");
1096
+ const mcpPath = join2(VSCODE_PLUGIN_DIR, ".mcp.json");
950
1097
  const mcp = readAgentJson(mcpPath);
951
1098
  const servers = mcp.mcpServers ?? {};
952
1099
  if (servers.pushary) {
@@ -1086,27 +1233,27 @@ var agentIsWired = (agent) => {
1086
1233
  return claudeWired({
1087
1234
  claudeJson: readJson2(CLAUDE_JSON),
1088
1235
  settings: readJson2(CLAUDE_SETTINGS),
1089
- skillExists: existsSync(join(CLAUDE_SKILL_DIR, "SKILL.md"))
1236
+ skillExists: existsSync(join2(CLAUDE_SKILL_DIR, "SKILL.md"))
1090
1237
  });
1091
1238
  case "codex": {
1092
1239
  let config = null;
1093
1240
  try {
1094
- config = parseTOML(readFileSync(join(CODEX_HOME, "config.toml"), "utf-8"));
1241
+ config = parseTOML(readFileSync(join2(CODEX_HOME, "config.toml"), "utf-8"));
1095
1242
  } catch {
1096
1243
  config = null;
1097
1244
  }
1098
1245
  return codexWired({
1099
1246
  config,
1100
1247
  hooks: readJson2(CODEX_HOOKS_JSON),
1101
- skillExists: existsSync(join(CODEX_SKILL_DIR, "SKILL.md"))
1248
+ skillExists: existsSync(join2(CODEX_SKILL_DIR, "SKILL.md"))
1102
1249
  });
1103
1250
  }
1104
1251
  case "gemini_cli":
1105
1252
  return geminiWired({ settings: readJson2(GEMINI_SETTINGS) });
1106
1253
  case "cursor":
1107
- return existsSync(join(CURSOR_PLUGIN_DIR, "mcp.json"));
1254
+ return existsSync(join2(CURSOR_PLUGIN_DIR, "mcp.json"));
1108
1255
  case "vscode":
1109
- return existsSync(join(VSCODE_PLUGIN_DIR, ".mcp.json"));
1256
+ return existsSync(join2(VSCODE_PLUGIN_DIR, ".mcp.json"));
1110
1257
  default:
1111
1258
  return true;
1112
1259
  }
@@ -1134,8 +1281,8 @@ var offerProjectInstructions = async (agents, options) => {
1134
1281
  });
1135
1282
  if (!wanted) return;
1136
1283
  for (const target of targets) {
1137
- await spinner(`Writing managed block to ${join(process.cwd(), target.file)}`, async () => {
1138
- writeInstructionBlock(join(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1284
+ await spinner(`Writing managed block to ${join2(process.cwd(), target.file)}`, async () => {
1285
+ writeInstructionBlock(join2(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1139
1286
  }, { optional: true });
1140
1287
  }
1141
1288
  console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
@@ -1286,12 +1433,12 @@ var resolveAgents = async (options) => {
1286
1433
  });
1287
1434
  };
1288
1435
  var AGENT_TARGETS = {
1289
- claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join(CLAUDE_SKILL_DIR, "SKILL.md")],
1290
- codex: [join(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1436
+ claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join2(CLAUDE_SKILL_DIR, "SKILL.md")],
1437
+ codex: [join2(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join2(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1291
1438
  gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
1292
1439
  hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
1293
- cursor: [join(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1294
- vscode: [join(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1440
+ cursor: [join2(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1441
+ vscode: [join2(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1295
1442
  custom: ["nothing (prints connection details only)"]
1296
1443
  };
1297
1444
  var reportDryRun = (apiKey, agents, keyCheck) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.70.0",
3
+ "version": "0.72.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",