@pushary/agent-hooks 0.71.0 → 0.73.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,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.73.0
4
+
5
+ ### Codex could finish setup with no skill installed
6
+
7
+ If your machine already uses skills.sh, setup installs the Pushary skill
8
+ through that CLI so the install registers rather than being invisible. For
9
+ Codex, `skills add --agent codex` prints "copy to Codex", says "Installation
10
+ complete" and exits 0, then writes only `~/.agents/skills/pushary`. Nothing
11
+ lands in `~/.codex/skills`, which is where Codex reads. Setup took the exit
12
+ code as proof, skipped the copy it ships, and Codex ended up with no skill.
13
+
14
+ Doctor caught it, and then gave advice that could not work: clean and set up
15
+ again reinstalled exactly the same way and failed the same check.
16
+
17
+ The exit code no longer decides this. What decides it is whether the run left
18
+ a SKILL.md where that agent reads, which is the same thing doctor looks at,
19
+ and setup writes its own copy when it did not. Anything skills.sh really did
20
+ install is left exactly as it installed it, symlinks included.
21
+
22
+ Everything else about the wiring was fine, so this cost you the skill and
23
+ nothing more. Approvals, hooks and the instructions in `~/.codex/AGENTS.md`
24
+ were all in place.
25
+
26
+ ### A skill that will not install no longer stops setup
27
+
28
+ The skill is the one piece of the wiring nothing else depends on. It used to
29
+ be able to abort setup partway, leaving an agent half configured. Now it warns
30
+ and setup finishes the rest, and `pushary doctor` tells you the skill is
31
+ missing.
32
+
33
+ ## 0.72.0
34
+
35
+ ### Setup tells you when Codex was already quarantined
36
+
37
+ 0.71.0 stopped setup from getting your Codex binary deleted. This handles the
38
+ machines where it already happened. What macOS leaves behind passes every check
39
+ setup made: the `codex` on your PATH is a small JavaScript launcher and it
40
+ survives, `which codex` still answers, only the binary it launches is gone. So
41
+ setup wired up an agent that could not start and reported success.
42
+
43
+ It now says so before writing anything, and only when it can prove it: a vendor
44
+ directory that exists and holds nothing the size of a native binary. A layout it
45
+ does not recognise stays quiet, because telling somebody their working install is
46
+ broken is the worse mistake. Reinstall with `npm install -g @openai/codex` or
47
+ `brew install --cask codex`.
48
+
49
+ ### Setup no longer runs the Hermes binary either
50
+
51
+ The same hazard, one agent over. When the config edit that enables the plugin
52
+ failed, setup fell back to running `hermes plugins enable pushary` — executing a
53
+ third-party agent binary, which is exactly what cost people their Codex install.
54
+ The fallback also did not work, because `hermes plugins enable` does not see a
55
+ pip install. It is gone, and a failed config edit now tells you the one line to
56
+ add by hand.
57
+
58
+ Executing an agent binary is now a build failure rather than a habit: every
59
+ source file in this package is scanned on every CI run, and the check knows the
60
+ difference between running `codex` and asking `which` where it is.
61
+
3
62
  ## 0.71.0
4
63
 
5
64
  ### Setting up Codex no longer gets Codex deleted by macOS
@@ -15,9 +74,6 @@ it. The version now comes from the package manifest next to the binary, which
15
74
  covers npm, bun, pnpm and yarn installs, and from `brew list` for the Homebrew
16
75
  cask, which ships no manifest. Same result, nothing launched.
17
76
 
18
- If your Codex was already quarantined, reinstall it: `npm install -g @openai/codex`
19
- or `brew install --cask codex`.
20
-
21
77
  ## 0.70.0
22
78
 
23
79
  ### 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, realpathSync } 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";
@@ -199,6 +199,7 @@ var describeClosing = (facts) => {
199
199
  };
200
200
 
201
201
  // src/setup/codex-version.ts
202
+ import { dirname, join } from "path";
202
203
  var parseCodexVersion = (raw) => {
203
204
  const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
204
205
  if (!match) return null;
@@ -210,21 +211,21 @@ var compareCodexVersion = (a, b) => {
210
211
  }
211
212
  return 0;
212
213
  };
213
- var versionFromPackageManifest = (probe, binPath) => {
214
- let dir = probe.dirname(binPath);
214
+ var versionFromManifest = (probe, binPath) => {
215
+ let dir = dirname(binPath);
215
216
  for (let depth = 0; depth < 4; depth++) {
216
- const raw = probe.readFile(probe.join(dir, "package.json"));
217
+ const raw = probe.readFile(join(dir, "package.json"));
217
218
  if (raw) {
218
219
  try {
219
- const manifest = JSON.parse(raw);
220
- if (typeof manifest.version === "string") {
221
- const parsed = parseCodexVersion(manifest.version);
220
+ const { version } = JSON.parse(raw);
221
+ if (typeof version === "string") {
222
+ const parsed = parseCodexVersion(version);
222
223
  if (parsed) return parsed;
223
224
  }
224
225
  } catch {
225
226
  }
226
227
  }
227
- const parent = probe.dirname(dir);
228
+ const parent = dirname(dir);
228
229
  if (parent === dir) break;
229
230
  dir = parent;
230
231
  }
@@ -233,16 +234,51 @@ var versionFromPackageManifest = (probe, binPath) => {
233
234
  var readCodexVersion = (probe) => {
234
235
  const onPath = probe.resolveOnPath();
235
236
  if (onPath) {
236
- const real = probe.realpath(onPath) ?? onPath;
237
- const fromManifest = versionFromPackageManifest(probe, real);
238
- if (fromManifest) return { kind: "known", version: fromManifest, source: "package manifest" };
237
+ const fromManifest = versionFromManifest(probe, probe.realpath(onPath) ?? onPath);
238
+ if (fromManifest) return fromManifest;
239
239
  }
240
240
  const brew = probe.brewVersions();
241
- if (brew) {
242
- const parsed = parseCodexVersion(brew);
243
- if (parsed) return { kind: "known", version: parsed, source: "homebrew" };
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;
244
253
  }
245
- return { kind: "unknown" };
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] };
246
282
  };
247
283
 
248
284
  // src/skills-cli.ts
@@ -258,6 +294,17 @@ var installSkillViaSkillsCli = (agent) => {
258
294
  return false;
259
295
  }
260
296
  };
297
+ var installAgentSkill = async (request, effects) => {
298
+ if (request.skillsCliInUse) {
299
+ const startedAt = effects.now();
300
+ if (effects.runSkillsCli(request.agent)) {
301
+ const modifiedAt = effects.skillModifiedAt(request.agentSkillDir);
302
+ if (modifiedAt !== null && modifiedAt >= startedAt) return "skills-cli";
303
+ }
304
+ }
305
+ await effects.writePackagedSkill(request.agentSkillDir);
306
+ return "packaged-copy";
307
+ };
261
308
 
262
309
  // src/setup/telemetry.ts
263
310
  var currentStep = "start";
@@ -482,7 +529,7 @@ var readAgentJson = (filePath) => {
482
529
  );
483
530
  };
484
531
  var writeJson = (filePath, data) => {
485
- const dir = dirname(filePath);
532
+ const dir = dirname2(filePath);
486
533
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
487
534
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
488
535
  };
@@ -561,24 +608,32 @@ var installGlobally2 = async () => {
561
608
  var _cachedSkillContent = null;
562
609
  var fetchSkillContent = async () => {
563
610
  if (_cachedSkillContent) return _cachedSkillContent;
564
- const __dirname = dirname(fileURLToPath(import.meta.url));
611
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
565
612
  const candidates = [
566
- join(__dirname, "..", "..", "data", "SKILL.md"),
567
- join(__dirname, "..", "data", "SKILL.md")
613
+ join2(__dirname, "..", "..", "data", "SKILL.md"),
614
+ join2(__dirname, "..", "data", "SKILL.md")
568
615
  ];
569
616
  const source = candidates.find((path) => existsSync(path));
570
617
  if (!source) throw new Error("packaged skill not found");
571
618
  _cachedSkillContent = readFileSync(source, "utf-8");
572
619
  return _cachedSkillContent;
573
620
  };
574
- var skillsCliInUse = () => isInstalled("skills") || existsSync(join(homedir(), ".agents", "skills"));
575
- var installSkill = async (agent, fallbackDir) => {
621
+ var writePackagedSkill = async (dir) => {
622
+ const content = await fetchSkillContent();
623
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
624
+ writeFileSync(join2(dir, "SKILL.md"), content, "utf-8");
625
+ };
626
+ var skillsCliInUse = () => isInstalled("skills") || existsSync(join2(homedir(), ".agents", "skills"));
627
+ var skillInstallEffects = {
628
+ runSkillsCli: installSkillViaSkillsCli,
629
+ skillModifiedAt: (dir) => statSync(join2(dir, "SKILL.md"), { throwIfNoEntry: false })?.mtimeMs ?? null,
630
+ writePackagedSkill,
631
+ now: () => Date.now()
632
+ };
633
+ var installSkill = async (agent, agentSkillDir) => {
576
634
  await spinner("Installing Pushary skill", async () => {
577
- if (skillsCliInUse() && installSkillViaSkillsCli(agent)) return;
578
- const content = await fetchSkillContent();
579
- if (!existsSync(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
580
- writeFileSync(join(fallbackDir, "SKILL.md"), content, "utf-8");
581
- });
635
+ await installAgentSkill({ agent, agentSkillDir, skillsCliInUse: skillsCliInUse() }, skillInstallEffects);
636
+ }, { optional: true });
582
637
  };
583
638
  var setupClaudeCode = async (apiKey) => {
584
639
  console.log(`
@@ -624,8 +679,8 @@ var resolveHermesPython = () => {
624
679
  }
625
680
  } catch {
626
681
  }
627
- const venvRoot = join(homedir(), ".hermes", "hermes-agent", "venv");
628
- const candidates = IS_WINDOWS ? [join(venvRoot, "Scripts", "python.exe"), join(venvRoot, "Scripts", "python3.exe")] : [join(venvRoot, "bin", "python3"), join(venvRoot, "bin", "python")];
682
+ const venvRoot = join2(homedir(), ".hermes", "hermes-agent", "venv");
683
+ const candidates = IS_WINDOWS ? [join2(venvRoot, "Scripts", "python.exe"), join2(venvRoot, "Scripts", "python3.exe")] : [join2(venvRoot, "bin", "python3"), join2(venvRoot, "bin", "python")];
629
684
  return candidates.find(existsSync) ?? null;
630
685
  };
631
686
  var ensurePip = (python) => {
@@ -641,13 +696,13 @@ var ensurePip = (python) => {
641
696
  };
642
697
  var enablePusharyPlugin = (python) => {
643
698
  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)';
644
- const scriptPath = join(tmpdir(), `pushary-hermes-${process.pid}.py`);
699
+ const scriptPath = join2(tmpdir(), `pushary-hermes-${process.pid}.py`);
645
700
  try {
646
701
  writeFileSync(scriptPath, snippet.split("; ").join("\n"), "utf-8");
647
702
  execSync2(`"${python}" "${scriptPath}"`, { stdio: "pipe", timeout: 15e3 });
648
- return;
703
+ return true;
649
704
  } catch {
650
- execSync2("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
705
+ return false;
651
706
  } finally {
652
707
  try {
653
708
  rmSync(scriptPath, { force: true });
@@ -674,9 +729,14 @@ var setupHermes = async (_apiKey) => {
674
729
  ensurePip(python);
675
730
  execSync2(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
676
731
  });
732
+ let pluginEnabled = false;
677
733
  await spinner("Enabling plugin + routing questions to push", async () => {
678
- enablePusharyPlugin(python);
734
+ pluginEnabled = enablePusharyPlugin(python);
679
735
  });
736
+ if (!pluginEnabled) {
737
+ console.log(` ${yellow("!")} Installed the plugin, but could not edit Hermes' config.`);
738
+ noteManual('Add "pushary" to plugins.enabled in ~/.hermes/config.yaml, and "clarify" to agent.disabled_toolsets');
739
+ }
680
740
  console.log();
681
741
  console.log(` ${dim("What this configured:")}`);
682
742
  console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
@@ -685,49 +745,71 @@ var setupHermes = async (_apiKey) => {
685
745
  console.log(` ${dim("\u2022")} Permission gating: set ${bold("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
686
746
  console.log(` ${dim("To re-enable terminal prompts:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
687
747
  };
688
- var CODEX_HOOKS_JSON = join(CODEX_HOME, "hooks.json");
748
+ var CODEX_HOOKS_JSON = join2(CODEX_HOME, "hooks.json");
689
749
  var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
690
750
  var CODEX_TRUST_VERIFIED_MAX = [0, 142, 2];
751
+ var codexOnPath = () => {
752
+ const whichCmd = IS_WINDOWS ? "where" : "which";
753
+ try {
754
+ const found = execSync2(`${whichCmd} codex`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
755
+ return found || null;
756
+ } catch {
757
+ return null;
758
+ }
759
+ };
760
+ var codexRealpath = (path) => {
761
+ try {
762
+ return realpathSync(path);
763
+ } catch {
764
+ return null;
765
+ }
766
+ };
691
767
  var detectCodexVersion = () => readCodexVersion({
692
- resolveOnPath: () => {
693
- const whichCmd = IS_WINDOWS ? "where" : "which";
768
+ resolveOnPath: codexOnPath,
769
+ realpath: codexRealpath,
770
+ readFile: (path) => {
694
771
  try {
695
- const found = execSync2(`${whichCmd} codex`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
696
- return found || null;
772
+ return readFileSync(path, "utf-8");
697
773
  } catch {
698
774
  return null;
699
775
  }
700
776
  },
701
- realpath: (path) => {
777
+ brewVersions: () => {
702
778
  try {
703
- return realpathSync(path);
779
+ return execSync2("brew list --versions codex", {
780
+ encoding: "utf-8",
781
+ stdio: "pipe",
782
+ timeout: 1e4
783
+ }).trim() || null;
704
784
  } catch {
705
785
  return null;
706
786
  }
707
- },
708
- readFile: (path) => {
787
+ }
788
+ });
789
+ var detectCodexInstall = () => checkCodexInstall({
790
+ resolveOnPath: codexOnPath,
791
+ realpath: codexRealpath,
792
+ exists: existsSync,
793
+ listDir: (path) => {
709
794
  try {
710
- return readFileSync(path, "utf-8");
795
+ return readdirSync(path);
711
796
  } catch {
712
797
  return null;
713
798
  }
714
799
  },
715
- brewVersions: () => {
800
+ fileSize: (path) => {
716
801
  try {
717
- return execSync2("brew list --versions codex", {
718
- encoding: "utf-8",
719
- stdio: "pipe",
720
- timeout: 1e4
721
- }).trim() || null;
802
+ const stat = statSync(path);
803
+ return stat.isFile() ? stat.size : null;
722
804
  } catch {
723
805
  return null;
724
806
  }
725
807
  },
726
- join,
727
- dirname
808
+ join: join2,
809
+ dirname: dirname2
728
810
  });
729
- var codexSupportsHooks = (result) => result.kind === "known" && compareCodexVersion(result.version, CODEX_HOOKS_MIN_VERSION) >= 0;
730
- var codexTrustAutoSupported = (result) => result.kind === "known" && codexSupportsHooks(result) && compareCodexVersion(result.version, CODEX_TRUST_VERIFIED_MAX) <= 0;
811
+ var codexSupportsHooks = (version) => version !== null && compareCodexVersion(version, CODEX_HOOKS_MIN_VERSION) >= 0;
812
+ var codexTrustAutoSupported = (version) => version !== null && codexSupportsHooks(version) && compareCodexVersion(version, CODEX_TRUST_VERIFIED_MAX) <= 0;
731
813
  var removeCodexNotifyEntry = (codexConfig) => {
732
814
  let raw = "";
733
815
  try {
@@ -771,6 +853,16 @@ var setupCodex = async (apiKey) => {
771
853
  console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
772
854
  return "skipped";
773
855
  }
856
+ if (detectCodexInstall().kind === "binary-missing") {
857
+ console.log(` ${yellow("!")} Your Codex install is missing the binary it runs.`);
858
+ console.log(` ${dim("A Pushary setup before 0.71.0 asked Codex its version. On macOS that hands")}`);
859
+ console.log(` ${dim("the binary to XProtect, which quarantines the Codex CLI and moves it to the")}`);
860
+ console.log(` ${dim("Trash. Setup no longer runs it. Reinstall Codex to get it back:")}`);
861
+ console.log(` ${cyan("npm install -g @openai/codex")}`);
862
+ console.log(` ${dim("Configuring anyway, so it is ready when Codex is.")}`);
863
+ console.log();
864
+ noteManual("Reinstall Codex: a Pushary setup before 0.71.0 got its binary quarantined by macOS");
865
+ }
774
866
  await installGlobally2();
775
867
  const codexConfig = codexConfigToml();
776
868
  await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
@@ -806,7 +898,7 @@ var setupCodex = async (apiKey) => {
806
898
  removeCodexNotifyEntry(codexConfig);
807
899
  });
808
900
  } else {
809
- if (codexVersion.kind === "known") {
901
+ if (codexVersion !== null) {
810
902
  console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
811
903
  console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
812
904
  console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
@@ -853,17 +945,17 @@ var setupCodex = async (apiKey) => {
853
945
  }
854
946
  };
855
947
  var resolveBundledPlugin = () => {
856
- const dir = dirname(fileURLToPath(import.meta.url));
948
+ const dir = dirname2(fileURLToPath(import.meta.url));
857
949
  const candidates = [
858
- join(dir, "..", "..", "data", "cursor-plugin"),
859
- join(dir, "..", "data", "cursor-plugin"),
860
- join(dir, "..", "..", "..", "cursor-plugin"),
861
- join(dir, "..", "..", "cursor-plugin")
950
+ join2(dir, "..", "..", "data", "cursor-plugin"),
951
+ join2(dir, "..", "data", "cursor-plugin"),
952
+ join2(dir, "..", "..", "..", "cursor-plugin"),
953
+ join2(dir, "..", "..", "cursor-plugin")
862
954
  ];
863
- return candidates.find((p) => existsSync(join(p, ".cursor-plugin", "plugin.json"))) ?? null;
955
+ return candidates.find((p) => existsSync(join2(p, ".cursor-plugin", "plugin.json"))) ?? null;
864
956
  };
865
957
  var installCursorUserHooks = (gateScript) => {
866
- const template = readJson(join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
958
+ const template = readJson(join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
867
959
  if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
868
960
  const entry = { ...template, command: `node "${gateScript}"` };
869
961
  let userHooks = {};
@@ -885,7 +977,7 @@ var installCursorUserHooks = (gateScript) => {
885
977
  writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
886
978
  };
887
979
  var neutralizePluginGate = () => {
888
- const path = join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
980
+ const path = join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
889
981
  if (!existsSync(path)) return;
890
982
  const data = readJson(path);
891
983
  if (data.hooks && "beforeShellExecution" in data.hooks) {
@@ -900,7 +992,7 @@ var setupCursor = async (apiKey) => {
900
992
  const source = resolveBundledPlugin();
901
993
  if (!source) throw new Error("bundled Cursor plugin not found in this package");
902
994
  await spinner("Installing Pushary plugin", async () => {
903
- const staging = join(dirname(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
995
+ const staging = join2(dirname2(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
904
996
  const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
905
997
  rmSync(staging, { recursive: true, force: true });
906
998
  try {
@@ -908,7 +1000,7 @@ var setupCursor = async (apiKey) => {
908
1000
  recursive: true,
909
1001
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
910
1002
  });
911
- const staged = readJsonSafe(join(staging, ".cursor-plugin", "plugin.json"));
1003
+ const staged = readJsonSafe(join2(staging, ".cursor-plugin", "plugin.json"));
912
1004
  if (staged.kind !== "ok") {
913
1005
  throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
914
1006
  }
@@ -926,7 +1018,7 @@ var setupCursor = async (apiKey) => {
926
1018
  }
927
1019
  });
928
1020
  await spinner("Linking your API key", async () => {
929
- const mcpPath = join(CURSOR_PLUGIN_DIR, "mcp.json");
1021
+ const mcpPath = join2(CURSOR_PLUGIN_DIR, "mcp.json");
930
1022
  const mcp = readAgentJson(mcpPath);
931
1023
  const servers = mcp.mcpServers ?? {};
932
1024
  if (servers.pushary) {
@@ -936,7 +1028,7 @@ var setupCursor = async (apiKey) => {
936
1028
  }
937
1029
  });
938
1030
  await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
939
- installCursorUserHooks(join(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
1031
+ installCursorUserHooks(join2(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
940
1032
  neutralizePluginGate();
941
1033
  });
942
1034
  console.log();
@@ -948,23 +1040,23 @@ var setupCursor = async (apiKey) => {
948
1040
  noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
949
1041
  };
950
1042
  var resolveBundledVsCodePlugin = () => {
951
- const dir = dirname(fileURLToPath(import.meta.url));
1043
+ const dir = dirname2(fileURLToPath(import.meta.url));
952
1044
  const candidates = [
953
- join(dir, "..", "..", "data", "vscode-plugin"),
954
- join(dir, "..", "data", "vscode-plugin"),
955
- join(dir, "..", "..", "..", "vscode-plugin"),
956
- join(dir, "..", "..", "vscode-plugin")
1045
+ join2(dir, "..", "..", "data", "vscode-plugin"),
1046
+ join2(dir, "..", "data", "vscode-plugin"),
1047
+ join2(dir, "..", "..", "..", "vscode-plugin"),
1048
+ join2(dir, "..", "..", "vscode-plugin")
957
1049
  ];
958
- return candidates.find((p) => existsSync(join(p, ".claude-plugin", "plugin.json"))) ?? null;
1050
+ return candidates.find((p) => existsSync(join2(p, ".claude-plugin", "plugin.json"))) ?? null;
959
1051
  };
960
1052
  var pinVsCodeGatePath = (pluginDir) => {
961
- const hooksPath = join(pluginDir, "hooks", "hooks.json");
1053
+ const hooksPath = join2(pluginDir, "hooks", "hooks.json");
962
1054
  const data = readJson(hooksPath);
963
1055
  const entries = data.hooks?.PreToolUse;
964
1056
  if (!Array.isArray(entries) || entries.length === 0) {
965
1057
  throw new Error("bundled VS Code hooks.json is missing a PreToolUse entry");
966
1058
  }
967
- const gate = join(pluginDir, "scripts", "pushary-gate.mjs");
1059
+ const gate = join2(pluginDir, "scripts", "pushary-gate.mjs");
968
1060
  data.hooks.PreToolUse = entries.map((entry) => ({ ...entry, command: `node "${gate}"` }));
969
1061
  writeJson(hooksPath, data);
970
1062
  };
@@ -980,7 +1072,7 @@ var registerVsCodePlugin = (pluginDir) => {
980
1072
  continue;
981
1073
  }
982
1074
  if (current !== null) backupFile(settingsPath);
983
- mkdirSync(dirname(settingsPath), { recursive: true });
1075
+ mkdirSync(dirname2(settingsPath), { recursive: true });
984
1076
  writeFileAtomic(settingsPath, result.content);
985
1077
  written.push(settingsPath);
986
1078
  }
@@ -993,16 +1085,16 @@ var setupVsCode = async (apiKey) => {
993
1085
  const source = resolveBundledVsCodePlugin();
994
1086
  if (!source) throw new Error("bundled VS Code plugin not found in this package");
995
1087
  await spinner("Installing Pushary plugin", async () => {
996
- const staging = join(dirname(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
1088
+ const staging = join2(dirname2(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
997
1089
  const backup = `${VSCODE_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
998
- mkdirSync(dirname(VSCODE_PLUGIN_DIR), { recursive: true });
1090
+ mkdirSync(dirname2(VSCODE_PLUGIN_DIR), { recursive: true });
999
1091
  rmSync(staging, { recursive: true, force: true });
1000
1092
  try {
1001
1093
  cpSync(source, staging, {
1002
1094
  recursive: true,
1003
1095
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
1004
1096
  });
1005
- const staged = readJsonSafe(join(staging, ".claude-plugin", "plugin.json"));
1097
+ const staged = readJsonSafe(join2(staging, ".claude-plugin", "plugin.json"));
1006
1098
  if (staged.kind !== "ok") {
1007
1099
  throw new Error("staged VS Code plugin is missing or has an unreadable plugin.json");
1008
1100
  }
@@ -1020,7 +1112,7 @@ var setupVsCode = async (apiKey) => {
1020
1112
  }
1021
1113
  });
1022
1114
  await spinner("Linking your API key", async () => {
1023
- const mcpPath = join(VSCODE_PLUGIN_DIR, ".mcp.json");
1115
+ const mcpPath = join2(VSCODE_PLUGIN_DIR, ".mcp.json");
1024
1116
  const mcp = readAgentJson(mcpPath);
1025
1117
  const servers = mcp.mcpServers ?? {};
1026
1118
  if (servers.pushary) {
@@ -1160,27 +1252,27 @@ var agentIsWired = (agent) => {
1160
1252
  return claudeWired({
1161
1253
  claudeJson: readJson2(CLAUDE_JSON),
1162
1254
  settings: readJson2(CLAUDE_SETTINGS),
1163
- skillExists: existsSync(join(CLAUDE_SKILL_DIR, "SKILL.md"))
1255
+ skillExists: existsSync(join2(CLAUDE_SKILL_DIR, "SKILL.md"))
1164
1256
  });
1165
1257
  case "codex": {
1166
1258
  let config = null;
1167
1259
  try {
1168
- config = parseTOML(readFileSync(join(CODEX_HOME, "config.toml"), "utf-8"));
1260
+ config = parseTOML(readFileSync(join2(CODEX_HOME, "config.toml"), "utf-8"));
1169
1261
  } catch {
1170
1262
  config = null;
1171
1263
  }
1172
1264
  return codexWired({
1173
1265
  config,
1174
1266
  hooks: readJson2(CODEX_HOOKS_JSON),
1175
- skillExists: existsSync(join(CODEX_SKILL_DIR, "SKILL.md"))
1267
+ skillExists: existsSync(join2(CODEX_SKILL_DIR, "SKILL.md"))
1176
1268
  });
1177
1269
  }
1178
1270
  case "gemini_cli":
1179
1271
  return geminiWired({ settings: readJson2(GEMINI_SETTINGS) });
1180
1272
  case "cursor":
1181
- return existsSync(join(CURSOR_PLUGIN_DIR, "mcp.json"));
1273
+ return existsSync(join2(CURSOR_PLUGIN_DIR, "mcp.json"));
1182
1274
  case "vscode":
1183
- return existsSync(join(VSCODE_PLUGIN_DIR, ".mcp.json"));
1275
+ return existsSync(join2(VSCODE_PLUGIN_DIR, ".mcp.json"));
1184
1276
  default:
1185
1277
  return true;
1186
1278
  }
@@ -1208,8 +1300,8 @@ var offerProjectInstructions = async (agents, options) => {
1208
1300
  });
1209
1301
  if (!wanted) return;
1210
1302
  for (const target of targets) {
1211
- await spinner(`Writing managed block to ${join(process.cwd(), target.file)}`, async () => {
1212
- writeInstructionBlock(join(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1303
+ await spinner(`Writing managed block to ${join2(process.cwd(), target.file)}`, async () => {
1304
+ writeInstructionBlock(join2(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1213
1305
  }, { optional: true });
1214
1306
  }
1215
1307
  console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
@@ -1360,12 +1452,12 @@ var resolveAgents = async (options) => {
1360
1452
  });
1361
1453
  };
1362
1454
  var AGENT_TARGETS = {
1363
- claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join(CLAUDE_SKILL_DIR, "SKILL.md")],
1364
- codex: [join(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1455
+ claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join2(CLAUDE_SKILL_DIR, "SKILL.md")],
1456
+ codex: [join2(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join2(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1365
1457
  gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
1366
1458
  hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
1367
- cursor: [join(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1368
- vscode: [join(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1459
+ cursor: [join2(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1460
+ vscode: [join2(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1369
1461
  custom: ["nothing (prints connection details only)"]
1370
1462
  };
1371
1463
  var reportDryRun = (apiKey, agents, keyCheck) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.71.0",
3
+ "version": "0.73.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",