@pushary/agent-hooks 1.6.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +3 -2
  3. package/dist/bin/pushary-bell-hook.js +24 -22
  4. package/dist/bin/pushary-bell.js +11 -12
  5. package/dist/bin/pushary-claude.js +29 -28
  6. package/dist/bin/pushary-clean.js +99 -40
  7. package/dist/bin/pushary-codex-bridge.js +25 -25
  8. package/dist/bin/pushary-codex-hook.js +39 -34
  9. package/dist/bin/pushary-codex.js +13 -12
  10. package/dist/bin/pushary-connect.js +13 -12
  11. package/dist/bin/pushary-cowork.js +5 -6
  12. package/dist/bin/pushary-daemon.js +31 -32
  13. package/dist/bin/pushary-disconnect.js +22 -21
  14. package/dist/bin/pushary-doctor.js +98 -61
  15. package/dist/bin/pushary-elicitation-hook.js +26 -25
  16. package/dist/bin/pushary-gemini-bridge.js +12 -13
  17. package/dist/bin/pushary-gemini-hook.js +35 -30
  18. package/dist/bin/pushary-hook.js +38 -33
  19. package/dist/bin/pushary-login.js +13 -15
  20. package/dist/bin/pushary-logout.js +12 -13
  21. package/dist/bin/pushary-mcp.d.ts +1 -0
  22. package/dist/bin/pushary-mcp.js +56 -0
  23. package/dist/bin/pushary-mode.js +10 -8
  24. package/dist/bin/pushary-notification-hook.js +28 -27
  25. package/dist/bin/pushary-opencode-hook.js +34 -29
  26. package/dist/bin/pushary-permission-denied-hook.js +32 -31
  27. package/dist/bin/pushary-permission-hook.js +31 -30
  28. package/dist/bin/pushary-post-hook.js +28 -27
  29. package/dist/bin/pushary-prompt-hook.js +28 -27
  30. package/dist/bin/pushary-session-end-hook.js +28 -27
  31. package/dist/bin/pushary-session-start-hook.js +32 -31
  32. package/dist/bin/pushary-setup.js +222 -110
  33. package/dist/bin/pushary-stats.js +5 -6
  34. package/dist/bin/pushary-status.js +15 -16
  35. package/dist/bin/pushary-stop-hook.js +28 -27
  36. package/dist/bin/pushary-stopfailure-hook.js +28 -27
  37. package/dist/bin/pushary-suggestions.js +4 -4
  38. package/dist/bin/pushary-transcript-register.js +19 -20
  39. package/dist/bin/pushary-transcripts.js +7 -8
  40. package/dist/bin/pushary-upgrade.js +28 -28
  41. package/dist/bin/pushary-wait.js +10 -8
  42. package/dist/bin/pushary.js +9 -5
  43. package/dist/{chunk-4KSTLHWY.js → chunk-2C7CCWVX.js} +1 -1
  44. package/dist/{chunk-P4BNYJ5Q.js → chunk-2PS6QWDW.js} +2 -2
  45. package/dist/{chunk-ILGL4L3J.js → chunk-4F46VIEE.js} +8 -8
  46. package/dist/{chunk-UAV5MNKY.js → chunk-52NVBNJT.js} +3 -3
  47. package/dist/{chunk-GQPCPEG2.js → chunk-5HLTWFJ6.js} +1 -1
  48. package/dist/{chunk-EJ7ADMNM.js → chunk-6ADZQFVW.js} +1 -1
  49. package/dist/{chunk-QKOVVL7I.js → chunk-73T6MSU5.js} +7 -7
  50. package/dist/{chunk-YOCXPNQ5.js → chunk-7OS4IHGI.js} +2 -2
  51. package/dist/{chunk-BY2CRWBO.js → chunk-B234346F.js} +1 -1
  52. package/dist/{chunk-EA3IELHP.js → chunk-B3UXMSMW.js} +6 -2
  53. package/dist/{chunk-STOMNX4S.js → chunk-B3YQANFN.js} +1 -1
  54. package/dist/{chunk-JBU7XKY2.js → chunk-B76WVEDX.js} +1 -1
  55. package/dist/{chunk-WA7SQX6S.js → chunk-BBKNJJVX.js} +6 -6
  56. package/dist/{chunk-KNIIFNNQ.js → chunk-BH2SKSSK.js} +1 -1
  57. package/dist/{chunk-CTOK3AYA.js → chunk-BWYJ6AAF.js} +6 -7
  58. package/dist/{chunk-HCVAMXHT.js → chunk-CVALVGA7.js} +1 -1
  59. package/dist/{chunk-DX5FOQSO.js → chunk-D6BULEJW.js} +2 -2
  60. package/dist/{chunk-RJUHSFYE.js → chunk-DANOZVZK.js} +1 -1
  61. package/dist/{chunk-3BJQYXTL.js → chunk-DNJUSFBL.js} +13 -5
  62. package/dist/{chunk-57YDFAPP.js → chunk-DVQDPI3T.js} +8 -8
  63. package/dist/{chunk-BEHGVU4W.js → chunk-DZURML2D.js} +1 -1
  64. package/dist/{chunk-LDS4ERW4.js → chunk-EAQMWJPG.js} +10 -10
  65. package/dist/{chunk-LVHGSD5E.js → chunk-EGKXSQCY.js} +13 -7
  66. package/dist/{chunk-HHYVBHHC.js → chunk-HDRNWNVP.js} +6 -6
  67. package/dist/{chunk-JBXUKGZS.js → chunk-IJ6ECPA6.js} +15 -9
  68. package/dist/{chunk-NMRH3VX4.js → chunk-J2355HR7.js} +1 -1
  69. package/dist/{chunk-HETOKNIL.js → chunk-JNXVOU6Y.js} +10 -6
  70. package/dist/{chunk-COGHCL6K.js → chunk-JXUAXO74.js} +2 -2
  71. package/dist/chunk-KW2RRR7D.js +124 -0
  72. package/dist/{chunk-66H2QQAK.js → chunk-KXMZMZD3.js} +1 -1
  73. package/dist/{chunk-H3FEVIQD.js → chunk-KYQXUGH7.js} +3 -3
  74. package/dist/{chunk-P35IUFFR.js → chunk-L4H2Q2S4.js} +276 -19
  75. package/dist/{chunk-NKR4YDRP.js → chunk-LEZFOTMI.js} +1 -1
  76. package/dist/{chunk-WD6W7QDI.js → chunk-O2LEPIU6.js} +1 -1
  77. package/dist/chunk-POFBE6ZY.js +12 -0
  78. package/dist/{chunk-SND3C2AV.js → chunk-PQ4F64M2.js} +3 -3
  79. package/dist/{chunk-3UOC3GRO.js → chunk-PUDBG3GM.js} +1 -1
  80. package/dist/{chunk-YQMFGWBD.js → chunk-QXAK5RFU.js} +1 -1
  81. package/dist/{chunk-REYWOCQJ.js → chunk-R2XCLT6I.js} +4 -4
  82. package/dist/{chunk-NPGOQQCV.js → chunk-ROQK7IN3.js} +2 -2
  83. package/dist/{chunk-GU3EJCVV.js → chunk-S3UD4V6W.js} +39 -0
  84. package/dist/{chunk-ICGAIM2R.js → chunk-SQRWHALT.js} +2 -2
  85. package/dist/{chunk-JX67DVMY.js → chunk-UC65FJJT.js} +5 -6
  86. package/dist/{chunk-CWIARY6F.js → chunk-UOAQ6QMP.js} +2 -2
  87. package/dist/{chunk-OSJEMXSR.js → chunk-V4AQBAVG.js} +1 -1
  88. package/dist/chunk-V6QRSU2G.js +142 -0
  89. package/dist/{chunk-VDMX5EMU.js → chunk-VKVIAZQO.js} +1 -1
  90. package/dist/{chunk-BG6OCZ4N.js → chunk-WUNZAE7I.js} +5 -5
  91. package/dist/{chunk-TZ4KUNZG.js → chunk-X6KDKOV5.js} +3 -3
  92. package/dist/{chunk-LM2IOEDK.js → chunk-XNCMT7YY.js} +3 -1
  93. package/dist/{chunk-GHOB245Q.js → chunk-YROGUPOZ.js} +132 -30
  94. package/dist/chunk-YYINL576.js +120 -0
  95. package/dist/{chunk-57DDP4TB.js → chunk-ZQ7IDOKY.js} +3 -3
  96. package/dist/{reapply-IB7YE7BU.js → reapply-6PH3FMPX.js} +9 -10
  97. package/dist/src/index.js +20 -19
  98. package/package.json +2 -1
  99. package/dist/chunk-HWJW3CPO.js +0 -65
  100. package/dist/chunk-SAF6HGAA.js +0 -57
@@ -1,12 +1,244 @@
1
+ import {
2
+ MCP_BRIDGE_BINARY
3
+ } from "./chunk-YYINL576.js";
1
4
  import {
2
5
  openCodeConfigDirFrom
3
- } from "./chunk-STOMNX4S.js";
6
+ } from "./chunk-B3YQANFN.js";
4
7
  import {
5
8
  codexHomeFrom
6
- } from "./chunk-3BJQYXTL.js";
9
+ } from "./chunk-DNJUSFBL.js";
7
10
  import {
8
11
  HOOK_COMPATIBLE_PROFILES
9
- } from "./chunk-GHOB245Q.js";
12
+ } from "./chunk-YROGUPOZ.js";
13
+
14
+ // src/fx-config.ts
15
+ var FX_SERVER_NAME = "pushary";
16
+ var FX_OPERATION_TIMEOUT_MS = 75e3;
17
+ var FX_PROFILE_MAX_BYTES = 1024 * 1024 - 1;
18
+ var FX_SETTINGS_MAX_BYTES = 64 * 1024;
19
+ 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.';
21
+ var ALLOW_ACTION = "allow";
22
+ var PERMISSION_KEY = "permission";
23
+ var WORKSPACES_KEY = "workspaces";
24
+ var CANONICAL_SERVERS_KEY = "mcp";
25
+ var ALIAS_SERVERS_KEY = "mcpServers";
26
+ var LOCAL_TRANSPORT = "local";
27
+ var SERVER_TRANSPORTS = /* @__PURE__ */ new Set(["local", "stdio", "http", "sse"]);
28
+ var STDIO_TRANSPORTS = /* @__PURE__ */ new Set(["local", "stdio"]);
29
+ var AMBIGUOUS_SERVER_KEYS = /* @__PURE__ */ new Set([
30
+ "mcpserver",
31
+ "mcpservers",
32
+ "modelcontextprotocol",
33
+ "modelcontextprotocolserver",
34
+ "modelcontextprotocolservers",
35
+ "servers"
36
+ ]);
37
+ var PROFILE_SCAN_LIMIT = 64;
38
+ var PROFILE_KEY_BYTES_LIMIT = 128;
39
+ var FX_JSON_REFUSALS = {
40
+ "too-large": "is larger than fx reads",
41
+ "unparseable": "is not strict JSON (fx rejects comments, trailing commas and a byte-order mark)",
42
+ "duplicate-key": "repeats a key, which fx rejects",
43
+ "root-not-object": "is not a JSON object"
44
+ };
45
+ var FX_PROFILE_REFUSALS = {
46
+ ...FX_JSON_REFUSALS,
47
+ "servers-not-object": 'has an "mcp" or "mcpServers" value that is not an object',
48
+ "ambiguous-server-key": "holds servers under a key fx does not read, so fx refuses to edit it too",
49
+ "too-many-keys": "has more top-level keys than fx checks for misplaced servers, so fx refuses to edit it too"
50
+ };
51
+ var FX_SETTINGS_REFUSALS = {
52
+ ...FX_JSON_REFUSALS,
53
+ "permission-not-object": 'has a "permission" value that is not an object'
54
+ };
55
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
56
+ var stringTokenEnd = (text, start) => {
57
+ let index = start + 1;
58
+ while (index < text.length && text[index] !== '"') index += text[index] === "\\" ? 2 : 1;
59
+ return index;
60
+ };
61
+ var hasDuplicateObjectKeys = (json) => {
62
+ const scopes = [];
63
+ let expectingKey = false;
64
+ for (let index = 0; index < json.length; index += 1) {
65
+ const char = json[index];
66
+ if (char === '"') {
67
+ const end = stringTokenEnd(json, index);
68
+ const scope = scopes.at(-1);
69
+ if (expectingKey && scope) {
70
+ const key = JSON.parse(json.slice(index, end + 1));
71
+ if (typeof key === "string" && scope.has(key)) return true;
72
+ if (typeof key === "string") scope.add(key);
73
+ expectingKey = false;
74
+ }
75
+ index = end;
76
+ } else if (char === "{") {
77
+ scopes.push(/* @__PURE__ */ new Set());
78
+ expectingKey = true;
79
+ } else if (char === "[") {
80
+ scopes.push(null);
81
+ expectingKey = false;
82
+ } else if (char === "}" || char === "]") {
83
+ scopes.pop();
84
+ expectingKey = false;
85
+ } else if (char === ",") {
86
+ expectingKey = scopes.at(-1) instanceof Set;
87
+ }
88
+ }
89
+ return false;
90
+ };
91
+ var normalizedServerKey = (key) => key.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
92
+ var looksLikeServerEntry = (value) => {
93
+ const entry = asRecord(value);
94
+ if (!entry) return false;
95
+ return typeof entry.command === "string" || Array.isArray(entry.command) || typeof entry.url === "string" || typeof entry.type === "string" && SERVER_TRANSPORTS.has(entry.type);
96
+ };
97
+ var holdsServerEntries = (value) => Object.values(asRecord(value) ?? {}).some(looksLikeServerEntry);
98
+ var isServerMapKey = (key) => key === CANONICAL_SERVERS_KEY || key === ALIAS_SERVERS_KEY;
99
+ var exceedsScanLimit = (value) => Object.keys(asRecord(value) ?? {}).length > PROFILE_SCAN_LIMIT;
100
+ var isAmbiguousServerKey = (key, value) => {
101
+ if (isServerMapKey(key)) return false;
102
+ const scanned = Buffer.byteLength(key) > PROFILE_KEY_BYTES_LIMIT || AMBIGUOUS_SERVER_KEYS.has(normalizedServerKey(key));
103
+ return scanned && (exceedsScanLimit(value) || holdsServerEntries(value));
104
+ };
105
+ var misplacedServersRefusal = (profile) => {
106
+ if (exceedsScanLimit(profile)) return "too-many-keys";
107
+ return Object.keys(profile).some((key) => isAmbiguousServerKey(key, profile[key])) ? "ambiguous-server-key" : null;
108
+ };
109
+ var fxServersKeyOf = (profile) => !(CANONICAL_SERVERS_KEY in profile) && ALIAS_SERVERS_KEY in profile ? ALIAS_SERVERS_KEY : CANONICAL_SERVERS_KEY;
110
+ var refused = (reason) => ({ kind: "refused", reason });
111
+ var parseJson = (text) => {
112
+ try {
113
+ return { ok: true, value: JSON.parse(text) };
114
+ } catch {
115
+ return { ok: false };
116
+ }
117
+ };
118
+ var readFxJsonObject = (text, maxBytes) => {
119
+ if (Buffer.byteLength(text) > maxBytes) return { kind: "refused", reason: "too-large" };
120
+ if (!text.trim()) return { kind: "object", value: {} };
121
+ const parsed = parseJson(text);
122
+ if (!parsed.ok) return { kind: "refused", reason: "unparseable" };
123
+ if (hasDuplicateObjectKeys(text)) return { kind: "refused", reason: "duplicate-key" };
124
+ const value = asRecord(parsed.value);
125
+ return value ? { kind: "object", value } : { kind: "refused", reason: "root-not-object" };
126
+ };
127
+ var readFxProfile = (text) => {
128
+ const read = readFxJsonObject(text, FX_PROFILE_MAX_BYTES);
129
+ if (read.kind === "refused") return refused(read.reason);
130
+ const profile = read.value;
131
+ const serversKey = fxServersKeyOf(profile);
132
+ if (serversKey in profile && !asRecord(profile[serversKey])) return refused("servers-not-object");
133
+ const misplaced = misplacedServersRefusal(profile);
134
+ return misplaced ? refused(misplaced) : { kind: "profile", profile };
135
+ };
136
+ var fxPusharyEntry = (command) => ({
137
+ type: LOCAL_TRANSPORT,
138
+ command,
139
+ operation_timeout_ms: FX_OPERATION_TIMEOUT_MS
140
+ });
141
+ var withFxPusharyServer = (profile, entry) => {
142
+ const serversKey = fxServersKeyOf(profile);
143
+ return {
144
+ ...profile,
145
+ [serversKey]: { ...asRecord(profile[serversKey]), [FX_SERVER_NAME]: { ...entry, command: [...entry.command] } }
146
+ };
147
+ };
148
+ var emptiedMapCanGo = (profile, serversKey) => serversKey === ALIAS_SERVERS_KEY || !(ALIAS_SERVERS_KEY in profile);
149
+ var withoutFxPusharyServer = (profile) => {
150
+ const serversKey = fxServersKeyOf(profile);
151
+ const servers = asRecord(profile[serversKey]);
152
+ if (!servers || !(FX_SERVER_NAME in servers)) return { profile, removed: false };
153
+ const remaining = Object.fromEntries(Object.entries(servers).filter(([name]) => name !== FX_SERVER_NAME));
154
+ const withoutServers = Object.fromEntries(Object.entries(profile).filter(([key]) => key !== serversKey));
155
+ const dropsMap = Object.keys(remaining).length === 0 && emptiedMapCanGo(profile, serversKey);
156
+ return {
157
+ profile: dropsMap ? withoutServers : { ...profile, [serversKey]: remaining },
158
+ removed: true
159
+ };
160
+ };
161
+ var namesBridge = (part) => typeof part === "string" && part.includes(MCP_BRIDGE_BINARY);
162
+ var commandNamesBridge = (command) => Array.isArray(command) ? command.some(namesBridge) : namesBridge(command);
163
+ var isFxPusharyEntry = (value) => {
164
+ const entry = asRecord(value);
165
+ if (!entry || entry.enabled === false) return false;
166
+ const transport = typeof entry.type === "string" ? entry.type : LOCAL_TRANSPORT;
167
+ return STDIO_TRANSPORTS.has(transport) && (commandNamesBridge(entry.command) || commandNamesBridge(entry.args));
168
+ };
169
+ var fxPusharyEntryOf = (profile) => asRecord(profile[fxServersKeyOf(profile)])?.[FX_SERVER_NAME];
170
+ var isBridgeTarget = (part) => namesBridge(part);
171
+ var fxBridgeTargetOf = (value) => {
172
+ const entry = asRecord(value);
173
+ const parts = [entry?.command, entry?.args].flatMap((part) => Array.isArray(part) ? part : [part]);
174
+ return parts.find(isBridgeTarget);
175
+ };
176
+ var fxFileText = (value, maxBytes) => {
177
+ const text = JSON.stringify(value);
178
+ return Buffer.byteLength(text) > maxBytes ? { kind: "refused", reason: "too-large" } : { kind: "text", text };
179
+ };
180
+ var fxProfileText = (profile) => fxFileText(profile, FX_PROFILE_MAX_BYTES);
181
+ var fxSettingsText = (settings) => fxFileText(settings, FX_SETTINGS_MAX_BYTES);
182
+ var readFxSettings = (text) => {
183
+ const read = readFxJsonObject(text, FX_SETTINGS_MAX_BYTES);
184
+ if (read.kind === "refused") return read;
185
+ const permission = read.value[PERMISSION_KEY];
186
+ if (permission !== void 0 && !asRecord(permission)) return { kind: "refused", reason: "permission-not-object" };
187
+ return { kind: "settings", settings: read.value };
188
+ };
189
+ var withPusharyToolsAllowed = (settings) => {
190
+ const permission = asRecord(settings[PERMISSION_KEY]) ?? {};
191
+ const existing = permission[FX_PUSHARY_TOOLS_RULE];
192
+ if (existing === ALLOW_ACTION) return { settings, change: "present" };
193
+ if (existing !== void 0) return { settings, change: "user-rule-kept" };
194
+ return {
195
+ settings: { ...settings, [PERMISSION_KEY]: { ...permission, [FX_PUSHARY_TOOLS_RULE]: ALLOW_ACTION } },
196
+ change: "added"
197
+ };
198
+ };
199
+ var withoutPusharyToolsAllowed = (settings) => {
200
+ 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
+ };
208
+ };
209
+ var pusharyToolsAllowed = (settings) => asRecord(settings[PERMISSION_KEY])?.[FX_PUSHARY_TOOLS_RULE] === ALLOW_ACTION;
210
+ var overridesPusharyRule = (workspace) => {
211
+ const permission = asRecord(asRecord(workspace)?.[PERMISSION_KEY]);
212
+ return permission !== void 0 && !(FX_PUSHARY_TOOLS_RULE in permission);
213
+ };
214
+ var workspacesOverridingPusharyRule = (settings) => Object.entries(asRecord(settings[WORKSPACES_KEY]) ?? {}).filter(([, workspace]) => overridesPusharyRule(workspace)).map(([path]) => path);
215
+
216
+ // src/fx-files.ts
217
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "fs";
218
+ import { relative, isAbsolute } from "path";
219
+ var readFxFile = (path, read) => {
220
+ if (!existsSync(path)) return { kind: "missing" };
221
+ let text;
222
+ try {
223
+ text = readFileSync(path, "utf-8");
224
+ } catch {
225
+ return { kind: "unreadable" };
226
+ }
227
+ return read(text);
228
+ };
229
+ var isInside = (child, parent) => {
230
+ const path = relative(parent, child);
231
+ return path !== "" && !path.startsWith("..") && !isAbsolute(path);
232
+ };
233
+ var fxIgnoresInstructionsFile = (path, fxDir) => {
234
+ const link = lstatSync(path, { throwIfNoEntry: false });
235
+ if (!link?.isSymbolicLink()) return false;
236
+ try {
237
+ return !isInside(realpathSync(path), realpathSync(fxDir));
238
+ } catch {
239
+ return true;
240
+ }
241
+ };
10
242
 
11
243
  // src/vscode-config.ts
12
244
  import { homedir } from "os";
@@ -54,12 +286,12 @@ var blankJsonComments = (text) => {
54
286
  }
55
287
  return out;
56
288
  };
57
- var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
289
+ var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
58
290
  var parseJsonc = (text) => {
59
291
  const blanked = blankJsonComments(text);
60
292
  const withoutTrailingCommas = blanked.replace(/,(\s*[}\]])/g, "$1");
61
293
  try {
62
- return asRecord(JSON.parse(withoutTrailingCommas));
294
+ return asRecord2(JSON.parse(withoutTrailingCommas));
63
295
  } catch {
64
296
  return void 0;
65
297
  }
@@ -77,9 +309,9 @@ var registerPluginLocation = (existing, pluginDir) => {
77
309
  }
78
310
  const indent = detectIndent(existing);
79
311
  try {
80
- const parsed2 = asRecord(JSON.parse(existing));
312
+ const parsed2 = asRecord2(JSON.parse(existing));
81
313
  if (parsed2) {
82
- const locations2 = asRecord(parsed2[PLUGIN_LOCATIONS_KEY]) ?? {};
314
+ const locations2 = asRecord2(parsed2[PLUGIN_LOCATIONS_KEY]) ?? {};
83
315
  if (locations2[pluginDir] === true) return { kind: "already" };
84
316
  parsed2[PLUGIN_LOCATIONS_KEY] = { ...locations2, [pluginDir]: true };
85
317
  return { kind: "merged", content: `${JSON.stringify(parsed2, null, indent)}
@@ -89,7 +321,7 @@ var registerPluginLocation = (existing, pluginDir) => {
89
321
  }
90
322
  const parsed = parseJsonc(existing);
91
323
  if (!parsed) return { kind: "manual" };
92
- const locations = asRecord(parsed[PLUGIN_LOCATIONS_KEY]);
324
+ const locations = asRecord2(parsed[PLUGIN_LOCATIONS_KEY]);
93
325
  if (locations?.[pluginDir] === true) return { kind: "already" };
94
326
  if (parsed[PLUGIN_LOCATIONS_KEY] !== void 0) return { kind: "manual" };
95
327
  const brace = rootBraceIndex(existing);
@@ -106,9 +338,9 @@ var unregisterPluginLocation = (existing, pluginDir) => {
106
338
  if (existing === null || existing.trim() === "") return { kind: "absent" };
107
339
  const indent = detectIndent(existing);
108
340
  try {
109
- const parsed2 = asRecord(JSON.parse(existing));
341
+ const parsed2 = asRecord2(JSON.parse(existing));
110
342
  if (parsed2) {
111
- const locations2 = asRecord(parsed2[PLUGIN_LOCATIONS_KEY]);
343
+ const locations2 = asRecord2(parsed2[PLUGIN_LOCATIONS_KEY]);
112
344
  if (!locations2 || !(pluginDir in locations2)) return { kind: "absent" };
113
345
  const remaining = { ...locations2 };
114
346
  delete remaining[pluginDir];
@@ -121,7 +353,7 @@ var unregisterPluginLocation = (existing, pluginDir) => {
121
353
  }
122
354
  const parsed = parseJsonc(existing);
123
355
  if (!parsed) return { kind: "manual" };
124
- const locations = asRecord(parsed[PLUGIN_LOCATIONS_KEY]);
356
+ const locations = asRecord2(parsed[PLUGIN_LOCATIONS_KEY]);
125
357
  if (!locations || !(pluginDir in locations)) return { kind: "absent" };
126
358
  const line = new RegExp(
127
359
  `\\n[ \\t]*${escapeRegExp(JSON.stringify(PLUGIN_LOCATIONS_KEY))}\\s*:\\s*\\{\\s*${escapeRegExp(JSON.stringify(pluginDir))}\\s*:\\s*true\\s*\\},?`
@@ -133,7 +365,7 @@ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
365
  var isPluginRegistered = (existing, pluginDir) => {
134
366
  if (!existing) return false;
135
367
  const parsed = parseJsonc(existing);
136
- return asRecord(parsed?.[PLUGIN_LOCATIONS_KEY])?.[pluginDir] === true;
368
+ return asRecord2(parsed?.[PLUGIN_LOCATIONS_KEY])?.[pluginDir] === true;
137
369
  };
138
370
  var pluginLocationSnippet = (pluginDir) => `"${PLUGIN_LOCATIONS_KEY}": {
139
371
  ${JSON.stringify(pluginDir)}: true
@@ -156,7 +388,7 @@ var vscodeSettingsTargets = (exists, home, platform) => {
156
388
 
157
389
  // src/setup/detect.ts
158
390
  import { execSync } from "child_process";
159
- import { existsSync } from "fs";
391
+ import { existsSync as existsSync2 } from "fs";
160
392
  import { homedir as homedir2 } from "os";
161
393
  import { dirname, join as join2 } from "path";
162
394
  var whichCommand = () => process.platform === "win32" ? "where" : "which";
@@ -168,7 +400,7 @@ var binaryOnPath = (binary) => {
168
400
  return false;
169
401
  }
170
402
  };
171
- var defaultDeps = { onPath: binaryOnPath, exists: existsSync };
403
+ var defaultDeps = { onPath: binaryOnPath, exists: existsSync2 };
172
404
  var detectAgent = (probe, deps = defaultDeps) => {
173
405
  const checked = [];
174
406
  if (probe.binary) {
@@ -237,6 +469,11 @@ var agentProbes = (home = homedir2()) => ({
237
469
  // never used.
238
470
  configMarkers: [openCodeConfigDirFrom(home)]
239
471
  },
472
+ fx: {
473
+ binary: null,
474
+ paths: [join2(home, ".fx", "settings.json"), join2(home, ".fx", "sessions"), join2(home, ".fx", "history.jsonl")],
475
+ configMarkers: []
476
+ },
240
477
  ...compatibleProbes(),
241
478
  // "Other" is a set of printed instructions for any MCP or HTTP client. There is
242
479
  // nothing to detect, and it must never be auto-selected.
@@ -266,7 +503,7 @@ var describeDetection = (result, home) => {
266
503
  };
267
504
 
268
505
  // src/instruction-file.ts
269
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
506
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync, rmSync } from "fs";
270
507
  import { dirname as dirname2 } from "path";
271
508
  var PUSHARY_BEGIN = "<!-- BEGIN PUSHARY (managed by @pushary/agent-hooks; do not edit this block) -->";
272
509
  var PUSHARY_END = "<!-- END PUSHARY -->";
@@ -296,10 +533,10 @@ ${body}
296
533
  ${PUSHARY_END}`;
297
534
  var writeInstructionBlock = (filePath, body) => {
298
535
  const dir = dirname2(filePath);
299
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
536
+ if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
300
537
  let existing = "";
301
538
  try {
302
- existing = readFileSync(filePath, "utf-8");
539
+ existing = readFileSync2(filePath, "utf-8");
303
540
  } catch {
304
541
  }
305
542
  const block = wrap(body);
@@ -317,7 +554,7 @@ var writeInstructionBlock = (filePath, body) => {
317
554
  var removeInstructionBlock = (filePath) => {
318
555
  let existing = "";
319
556
  try {
320
- existing = readFileSync(filePath, "utf-8");
557
+ existing = readFileSync2(filePath, "utf-8");
321
558
  } catch {
322
559
  return false;
323
560
  }
@@ -334,13 +571,33 @@ var removeInstructionBlock = (filePath) => {
334
571
  };
335
572
  var hasInstructionBlock = (filePath) => {
336
573
  try {
337
- return readFileSync(filePath, "utf-8").includes(PUSHARY_BEGIN);
574
+ return readFileSync2(filePath, "utf-8").includes(PUSHARY_BEGIN);
338
575
  } catch {
339
576
  return false;
340
577
  }
341
578
  };
342
579
 
343
580
  export {
581
+ FX_PUSHARY_TOOLS_RULE,
582
+ FX_TOOL_DISCOVERY_NOTE,
583
+ FX_PROFILE_REFUSALS,
584
+ FX_SETTINGS_REFUSALS,
585
+ readFxProfile,
586
+ fxPusharyEntry,
587
+ withFxPusharyServer,
588
+ withoutFxPusharyServer,
589
+ isFxPusharyEntry,
590
+ fxPusharyEntryOf,
591
+ fxBridgeTargetOf,
592
+ fxProfileText,
593
+ fxSettingsText,
594
+ readFxSettings,
595
+ withPusharyToolsAllowed,
596
+ withoutPusharyToolsAllowed,
597
+ pusharyToolsAllowed,
598
+ workspacesOverridingPusharyRule,
599
+ readFxFile,
600
+ fxIgnoresInstructionsFile,
344
601
  registerPluginLocation,
345
602
  unregisterPluginLocation,
346
603
  isPluginRegistered,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-3BJQYXTL.js";
3
+ } from "./chunk-DNJUSFBL.js";
4
4
  import {
5
5
  readJsonSafe
6
6
  } from "./chunk-T7E75EKA.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseVersion
3
- } from "./chunk-57DDP4TB.js";
3
+ } from "./chunk-ZQ7IDOKY.js";
4
4
 
5
5
  // src/setup/agent-probe.ts
6
6
  import { execSync } from "child_process";
@@ -0,0 +1,12 @@
1
+ // src/cli/env-switch.ts
2
+ var CLIENT_REPORT_DISABLE_ENV = "PUSHARY_DISABLE_CLIENT_REPORTS";
3
+ var NOT_DISABLED_VALUES = /* @__PURE__ */ new Set(["", "0"]);
4
+ var isDisabledByEnv = (env, name) => {
5
+ const value = env[name];
6
+ return value !== void 0 && !NOT_DISABLED_VALUES.has(value.trim());
7
+ };
8
+
9
+ export {
10
+ CLIENT_REPORT_DISABLE_ENV,
11
+ isDisabledByEnv
12
+ };
@@ -2,17 +2,17 @@ import {
2
2
  CLAUDE_HOOK_EVENTS,
3
3
  GEMINI_HOOK_EVENTS,
4
4
  hasGeminiHooks
5
- } from "./chunk-57DDP4TB.js";
5
+ } from "./chunk-ZQ7IDOKY.js";
6
6
  import {
7
7
  CODEX_HOOK_EVENTS,
8
8
  hasCodexHooks,
9
9
  holdBudgetOf
10
- } from "./chunk-3BJQYXTL.js";
10
+ } from "./chunk-DNJUSFBL.js";
11
11
  import {
12
12
  CURSOR_GATE_EVENTS,
13
13
  HOOK_LOCATOR_BINARY,
14
14
  hookCommandHoldBudget
15
- } from "./chunk-GHOB245Q.js";
15
+ } from "./chunk-YROGUPOZ.js";
16
16
 
17
17
  // src/diagnostics/wiring.ts
18
18
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  describeKeyCheck,
3
3
  keyCheckFromResponse
4
- } from "./chunk-TZ4KUNZG.js";
4
+ } from "./chunk-X6KDKOV5.js";
5
5
  import {
6
6
  createIo
7
7
  } from "./chunk-5RUIFDTP.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-3EGEA4KH.js";
10
10
  import {
11
11
  getBaseUrl
12
- } from "./chunk-OSJEMXSR.js";
12
+ } from "./chunk-V4AQBAVG.js";
13
13
 
14
14
  // src/cli/login-ui.ts
15
15
  import { hostname } from "os";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  AGENT_CHOICES
3
- } from "./chunk-HCVAMXHT.js";
3
+ } from "./chunk-CVALVGA7.js";
4
4
  import {
5
5
  isBridgeLocator
6
- } from "./chunk-GQPCPEG2.js";
6
+ } from "./chunk-5HLTWFJ6.js";
7
7
  import {
8
8
  pusharyHookExecutable
9
- } from "./chunk-SND3C2AV.js";
9
+ } from "./chunk-PQ4F64M2.js";
10
10
  import {
11
11
  BELL_HOOK_BINARY
12
- } from "./chunk-VDMX5EMU.js";
12
+ } from "./chunk-VKVIAZQO.js";
13
13
 
14
14
  // src/setup/hook-ownership.ts
15
15
  var commandsIn = (value) => {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  configFilePath
3
- } from "./chunk-OSJEMXSR.js";
3
+ } from "./chunk-V4AQBAVG.js";
4
4
  import {
5
5
  normalizeInstallSource
6
- } from "./chunk-GHOB245Q.js";
6
+ } from "./chunk-YROGUPOZ.js";
7
7
 
8
8
  // src/install-source.ts
9
9
  import { readFileSync } from "fs";
@@ -1,3 +1,36 @@
1
+ import {
2
+ NPX_LATEST_COMMAND
3
+ } from "./chunk-B3UXMSMW.js";
4
+
5
+ // src/setup/agent-failure.ts
6
+ var AgentConfigError = class extends Error {
7
+ kind;
8
+ filePath;
9
+ constructor(kind, filePath, message) {
10
+ super(message);
11
+ this.name = "AgentConfigError";
12
+ this.kind = kind;
13
+ this.filePath = filePath;
14
+ }
15
+ };
16
+ var configFailureKind = (readFailure) => readFailure === "unparseable" ? "config-unparseable" : "config-unreadable";
17
+ var AGENT_FAILURE_DETAILS = {
18
+ "not-wired": "reported success but its config is not there.",
19
+ "app-account-mismatch": "Mac app hooks kept, but their Pushary account could not be matched to this setup."
20
+ };
21
+ var describedAgentFailure = (agent, kind) => ({
22
+ agent,
23
+ kind,
24
+ detail: AGENT_FAILURE_DETAILS[kind]
25
+ });
26
+ var agentFailureFromError = (agent, error, describeError) => ({
27
+ agent,
28
+ kind: error instanceof AgentConfigError ? error.kind : "installer-error",
29
+ detail: describeError(error)
30
+ });
31
+ var retryAgentCommand = (agent) => `${NPX_LATEST_COMMAND} setup --agents ${agent}`;
32
+ var agentFailureWire = (failures) => failures.map(({ agent, kind }) => ({ agent, kind }));
33
+
1
34
  // src/diagnostics/service-runtime.ts
2
35
  var unescapeXml = (value) => value.replaceAll("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
3
36
  var unquoteSystemd = (value) => value.replaceAll('\\"', '"').replaceAll("\\\\", "\\").replaceAll("%%", "%");
@@ -37,6 +70,12 @@ var versionManagerOf = (execPath) => {
37
70
  var describeVersionManagerPin = (execPath, manager) => `The background service is pinned to ${execPath}, which ${manager} manages. Switching Node versions strands it; run pushary daemon install afterwards.`;
38
71
 
39
72
  export {
73
+ AgentConfigError,
74
+ configFailureKind,
75
+ describedAgentFailure,
76
+ agentFailureFromError,
77
+ retryAgentCommand,
78
+ agentFailureWire,
40
79
  readServiceExecPath,
41
80
  versionManagerOf,
42
81
  describeVersionManagerPin
@@ -3,11 +3,11 @@ import {
3
3
  decodePublicKey,
4
4
  generateSessionKey,
5
5
  wrapSessionKey
6
- } from "./chunk-KNIIFNNQ.js";
6
+ } from "./chunk-BH2SKSSK.js";
7
7
  import {
8
8
  fetchTranscriptRecipients,
9
9
  uploadTranscriptRecords
10
- } from "./chunk-66H2QQAK.js";
10
+ } from "./chunk-KXMZMZD3.js";
11
11
 
12
12
  // src/live-session.ts
13
13
  var MAX_TRANSCRIPT_QUEUE = 100;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  registerTranscript
3
- } from "./chunk-LDS4ERW4.js";
3
+ } from "./chunk-EAQMWJPG.js";
4
4
  import {
5
- degradedLine
6
- } from "./chunk-GHOB245Q.js";
5
+ writeDegraded
6
+ } from "./chunk-V6QRSU2G.js";
7
7
 
8
8
  // src/tool-hook-input.ts
9
9
  var OPTIONAL_TEXT_FIELDS = ["hook_event_name", "session_id", "cwd", "transcript_path", "tool_use_id", "permission_mode"];
@@ -63,12 +63,11 @@ var matches = (rule, source, event) => {
63
63
  var isAdmitted = (rules, source, event) => !rules.disabled.some((rule) => matches(rule, source, event));
64
64
  var readHookInput = async (source, fallbackEvent = "", stdin = process.stdin, parseInput) => {
65
65
  if (source === null) {
66
- process.stderr.write(`${degradedLine("hook", {
66
+ writeDegraded("hook", {
67
67
  reason: "unknown_agent",
68
68
  fallback: "stood aside, so nothing is gated or reported for this call",
69
69
  event: fallbackEvent
70
- })}
71
- `);
70
+ });
72
71
  return null;
73
72
  }
74
73
  let raw = "";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveInstallSourceFor
3
- } from "./chunk-NPGOQQCV.js";
3
+ } from "./chunk-ROQK7IN3.js";
4
4
  import {
5
5
  backupFile,
6
6
  readJsonSafe,
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-T7E75EKA.js";
10
10
  import {
11
11
  configFilePath
12
- } from "./chunk-OSJEMXSR.js";
12
+ } from "./chunk-V4AQBAVG.js";
13
13
 
14
14
  // src/shell-alias.ts
15
15
  import { homedir } from "os";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NPX_LATEST_COMMAND
3
- } from "./chunk-EA3IELHP.js";
3
+ } from "./chunk-B3UXMSMW.js";
4
4
 
5
5
  // src/config.ts
6
6
  import { readFileSync } from "fs";