@pushary/agent-hooks 1.7.0 → 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 (102) hide show
  1. package/CHANGELOG.md +28 -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 +194 -102
  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 +99 -43
  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 +143 -105
  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-IJ6ECPA6.js → chunk-2B4XCMH6.js} +75 -232
  42. package/dist/{chunk-EGKXSQCY.js → chunk-2XOHXZRC.js} +12 -4
  43. package/dist/{chunk-D6BULEJW.js → chunk-3NSRL5E7.js} +2 -2
  44. package/dist/{chunk-UC65FJJT.js → chunk-3RO2UWJQ.js} +2 -2
  45. package/dist/{chunk-73T6MSU5.js → chunk-4S7CCOXB.js} +5 -5
  46. package/dist/{chunk-7OS4IHGI.js → chunk-5GIBNUBD.js} +2 -2
  47. package/dist/chunk-5OP5MQI7.js +45 -0
  48. package/dist/{chunk-B3UXMSMW.js → chunk-65RN6ZCO.js} +1 -1
  49. package/dist/{chunk-2PS6QWDW.js → chunk-6NHXKTQU.js} +2 -2
  50. package/dist/{chunk-V6QRSU2G.js → chunk-6QZK4O36.js} +4 -4
  51. package/dist/{chunk-ZQ7IDOKY.js → chunk-A6WQF7FQ.js} +3 -3
  52. package/dist/{chunk-QXAK5RFU.js → chunk-AWZNDM3B.js} +1 -1
  53. package/dist/{chunk-JNXVOU6Y.js → chunk-D4OGEWLH.js} +19 -2
  54. package/dist/{chunk-R2XCLT6I.js → chunk-E3PLF7YB.js} +4 -4
  55. package/dist/{chunk-2C7CCWVX.js → chunk-F6BMJOSW.js} +1 -1
  56. package/dist/{chunk-XNCMT7YY.js → chunk-HOIGXMI7.js} +1 -1
  57. package/dist/{chunk-CVALVGA7.js → chunk-I7B2DNVU.js} +1 -1
  58. package/dist/{chunk-S3UD4V6W.js → chunk-INR3RVQ3.js} +13 -3
  59. package/dist/{chunk-52NVBNJT.js → chunk-IZVAVXHQ.js} +6 -4
  60. package/dist/{chunk-5HLTWFJ6.js → chunk-JMSB6CJT.js} +1 -1
  61. package/dist/{chunk-KYQXUGH7.js → chunk-KAJT6HPI.js} +3 -3
  62. package/dist/{chunk-VKVIAZQO.js → chunk-KLXYMPTM.js} +1 -1
  63. package/dist/{chunk-PQ4F64M2.js → chunk-L6IF4JOF.js} +3 -3
  64. package/dist/{chunk-ROQK7IN3.js → chunk-LI3C2IQY.js} +2 -2
  65. package/dist/{chunk-B234346F.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-DNJUSFBL.js → chunk-NYBZJ44G.js} +40 -5
  69. package/dist/{chunk-BBKNJJVX.js → chunk-OJRB5QKN.js} +6 -6
  70. package/dist/{chunk-PUDBG3GM.js → chunk-OXEF6JHD.js} +4 -4
  71. package/dist/{chunk-LEZFOTMI.js → chunk-PWNQGITG.js} +1 -1
  72. package/dist/{chunk-B76WVEDX.js → chunk-Q7CDBJXU.js} +1 -1
  73. package/dist/{chunk-KXMZMZD3.js → chunk-QSZ37ROP.js} +1 -1
  74. package/dist/{chunk-BH2SKSSK.js → chunk-R7ZETVNR.js} +1 -1
  75. package/dist/{chunk-DZURML2D.js → chunk-RLRDPZG4.js} +1 -1
  76. package/dist/{chunk-J2355HR7.js → chunk-SEIZQZEE.js} +1 -1
  77. package/dist/{chunk-V4AQBAVG.js → chunk-SJN64NJX.js} +1 -1
  78. package/dist/{chunk-B3YQANFN.js → chunk-SKEW3TQX.js} +1 -1
  79. package/dist/{chunk-L4H2Q2S4.js → chunk-SKMWPXGV.js} +500 -46
  80. package/dist/{chunk-WUNZAE7I.js → chunk-T3PRPFDY.js} +5 -5
  81. package/dist/{chunk-YYINL576.js → chunk-TVJYGZXB.js} +2 -2
  82. package/dist/{chunk-O2LEPIU6.js → chunk-U3MS6D5X.js} +1 -1
  83. package/dist/{chunk-5RUIFDTP.js → chunk-U4TMAANR.js} +33 -2
  84. package/dist/{chunk-KW2RRR7D.js → chunk-UFNBKYWT.js} +3 -3
  85. package/dist/{chunk-DVQDPI3T.js → chunk-UUTWHVSV.js} +35 -35
  86. package/dist/{chunk-X6KDKOV5.js → chunk-VQDJ6KB3.js} +3 -3
  87. package/dist/{chunk-UOAQ6QMP.js → chunk-VSGKQ3HI.js} +2 -2
  88. package/dist/{chunk-YROGUPOZ.js → chunk-WWTLAZQA.js} +25 -8
  89. package/dist/{chunk-SQRWHALT.js → chunk-WXIVUIXA.js} +2 -2
  90. package/dist/{chunk-HDRNWNVP.js → chunk-YLCSKVQQ.js} +9 -28
  91. package/dist/{chunk-JXUAXO74.js → chunk-YNJ2H2FD.js} +2 -2
  92. package/dist/{chunk-BWYJ6AAF.js → chunk-YU4222G2.js} +2 -2
  93. package/dist/{chunk-4F46VIEE.js → chunk-YU54LNC6.js} +6 -6
  94. package/dist/{chunk-EAQMWJPG.js → chunk-ZMWX5SDA.js} +8 -8
  95. package/dist/{chunk-6ADZQFVW.js → chunk-ZUKZJQGR.js} +1 -1
  96. package/dist/{reapply-6PH3FMPX.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-DANOZVZK.js +0 -307
  101. package/dist/chunk-P32LPT2L.js +0 -6
  102. package/dist/chunk-YHG74UFF.js +0 -12
@@ -1,15 +1,322 @@
1
1
  import {
2
2
  MCP_BRIDGE_BINARY
3
- } from "./chunk-YYINL576.js";
3
+ } from "./chunk-TVJYGZXB.js";
4
4
  import {
5
5
  openCodeConfigDirFrom
6
- } from "./chunk-B3YQANFN.js";
6
+ } from "./chunk-SKEW3TQX.js";
7
7
  import {
8
- codexHomeFrom
9
- } from "./chunk-DNJUSFBL.js";
8
+ codexHomeFrom,
9
+ resolvePathBinary
10
+ } from "./chunk-NYBZJ44G.js";
10
11
  import {
11
12
  HOOK_COMPATIBLE_PROFILES
12
- } from "./chunk-YROGUPOZ.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";
@@ -17,7 +324,7 @@ var FX_OPERATION_TIMEOUT_MS = 75e3;
17
324
  var FX_PROFILE_MAX_BYTES = 1024 * 1024 - 1;
18
325
  var FX_SETTINGS_MAX_BYTES = 64 * 1024;
19
326
  var FX_PUSHARY_TOOLS_RULE = "mcp_pushary_*";
20
- var FX_TOOL_DISCOVERY_NOTE = 'In fx, these tools are named mcp_pushary_ask_user, mcp_pushary_send_notification, mcp_pushary_wait_for_answer and mcp_pushary_cancel_question. They are not loaded at the start of a turn: find them with capability_search (query "pushary") or select one with mcp_select_tool. When the user may be away from this terminal, use mcp_pushary_ask_user rather than your built-in question prompt.';
327
+ var FX_TOOL_DISCOVERY_NOTE = 'In fx, these tools are named mcp_pushary_ask_user, mcp_pushary_send_notification, mcp_pushary_wait_for_answer, mcp_pushary_cancel_question and mcp_pushary_propose_scope. fx does not load them at the start of a turn: call mcp_select_tool with the exact name, for example {"name": "mcp_pushary_ask_user"}, then call the tool. For fx this replaces the guidance above to ask the whole round in the terminal while the user is there: ask every question through mcp_pushary_ask_user, even while the user is at this terminal, rather than through ask_user_question. fx cannot show its own question prompt in the Mac notch or on the phone, so a question asked there waits unseen once the user steps away. Ask in your reply instead only when a Pushary handoffAction tells you to ask here, or when mcp_pushary_ask_user is unavailable or fails without one.';
21
328
  var ALLOW_ACTION = "allow";
22
329
  var PERMISSION_KEY = "permission";
23
330
  var WORKSPACES_KEY = "workspaces";
@@ -50,7 +357,9 @@ var FX_PROFILE_REFUSALS = {
50
357
  };
51
358
  var FX_SETTINGS_REFUSALS = {
52
359
  ...FX_JSON_REFUSALS,
53
- "permission-not-object": 'has a "permission" value that is not an object'
360
+ "permission-not-object": 'has a "permission" value that is not an object',
361
+ "invalid-permission-rule": "has a permission rule fx rejects, so fx ignores the whole file",
362
+ "invalid-permission-mode": "has a permission_mode fx does not recognise, so fx ignores the whole file"
54
363
  };
55
364
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
56
365
  var stringTokenEnd = (text, start) => {
@@ -182,6 +491,8 @@ var fxSettingsText = (settings) => fxFileText(settings, FX_SETTINGS_MAX_BYTES);
182
491
  var readFxSettings = (text) => {
183
492
  const read = readFxJsonObject(text, FX_SETTINGS_MAX_BYTES);
184
493
  if (read.kind === "refused") return read;
494
+ const invalid = fxSettingsRejection(read.value);
495
+ if (invalid) return { kind: "refused", reason: invalid };
185
496
  const permission = read.value[PERMISSION_KEY];
186
497
  if (permission !== void 0 && !asRecord(permission)) return { kind: "refused", reason: "permission-not-object" };
187
498
  return { kind: "settings", settings: read.value };
@@ -196,15 +507,15 @@ var withPusharyToolsAllowed = (settings) => {
196
507
  change: "added"
197
508
  };
198
509
  };
510
+ var withoutPermissionRule = (settings, permission, rule) => {
511
+ const remaining = Object.fromEntries(Object.entries(permission).filter(([name]) => name !== rule));
512
+ const withoutPermission = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== PERMISSION_KEY));
513
+ return Object.keys(remaining).length === 0 ? withoutPermission : { ...settings, [PERMISSION_KEY]: remaining };
514
+ };
199
515
  var withoutPusharyToolsAllowed = (settings) => {
200
516
  const permission = asRecord(settings[PERMISSION_KEY]);
201
- if (permission?.[FX_PUSHARY_TOOLS_RULE] !== ALLOW_ACTION) return { settings, removed: false };
202
- const remaining = Object.fromEntries(Object.entries(permission).filter(([rule]) => rule !== FX_PUSHARY_TOOLS_RULE));
203
- const withoutPermission = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== PERMISSION_KEY));
204
- return {
205
- settings: Object.keys(remaining).length === 0 ? withoutPermission : { ...settings, [PERMISSION_KEY]: remaining },
206
- removed: true
207
- };
517
+ if (!permission || permission[FX_PUSHARY_TOOLS_RULE] !== ALLOW_ACTION) return { settings, removed: false };
518
+ return { settings: withoutPermissionRule(settings, permission, FX_PUSHARY_TOOLS_RULE), removed: true };
208
519
  };
209
520
  var pusharyToolsAllowed = (settings) => asRecord(settings[PERMISSION_KEY])?.[FX_PUSHARY_TOOLS_RULE] === ALLOW_ACTION;
210
521
  var overridesPusharyRule = (workspace) => {
@@ -212,15 +523,144 @@ var overridesPusharyRule = (workspace) => {
212
523
  return permission !== void 0 && !(FX_PUSHARY_TOOLS_RULE in permission);
213
524
  };
214
525
  var workspacesOverridingPusharyRule = (settings) => Object.entries(asRecord(settings[WORKSPACES_KEY]) ?? {}).filter(([, workspace]) => overridesPusharyRule(workspace)).map(([path]) => path);
526
+ var FX_QUESTION_TOOL = "ask_user_question";
527
+ var DENY_ACTION = "deny";
528
+ var GLOBAL_TARGET_PATTERN = "*";
529
+ var PERMISSION_MODE_KEY = "permission_mode";
530
+ var FX_PERMISSION_MODE_ENV = "FX_PERMISSION_MODE";
531
+ var FX_DEFAULT_PERMISSION_MODE = "auto";
532
+ var FX_PERMISSION_MODE_SPELLINGS = {
533
+ ask: "ask",
534
+ auto: "auto",
535
+ "full-access": "full-access",
536
+ "full access": "full-access",
537
+ yolo: "full-access"
538
+ };
539
+ var FX_RULE_WHITESPACE = /^[ \t\r\n]+|[ \t\r\n]+$/g;
540
+ var FX_QUESTION_RULE_RECEIPT = { rule: FX_QUESTION_TOOL, action: DENY_ACTION };
541
+ var isFxQuestionRuleReceipt = (value) => {
542
+ const receipt = asRecord(value);
543
+ return receipt?.rule === FX_QUESTION_TOOL && receipt.action === DENY_ACTION;
544
+ };
545
+ var isFxRuleAction = (value) => value === "allow" || value === "ask" || value === DENY_ACTION;
546
+ var fxRuleAction = (value) => {
547
+ if (typeof value !== "string") return null;
548
+ const action = value.toLowerCase();
549
+ return isFxRuleAction(action) ? action : null;
550
+ };
551
+ var parseFxPermissionMode = (value) => typeof value === "string" ? FX_PERMISSION_MODE_SPELLINGS[value.toLowerCase()] ?? null : null;
552
+ var trimFxRuleText = (text) => text.replace(FX_RULE_WHITESPACE, "");
553
+ var isValidPatternMap = (value) => {
554
+ const patterns = asRecord(value);
555
+ return patterns !== void 0 && Object.values(patterns).every((action) => fxRuleAction(action) !== null);
556
+ };
557
+ var isValidPermissionValue = (value) => {
558
+ if (fxRuleAction(value) !== null) return true;
559
+ const permission = asRecord(value);
560
+ return permission !== void 0 && Object.entries(permission).every(([name, rule]) => trimFxRuleText(name).length > 0 && (fxRuleAction(rule) !== null || isValidPatternMap(rule)));
561
+ };
562
+ var permissionScopeRejection = (scope) => {
563
+ if (scope[PERMISSION_KEY] !== void 0 && !isValidPermissionValue(scope[PERMISSION_KEY])) return "invalid-permission-rule";
564
+ if (scope[PERMISSION_MODE_KEY] !== void 0 && parseFxPermissionMode(scope[PERMISSION_MODE_KEY]) === null) return "invalid-permission-mode";
565
+ return null;
566
+ };
567
+ var workspaceScopes = (settings) => Object.values(asRecord(settings[WORKSPACES_KEY]) ?? {}).flatMap((workspace) => {
568
+ const scope = asRecord(workspace);
569
+ return scope ? [scope] : [];
570
+ });
571
+ var fxSettingsRejection = (settings) => [settings, ...workspaceScopes(settings)].map(permissionScopeRejection).find((rejection) => rejection !== null) ?? null;
572
+ var rulesOfPermissionEntry = ([name, value]) => {
573
+ const permission = trimFxRuleText(name);
574
+ const action = fxRuleAction(value);
575
+ if (action) return [{ permission, pattern: GLOBAL_TARGET_PATTERN, action }];
576
+ return Object.entries(asRecord(value) ?? {}).flatMap(([pattern, target]) => {
577
+ const targetAction = fxRuleAction(target);
578
+ return targetAction ? [{ permission, pattern: trimFxRuleText(pattern), action: targetAction }] : [];
579
+ });
580
+ };
581
+ var fxPermissionRules = (permission) => {
582
+ const everything = fxRuleAction(permission);
583
+ if (everything) return [{ permission: GLOBAL_TARGET_PATTERN, pattern: GLOBAL_TARGET_PATTERN, action: everything }];
584
+ return Object.entries(asRecord(permission) ?? {}).flatMap(rulesOfPermissionEntry);
585
+ };
586
+ var fxWildcardMatch = (pattern, candidate) => {
587
+ let patternIndex = 0;
588
+ let candidateIndex = 0;
589
+ let starIndex = null;
590
+ let starCandidateIndex = 0;
591
+ while (candidateIndex < candidate.length) {
592
+ if (patternIndex < pattern.length && (pattern[patternIndex] === "?" || pattern[patternIndex] === candidate[candidateIndex])) {
593
+ patternIndex += 1;
594
+ candidateIndex += 1;
595
+ } else if (patternIndex < pattern.length && pattern[patternIndex] === "*") {
596
+ starIndex = patternIndex;
597
+ patternIndex += 1;
598
+ starCandidateIndex = candidateIndex;
599
+ } else if (starIndex === null) {
600
+ return false;
601
+ } else {
602
+ starCandidateIndex += 1;
603
+ candidateIndex = starCandidateIndex;
604
+ patternIndex = starIndex + 1;
605
+ }
606
+ }
607
+ while (pattern[patternIndex] === "*") patternIndex += 1;
608
+ return patternIndex === pattern.length;
609
+ };
610
+ var DIRECTORY_TREE_SUFFIX = "/**";
611
+ var directoryTreeMatches = (pattern, candidate) => {
612
+ if (!pattern.endsWith(DIRECTORY_TREE_SUFFIX)) return false;
613
+ const directory = pattern.slice(0, -DIRECTORY_TREE_SUFFIX.length);
614
+ return candidate === directory || candidate.startsWith(directory) && candidate[directory.length] === "/";
615
+ };
616
+ var targetMatches = (pattern, candidate) => directoryTreeMatches(pattern, candidate) || fxWildcardMatch(pattern, candidate);
617
+ var isGlobalTargetPattern = (pattern) => /^\*+$/.test(pattern);
618
+ var decisionForTarget = (rules, target) => rules.filter((rule) => targetMatches(rule.pattern, target)).at(-1)?.action ?? null;
619
+ var reopensTool = (rules, rule) => rule.action !== DENY_ACTION && decisionForTarget(rules, rule.pattern) !== DENY_ACTION && decisionForTarget(rules, rule.pattern) !== null;
620
+ var fxRulesHideTool = (rules, toolName) => {
621
+ const matching = rules.filter((rule) => fxWildcardMatch(rule.permission, toolName));
622
+ const lastGlobalRule = matching.filter((rule) => isGlobalTargetPattern(rule.pattern)).at(-1);
623
+ if (lastGlobalRule?.action !== DENY_ACTION) return false;
624
+ return !matching.slice(matching.lastIndexOf(lastGlobalRule) + 1).some((rule) => reopensTool(matching, rule));
625
+ };
626
+ var effectivePermissionMode = (scope, environment) => parseFxPermissionMode(environment[FX_PERMISSION_MODE_ENV]) ?? parseFxPermissionMode(scope[PERMISSION_MODE_KEY]) ?? FX_DEFAULT_PERMISSION_MODE;
627
+ var fxRunsWithoutRules = (settings, environment) => effectivePermissionMode(settings, environment) === "full-access";
628
+ var fxQuestionToolHidden = (permission) => fxRulesHideTool(fxPermissionRules(permission), FX_QUESTION_TOOL);
629
+ var fxQuestionRouting = (settings, environment) => {
630
+ if (fxRunsWithoutRules(settings, environment)) return "full-access";
631
+ return fxQuestionToolHidden(settings[PERMISSION_KEY]) ? "routed" : "overridden";
632
+ };
633
+ var withQuestionsRoutedToPushary = (settings) => {
634
+ const permission = asRecord(settings[PERMISSION_KEY]) ?? {};
635
+ const existing = permission[FX_QUESTION_TOOL];
636
+ if (existing === void 0) {
637
+ return { settings: { ...settings, [PERMISSION_KEY]: { ...permission, [FX_QUESTION_TOOL]: DENY_ACTION } }, change: "added" };
638
+ }
639
+ return { settings, change: fxRuleAction(existing) === DENY_ACTION ? "present" : "user-rule-kept" };
640
+ };
641
+ var withoutQuestionsRoutedToPushary = (settings, receipt) => {
642
+ const permission = asRecord(settings[PERMISSION_KEY]);
643
+ if (!permission || permission[FX_QUESTION_TOOL] === void 0) return { settings, outcome: "absent" };
644
+ if (!receipt) return { settings, outcome: "not-ours" };
645
+ if (fxRuleAction(permission[FX_QUESTION_TOOL]) !== DENY_ACTION) return { settings, outcome: "changed-after-setup" };
646
+ return { settings: withoutPermissionRule(settings, permission, FX_QUESTION_TOOL), outcome: "removed" };
647
+ };
648
+ var workspaceShowsQuestionTool = (workspace) => {
649
+ const scope = asRecord(workspace);
650
+ if (!scope) return false;
651
+ if (parseFxPermissionMode(scope[PERMISSION_MODE_KEY]) === "full-access") return true;
652
+ return scope[PERMISSION_KEY] !== void 0 && !fxQuestionToolHidden(scope[PERMISSION_KEY]);
653
+ };
654
+ var workspacesShowingQuestionTool = (settings) => Object.entries(asRecord(settings[WORKSPACES_KEY]) ?? {}).filter(([, workspace]) => workspaceShowsQuestionTool(workspace)).map(([path]) => path);
215
655
 
216
656
  // src/fx-files.ts
217
- import { existsSync, lstatSync, readFileSync, realpathSync } from "fs";
657
+ import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, realpathSync } from "fs";
218
658
  import { relative, isAbsolute } from "path";
219
659
  var readFxFile = (path, read) => {
220
- if (!existsSync(path)) return { kind: "missing" };
660
+ if (!existsSync2(path)) return { kind: "missing" };
221
661
  let text;
222
662
  try {
223
- text = readFileSync(path, "utf-8");
663
+ text = readFileSync2(path, "utf-8");
224
664
  } catch {
225
665
  return { kind: "unreadable" };
226
666
  }
@@ -241,8 +681,8 @@ var fxIgnoresInstructionsFile = (path, fxDir) => {
241
681
  };
242
682
 
243
683
  // src/vscode-config.ts
244
- import { homedir } from "os";
245
- import { join } from "path";
684
+ import { homedir as homedir2 } from "os";
685
+ import { join as join2 } from "path";
246
686
  var PLUGIN_LOCATIONS_KEY = "chat.pluginLocations";
247
687
  var blankJsonComments = (text) => {
248
688
  let out = "";
@@ -372,13 +812,13 @@ var pluginLocationSnippet = (pluginDir) => `"${PLUGIN_LOCATIONS_KEY}": {
372
812
  }`;
373
813
  var VSCODE_PRODUCTS = ["Code", "Code - Insiders"];
374
814
  var settingsRoot = (home, platform) => {
375
- if (platform === "darwin") return join(home, "Library", "Application Support");
376
- if (platform === "win32") return process.env.APPDATA?.trim() || join(home, "AppData", "Roaming");
377
- 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");
378
818
  };
379
- var vscodeSettingsCandidates = (home = homedir(), platform = process.platform) => {
819
+ var vscodeSettingsCandidates = (home = homedir2(), platform = process.platform) => {
380
820
  const root = settingsRoot(home, platform);
381
- return VSCODE_PRODUCTS.map((product) => join(root, product, "User", "settings.json"));
821
+ return VSCODE_PRODUCTS.map((product) => join2(root, product, "User", "settings.json"));
382
822
  };
383
823
  var vscodeSettingsTargets = (exists, home, platform) => {
384
824
  const candidates = vscodeSettingsCandidates(home, platform);
@@ -388,9 +828,9 @@ var vscodeSettingsTargets = (exists, home, platform) => {
388
828
 
389
829
  // src/setup/detect.ts
390
830
  import { execSync } from "child_process";
391
- import { existsSync as existsSync2 } from "fs";
392
- import { homedir as homedir2 } from "os";
393
- 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";
394
834
  var whichCommand = () => process.platform === "win32" ? "where" : "which";
395
835
  var binaryOnPath = (binary) => {
396
836
  try {
@@ -400,7 +840,7 @@ var binaryOnPath = (binary) => {
400
840
  return false;
401
841
  }
402
842
  };
403
- var defaultDeps = { onPath: binaryOnPath, exists: existsSync2 };
843
+ var defaultDeps = { onPath: binaryOnPath, exists: existsSync3 };
404
844
  var detectAgent = (probe, deps = defaultDeps) => {
405
845
  const checked = [];
406
846
  if (probe.binary) {
@@ -422,12 +862,12 @@ var detectAgent = (probe, deps = defaultDeps) => {
422
862
  }
423
863
  return { kind: "not-found", checked };
424
864
  };
425
- var agentProbes = (home = homedir2()) => ({
865
+ var agentProbes = (home = homedir3()) => ({
426
866
  claude_code: {
427
867
  binary: "claude",
428
868
  // The native installer puts it here, outside a default non-login PATH.
429
- paths: [join2(home, ".local", "bin", "claude")],
430
- configMarkers: [join2(home, ".claude")]
869
+ paths: [join3(home, ".local", "bin", "claude")],
870
+ configMarkers: [join3(home, ".claude")]
431
871
  },
432
872
  codex: {
433
873
  binary: "codex",
@@ -437,25 +877,25 @@ var agentProbes = (home = homedir2()) => ({
437
877
  gemini_cli: {
438
878
  binary: "gemini",
439
879
  paths: [],
440
- configMarkers: [join2(home, ".gemini")]
880
+ configMarkers: [join3(home, ".gemini")]
441
881
  },
442
882
  hermes: {
443
883
  binary: "hermes",
444
884
  paths: [],
445
- configMarkers: [join2(home, ".hermes")]
885
+ configMarkers: [join3(home, ".hermes")]
446
886
  },
447
887
  cursor: {
448
888
  // GUI editor. `cursor` on PATH only exists if the user ran "Install 'cursor'
449
889
  // command in PATH" from the command palette, which most never do.
450
890
  binary: "cursor",
451
- 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")],
452
- 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")]
453
893
  },
454
894
  vscode: {
455
895
  // Like Cursor, a GUI editor: `code` on PATH only exists if the user ran
456
896
  // "Shell Command: Install 'code' command in PATH", which on macOS is opt-in.
457
897
  binary: "code",
458
- 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")],
459
899
  // The settings directory rather than ~/.vscode, because the settings
460
900
  // directory is what this installer actually writes to. Insiders counts:
461
901
  // someone running only Insiders still has VS Code.
@@ -471,7 +911,7 @@ var agentProbes = (home = homedir2()) => ({
471
911
  },
472
912
  fx: {
473
913
  binary: null,
474
- 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")],
475
915
  configMarkers: []
476
916
  },
477
917
  ...compatibleProbes(),
@@ -495,7 +935,7 @@ var detectAllAgents = (deps = defaultDeps, home) => {
495
935
  }));
496
936
  };
497
937
  var isDetected = (result) => result.kind === "installed";
498
- 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;
499
939
  var describeDetection = (result, home) => {
500
940
  if (result.kind !== "installed") return null;
501
941
  const evidence = shortenHome(result.evidence, home);
@@ -503,7 +943,7 @@ var describeDetection = (result, home) => {
503
943
  };
504
944
 
505
945
  // src/instruction-file.ts
506
- 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";
507
947
  import { dirname as dirname2 } from "path";
508
948
  var PUSHARY_BEGIN = "<!-- BEGIN PUSHARY (managed by @pushary/agent-hooks; do not edit this block) -->";
509
949
  var PUSHARY_END = "<!-- END PUSHARY -->";
@@ -533,10 +973,10 @@ ${body}
533
973
  ${PUSHARY_END}`;
534
974
  var writeInstructionBlock = (filePath, body) => {
535
975
  const dir = dirname2(filePath);
536
- if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
976
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
537
977
  let existing = "";
538
978
  try {
539
- existing = readFileSync2(filePath, "utf-8");
979
+ existing = readFileSync3(filePath, "utf-8");
540
980
  } catch {
541
981
  }
542
982
  const block = wrap(body);
@@ -549,12 +989,12 @@ var writeInstructionBlock = (filePath, body) => {
549
989
  const prefix = existing.trim() ? existing.replace(/\s*$/, "") + "\n\n" : "";
550
990
  next = prefix + block + "\n";
551
991
  }
552
- writeFileSync(filePath, next, "utf-8");
992
+ writeFileSync2(filePath, next, "utf-8");
553
993
  };
554
994
  var removeInstructionBlock = (filePath) => {
555
995
  let existing = "";
556
996
  try {
557
- existing = readFileSync2(filePath, "utf-8");
997
+ existing = readFileSync3(filePath, "utf-8");
558
998
  } catch {
559
999
  return false;
560
1000
  }
@@ -563,21 +1003,28 @@ var removeInstructionBlock = (filePath) => {
563
1003
  if (start === -1 || end === -1 || end < start) return false;
564
1004
  const remaining = (existing.slice(0, start) + existing.slice(end + PUSHARY_END.length)).trim();
565
1005
  if (remaining === "") {
566
- rmSync(filePath, { force: true });
1006
+ rmSync2(filePath, { force: true });
567
1007
  } else {
568
- writeFileSync(filePath, remaining + "\n", "utf-8");
1008
+ writeFileSync2(filePath, remaining + "\n", "utf-8");
569
1009
  }
570
1010
  return true;
571
1011
  };
572
1012
  var hasInstructionBlock = (filePath) => {
573
1013
  try {
574
- return readFileSync2(filePath, "utf-8").includes(PUSHARY_BEGIN);
1014
+ return readFileSync3(filePath, "utf-8").includes(PUSHARY_BEGIN);
575
1015
  } catch {
576
1016
  return false;
577
1017
  }
578
1018
  };
579
1019
 
580
1020
  export {
1021
+ isHermesConfigReceipt,
1022
+ HERMES_PIP_PACKAGE,
1023
+ HERMES_PLUGIN_NAME,
1024
+ resolveHermesPython,
1025
+ runHermesConfigScript,
1026
+ hermesWiredByPushary,
1027
+ readHermesStatus,
581
1028
  FX_PUSHARY_TOOLS_RULE,
582
1029
  FX_TOOL_DISCOVERY_NOTE,
583
1030
  FX_PROFILE_REFUSALS,
@@ -596,6 +1043,13 @@ export {
596
1043
  withoutPusharyToolsAllowed,
597
1044
  pusharyToolsAllowed,
598
1045
  workspacesOverridingPusharyRule,
1046
+ FX_QUESTION_TOOL,
1047
+ FX_QUESTION_RULE_RECEIPT,
1048
+ isFxQuestionRuleReceipt,
1049
+ fxQuestionRouting,
1050
+ withQuestionsRoutedToPushary,
1051
+ withoutQuestionsRoutedToPushary,
1052
+ workspacesShowingQuestionTool,
599
1053
  readFxFile,
600
1054
  fxIgnoresInstructionsFile,
601
1055
  registerPluginLocation,