@pushary/agent-hooks 0.87.2 → 0.87.4

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.
Files changed (63) hide show
  1. package/data/cursor-plugin/hooks/hooks.json +7 -0
  2. package/data/cursor-plugin/scripts/pushary-gate.mjs +94 -28
  3. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +81 -1
  4. package/dist/bin/pushary-bell-hook.js +4 -4
  5. package/dist/bin/pushary-bell.js +7 -6
  6. package/dist/bin/pushary-claude.js +14 -11
  7. package/dist/bin/pushary-clean.js +36 -20
  8. package/dist/bin/pushary-codex-bridge.js +6 -5
  9. package/dist/bin/pushary-codex-hook.js +129 -52
  10. package/dist/bin/pushary-codex.js +4 -3
  11. package/dist/bin/pushary-connect.js +6 -6
  12. package/dist/bin/pushary-daemon.js +5 -4
  13. package/dist/bin/pushary-disconnect.js +65 -8
  14. package/dist/bin/pushary-doctor.js +79 -18
  15. package/dist/bin/pushary-elicitation-hook.js +1 -1
  16. package/dist/bin/pushary-gemini-bridge.js +6 -5
  17. package/dist/bin/pushary-gemini-hook.js +12 -10
  18. package/dist/bin/pushary-hook.js +6 -5
  19. package/dist/bin/pushary-login.js +4 -4
  20. package/dist/bin/pushary-logout.js +5 -4
  21. package/dist/bin/pushary-mode.js +3 -3
  22. package/dist/bin/pushary-notification-hook.js +4 -3
  23. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  24. package/dist/bin/pushary-opencode-hook.js +329 -0
  25. package/dist/bin/pushary-permission-denied-hook.js +6 -5
  26. package/dist/bin/pushary-permission-hook.js +6 -5
  27. package/dist/bin/pushary-post-hook.js +4 -3
  28. package/dist/bin/pushary-prompt-hook.js +4 -3
  29. package/dist/bin/pushary-session-end-hook.js +4 -3
  30. package/dist/bin/pushary-session-start-hook.js +4 -3
  31. package/dist/bin/pushary-setup.js +238 -84
  32. package/dist/bin/pushary-stats.js +2 -2
  33. package/dist/bin/pushary-status.js +5 -5
  34. package/dist/bin/pushary-stop-hook.js +4 -3
  35. package/dist/bin/pushary-stopfailure-hook.js +4 -3
  36. package/dist/bin/pushary-upgrade.js +7 -6
  37. package/dist/bin/pushary-wait.js +3 -3
  38. package/dist/{chunk-5646VKR4.js → chunk-2JYQAW4B.js} +5 -53
  39. package/dist/chunk-7J67NYK4.js +287 -0
  40. package/dist/{chunk-M7RYIZX6.js → chunk-B7ZBHWOW.js} +10 -1
  41. package/dist/{chunk-QBWYNVKY.js → chunk-BFBAHOGR.js} +1 -1
  42. package/dist/{chunk-7YJS2VTG.js → chunk-DMW54VWK.js} +12 -1
  43. package/dist/{chunk-3LKYBFWA.js → chunk-EJL3PJU7.js} +1 -1
  44. package/dist/chunk-GIRT7677.js +11 -0
  45. package/dist/{chunk-SJVLFIYP.js → chunk-H5PIPIKQ.js} +1 -1
  46. package/dist/{chunk-HAUTLCMA.js → chunk-ICERLAE2.js} +1 -1
  47. package/dist/{chunk-ZZE6FN7T.js → chunk-K42RXJPG.js} +52 -1
  48. package/dist/{chunk-Y3UTTFTZ.js → chunk-KBS53WOE.js} +1 -1
  49. package/dist/{chunk-A4I5JGCW.js → chunk-KFAH6KRE.js} +1 -1
  50. package/dist/{chunk-LT2VLPUK.js → chunk-KULUNLRO.js} +18 -2
  51. package/dist/{chunk-B3V2H6SA.js → chunk-NAPRGHZ3.js} +54 -13
  52. package/dist/{chunk-HJ44HIUW.js → chunk-OMA2OZRE.js} +11 -9
  53. package/dist/{chunk-ECEUMWBH.js → chunk-QLMX5LLV.js} +1 -1
  54. package/dist/{chunk-XW5CHAQX.js → chunk-QXZ2CKRK.js} +2 -2
  55. package/dist/{chunk-JAICP7BA.js → chunk-TD3M232T.js} +1 -1
  56. package/dist/{chunk-WLLBFDVY.js → chunk-TV3UFGT7.js} +1 -1
  57. package/dist/{chunk-7MBDFNUC.js → chunk-VIHRP25S.js} +1 -1
  58. package/dist/{chunk-V2XPF77V.js → chunk-VOE4SYPR.js} +42 -279
  59. package/dist/{chunk-OT4YD54Q.js → chunk-YX6ZKCC5.js} +1 -1
  60. package/dist/{chunk-2T3GX7WG.js → chunk-YYTLTEGU.js} +1 -1
  61. package/dist/chunk-ZGC3W23N.js +371 -0
  62. package/dist/src/index.js +6 -5
  63. package/package.json +2 -1
@@ -1,26 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  detectClaudeVersion
4
- } from "../chunk-HAUTLCMA.js";
4
+ } from "../chunk-ICERLAE2.js";
5
5
  import {
6
6
  claudeWired,
7
7
  codexWired,
8
8
  geminiWired
9
- } from "../chunk-XW5CHAQX.js";
9
+ } from "../chunk-QXZ2CKRK.js";
10
10
  import {
11
11
  runBrowserLogin
12
12
  } from "../chunk-Z3Z4VK5F.js";
13
13
  import {
14
14
  ensureDaemonRunning
15
- } from "../chunk-SJVLFIYP.js";
15
+ } from "../chunk-H5PIPIKQ.js";
16
16
  import {
17
17
  addClaudeMcpServer,
18
18
  addPusharyHooks,
19
- addPusharyToolPermissions,
20
- compareVersion,
21
- parseVersion
22
- } from "../chunk-5646VKR4.js";
19
+ addPusharyToolPermissions
20
+ } from "../chunk-2JYQAW4B.js";
23
21
  import {
22
+ CURSOR_GATE_EVENTS,
24
23
  describeDetection,
25
24
  detectAllAgents,
26
25
  isDetected,
@@ -31,17 +30,30 @@ import {
31
30
  shortenHome,
32
31
  vscodeSettingsTargets,
33
32
  writeInstructionBlock
34
- } from "../chunk-LT2VLPUK.js";
33
+ } from "../chunk-KULUNLRO.js";
34
+ import {
35
+ OPENCODE_HOOK_BINARY,
36
+ addOpenCodeMcpServer,
37
+ installOpenCodePlugin,
38
+ openCodeConfigDir,
39
+ openCodeConfigFile,
40
+ openCodePluginDir,
41
+ openCodePluginFile,
42
+ openCodeWired
43
+ } from "../chunk-ZGC3W23N.js";
35
44
  import {
36
45
  GEMINI_HOOK_BINARY,
37
46
  addGeminiHooks,
38
- addGeminiMcpServer
39
- } from "../chunk-ZZE6FN7T.js";
47
+ addGeminiMcpServer,
48
+ compareVersion,
49
+ parseVersion
50
+ } from "../chunk-K42RXJPG.js";
40
51
  import "../chunk-ATBKJNWS.js";
41
52
  import {
42
53
  guardBinary,
43
- guardNodeScript
44
- } from "../chunk-M7RYIZX6.js";
54
+ guardNodeScript,
55
+ guardedArgv
56
+ } from "../chunk-B7ZBHWOW.js";
45
57
  import "../chunk-XHKBHWLX.js";
46
58
  import {
47
59
  installGlobally,
@@ -61,17 +73,20 @@ import {
61
73
  codexHome,
62
74
  codexHookPositions,
63
75
  codexSkillDir2 as codexSkillDir,
76
+ codexTrustSchemeAgrees,
64
77
  cursorPluginDir,
65
78
  cursorUserHooks,
66
79
  geminiMd,
67
80
  geminiSettings,
68
81
  pusharyConfigFile,
69
82
  vscodePluginDir
70
- } from "../chunk-B3V2H6SA.js";
83
+ } from "../chunk-NAPRGHZ3.js";
84
+ import "../chunk-GIRT7677.js";
71
85
  import {
72
86
  reportEvent
73
- } from "../chunk-V2XPF77V.js";
87
+ } from "../chunk-VOE4SYPR.js";
74
88
  import "../chunk-J3YDT4HM.js";
89
+ import "../chunk-7J67NYK4.js";
75
90
  import "../chunk-RN3NOEJF.js";
76
91
  import {
77
92
  confirmAppConnection,
@@ -81,12 +96,12 @@ import {
81
96
  printConnectInstructions,
82
97
  setHumanStream,
83
98
  verifyRoundTrip
84
- } from "../chunk-3LKYBFWA.js";
99
+ } from "../chunk-EJL3PJU7.js";
85
100
  import "../chunk-3EGEA4KH.js";
86
101
  import {
87
102
  checkApiKey,
88
103
  describeKeyCheck
89
- } from "../chunk-A4I5JGCW.js";
104
+ } from "../chunk-KFAH6KRE.js";
90
105
  import {
91
106
  useColor
92
107
  } from "../chunk-5RUIFDTP.js";
@@ -97,7 +112,7 @@ import {
97
112
  readConfigFileKey,
98
113
  readKeySource,
99
114
  writeKey
100
- } from "../chunk-OT4YD54Q.js";
115
+ } from "../chunk-YX6ZKCC5.js";
101
116
  import {
102
117
  parseFlags
103
118
  } from "../chunk-GMXKITVA.js";
@@ -120,7 +135,7 @@ import {
120
135
  } from "../chunk-6MTNS63X.js";
121
136
  import "../chunk-BSZYIAZL.js";
122
137
  import "../chunk-SAF6HGAA.js";
123
- import "../chunk-Y3UTTFTZ.js";
138
+ import "../chunk-KBS53WOE.js";
124
139
  import {
125
140
  UsageError
126
141
  } from "../chunk-7QLSKOSU.js";
@@ -130,11 +145,11 @@ import {
130
145
  import "../chunk-2UMNXADU.js";
131
146
  import {
132
147
  isValidApiKey
133
- } from "../chunk-7YJS2VTG.js";
148
+ } from "../chunk-DMW54VWK.js";
134
149
 
135
150
  // bin/pushary-setup.ts
136
- import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync, realpathSync, readdirSync, statSync } from "fs";
137
- import { join as join2, dirname as dirname2, basename } from "path";
151
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, renameSync, realpathSync, readdirSync, statSync, openSync, readSync, closeSync } from "fs";
152
+ import { join as join3, dirname as dirname2, basename } from "path";
138
153
  import { homedir, tmpdir } from "os";
139
154
  import { execSync as execSync2 } from "child_process";
140
155
  import { checkbox, input, confirm } from "@inquirer/prompts";
@@ -274,7 +289,12 @@ var readCodexVersion = (probe) => {
274
289
  if (fromManifest) return fromManifest;
275
290
  }
276
291
  const brew = probe.brewVersions();
277
- return brew ? parseCodexVersion(brew) : null;
292
+ if (brew) {
293
+ const parsed = parseCodexVersion(brew);
294
+ if (parsed) return parsed;
295
+ }
296
+ const bundled = probe.bundledVersion?.();
297
+ return bundled ? parseCodexVersion(bundled) : null;
278
298
  };
279
299
 
280
300
  // src/setup/codex-install.ts
@@ -317,6 +337,30 @@ var checkCodexInstall = (probe) => {
317
337
  return { kind: "binary-missing", vendorDir: vendors[0] };
318
338
  };
319
339
 
340
+ // src/setup/codex-bundled.ts
341
+ import { join as join2 } from "path";
342
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
343
+ var codexCliPathFromConfig = (config) => {
344
+ const servers = asRecord(config.mcp_servers);
345
+ if (!servers) return null;
346
+ for (const server of Object.values(servers)) {
347
+ const declared = asRecord(asRecord(server)?.env)?.CODEX_CLI_PATH;
348
+ if (typeof declared === "string" && declared.trim()) return declared.trim();
349
+ }
350
+ return null;
351
+ };
352
+ var bundledCodexCandidates = (home, platform) => platform === "darwin" ? [
353
+ "/Applications/ChatGPT.app/Contents/Resources/codex",
354
+ join2(home, "Applications", "ChatGPT.app", "Contents", "Resources", "codex")
355
+ ] : [];
356
+ var findBundledCodex = (probe) => {
357
+ const config = probe.config();
358
+ const declared = config ? codexCliPathFromConfig(config) : null;
359
+ if (declared && probe.exists(declared)) return declared;
360
+ return bundledCodexCandidates(probe.home, probe.platform).find(probe.exists) ?? null;
361
+ };
362
+ var parseEmbeddedCodexVersion = (haystack) => haystack.match(/cli_version(\d+\.\d+\.\d+)/)?.[1] ?? null;
363
+
320
364
  // src/skills-cli.ts
321
365
  import { execSync } from "child_process";
322
366
  var installSkillViaSkillsCli = (agent) => {
@@ -414,7 +458,7 @@ var reportSetupAbortWith = async (deps, reason, exitCode, keyCheck = "unknown")
414
458
  var reportSetupAbort = (reason, exitCode, keyCheck) => reportSetupAbortWith(defaultDeps, reason, exitCode, keyCheck);
415
459
 
416
460
  // src/setup/options.ts
417
- var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "vscode", "custom"];
461
+ var AGENT_CHOICES = ["claude_code", "codex", "gemini_cli", "hermes", "cursor", "vscode", "opencode", "custom"];
418
462
  var CONNECT_MODES = ["app", "browser", "web", "none"];
419
463
  var CONNECT_ALIASES = { auto: "app", pwa: "web" };
420
464
  var parseConnectMode = (raw) => {
@@ -677,8 +721,8 @@ var fetchSkillContent = async () => {
677
721
  if (_cachedSkillContent) return _cachedSkillContent;
678
722
  const __dirname = dirname2(fileURLToPath(import.meta.url));
679
723
  const candidates = [
680
- join2(__dirname, "..", "..", "data", "SKILL.md"),
681
- join2(__dirname, "..", "data", "SKILL.md")
724
+ join3(__dirname, "..", "..", "data", "SKILL.md"),
725
+ join3(__dirname, "..", "data", "SKILL.md")
682
726
  ];
683
727
  const source = candidates.find((path) => existsSync(path));
684
728
  if (!source) throw new Error("packaged skill not found");
@@ -688,12 +732,12 @@ var fetchSkillContent = async () => {
688
732
  var writePackagedSkill = async (dir) => {
689
733
  const content = await fetchSkillContent();
690
734
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
691
- writeFileSync(join2(dir, "SKILL.md"), content, "utf-8");
735
+ writeFileSync(join3(dir, "SKILL.md"), content, "utf-8");
692
736
  };
693
- var skillsCliInUse = () => isInstalled("skills") || existsSync(join2(homedir(), ".agents", "skills"));
737
+ var skillsCliInUse = () => isInstalled("skills") || existsSync(join3(homedir(), ".agents", "skills"));
694
738
  var skillInstallEffects = {
695
739
  runSkillsCli: installSkillViaSkillsCli,
696
- skillModifiedAt: (dir) => statSync(join2(dir, "SKILL.md"), { throwIfNoEntry: false })?.mtimeMs ?? null,
740
+ skillModifiedAt: (dir) => statSync(join3(dir, "SKILL.md"), { throwIfNoEntry: false })?.mtimeMs ?? null,
697
741
  writePackagedSkill,
698
742
  now: () => Date.now()
699
743
  };
@@ -746,8 +790,8 @@ var resolveHermesPython = () => {
746
790
  }
747
791
  } catch {
748
792
  }
749
- const venvRoot = join2(homedir(), ".hermes", "hermes-agent", "venv");
750
- const candidates = IS_WINDOWS ? [join2(venvRoot, "Scripts", "python.exe"), join2(venvRoot, "Scripts", "python3.exe")] : [join2(venvRoot, "bin", "python3"), join2(venvRoot, "bin", "python")];
793
+ const venvRoot = join3(homedir(), ".hermes", "hermes-agent", "venv");
794
+ const candidates = IS_WINDOWS ? [join3(venvRoot, "Scripts", "python.exe"), join3(venvRoot, "Scripts", "python3.exe")] : [join3(venvRoot, "bin", "python3"), join3(venvRoot, "bin", "python")];
751
795
  return candidates.find(existsSync) ?? null;
752
796
  };
753
797
  var ensurePip = (python) => {
@@ -763,7 +807,7 @@ var ensurePip = (python) => {
763
807
  };
764
808
  var enablePusharyPlugin = (python) => {
765
809
  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)';
766
- const scriptPath = join2(tmpdir(), `pushary-hermes-${process.pid}.py`);
810
+ const scriptPath = join3(tmpdir(), `pushary-hermes-${process.pid}.py`);
767
811
  try {
768
812
  writeFileSync(scriptPath, snippet.split("; ").join("\n"), "utf-8");
769
813
  execSync2(`"${python}" "${scriptPath}"`, { stdio: "pipe", timeout: 15e3 });
@@ -812,9 +856,9 @@ var setupHermes = async (_apiKey) => {
812
856
  console.log(` ${dim("\u2022")} Permission gating: set ${bold("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
813
857
  console.log(` ${dim("To re-enable terminal prompts:")} remove ${bold("clarify")} from ${dim("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
814
858
  };
815
- var CODEX_HOOKS_JSON = join2(CODEX_HOME, "hooks.json");
859
+ var CODEX_HOOKS_JSON = join3(CODEX_HOME, "hooks.json");
816
860
  var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
817
- var CODEX_TRUST_VERIFIED_MAX = [0, 142, 2];
861
+ var CODEX_TRUST_VERIFIED_MAX = [0, 147, 0];
818
862
  var codexOnPath = () => {
819
863
  const whichCmd = IS_WINDOWS ? "where" : "which";
820
864
  try {
@@ -831,6 +875,49 @@ var codexRealpath = (path) => {
831
875
  return null;
832
876
  }
833
877
  };
878
+ var EMBEDDED_SCAN_CHUNK_BYTES = 4 * 1024 * 1024;
879
+ var EMBEDDED_SCAN_MAX_BYTES = 320 * 1024 * 1024;
880
+ var readEmbeddedCodexVersion = (path) => {
881
+ let fd;
882
+ try {
883
+ fd = openSync(path, "r");
884
+ const chunk = Buffer.allocUnsafe(EMBEDDED_SCAN_CHUNK_BYTES);
885
+ let carry = "";
886
+ let scanned = 0;
887
+ while (scanned < EMBEDDED_SCAN_MAX_BYTES) {
888
+ const bytes = readSync(fd, chunk, 0, chunk.length, null);
889
+ if (bytes <= 0) return null;
890
+ scanned += bytes;
891
+ const text = carry + chunk.toString("latin1", 0, bytes);
892
+ const found = parseEmbeddedCodexVersion(text);
893
+ if (found) return found;
894
+ carry = text.slice(-32);
895
+ }
896
+ return null;
897
+ } catch {
898
+ return null;
899
+ } finally {
900
+ if (fd !== void 0) {
901
+ try {
902
+ closeSync(fd);
903
+ } catch {
904
+ fd = void 0;
905
+ }
906
+ }
907
+ }
908
+ };
909
+ var bundledCodex = () => findBundledCodex({
910
+ config: () => {
911
+ try {
912
+ return parseTOML(readFileSync(codexConfigToml(), "utf-8"));
913
+ } catch {
914
+ return null;
915
+ }
916
+ },
917
+ exists: existsSync,
918
+ home: homedir(),
919
+ platform: process.platform
920
+ });
834
921
  var detectCodexVersion = () => readCodexVersion({
835
922
  resolveOnPath: codexOnPath,
836
923
  realpath: codexRealpath,
@@ -841,6 +928,10 @@ var detectCodexVersion = () => readCodexVersion({
841
928
  return null;
842
929
  }
843
930
  },
931
+ bundledVersion: () => {
932
+ const path = bundledCodex();
933
+ return path ? readEmbeddedCodexVersion(path) : null;
934
+ },
844
935
  brewVersions: () => {
845
936
  try {
846
937
  return execSync2("brew list --versions codex", {
@@ -872,7 +963,7 @@ var detectCodexInstall = () => checkCodexInstall({
872
963
  return null;
873
964
  }
874
965
  },
875
- join: join2,
966
+ join: join3,
876
967
  dirname: dirname2
877
968
  });
878
969
  var codexSupportsHooks = (version) => version !== null && compareCodexVersion(version, CODEX_HOOKS_MIN_VERSION) >= 0;
@@ -915,12 +1006,19 @@ var setupCodex = async (apiKey) => {
915
1006
  console.log(`
916
1007
  ${bold("Setting up Codex")}
917
1008
  `);
918
- if (!isInstalled("codex")) {
919
- console.log(` ${yellow("!")} Codex CLI not found. Skipping.`);
920
- console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
1009
+ const onPath = isInstalled("codex");
1010
+ const bundled = onPath ? null : bundledCodex();
1011
+ if (!onPath && !bundled) {
1012
+ console.log(` ${yellow("!")} Codex not found. Skipping.`);
1013
+ console.log(` ${dim("Install the Codex CLI or the ChatGPT desktop app, then re-run setup.")}`);
921
1014
  return "skipped";
922
1015
  }
923
- if (detectCodexInstall().kind === "binary-missing") {
1016
+ if (bundled) {
1017
+ console.log(` ${dim("Using the Codex inside the ChatGPT desktop app.")}`);
1018
+ console.log(` ${dim(shortenHome(bundled))}`);
1019
+ console.log();
1020
+ }
1021
+ if (onPath && detectCodexInstall().kind === "binary-missing") {
924
1022
  console.log(` ${yellow("!")} Your Codex install is missing the binary it runs.`);
925
1023
  console.log(` ${dim("A Pushary setup before 0.71.0 asked Codex its version. On macOS that hands")}`);
926
1024
  console.log(` ${dim("the binary to XProtect, which quarantines the Codex CLI and moves it to the")}`);
@@ -939,7 +1037,7 @@ var setupCodex = async (apiKey) => {
939
1037
  });
940
1038
  const codexVersion = detectCodexVersion();
941
1039
  const hooksSupported = codexSupportsHooks(codexVersion);
942
- const trustAuto = codexTrustAutoSupported(codexVersion);
1040
+ const trustAuto = hooksSupported && (codexTrustSchemeAgrees(readAgentJson(CODEX_HOOKS_JSON), readAgentToml(codexConfig), CODEX_HOOKS_JSON) || codexTrustAutoSupported(codexVersion));
943
1041
  let trusted = false;
944
1042
  if (hooksSupported) {
945
1043
  const hookCommand = guardBinary(resolveGlobalBinary("pushary-codex-hook") ?? "pushary-codex-hook");
@@ -994,20 +1092,25 @@ var setupCodex = async (apiKey) => {
994
1092
  if (hooksSupported) {
995
1093
  console.log(` ${dim("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
996
1094
  if (trusted) {
997
- console.log(` ${dim("\u2022")} Hook pre-trusted: no ${cyan("/hooks")} step, phone approvals work right away`);
1095
+ console.log(` ${dim("\u2022")} Hook pre-trusted: no ${cyan("/hooks")} step. If approvals never arrive, ${cyan("/hooks")} re-trusts it`);
998
1096
  } else {
999
1097
  console.log();
1000
1098
  console.log(` ${bold("One step left, and it happens inside Codex")}`);
1001
1099
  console.log(` ${dim("Codex makes you trust any hook before it runs. This is Codex's own")}`);
1002
1100
  console.log(` ${dim("security check, not a Pushary thing, and you only do it once.")}`);
1003
1101
  console.log();
1004
- console.log(` ${cyan("1.")} Open Codex and type ${cyan("/hooks")}`);
1005
- console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
1102
+ if (bundled) {
1103
+ console.log(` ${cyan("1.")} Open ChatGPT, then Settings and ${cyan("Hooks")}`);
1104
+ console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
1105
+ } else {
1106
+ console.log(` ${cyan("1.")} Open Codex and type ${cyan("/hooks")}`);
1107
+ console.log(` ${cyan("2.")} You will see the Pushary hook commands. Trust them.`);
1108
+ }
1006
1109
  console.log();
1007
1110
  console.log(` ${dim("Until you do, Codex just runs as usual without the phone approvals.")}`);
1008
1111
  console.log(` ${dim("To confirm:")} run a command that needs approval and watch your phone.`);
1009
1112
  console.log(` ${dim("Pushary updates keep the same trust, so Codex will not ask again.")}`);
1010
- noteManual("Open Codex, type /hooks, and trust the Pushary hook. Until then Codex runs without phone approvals.");
1113
+ noteManual(bundled ? "Open ChatGPT, then Settings and Hooks, and trust the Pushary hook. Until then Codex runs without phone approvals." : "Open Codex, type /hooks, and trust the Pushary hook. Until then Codex runs without phone approvals.");
1011
1114
  }
1012
1115
  } else {
1013
1116
  console.log(` ${dim("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
@@ -1016,17 +1119,15 @@ var setupCodex = async (apiKey) => {
1016
1119
  var resolveBundledPlugin = () => {
1017
1120
  const dir = dirname2(fileURLToPath(import.meta.url));
1018
1121
  const candidates = [
1019
- join2(dir, "..", "..", "data", "cursor-plugin"),
1020
- join2(dir, "..", "data", "cursor-plugin"),
1021
- join2(dir, "..", "..", "..", "cursor-plugin"),
1022
- join2(dir, "..", "..", "cursor-plugin")
1122
+ join3(dir, "..", "..", "data", "cursor-plugin"),
1123
+ join3(dir, "..", "data", "cursor-plugin"),
1124
+ join3(dir, "..", "..", "..", "cursor-plugin"),
1125
+ join3(dir, "..", "..", "cursor-plugin")
1023
1126
  ];
1024
- return candidates.find((p) => existsSync(join2(p, ".cursor-plugin", "plugin.json"))) ?? null;
1127
+ return candidates.find((p) => existsSync(join3(p, ".cursor-plugin", "plugin.json"))) ?? null;
1025
1128
  };
1026
1129
  var installCursorUserHooks = (gateScript) => {
1027
- const template = readJson(join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks?.beforeShellExecution?.[0];
1028
- if (!template) throw new Error("bundled Cursor hooks.json missing a beforeShellExecution entry");
1029
- const entry = { ...template, command: guardNodeScript(gateScript) };
1130
+ const bundled = readJson(join3(CURSOR_PLUGIN_DIR, "hooks", "hooks.json")).hooks ?? {};
1030
1131
  let userHooks = {};
1031
1132
  if (existsSync(CURSOR_USER_HOOKS)) {
1032
1133
  try {
@@ -1040,19 +1141,24 @@ var installCursorUserHooks = (gateScript) => {
1040
1141
  }
1041
1142
  }
1042
1143
  const hooks = userHooks.hooks ?? {};
1043
- const existing = Array.isArray(hooks.beforeShellExecution) ? hooks.beforeShellExecution : [];
1044
- const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
1045
- hooks.beforeShellExecution = [...others, entry];
1144
+ for (const event of CURSOR_GATE_EVENTS) {
1145
+ const template = bundled[event]?.[0];
1146
+ if (!template) throw new Error(`bundled Cursor hooks.json missing a ${event} entry`);
1147
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
1148
+ const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
1149
+ hooks[event] = [...others, { ...template, command: guardNodeScript(gateScript) }];
1150
+ }
1046
1151
  writeJson(CURSOR_USER_HOOKS, { ...userHooks, version: userHooks.version ?? 1, hooks });
1047
1152
  };
1048
1153
  var neutralizePluginGate = () => {
1049
- const path = join2(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
1154
+ const path = join3(CURSOR_PLUGIN_DIR, "hooks", "hooks.json");
1050
1155
  if (!existsSync(path)) return;
1051
1156
  const data = readJson(path);
1052
- if (data.hooks && "beforeShellExecution" in data.hooks) {
1053
- delete data.hooks.beforeShellExecution;
1054
- writeJson(path, { version: data.version ?? 1, hooks: data.hooks });
1055
- }
1157
+ if (!data.hooks) return;
1158
+ const stripped = CURSOR_GATE_EVENTS.filter((event) => event in data.hooks);
1159
+ if (stripped.length === 0) return;
1160
+ for (const event of stripped) delete data.hooks[event];
1161
+ writeJson(path, { version: data.version ?? 1, hooks: data.hooks });
1056
1162
  };
1057
1163
  var setupCursor = async (apiKey) => {
1058
1164
  console.log(`
@@ -1061,7 +1167,7 @@ var setupCursor = async (apiKey) => {
1061
1167
  const source = resolveBundledPlugin();
1062
1168
  if (!source) throw new Error("bundled Cursor plugin not found in this package");
1063
1169
  await spinner("Installing Pushary plugin", async () => {
1064
- const staging = join2(dirname2(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
1170
+ const staging = join3(dirname2(CURSOR_PLUGIN_DIR), `.pushary-staging-${process.pid}`);
1065
1171
  const backup = `${CURSOR_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1066
1172
  rmSync(staging, { recursive: true, force: true });
1067
1173
  try {
@@ -1069,7 +1175,7 @@ var setupCursor = async (apiKey) => {
1069
1175
  recursive: true,
1070
1176
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
1071
1177
  });
1072
- const staged = readJsonSafe(join2(staging, ".cursor-plugin", "plugin.json"));
1178
+ const staged = readJsonSafe(join3(staging, ".cursor-plugin", "plugin.json"));
1073
1179
  if (staged.kind !== "ok") {
1074
1180
  throw new Error("staged Cursor plugin is missing or has an unreadable plugin.json");
1075
1181
  }
@@ -1087,7 +1193,7 @@ var setupCursor = async (apiKey) => {
1087
1193
  }
1088
1194
  });
1089
1195
  await spinner("Linking your API key", async () => {
1090
- const mcpPath = join2(CURSOR_PLUGIN_DIR, "mcp.json");
1196
+ const mcpPath = join3(CURSOR_PLUGIN_DIR, "mcp.json");
1091
1197
  const mcp = readAgentJson(mcpPath);
1092
1198
  const servers = mcp.mcpServers ?? {};
1093
1199
  if (servers.pushary) {
@@ -1097,7 +1203,7 @@ var setupCursor = async (apiKey) => {
1097
1203
  }
1098
1204
  });
1099
1205
  await spinner("Registering permission gate (~/.cursor/hooks.json)", async () => {
1100
- installCursorUserHooks(join2(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
1206
+ installCursorUserHooks(join3(CURSOR_PLUGIN_DIR, "scripts", "pushary-gate.mjs"));
1101
1207
  neutralizePluginGate();
1102
1208
  });
1103
1209
  console.log();
@@ -1111,21 +1217,21 @@ var setupCursor = async (apiKey) => {
1111
1217
  var resolveBundledVsCodePlugin = () => {
1112
1218
  const dir = dirname2(fileURLToPath(import.meta.url));
1113
1219
  const candidates = [
1114
- join2(dir, "..", "..", "data", "vscode-plugin"),
1115
- join2(dir, "..", "data", "vscode-plugin"),
1116
- join2(dir, "..", "..", "..", "vscode-plugin"),
1117
- join2(dir, "..", "..", "vscode-plugin")
1220
+ join3(dir, "..", "..", "data", "vscode-plugin"),
1221
+ join3(dir, "..", "data", "vscode-plugin"),
1222
+ join3(dir, "..", "..", "..", "vscode-plugin"),
1223
+ join3(dir, "..", "..", "vscode-plugin")
1118
1224
  ];
1119
- return candidates.find((p) => existsSync(join2(p, ".claude-plugin", "plugin.json"))) ?? null;
1225
+ return candidates.find((p) => existsSync(join3(p, ".claude-plugin", "plugin.json"))) ?? null;
1120
1226
  };
1121
1227
  var pinVsCodeGatePath = (pluginDir) => {
1122
- const hooksPath = join2(pluginDir, "hooks", "hooks.json");
1228
+ const hooksPath = join3(pluginDir, "hooks", "hooks.json");
1123
1229
  const data = readJson(hooksPath);
1124
1230
  const entries = data.hooks?.PreToolUse;
1125
1231
  if (!Array.isArray(entries) || entries.length === 0) {
1126
1232
  throw new Error("bundled VS Code hooks.json is missing a PreToolUse entry");
1127
1233
  }
1128
- const gate = join2(pluginDir, "scripts", "pushary-gate.mjs");
1234
+ const gate = join3(pluginDir, "scripts", "pushary-gate.mjs");
1129
1235
  data.hooks.PreToolUse = entries.map((entry) => ({ ...entry, command: `node "${gate}"` }));
1130
1236
  writeJson(hooksPath, data);
1131
1237
  };
@@ -1154,7 +1260,7 @@ var setupVsCode = async (apiKey) => {
1154
1260
  const source = resolveBundledVsCodePlugin();
1155
1261
  if (!source) throw new Error("bundled VS Code plugin not found in this package");
1156
1262
  await spinner("Installing Pushary plugin", async () => {
1157
- const staging = join2(dirname2(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
1263
+ const staging = join3(dirname2(VSCODE_PLUGIN_DIR), `.pushary-staging-vscode-${process.pid}`);
1158
1264
  const backup = `${VSCODE_PLUGIN_DIR}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
1159
1265
  mkdirSync(dirname2(VSCODE_PLUGIN_DIR), { recursive: true });
1160
1266
  rmSync(staging, { recursive: true, force: true });
@@ -1163,7 +1269,7 @@ var setupVsCode = async (apiKey) => {
1163
1269
  recursive: true,
1164
1270
  filter: (p) => !["tools", "node_modules", ".git", ".DS_Store"].includes(basename(p))
1165
1271
  });
1166
- const staged = readJsonSafe(join2(staging, ".claude-plugin", "plugin.json"));
1272
+ const staged = readJsonSafe(join3(staging, ".claude-plugin", "plugin.json"));
1167
1273
  if (staged.kind !== "ok") {
1168
1274
  throw new Error("staged VS Code plugin is missing or has an unreadable plugin.json");
1169
1275
  }
@@ -1181,7 +1287,7 @@ var setupVsCode = async (apiKey) => {
1181
1287
  }
1182
1288
  });
1183
1289
  await spinner("Linking your API key", async () => {
1184
- const mcpPath = join2(VSCODE_PLUGIN_DIR, ".mcp.json");
1290
+ const mcpPath = join3(VSCODE_PLUGIN_DIR, ".mcp.json");
1185
1291
  const mcp = readAgentJson(mcpPath);
1186
1292
  const servers = mcp.mcpServers ?? {};
1187
1293
  if (servers.pushary) {
@@ -1308,6 +1414,43 @@ var setupGemini = async (apiKey) => {
1308
1414
  console.log(` ${dim("Restart Gemini CLI to load the new config.")}`);
1309
1415
  noteManual("Restart Gemini CLI to load the new config.");
1310
1416
  };
1417
+ var OPENCODE_INSTRUCTIONS = () => join3(openCodeConfigDir(), "AGENTS.md");
1418
+ var setupOpenCode = async (apiKey) => {
1419
+ console.log(`
1420
+ ${bold("Setting up OpenCode")}
1421
+ `);
1422
+ if (!isInstalled("opencode") && !existsSync(openCodeConfigDir())) {
1423
+ console.log(` ${yellow("!")} OpenCode not found. Skipping.`);
1424
+ console.log(` ${dim("Install OpenCode and re-run setup to configure.")}`);
1425
+ return "skipped";
1426
+ }
1427
+ await installGlobally2();
1428
+ await spinner(`Writing the Pushary plugin (${openCodePluginFile()})`, async () => {
1429
+ const target = resolveGlobalBinary(OPENCODE_HOOK_BINARY) ?? OPENCODE_HOOK_BINARY;
1430
+ installOpenCodePlugin(
1431
+ guardedArgv({ kind: "binary", path: target, runtime: "node" }, ["--source", "opencode"]),
1432
+ openCodePluginDir()
1433
+ );
1434
+ });
1435
+ await spinner(`Adding MCP server (${openCodeConfigFile()})`, async () => {
1436
+ const config = readAgentJson(openCodeConfigFile());
1437
+ addOpenCodeMcpServer(config, apiKey);
1438
+ writeJsonAtomic(openCodeConfigFile(), config, KEY_FILE_MODE);
1439
+ });
1440
+ await spinner("Teaching OpenCode to ask via push", async () => {
1441
+ writeInstructionBlock(OPENCODE_INSTRUCTIONS(), renderAgentInstructions("OpenCode"));
1442
+ });
1443
+ console.log();
1444
+ console.log(` ${dim("What this configured:")}`);
1445
+ console.log(` ${dim("\u2022")} Plugin: ${openCodePluginFile()}`);
1446
+ console.log(` ${dim("\u2022")} MCP server: OpenCode can send notifications and ask questions`);
1447
+ console.log(` ${dim("\u2022")} Approvals: a permission OpenCode raises is answered from your phone`);
1448
+ console.log(` ${dim("\u2022")} Gate covers ${bold("bash")}, ${bold("edit")}, ${bold("write")} and ${bold("webfetch")}`);
1449
+ console.log(` ${dim("\u2022")} Receipts: tool results, prompts and turn boundaries reach your account`);
1450
+ console.log(` ${dim("Restart OpenCode to load the plugin.")}`);
1451
+ noteManual("Restart OpenCode to load the plugin.");
1452
+ return "configured";
1453
+ };
1311
1454
  var agentIsWired = (agent) => {
1312
1455
  const readJson2 = (path) => {
1313
1456
  try {
@@ -1321,27 +1464,29 @@ var agentIsWired = (agent) => {
1321
1464
  return claudeWired({
1322
1465
  claudeJson: readJson2(CLAUDE_JSON),
1323
1466
  settings: readJson2(CLAUDE_SETTINGS),
1324
- skillExists: existsSync(join2(CLAUDE_SKILL_DIR, "SKILL.md"))
1467
+ skillExists: existsSync(join3(CLAUDE_SKILL_DIR, "SKILL.md"))
1325
1468
  });
1326
1469
  case "codex": {
1327
1470
  let config = null;
1328
1471
  try {
1329
- config = parseTOML(readFileSync(join2(CODEX_HOME, "config.toml"), "utf-8"));
1472
+ config = parseTOML(readFileSync(join3(CODEX_HOME, "config.toml"), "utf-8"));
1330
1473
  } catch {
1331
1474
  config = null;
1332
1475
  }
1333
1476
  return codexWired({
1334
1477
  config,
1335
1478
  hooks: readJson2(CODEX_HOOKS_JSON),
1336
- skillExists: existsSync(join2(CODEX_SKILL_DIR, "SKILL.md"))
1479
+ skillExists: existsSync(join3(CODEX_SKILL_DIR, "SKILL.md"))
1337
1480
  });
1338
1481
  }
1339
1482
  case "gemini_cli":
1340
1483
  return geminiWired({ settings: readJson2(GEMINI_SETTINGS) });
1341
1484
  case "cursor":
1342
- return existsSync(join2(CURSOR_PLUGIN_DIR, "mcp.json"));
1485
+ return existsSync(join3(CURSOR_PLUGIN_DIR, "mcp.json"));
1343
1486
  case "vscode":
1344
- return existsSync(join2(VSCODE_PLUGIN_DIR, ".mcp.json"));
1487
+ return existsSync(join3(VSCODE_PLUGIN_DIR, ".mcp.json"));
1488
+ case "opencode":
1489
+ return openCodeWired();
1345
1490
  default:
1346
1491
  return true;
1347
1492
  }
@@ -1353,6 +1498,7 @@ var AGENT_SETUP = {
1353
1498
  hermes: setupHermes,
1354
1499
  cursor: setupCursor,
1355
1500
  vscode: setupVsCode,
1501
+ opencode: setupOpenCode,
1356
1502
  custom: setupCustom
1357
1503
  };
1358
1504
  var PROJECT_INSTRUCTION_TARGETS = {
@@ -1369,8 +1515,8 @@ var offerProjectInstructions = async (agents, options) => {
1369
1515
  });
1370
1516
  if (!wanted) return;
1371
1517
  for (const target of targets) {
1372
- await spinner(`Writing managed block to ${join2(process.cwd(), target.file)}`, async () => {
1373
- writeInstructionBlock(join2(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1518
+ await spinner(`Writing managed block to ${join3(process.cwd(), target.file)}`, async () => {
1519
+ writeInstructionBlock(join3(process.cwd(), target.file), renderProjectAgentInstructions(target.label));
1374
1520
  }, { optional: true });
1375
1521
  }
1376
1522
  console.log(` ${dim("Commit the file to share it. Teammates without a key fall back to the terminal.")}`);
@@ -1455,6 +1601,7 @@ var AGENT_NAMES = {
1455
1601
  hermes: "Hermes",
1456
1602
  cursor: "Cursor",
1457
1603
  vscode: "VS Code",
1604
+ opencode: "OpenCode",
1458
1605
  custom: "Other"
1459
1606
  };
1460
1607
  var AGENT_CAPABILITIES = {
@@ -1464,6 +1611,7 @@ var AGENT_CAPABILITIES = {
1464
1611
  hermes: "native plugin + auto-error notifications",
1465
1612
  cursor: "plugin + permission gate",
1466
1613
  vscode: "agent plugin + permission gate",
1614
+ opencode: "generated plugin + permission gate",
1467
1615
  custom: "any MCP or HTTP agent (Windsurf, n8n, custom)"
1468
1616
  };
1469
1617
  var NAME_COLUMN = Math.max(...Object.values(AGENT_NAMES).map((name) => name.length)) + 2;
@@ -1522,12 +1670,18 @@ var resolveAgents = async (options) => {
1522
1670
  });
1523
1671
  };
1524
1672
  var AGENT_TARGETS = {
1525
- claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join2(CLAUDE_SKILL_DIR, "SKILL.md")],
1526
- codex: [join2(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join2(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1673
+ claude_code: [CLAUDE_JSON, CLAUDE_SETTINGS, join3(CLAUDE_SKILL_DIR, "SKILL.md")],
1674
+ codex: [join3(CODEX_HOME, "config.toml"), CODEX_HOOKS_JSON, join3(CODEX_SKILL_DIR, "SKILL.md"), CODEX_AGENTS_MD],
1527
1675
  gemini_cli: [GEMINI_SETTINGS, GEMINI_MD],
1528
1676
  hermes: ["the Hermes virtualenv (pip install pushary-hermes)"],
1529
- cursor: [join2(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1530
- vscode: [join2(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1677
+ cursor: [join3(CURSOR_PLUGIN_DIR, "mcp.json"), CURSOR_USER_HOOKS],
1678
+ vscode: [join3(VSCODE_PLUGIN_DIR, ".mcp.json"), ...vscodeSettingsTargets(existsSync)],
1679
+ opencode: [
1680
+ openCodePluginFile(),
1681
+ join3(openCodePluginDir(), "package.json"),
1682
+ openCodeConfigFile(),
1683
+ OPENCODE_INSTRUCTIONS()
1684
+ ],
1531
1685
  custom: ["nothing (prints connection details only)"]
1532
1686
  };
1533
1687
  var reportDryRun = (apiKey, agents, keyCheck) => {