@pushary/agent-hooks 1.7.1 → 1.7.2

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 (101) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/bin/pushary-bell-hook.js +23 -23
  3. package/dist/bin/pushary-bell.js +10 -10
  4. package/dist/bin/pushary-claude.js +46 -35
  5. package/dist/bin/pushary-clean.js +107 -60
  6. package/dist/bin/pushary-codex-bridge.js +18 -17
  7. package/dist/bin/pushary-codex-hook.js +53 -39
  8. package/dist/bin/pushary-codex.js +14 -12
  9. package/dist/bin/pushary-connect.js +16 -16
  10. package/dist/bin/pushary-cowork.js +5 -5
  11. package/dist/bin/pushary-daemon.js +27 -27
  12. package/dist/bin/pushary-disconnect.js +19 -17
  13. package/dist/bin/pushary-doctor.js +63 -35
  14. package/dist/bin/pushary-elicitation-hook.js +35 -26
  15. package/dist/bin/pushary-gemini-bridge.js +12 -12
  16. package/dist/bin/pushary-gemini-hook.js +47 -32
  17. package/dist/bin/pushary-hook.js +34 -32
  18. package/dist/bin/pushary-login.js +16 -16
  19. package/dist/bin/pushary-logout.js +8 -8
  20. package/dist/bin/pushary-mcp.js +9 -8
  21. package/dist/bin/pushary-mode.js +10 -10
  22. package/dist/bin/pushary-notification-hook.js +29 -27
  23. package/dist/bin/pushary-opencode-hook.js +46 -32
  24. package/dist/bin/pushary-permission-denied-hook.js +34 -32
  25. package/dist/bin/pushary-permission-hook.js +33 -31
  26. package/dist/bin/pushary-post-hook.js +29 -27
  27. package/dist/bin/pushary-prompt-hook.js +29 -27
  28. package/dist/bin/pushary-session-end-hook.js +29 -27
  29. package/dist/bin/pushary-session-start-hook.js +33 -31
  30. package/dist/bin/pushary-setup.js +69 -56
  31. package/dist/bin/pushary-stats.js +5 -5
  32. package/dist/bin/pushary-status.js +19 -19
  33. package/dist/bin/pushary-stop-hook.js +29 -27
  34. package/dist/bin/pushary-stopfailure-hook.js +29 -27
  35. package/dist/bin/pushary-suggestions.js +5 -5
  36. package/dist/bin/pushary-transcript-register.js +19 -19
  37. package/dist/bin/pushary-transcripts.js +7 -7
  38. package/dist/bin/pushary-upgrade.js +20 -20
  39. package/dist/bin/pushary-wait.js +10 -10
  40. package/dist/bin/pushary.js +6 -6
  41. package/dist/{chunk-WM3U2RUK.js → chunk-2B4XCMH6.js} +75 -232
  42. package/dist/{chunk-ZIIYTQHC.js → chunk-2XOHXZRC.js} +12 -4
  43. package/dist/{chunk-3QE3BHBW.js → chunk-3NSRL5E7.js} +2 -2
  44. package/dist/{chunk-7GI6QC3M.js → chunk-3RO2UWJQ.js} +2 -2
  45. package/dist/{chunk-AAGLDXH4.js → chunk-4S7CCOXB.js} +5 -5
  46. package/dist/{chunk-6SJQ442W.js → chunk-5GIBNUBD.js} +2 -2
  47. package/dist/chunk-5OP5MQI7.js +45 -0
  48. package/dist/{chunk-EIZW73P5.js → chunk-65RN6ZCO.js} +1 -1
  49. package/dist/{chunk-6PV2JQ64.js → chunk-6NHXKTQU.js} +2 -2
  50. package/dist/{chunk-KSNAAMU5.js → chunk-6QZK4O36.js} +4 -4
  51. package/dist/{chunk-WZWXBTRF.js → chunk-A6WQF7FQ.js} +3 -3
  52. package/dist/{chunk-62TEPRWQ.js → chunk-AWZNDM3B.js} +1 -1
  53. package/dist/{chunk-NQFGVAT6.js → chunk-D4OGEWLH.js} +19 -2
  54. package/dist/{chunk-SVEMF2VO.js → chunk-E3PLF7YB.js} +4 -4
  55. package/dist/{chunk-MCNS4725.js → chunk-F6BMJOSW.js} +1 -1
  56. package/dist/{chunk-P5CBNSYL.js → chunk-HOIGXMI7.js} +1 -1
  57. package/dist/{chunk-XMN37YJH.js → chunk-I7B2DNVU.js} +1 -1
  58. package/dist/{chunk-ZEMSCGDD.js → chunk-INR3RVQ3.js} +8 -3
  59. package/dist/{chunk-3QORH5EJ.js → chunk-IZVAVXHQ.js} +6 -4
  60. package/dist/{chunk-MWVCDP64.js → chunk-JMSB6CJT.js} +1 -1
  61. package/dist/{chunk-WSC53O7I.js → chunk-KAJT6HPI.js} +3 -3
  62. package/dist/{chunk-XPMOCJHN.js → chunk-KLXYMPTM.js} +1 -1
  63. package/dist/{chunk-XDASEYRS.js → chunk-L6IF4JOF.js} +3 -3
  64. package/dist/{chunk-UPKP67TA.js → chunk-LI3C2IQY.js} +2 -2
  65. package/dist/{chunk-H6JV44TE.js → chunk-M2JGLFFS.js} +1 -1
  66. package/dist/chunk-MY3UZO5U.js +99 -0
  67. package/dist/chunk-N6IOSZOE.js +185 -0
  68. package/dist/{chunk-454KMJHF.js → chunk-NYBZJ44G.js} +34 -1
  69. package/dist/{chunk-QPR33SHS.js → chunk-OJRB5QKN.js} +6 -6
  70. package/dist/{chunk-3T35OE7C.js → chunk-OXEF6JHD.js} +4 -4
  71. package/dist/{chunk-SNWNA3MQ.js → chunk-PWNQGITG.js} +1 -1
  72. package/dist/{chunk-XPTCPU6V.js → chunk-Q7CDBJXU.js} +1 -1
  73. package/dist/{chunk-OTZHT34O.js → chunk-QSZ37ROP.js} +1 -1
  74. package/dist/{chunk-O5JD7UI5.js → chunk-R7ZETVNR.js} +1 -1
  75. package/dist/{chunk-4C4NRDIC.js → chunk-RLRDPZG4.js} +1 -1
  76. package/dist/{chunk-IGBDOZUO.js → chunk-SEIZQZEE.js} +1 -1
  77. package/dist/{chunk-OAL44FM2.js → chunk-SJN64NJX.js} +1 -1
  78. package/dist/{chunk-NID4UZDN.js → chunk-SKEW3TQX.js} +1 -1
  79. package/dist/{chunk-NAAZCNKI.js → chunk-SKMWPXGV.js} +351 -37
  80. package/dist/{chunk-KBLIVEFJ.js → chunk-T3PRPFDY.js} +5 -5
  81. package/dist/{chunk-2FU43B5Q.js → chunk-TVJYGZXB.js} +2 -2
  82. package/dist/{chunk-Y6GKOZBT.js → chunk-U3MS6D5X.js} +1 -1
  83. package/dist/{chunk-O5PRIYFZ.js → chunk-U4TMAANR.js} +3 -0
  84. package/dist/{chunk-JTQDP4ZL.js → chunk-UFNBKYWT.js} +3 -3
  85. package/dist/{chunk-42XR73I2.js → chunk-UUTWHVSV.js} +35 -35
  86. package/dist/{chunk-43SSQCWO.js → chunk-VQDJ6KB3.js} +3 -3
  87. package/dist/{chunk-YZKS3ME3.js → chunk-VSGKQ3HI.js} +2 -2
  88. package/dist/{chunk-XFNVG3OV.js → chunk-WWTLAZQA.js} +24 -7
  89. package/dist/{chunk-VCMFKZON.js → chunk-WXIVUIXA.js} +2 -2
  90. package/dist/{chunk-AHWJHLG5.js → chunk-YLCSKVQQ.js} +7 -5
  91. package/dist/{chunk-Q7QRR6YL.js → chunk-YNJ2H2FD.js} +2 -2
  92. package/dist/{chunk-BYVB3RX3.js → chunk-YU4222G2.js} +2 -2
  93. package/dist/{chunk-YQN7S57T.js → chunk-YU54LNC6.js} +6 -6
  94. package/dist/{chunk-7GYGVQBU.js → chunk-ZMWX5SDA.js} +8 -8
  95. package/dist/{chunk-55RO62FV.js → chunk-ZUKZJQGR.js} +1 -1
  96. package/dist/{reapply-66GQIBXN.js → reapply-D6RSDYCK.js} +9 -9
  97. package/dist/src/index.d.ts +1 -0
  98. package/dist/src/index.js +22 -20
  99. package/package.json +2 -1
  100. package/dist/chunk-JMBG47WV.js +0 -336
  101. package/dist/chunk-YHG74UFF.js +0 -12
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  detectClaudeVersion
3
- } from "./chunk-Y6GKOZBT.js";
3
+ } from "./chunk-U3MS6D5X.js";
4
4
  import {
5
5
  ensureCodexTrustsPusharyHooks
6
- } from "./chunk-6SJQ442W.js";
6
+ } from "./chunk-5GIBNUBD.js";
7
7
  import {
8
8
  addClaudeMcpServer,
9
9
  addPusharyHooks,
10
10
  addPusharyToolPermissions
11
- } from "./chunk-WSC53O7I.js";
11
+ } from "./chunk-KAJT6HPI.js";
12
12
  import {
13
13
  claudeUnprovenHolds,
14
14
  claudeWired,
15
15
  codexUnprovenHolds,
16
16
  codexWired,
17
17
  geminiWired
18
- } from "./chunk-XDASEYRS.js";
18
+ } from "./chunk-L6IF4JOF.js";
19
19
  import {
20
20
  CLAUDE_HOOK_BINARY,
21
21
  CLAUDE_HOOK_EVENTS,
@@ -27,7 +27,7 @@ import {
27
27
  needsReregistration,
28
28
  registrationSignatures,
29
29
  supportedClaudeEvents
30
- } from "./chunk-WZWXBTRF.js";
30
+ } from "./chunk-A6WQF7FQ.js";
31
31
  import {
32
32
  CODEX_HOOK_BINARY,
33
33
  addCodexHookTrust,
@@ -47,7 +47,7 @@ import {
47
47
  registrationHolderOf,
48
48
  resolveGlobalBinary,
49
49
  vscodePluginMcp
50
- } from "./chunk-454KMJHF.js";
50
+ } from "./chunk-NYBZJ44G.js";
51
51
  import {
52
52
  readJsonSafe,
53
53
  writeFileAtomic,
@@ -1,10 +1,10 @@
1
+ import {
2
+ createIo
3
+ } from "./chunk-U4TMAANR.js";
1
4
  import {
2
5
  describeKeyCheck,
3
6
  keyCheckFromResponse
4
- } from "./chunk-43SSQCWO.js";
5
- import {
6
- createIo
7
- } from "./chunk-O5PRIYFZ.js";
7
+ } from "./chunk-VQDJ6KB3.js";
8
8
  import {
9
9
  EXIT
10
10
  } from "./chunk-KZERVKTD.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-454KMJHF.js";
3
+ } from "./chunk-NYBZJ44G.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-T7E75EKA.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readTranscriptPreference
3
- } from "./chunk-YZKS3ME3.js";
3
+ } from "./chunk-VSGKQ3HI.js";
4
4
 
5
5
  // src/transcript-setting.ts
6
6
  var transcriptEnvironmentPreference = () => {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getBaseUrl
3
- } from "./chunk-OAL44FM2.js";
3
+ } from "./chunk-SJN64NJX.js";
4
4
 
5
5
  // src/transcript-api.ts
6
6
  var fetchTranscriptRecipients = async (apiKey, baseUrl = getBaseUrl()) => {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  redactSecrets
3
- } from "./chunk-XFNVG3OV.js";
3
+ } from "./chunk-WWTLAZQA.js";
4
4
 
5
5
  // src/crypto.ts
6
6
  import nacl from "tweetnacl";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PRETOOLUSE_GATED_TOOLS
3
- } from "./chunk-XFNVG3OV.js";
3
+ } from "./chunk-WWTLAZQA.js";
4
4
 
5
5
  // src/claude-tools.ts
6
6
  var POSTTOOLUSE_MIRROR_ONLY_TOOLS = ["TodoWrite", "Task", "WebFetch", "WebSearch"];
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  HOOK_HOLD_BUDGET_ENV,
3
3
  hookHoldBudgetProof
4
- } from "./chunk-XFNVG3OV.js";
4
+ } from "./chunk-WWTLAZQA.js";
5
5
 
6
6
  // src/hold-budget.ts
7
7
  var takeHookHoldBudget = (args = process.argv.slice(2), env = process.env) => {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NPX_LATEST_COMMAND
3
- } from "./chunk-EIZW73P5.js";
3
+ } from "./chunk-65RN6ZCO.js";
4
4
 
5
5
  // src/config.ts
6
6
  import { readFileSync } from "fs";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  HOOK_BUDGETS,
3
3
  OPENCODE_APPROVAL_EVENT
4
- } from "./chunk-XFNVG3OV.js";
4
+ } from "./chunk-WWTLAZQA.js";
5
5
 
6
6
  // src/opencode-plugin.ts
7
7
  var OPENCODE_PLUGIN_MARKER = "pushary-opencode-plugin w1";
@@ -1,15 +1,322 @@
1
1
  import {
2
2
  MCP_BRIDGE_BINARY
3
- } from "./chunk-2FU43B5Q.js";
3
+ } from "./chunk-TVJYGZXB.js";
4
4
  import {
5
5
  openCodeConfigDirFrom
6
- } from "./chunk-NID4UZDN.js";
6
+ } from "./chunk-SKEW3TQX.js";
7
7
  import {
8
- codexHomeFrom
9
- } from "./chunk-454KMJHF.js";
8
+ codexHomeFrom,
9
+ resolvePathBinary
10
+ } from "./chunk-NYBZJ44G.js";
10
11
  import {
11
12
  HOOK_COMPATIBLE_PROFILES
12
- } from "./chunk-XFNVG3OV.js";
13
+ } from "./chunk-WWTLAZQA.js";
14
+
15
+ // src/hermes-config.ts
16
+ import { execFileSync } from "child_process";
17
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
18
+ import { homedir, tmpdir } from "os";
19
+ import { join } from "path";
20
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
21
+ var isSnapshot = (value) => isRecord(value) && typeof value.present === "boolean" && Object.hasOwn(value, "value");
22
+ var isHermesConfigReceipt = (value) => {
23
+ if (!isRecord(value)) return false;
24
+ return ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"].every((key) => isSnapshot(value[key])) && typeof value.pushary_enabled === "boolean" && typeof value.clarify_disabled === "boolean";
25
+ };
26
+ var HERMES_PIP_PACKAGE = "hermes-plugin-pushary";
27
+ var HERMES_PLUGIN_NAME = "pushary";
28
+ var HERMES_CONFIG_SCRIPT = String.raw`
29
+ import copy, importlib.util, json, os, sys
30
+ from hermes_cli.config import load_config, save_config
31
+
32
+ PUSHARY_TRANSPORT = "pushary"
33
+ PUSHARY_FALLBACK = "builtin"
34
+ PUSHARY_PLUGIN = "pushary"
35
+ PUSHARY_PLUGIN_MODULE = "pushary_plugin"
36
+ QUESTION_TOOLSET = "clarify"
37
+
38
+ mode = sys.argv[1]
39
+ receipt_path = sys.argv[2] if len(sys.argv) > 2 else None
40
+ c = load_config()
41
+ original = copy.deepcopy(c)
42
+
43
+ def section(obj, key):
44
+ value = obj.get(key)
45
+ return value if isinstance(value, dict) else {}
46
+
47
+ def members(obj, key):
48
+ value = obj.get(key)
49
+ return value if isinstance(value, list) else []
50
+
51
+ if mode == "status":
52
+ print(json.dumps({
53
+ "plugin_installed": importlib.util.find_spec(PUSHARY_PLUGIN_MODULE) is not None,
54
+ "plugin_enabled": PUSHARY_PLUGIN in members(section(c, "plugins"), "enabled"),
55
+ "approvals_routed": section(section(c, "security"), "approval").get("transport") == PUSHARY_TRANSPORT,
56
+ "questions_routed": QUESTION_TOOLSET in members(section(c, "agent"), "disabled_toolsets"),
57
+ }))
58
+ sys.exit(0)
59
+
60
+ def recorded(snapshot, value):
61
+ return bool(snapshot.get("present")) and snapshot.get("value") == value
62
+
63
+ def pop_if(obj, key, value):
64
+ if obj.get(key) == value:
65
+ obj.pop(key, None)
66
+
67
+ def snap(obj, key):
68
+ return {"present": key in obj, "value": obj.get(key)}
69
+
70
+ restore_conflicts = []
71
+
72
+ def restore(obj, key, before, ours):
73
+ if before.get("present") and before.get("value") == ours:
74
+ return
75
+ if before.get("present"):
76
+ if key in obj and obj.get(key) == before.get("value"):
77
+ return
78
+ elif key not in obj:
79
+ return
80
+ if obj.get(key) != ours:
81
+ restore_conflicts.append(key)
82
+ return
83
+ if before.get("present"):
84
+ obj[key] = before.get("value")
85
+ else:
86
+ obj.pop(key, None)
87
+
88
+ def restore_container(obj, key, before, current, changed):
89
+ raw = obj.get(key)
90
+ if isinstance(before.get("value"), dict):
91
+ if not isinstance(raw, dict):
92
+ if changed:
93
+ restore_conflicts.append(key)
94
+ return
95
+ if current or before.get("present"):
96
+ obj[key] = current
97
+ else:
98
+ obj.pop(key, None)
99
+ elif isinstance(raw, dict) and current:
100
+ restore_conflicts.append(key)
101
+ else:
102
+ restore(obj, key, before, {})
103
+
104
+ def restore_list_member(obj, key, before, ours, ours_before):
105
+ raw = obj.get(key)
106
+ if isinstance(before.get("value"), list):
107
+ if ours_before:
108
+ return
109
+ if not isinstance(raw, list):
110
+ restore_conflicts.append(key)
111
+ return
112
+ remaining = [item for item in raw if item != ours]
113
+ if remaining or before.get("present"):
114
+ obj[key] = remaining
115
+ else:
116
+ obj.pop(key, None)
117
+ return
118
+ if before.get("present") and raw == before.get("value"):
119
+ return
120
+ if not before.get("present") and key not in obj:
121
+ return
122
+ if not isinstance(raw, list):
123
+ restore_conflicts.append(key)
124
+ return
125
+ remaining = [item for item in raw if item != ours]
126
+ if remaining:
127
+ restore_conflicts.append(key)
128
+ else:
129
+ restore(obj, key, before, raw)
130
+
131
+ def valid_snapshot(value):
132
+ return isinstance(value, dict) and isinstance(value.get("present"), bool) and "value" in value
133
+
134
+ def valid_receipt(value):
135
+ snapshots = ["security", "approval", "transport", "fallback", "plugins", "enabled", "agent", "disabled"]
136
+ return (isinstance(value, dict)
137
+ and all(valid_snapshot(value.get(key)) for key in snapshots)
138
+ and isinstance(value.get("pushary_enabled"), bool)
139
+ and isinstance(value.get("clarify_disabled"), bool))
140
+
141
+ if mode == "setup":
142
+ security_before = snap(c, "security")
143
+ s = section(c, "security")
144
+ approval_before = snap(s, "approval")
145
+ ap = section(s, "approval")
146
+ plugins_before = snap(c, "plugins")
147
+ p = section(c, "plugins")
148
+ agent_before = snap(c, "agent")
149
+ a = section(c, "agent")
150
+ enabled = members(p, "enabled")
151
+ disabled = members(a, "disabled_toolsets")
152
+ receipt = {
153
+ "security": security_before,
154
+ "approval": approval_before,
155
+ "transport": snap(ap, "transport"),
156
+ "fallback": snap(ap, "transport_fallback"),
157
+ "plugins": plugins_before,
158
+ "enabled": snap(p, "enabled"),
159
+ "pushary_enabled": PUSHARY_PLUGIN in enabled,
160
+ "agent": agent_before,
161
+ "disabled": snap(a, "disabled_toolsets"),
162
+ "clarify_disabled": QUESTION_TOOLSET in disabled,
163
+ }
164
+ if os.path.exists(receipt_path):
165
+ with open(receipt_path, encoding="utf-8") as handle:
166
+ existing_receipt = json.load(handle)
167
+ if not valid_receipt(existing_receipt):
168
+ raise ValueError("invalid Pushary Hermes setup receipt")
169
+ else:
170
+ os.makedirs(os.path.dirname(receipt_path), exist_ok=True)
171
+ temp = receipt_path + ".tmp"
172
+ with open(temp, "x", encoding="utf-8") as handle:
173
+ json.dump(receipt, handle)
174
+ os.chmod(temp, 0o600)
175
+ os.replace(temp, receipt_path)
176
+ ap["transport"] = PUSHARY_TRANSPORT
177
+ ap["transport_fallback"] = PUSHARY_FALLBACK
178
+ s["approval"] = ap
179
+ c["security"] = s
180
+ p["enabled"] = enabled if PUSHARY_PLUGIN in enabled else enabled + [PUSHARY_PLUGIN]
181
+ c["plugins"] = p
182
+ a["disabled_toolsets"] = disabled if QUESTION_TOOLSET in disabled else disabled + [QUESTION_TOOLSET]
183
+ c["agent"] = a
184
+ else:
185
+ receipt_exists = os.path.exists(receipt_path)
186
+ try:
187
+ with open(receipt_path, encoding="utf-8") as handle:
188
+ receipt = json.load(handle)
189
+ except (OSError, ValueError):
190
+ if receipt_exists:
191
+ raise
192
+ receipt = None
193
+ s = section(c, "security")
194
+ ap = section(s, "approval")
195
+ if receipt:
196
+ if not valid_receipt(receipt):
197
+ raise ValueError("invalid Pushary Hermes setup receipt")
198
+ approval_changed = (not receipt["transport"].get("present")
199
+ or receipt["transport"].get("value") != PUSHARY_TRANSPORT
200
+ or not receipt["fallback"].get("present")
201
+ or receipt["fallback"].get("value") != PUSHARY_FALLBACK)
202
+ plugins_changed = not (isinstance(receipt["enabled"].get("value"), list)
203
+ and receipt["pushary_enabled"])
204
+ agent_changed = not (isinstance(receipt["disabled"].get("value"), list)
205
+ and receipt["clarify_disabled"])
206
+ transport_left_over = recorded(receipt["transport"], PUSHARY_TRANSPORT)
207
+ if transport_left_over:
208
+ pop_if(ap, "transport", PUSHARY_TRANSPORT)
209
+ else:
210
+ restore(ap, "transport", receipt["transport"], PUSHARY_TRANSPORT)
211
+ if transport_left_over and recorded(receipt["fallback"], PUSHARY_FALLBACK):
212
+ pop_if(ap, "transport_fallback", PUSHARY_FALLBACK)
213
+ else:
214
+ restore(ap, "transport_fallback", receipt["fallback"], PUSHARY_FALLBACK)
215
+ else:
216
+ pop_if(ap, "transport", PUSHARY_TRANSPORT)
217
+ pop_if(ap, "transport_fallback", PUSHARY_FALLBACK)
218
+ if receipt:
219
+ restore_container(s, "approval", receipt["approval"], ap, approval_changed)
220
+ restore_container(c, "security", receipt["security"], s, approval_changed)
221
+ else:
222
+ if ap: s["approval"] = ap
223
+ else: s.pop("approval", None)
224
+ if s: c["security"] = s
225
+ else: c.pop("security", None)
226
+
227
+ p = section(c, "plugins")
228
+ if receipt:
229
+ restore_list_member(p, "enabled", receipt["enabled"], PUSHARY_PLUGIN, False)
230
+ restore_container(c, "plugins", receipt["plugins"], p, plugins_changed)
231
+ else:
232
+ enabled = [item for item in members(p, "enabled") if item != PUSHARY_PLUGIN]
233
+ if enabled: p["enabled"] = enabled
234
+ else: p.pop("enabled", None)
235
+ if p: c["plugins"] = p
236
+ else: c.pop("plugins", None)
237
+
238
+ a = section(c, "agent")
239
+ if receipt:
240
+ clarify_was_users = receipt["clarify_disabled"] and not recorded(receipt["transport"], PUSHARY_TRANSPORT)
241
+ restore_list_member(a, "disabled_toolsets", receipt["disabled"], QUESTION_TOOLSET, clarify_was_users)
242
+ restore_container(c, "agent", receipt["agent"], a, agent_changed)
243
+ else:
244
+ disabled = [item for item in members(a, "disabled_toolsets") if item != QUESTION_TOOLSET]
245
+ if disabled: a["disabled_toolsets"] = disabled
246
+ else: a.pop("disabled_toolsets", None)
247
+ if a: c["agent"] = a
248
+ else: c.pop("agent", None)
249
+
250
+ if receipt and restore_conflicts:
251
+ raise ValueError("Hermes config changed after Pushary setup")
252
+
253
+ changed = c != original
254
+ if changed and mode != "clean-preview":
255
+ save_config(c)
256
+ print(json.dumps({"changed": changed}))
257
+ `;
258
+ var resolveHermesPython = () => {
259
+ const launcher = resolvePathBinary("hermes");
260
+ if (launcher && process.platform !== "win32") {
261
+ try {
262
+ const shebang = readFileSync(launcher, "utf-8").split("\n", 1)[0];
263
+ const interpreter = shebang?.startsWith("#!") ? shebang.slice(2).trim().split(/\s+/)[0] : void 0;
264
+ if (interpreter && /python/i.test(interpreter) && existsSync(interpreter)) return interpreter;
265
+ } catch {
266
+ }
267
+ }
268
+ const root = join(homedir(), ".hermes", "hermes-agent", "venv");
269
+ const candidates = process.platform === "win32" ? [join(root, "Scripts", "python.exe"), join(root, "Scripts", "python3.exe")] : [join(root, "bin", "python3"), join(root, "bin", "python")];
270
+ return candidates.find(existsSync) ?? null;
271
+ };
272
+ var HERMES_SCRIPT_TIMEOUT_MS = 15e3;
273
+ var runPythonScript = (python, script, args) => {
274
+ const path = join(tmpdir(), `pushary-hermes-${process.pid}-${Date.now()}.py`);
275
+ try {
276
+ writeFileSync(path, script, { encoding: "utf-8", mode: 384 });
277
+ return execFileSync(python, [path, ...args], {
278
+ env: { ...process.env },
279
+ encoding: "utf-8",
280
+ stdio: "pipe",
281
+ timeout: HERMES_SCRIPT_TIMEOUT_MS
282
+ });
283
+ } catch {
284
+ return null;
285
+ } finally {
286
+ try {
287
+ rmSync(path, { force: true });
288
+ } catch {
289
+ }
290
+ }
291
+ };
292
+ var lastReport = (stdout) => {
293
+ const line = stdout.trim().split("\n").at(-1) ?? "";
294
+ try {
295
+ return JSON.parse(line);
296
+ } catch {
297
+ return void 0;
298
+ }
299
+ };
300
+ var configRunFrom = (stdout) => {
301
+ const report = stdout === null ? void 0 : lastReport(stdout);
302
+ if (!isRecord(report) || typeof report.changed !== "boolean") return "failed";
303
+ return report.changed ? "changed" : "unchanged";
304
+ };
305
+ var runHermesConfigScript = (python, mode, receiptPath) => configRunFrom(runPythonScript(python, HERMES_CONFIG_SCRIPT, [mode, receiptPath]));
306
+ var HERMES_STATUS_FIELDS = ["plugin_installed", "plugin_enabled", "approvals_routed", "questions_routed"];
307
+ var isHermesStatusReport = (value) => isRecord(value) && HERMES_STATUS_FIELDS.every((field) => typeof value[field] === "boolean");
308
+ var hermesWiredByPushary = (status, setupRecorded) => setupRecorded || status.pluginEnabled || status.approvalsRouted;
309
+ var readHermesStatus = (python) => {
310
+ const stdout = runPythonScript(python, HERMES_CONFIG_SCRIPT, ["status"]);
311
+ const report = stdout === null ? void 0 : lastReport(stdout);
312
+ if (!isHermesStatusReport(report)) return null;
313
+ return {
314
+ pluginInstalled: report.plugin_installed,
315
+ pluginEnabled: report.plugin_enabled,
316
+ approvalsRouted: report.approvals_routed,
317
+ questionsRouted: report.questions_routed
318
+ };
319
+ };
13
320
 
14
321
  // src/fx-config.ts
15
322
  var FX_SERVER_NAME = "pushary";
@@ -347,13 +654,13 @@ var workspaceShowsQuestionTool = (workspace) => {
347
654
  var workspacesShowingQuestionTool = (settings) => Object.entries(asRecord(settings[WORKSPACES_KEY]) ?? {}).filter(([, workspace]) => workspaceShowsQuestionTool(workspace)).map(([path]) => path);
348
655
 
349
656
  // src/fx-files.ts
350
- import { existsSync, lstatSync, readFileSync, realpathSync } from "fs";
657
+ import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, realpathSync } from "fs";
351
658
  import { relative, isAbsolute } from "path";
352
659
  var readFxFile = (path, read) => {
353
- if (!existsSync(path)) return { kind: "missing" };
660
+ if (!existsSync2(path)) return { kind: "missing" };
354
661
  let text;
355
662
  try {
356
- text = readFileSync(path, "utf-8");
663
+ text = readFileSync2(path, "utf-8");
357
664
  } catch {
358
665
  return { kind: "unreadable" };
359
666
  }
@@ -374,8 +681,8 @@ var fxIgnoresInstructionsFile = (path, fxDir) => {
374
681
  };
375
682
 
376
683
  // src/vscode-config.ts
377
- import { homedir } from "os";
378
- import { join } from "path";
684
+ import { homedir as homedir2 } from "os";
685
+ import { join as join2 } from "path";
379
686
  var PLUGIN_LOCATIONS_KEY = "chat.pluginLocations";
380
687
  var blankJsonComments = (text) => {
381
688
  let out = "";
@@ -505,13 +812,13 @@ var pluginLocationSnippet = (pluginDir) => `"${PLUGIN_LOCATIONS_KEY}": {
505
812
  }`;
506
813
  var VSCODE_PRODUCTS = ["Code", "Code - Insiders"];
507
814
  var settingsRoot = (home, platform) => {
508
- if (platform === "darwin") return join(home, "Library", "Application Support");
509
- if (platform === "win32") return process.env.APPDATA?.trim() || join(home, "AppData", "Roaming");
510
- return process.env.XDG_CONFIG_HOME?.trim() || join(home, ".config");
815
+ if (platform === "darwin") return join2(home, "Library", "Application Support");
816
+ if (platform === "win32") return process.env.APPDATA?.trim() || join2(home, "AppData", "Roaming");
817
+ return process.env.XDG_CONFIG_HOME?.trim() || join2(home, ".config");
511
818
  };
512
- var vscodeSettingsCandidates = (home = homedir(), platform = process.platform) => {
819
+ var vscodeSettingsCandidates = (home = homedir2(), platform = process.platform) => {
513
820
  const root = settingsRoot(home, platform);
514
- return VSCODE_PRODUCTS.map((product) => join(root, product, "User", "settings.json"));
821
+ return VSCODE_PRODUCTS.map((product) => join2(root, product, "User", "settings.json"));
515
822
  };
516
823
  var vscodeSettingsTargets = (exists, home, platform) => {
517
824
  const candidates = vscodeSettingsCandidates(home, platform);
@@ -521,9 +828,9 @@ var vscodeSettingsTargets = (exists, home, platform) => {
521
828
 
522
829
  // src/setup/detect.ts
523
830
  import { execSync } from "child_process";
524
- import { existsSync as existsSync2 } from "fs";
525
- import { homedir as homedir2 } from "os";
526
- import { dirname, join as join2 } from "path";
831
+ import { existsSync as existsSync3 } from "fs";
832
+ import { homedir as homedir3 } from "os";
833
+ import { dirname, join as join3 } from "path";
527
834
  var whichCommand = () => process.platform === "win32" ? "where" : "which";
528
835
  var binaryOnPath = (binary) => {
529
836
  try {
@@ -533,7 +840,7 @@ var binaryOnPath = (binary) => {
533
840
  return false;
534
841
  }
535
842
  };
536
- var defaultDeps = { onPath: binaryOnPath, exists: existsSync2 };
843
+ var defaultDeps = { onPath: binaryOnPath, exists: existsSync3 };
537
844
  var detectAgent = (probe, deps = defaultDeps) => {
538
845
  const checked = [];
539
846
  if (probe.binary) {
@@ -555,12 +862,12 @@ var detectAgent = (probe, deps = defaultDeps) => {
555
862
  }
556
863
  return { kind: "not-found", checked };
557
864
  };
558
- var agentProbes = (home = homedir2()) => ({
865
+ var agentProbes = (home = homedir3()) => ({
559
866
  claude_code: {
560
867
  binary: "claude",
561
868
  // The native installer puts it here, outside a default non-login PATH.
562
- paths: [join2(home, ".local", "bin", "claude")],
563
- configMarkers: [join2(home, ".claude")]
869
+ paths: [join3(home, ".local", "bin", "claude")],
870
+ configMarkers: [join3(home, ".claude")]
564
871
  },
565
872
  codex: {
566
873
  binary: "codex",
@@ -570,25 +877,25 @@ var agentProbes = (home = homedir2()) => ({
570
877
  gemini_cli: {
571
878
  binary: "gemini",
572
879
  paths: [],
573
- configMarkers: [join2(home, ".gemini")]
880
+ configMarkers: [join3(home, ".gemini")]
574
881
  },
575
882
  hermes: {
576
883
  binary: "hermes",
577
884
  paths: [],
578
- configMarkers: [join2(home, ".hermes")]
885
+ configMarkers: [join3(home, ".hermes")]
579
886
  },
580
887
  cursor: {
581
888
  // GUI editor. `cursor` on PATH only exists if the user ran "Install 'cursor'
582
889
  // command in PATH" from the command palette, which most never do.
583
890
  binary: "cursor",
584
- paths: process.platform === "darwin" ? ["/Applications/Cursor.app"] : process.platform === "win32" ? [join2(process.env.LOCALAPPDATA ?? join2(home, "AppData", "Local"), "Programs", "cursor")] : ["/usr/share/cursor", join2(home, ".local", "share", "cursor")],
585
- configMarkers: [join2(home, ".cursor")]
891
+ paths: process.platform === "darwin" ? ["/Applications/Cursor.app"] : process.platform === "win32" ? [join3(process.env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "Programs", "cursor")] : ["/usr/share/cursor", join3(home, ".local", "share", "cursor")],
892
+ configMarkers: [join3(home, ".cursor")]
586
893
  },
587
894
  vscode: {
588
895
  // Like Cursor, a GUI editor: `code` on PATH only exists if the user ran
589
896
  // "Shell Command: Install 'code' command in PATH", which on macOS is opt-in.
590
897
  binary: "code",
591
- paths: process.platform === "darwin" ? ["/Applications/Visual Studio Code.app"] : process.platform === "win32" ? [join2(process.env.LOCALAPPDATA ?? join2(home, "AppData", "Local"), "Programs", "Microsoft VS Code")] : ["/usr/share/code", join2(home, ".local", "share", "code")],
898
+ paths: process.platform === "darwin" ? ["/Applications/Visual Studio Code.app"] : process.platform === "win32" ? [join3(process.env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "Programs", "Microsoft VS Code")] : ["/usr/share/code", join3(home, ".local", "share", "code")],
592
899
  // The settings directory rather than ~/.vscode, because the settings
593
900
  // directory is what this installer actually writes to. Insiders counts:
594
901
  // someone running only Insiders still has VS Code.
@@ -604,7 +911,7 @@ var agentProbes = (home = homedir2()) => ({
604
911
  },
605
912
  fx: {
606
913
  binary: null,
607
- paths: [join2(home, ".fx", "settings.json"), join2(home, ".fx", "sessions"), join2(home, ".fx", "history.jsonl")],
914
+ paths: [join3(home, ".fx", "settings.json"), join3(home, ".fx", "sessions"), join3(home, ".fx", "history.jsonl")],
608
915
  configMarkers: []
609
916
  },
610
917
  ...compatibleProbes(),
@@ -628,7 +935,7 @@ var detectAllAgents = (deps = defaultDeps, home) => {
628
935
  }));
629
936
  };
630
937
  var isDetected = (result) => result.kind === "installed";
631
- var shortenHome = (path, home = homedir2()) => home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
938
+ var shortenHome = (path, home = homedir3()) => home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
632
939
  var describeDetection = (result, home) => {
633
940
  if (result.kind !== "installed") return null;
634
941
  const evidence = shortenHome(result.evidence, home);
@@ -636,7 +943,7 @@ var describeDetection = (result, home) => {
636
943
  };
637
944
 
638
945
  // src/instruction-file.ts
639
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync, rmSync } from "fs";
946
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, rmSync as rmSync2 } from "fs";
640
947
  import { dirname as dirname2 } from "path";
641
948
  var PUSHARY_BEGIN = "<!-- BEGIN PUSHARY (managed by @pushary/agent-hooks; do not edit this block) -->";
642
949
  var PUSHARY_END = "<!-- END PUSHARY -->";
@@ -666,10 +973,10 @@ ${body}
666
973
  ${PUSHARY_END}`;
667
974
  var writeInstructionBlock = (filePath, body) => {
668
975
  const dir = dirname2(filePath);
669
- if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
976
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
670
977
  let existing = "";
671
978
  try {
672
- existing = readFileSync2(filePath, "utf-8");
979
+ existing = readFileSync3(filePath, "utf-8");
673
980
  } catch {
674
981
  }
675
982
  const block = wrap(body);
@@ -682,12 +989,12 @@ var writeInstructionBlock = (filePath, body) => {
682
989
  const prefix = existing.trim() ? existing.replace(/\s*$/, "") + "\n\n" : "";
683
990
  next = prefix + block + "\n";
684
991
  }
685
- writeFileSync(filePath, next, "utf-8");
992
+ writeFileSync2(filePath, next, "utf-8");
686
993
  };
687
994
  var removeInstructionBlock = (filePath) => {
688
995
  let existing = "";
689
996
  try {
690
- existing = readFileSync2(filePath, "utf-8");
997
+ existing = readFileSync3(filePath, "utf-8");
691
998
  } catch {
692
999
  return false;
693
1000
  }
@@ -696,21 +1003,28 @@ var removeInstructionBlock = (filePath) => {
696
1003
  if (start === -1 || end === -1 || end < start) return false;
697
1004
  const remaining = (existing.slice(0, start) + existing.slice(end + PUSHARY_END.length)).trim();
698
1005
  if (remaining === "") {
699
- rmSync(filePath, { force: true });
1006
+ rmSync2(filePath, { force: true });
700
1007
  } else {
701
- writeFileSync(filePath, remaining + "\n", "utf-8");
1008
+ writeFileSync2(filePath, remaining + "\n", "utf-8");
702
1009
  }
703
1010
  return true;
704
1011
  };
705
1012
  var hasInstructionBlock = (filePath) => {
706
1013
  try {
707
- return readFileSync2(filePath, "utf-8").includes(PUSHARY_BEGIN);
1014
+ return readFileSync3(filePath, "utf-8").includes(PUSHARY_BEGIN);
708
1015
  } catch {
709
1016
  return false;
710
1017
  }
711
1018
  };
712
1019
 
713
1020
  export {
1021
+ isHermesConfigReceipt,
1022
+ HERMES_PIP_PACKAGE,
1023
+ HERMES_PLUGIN_NAME,
1024
+ resolveHermesPython,
1025
+ runHermesConfigScript,
1026
+ hermesWiredByPushary,
1027
+ readHermesStatus,
714
1028
  FX_PUSHARY_TOOLS_RULE,
715
1029
  FX_TOOL_DISCOVERY_NOTE,
716
1030
  FX_PROFILE_REFUSALS,