@pushary/agent-hooks 0.71.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,34 @@
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
+
3
32
  ## 0.71.0
4
33
 
5
34
  ### Setting up Codex no longer gets Codex deleted by macOS
@@ -15,9 +44,6 @@ it. The version now comes from the package manifest next to the binary, which
15
44
  covers npm, bun, pnpm and yarn installs, and from `brew list` for the Homebrew
16
45
  cask, which ships no manifest. Same result, nothing launched.
17
46
 
18
- If your Codex was already quarantined, reinstall it: `npm install -g @openai/codex`
19
- or `brew install --cask codex`.
20
-
21
47
  ## 0.70.0
22
48
 
23
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, 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
@@ -482,7 +518,7 @@ var readAgentJson = (filePath) => {
482
518
  );
483
519
  };
484
520
  var writeJson = (filePath, data) => {
485
- const dir = dirname(filePath);
521
+ const dir = dirname2(filePath);
486
522
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
487
523
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
488
524
  };
@@ -561,23 +597,23 @@ var installGlobally2 = async () => {
561
597
  var _cachedSkillContent = null;
562
598
  var fetchSkillContent = async () => {
563
599
  if (_cachedSkillContent) return _cachedSkillContent;
564
- const __dirname = dirname(fileURLToPath(import.meta.url));
600
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
565
601
  const candidates = [
566
- join(__dirname, "..", "..", "data", "SKILL.md"),
567
- join(__dirname, "..", "data", "SKILL.md")
602
+ join2(__dirname, "..", "..", "data", "SKILL.md"),
603
+ join2(__dirname, "..", "data", "SKILL.md")
568
604
  ];
569
605
  const source = candidates.find((path) => existsSync(path));
570
606
  if (!source) throw new Error("packaged skill not found");
571
607
  _cachedSkillContent = readFileSync(source, "utf-8");
572
608
  return _cachedSkillContent;
573
609
  };
574
- var skillsCliInUse = () => isInstalled("skills") || existsSync(join(homedir(), ".agents", "skills"));
610
+ var skillsCliInUse = () => isInstalled("skills") || existsSync(join2(homedir(), ".agents", "skills"));
575
611
  var installSkill = async (agent, fallbackDir) => {
576
612
  await spinner("Installing Pushary skill", async () => {
577
613
  if (skillsCliInUse() && installSkillViaSkillsCli(agent)) return;
578
614
  const content = await fetchSkillContent();
579
615
  if (!existsSync(fallbackDir)) mkdirSync(fallbackDir, { recursive: true });
580
- writeFileSync(join(fallbackDir, "SKILL.md"), content, "utf-8");
616
+ writeFileSync(join2(fallbackDir, "SKILL.md"), content, "utf-8");
581
617
  });
582
618
  };
583
619
  var setupClaudeCode = async (apiKey) => {
@@ -624,8 +660,8 @@ var resolveHermesPython = () => {
624
660
  }
625
661
  } catch {
626
662
  }
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")];
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")];
629
665
  return candidates.find(existsSync) ?? null;
630
666
  };
631
667
  var ensurePip = (python) => {
@@ -641,13 +677,13 @@ var ensurePip = (python) => {
641
677
  };
642
678
  var enablePusharyPlugin = (python) => {
643
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)';
644
- const scriptPath = join(tmpdir(), `pushary-hermes-${process.pid}.py`);
680
+ const scriptPath = join2(tmpdir(), `pushary-hermes-${process.pid}.py`);
645
681
  try {
646
682
  writeFileSync(scriptPath, snippet.split("; ").join("\n"), "utf-8");
647
683
  execSync2(`"${python}" "${scriptPath}"`, { stdio: "pipe", timeout: 15e3 });
648
- return;
684
+ return true;
649
685
  } catch {
650
- execSync2("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
686
+ return false;
651
687
  } finally {
652
688
  try {
653
689
  rmSync(scriptPath, { force: true });
@@ -674,9 +710,14 @@ var setupHermes = async (_apiKey) => {
674
710
  ensurePip(python);
675
711
  execSync2(`"${python}" -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 18e4 });
676
712
  });
713
+ let pluginEnabled = false;
677
714
  await spinner("Enabling plugin + routing questions to push", async () => {
678
- enablePusharyPlugin(python);
715
+ pluginEnabled = enablePusharyPlugin(python);
679
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
+ }
680
721
  console.log();
681
722
  console.log(` ${dim("What this configured:")}`);
682
723
  console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
@@ -685,49 +726,71 @@ var setupHermes = async (_apiKey) => {
685
726
  console.log(` ${dim("\u2022")} Permission gating: set ${bold("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
686
727
  console.log(` ${dim("To re-enable terminal prompts:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
687
728
  };
688
- var CODEX_HOOKS_JSON = join(CODEX_HOME, "hooks.json");
729
+ var CODEX_HOOKS_JSON = join2(CODEX_HOME, "hooks.json");
689
730
  var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
690
731
  var CODEX_TRUST_VERIFIED_MAX = [0, 142, 2];
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;
739
+ }
740
+ };
741
+ var codexRealpath = (path) => {
742
+ try {
743
+ return realpathSync(path);
744
+ } catch {
745
+ return null;
746
+ }
747
+ };
691
748
  var detectCodexVersion = () => readCodexVersion({
692
- resolveOnPath: () => {
693
- const whichCmd = IS_WINDOWS ? "where" : "which";
749
+ resolveOnPath: codexOnPath,
750
+ realpath: codexRealpath,
751
+ readFile: (path) => {
694
752
  try {
695
- const found = execSync2(`${whichCmd} codex`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).split("\n")[0].trim();
696
- return found || null;
753
+ return readFileSync(path, "utf-8");
697
754
  } catch {
698
755
  return null;
699
756
  }
700
757
  },
701
- realpath: (path) => {
758
+ brewVersions: () => {
702
759
  try {
703
- return realpathSync(path);
760
+ return execSync2("brew list --versions codex", {
761
+ encoding: "utf-8",
762
+ stdio: "pipe",
763
+ timeout: 1e4
764
+ }).trim() || null;
704
765
  } catch {
705
766
  return null;
706
767
  }
707
- },
708
- readFile: (path) => {
768
+ }
769
+ });
770
+ var detectCodexInstall = () => checkCodexInstall({
771
+ resolveOnPath: codexOnPath,
772
+ realpath: codexRealpath,
773
+ exists: existsSync,
774
+ listDir: (path) => {
709
775
  try {
710
- return readFileSync(path, "utf-8");
776
+ return readdirSync(path);
711
777
  } catch {
712
778
  return null;
713
779
  }
714
780
  },
715
- brewVersions: () => {
781
+ fileSize: (path) => {
716
782
  try {
717
- return execSync2("brew list --versions codex", {
718
- encoding: "utf-8",
719
- stdio: "pipe",
720
- timeout: 1e4
721
- }).trim() || null;
783
+ const stat = statSync(path);
784
+ return stat.isFile() ? stat.size : null;
722
785
  } catch {
723
786
  return null;
724
787
  }
725
788
  },
726
- join,
727
- dirname
789
+ join: join2,
790
+ dirname: dirname2
728
791
  });
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;
792
+ var codexSupportsHooks = (version) => version !== null && compareCodexVersion(version, CODEX_HOOKS_MIN_VERSION) >= 0;
793
+ var codexTrustAutoSupported = (version) => version !== null && codexSupportsHooks(version) && compareCodexVersion(version, CODEX_TRUST_VERIFIED_MAX) <= 0;
731
794
  var removeCodexNotifyEntry = (codexConfig) => {
732
795
  let raw = "";
733
796
  try {
@@ -771,6 +834,16 @@ var setupCodex = async (apiKey) => {
771
834
  console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
772
835
  return "skipped";
773
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
+ }
774
847
  await installGlobally2();
775
848
  const codexConfig = codexConfigToml();
776
849
  await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
@@ -806,7 +879,7 @@ var setupCodex = async (apiKey) => {
806
879
  removeCodexNotifyEntry(codexConfig);
807
880
  });
808
881
  } else {
809
- if (codexVersion.kind === "known") {
882
+ if (codexVersion !== null) {
810
883
  console.log(` ${yellow("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
811
884
  console.log(` ${dim("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
812
885
  console.log(` ${dim("to get policy enforcement, phone approvals, and session tracking.")}`);
@@ -853,17 +926,17 @@ var setupCodex = async (apiKey) => {
853
926
  }
854
927
  };
855
928
  var resolveBundledPlugin = () => {
856
- const dir = dirname(fileURLToPath(import.meta.url));
929
+ const dir = dirname2(fileURLToPath(import.meta.url));
857
930
  const candidates = [
858
- join(dir, "..", "..", "data", "cursor-plugin"),
859
- join(dir, "..", "data", "cursor-plugin"),
860
- join(dir, "..", "..", "..", "cursor-plugin"),
861
- 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")
862
935
  ];
863
- 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;
864
937
  };
865
938
  var installCursorUserHooks = (gateScript) => {
866
- 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];
867
940
  if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
868
941
  const entry = { ...template, command: `node "${gateScript}"` };
869
942
  let userHooks = {};
@@ -885,7 +958,7 @@ var installCursorUserHooks = (gateScript) => {
885
958
  writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
886
959
  };
887
960
  var neutralizePluginGate = () => {
888
- const path = join(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
961
+ const path = join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
889
962
  if (!existsSync(path)) return;
890
963
  const data = readJson(path);
891
964
  if (data.hooks && "beforeShellExecution" in data.hooks) {
@@ -900,7 +973,7 @@ var setupCursor = async (apiKey) => {
900
973
  const source = resolveBundledPlugin();
901
974
  if (!source) throw new Error("bundled Cursor plugin not found in this package");
902
975
  await spinner("Installing Pushary plugin", async () => {
903
- const staging = join(dirname(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
976
+ const staging = join2(dirname2(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
904
977
  const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
905
978
  rmSync(staging, { recursive: true, force: true });
906
979
  try {
@@ -908,7 +981,7 @@ var setupCursor = async (apiKey) => {
908
981
  recursive: true,
909
982
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
910
983
  });
911
- const staged = readJsonSafe(join(staging, ".cursor-plugin", "plugin.json"));
984
+ const staged = readJsonSafe(join2(staging, ".cursor-plugin", "plugin.json"));
912
985
  if (staged.kind !== "ok") {
913
986
  throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
914
987
  }
@@ -926,7 +999,7 @@ var setupCursor = async (apiKey) => {
926
999
  }
927
1000
  });
928
1001
  await spinner("Linking your API key", async () => {
929
- const mcpPath = join(CURSOR_PLUGIN_DIR, "mcp.json");
1002
+ const mcpPath = join2(CURSOR_PLUGIN_DIR, "mcp.json");
930
1003
  const mcp = readAgentJson(mcpPath);
931
1004
  const servers = mcp.mcpServers ?? {};
932
1005
  if (servers.pushary) {
@@ -936,7 +1009,7 @@ var setupCursor = async (apiKey) => {
936
1009
  }
937
1010
  });
938
1011
  await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
939
- installCursorUserHooks(join(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
1012
+ installCursorUserHooks(join2(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
940
1013
  neutralizePluginGate();
941
1014
  });
942
1015
  console.log();
@@ -948,23 +1021,23 @@ var setupCursor = async (apiKey) => {
948
1021
  noteManual("Fully quit and reopen Cursor. A Reload Window may not be enough.");
949
1022
  };
950
1023
  var resolveBundledVsCodePlugin = () => {
951
- const dir = dirname(fileURLToPath(import.meta.url));
1024
+ const dir = dirname2(fileURLToPath(import.meta.url));
952
1025
  const candidates = [
953
- join(dir, "..", "..", "data", "vscode-plugin"),
954
- join(dir, "..", "data", "vscode-plugin"),
955
- join(dir, "..", "..", "..", "vscode-plugin"),
956
- 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")
957
1030
  ];
958
- 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;
959
1032
  };
960
1033
  var pinVsCodeGatePath = (pluginDir) => {
961
- const hooksPath = join(pluginDir, "hooks", "hooks.json");
1034
+ const hooksPath = join2(pluginDir, "hooks", "hooks.json");
962
1035
  const data = readJson(hooksPath);
963
1036
  const entries = data.hooks?.PreToolUse;
964
1037
  if (!Array.isArray(entries) || entries.length === 0) {
965
1038
  throw new Error("bundled VS Code hooks.json is missing a PreToolUse entry");
966
1039
  }
967
- const gate = join(pluginDir, "scripts", "pushary-gate.mjs");
1040
+ const gate = join2(pluginDir, "scripts", "pushary-gate.mjs");
968
1041
  data.hooks.PreToolUse = entries.map((entry) => ({ ...entry, command: `node "${gate}"` }));
969
1042
  writeJson(hooksPath, data);
970
1043
  };
@@ -980,7 +1053,7 @@ var registerVsCodePlugin = (pluginDir) => {
980
1053
  continue;
981
1054
  }
982
1055
  if (current !== null) backupFile(settingsPath);
983
- mkdirSync(dirname(settingsPath), { recursive: true });
1056
+ mkdirSync(dirname2(settingsPath), { recursive: true });
984
1057
  writeFileAtomic(settingsPath, result.content);
985
1058
  written.push(settingsPath);
986
1059
  }
@@ -993,16 +1066,16 @@ var setupVsCode = async (apiKey) => {
993
1066
  const source = resolveBundledVsCodePlugin();
994
1067
  if (!source) throw new Error("bundled VS Code plugin not found in this package");
995
1068
  await spinner("Installing Pushary plugin", async () => {
996
- 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}`);
997
1070
  const backup = `${VSCODE_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
998
- mkdirSync(dirname(VSCODE_PLUGIN_DIR), { recursive: true });
1071
+ mkdirSync(dirname2(VSCODE_PLUGIN_DIR), { recursive: true });
999
1072
  rmSync(staging, { recursive: true, force: true });
1000
1073
  try {
1001
1074
  cpSync(source, staging, {
1002
1075
  recursive: true,
1003
1076
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
1004
1077
  });
1005
- const staged = readJsonSafe(join(staging, ".claude-plugin", "plugin.json"));
1078
+ const staged = readJsonSafe(join2(staging, ".claude-plugin", "plugin.json"));
1006
1079
  if (staged.kind !== "ok") {
1007
1080
  throw new Error("staged VS Code plugin is missing or has an unreadable plugin.json");
1008
1081
  }
@@ -1020,7 +1093,7 @@ var setupVsCode = async (apiKey) => {
1020
1093
  }
1021
1094
  });
1022
1095
  await spinner("Linking your API key", async () => {
1023
- const mcpPath = join(VSCODE_PLUGIN_DIR, ".mcp.json");
1096
+ const mcpPath = join2(VSCODE_PLUGIN_DIR, ".mcp.json");
1024
1097
  const mcp = readAgentJson(mcpPath);
1025
1098
  const servers = mcp.mcpServers ?? {};
1026
1099
  if (servers.pushary) {
@@ -1160,27 +1233,27 @@ var agentIsWired = (agent) => {
1160
1233
  return claudeWired({
1161
1234
  claudeJson: readJson2(CLAUDE_JSON),
1162
1235
  settings: readJson2(CLAUDE_SETTINGS),
1163
- skillExists: existsSync(join(CLAUDE_SKILL_DIR, "SKILL.md"))
1236
+ skillExists: existsSync(join2(CLAUDE_SKILL_DIR, "SKILL.md"))
1164
1237
  });
1165
1238
  case "codex": {
1166
1239
  let config = null;
1167
1240
  try {
1168
- config = parseTOML(readFileSync(join(CODEX_HOME, "config.toml"), "utf-8"));
1241
+ config = parseTOML(readFileSync(join2(CODEX_HOME, "config.toml"), "utf-8"));
1169
1242
  } catch {
1170
1243
  config = null;
1171
1244
  }
1172
1245
  return codexWired({
1173
1246
  config,
1174
1247
  hooks: readJson2(CODEX_HOOKS_JSON),
1175
- skillExists: existsSync(join(CODEX_SKILL_DIR, "SKILL.md"))
1248
+ skillExists: existsSync(join2(CODEX_SKILL_DIR, "SKILL.md"))
1176
1249
  });
1177
1250
  }
1178
1251
  case "gemini_cli":
1179
1252
  return geminiWired({ settings: readJson2(GEMINI_SETTINGS) });
1180
1253
  case "cursor":
1181
- return existsSync(join(CURSOR_PLUGIN_DIR, "mcp.json"));
1254
+ return existsSync(join2(CURSOR_PLUGIN_DIR, "mcp.json"));
1182
1255
  case "vscode":
1183
- return existsSync(join(VSCODE_PLUGIN_DIR, ".mcp.json"));
1256
+ return existsSync(join2(VSCODE_PLUGIN_DIR, ".mcp.json"));
1184
1257
  default:
1185
1258
  return true;
1186
1259
  }
@@ -1208,8 +1281,8 @@ var offerProjectInstructions = async (agents, options) => {
1208
1281
  });
1209
1282
  if (!wanted) return;
1210
1283
  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));
1284
+ await spinner(`Writing managed block to ${join2(process.cwd(), target.file)}`, async () => {
1285
+ writeInstructionBlock(join2(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1213
1286
  }, { optional: true });
1214
1287
  }
1215
1288
  console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
@@ -1360,12 +1433,12 @@ var resolveAgents = async (options) => {
1360
1433
  });
1361
1434
  };
1362
1435
  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],
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],
1365
1438
  gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
1366
1439
  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)],
1440
+ cursor: [join2(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1441
+ vscode: [join2(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1369
1442
  custom: ["nothing (prints connection details only)"]
1370
1443
  };
1371
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.71.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",