@pushary/agent-hooks 1.3.0 → 1.4.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 (90) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/data/SKILL.md +3 -3
  4. package/data/cursor-plugin/skills/pushary/SKILL.md +2 -2
  5. package/data/vscode-plugin/skills/pushary/SKILL.md +2 -2
  6. package/dist/bin/pushary-bell-hook.js +20 -20
  7. package/dist/bin/pushary-bell.js +9 -9
  8. package/dist/bin/pushary-claude.js +20 -20
  9. package/dist/bin/pushary-clean.js +472 -94
  10. package/dist/bin/pushary-codex-bridge.js +11 -11
  11. package/dist/bin/pushary-codex-hook.js +30 -28
  12. package/dist/bin/pushary-codex.js +7 -7
  13. package/dist/bin/pushary-connect.js +9 -9
  14. package/dist/bin/pushary-cowork.js +5 -5
  15. package/dist/bin/pushary-daemon.js +26 -26
  16. package/dist/bin/pushary-disconnect.js +13 -13
  17. package/dist/bin/pushary-doctor.js +86 -24
  18. package/dist/bin/pushary-elicitation-hook.js +19 -19
  19. package/dist/bin/pushary-gemini-bridge.js +10 -10
  20. package/dist/bin/pushary-gemini-hook.js +21 -21
  21. package/dist/bin/pushary-hook.js +34 -44
  22. package/dist/bin/pushary-login.js +10 -10
  23. package/dist/bin/pushary-logout.js +7 -7
  24. package/dist/bin/pushary-mode.js +5 -5
  25. package/dist/bin/pushary-notification-hook.js +22 -22
  26. package/dist/bin/pushary-opencode-hook.js +21 -21
  27. package/dist/bin/pushary-permission-denied-hook.js +26 -26
  28. package/dist/bin/pushary-permission-hook.js +32 -31
  29. package/dist/bin/pushary-post-hook.js +22 -22
  30. package/dist/bin/pushary-prompt-hook.js +22 -22
  31. package/dist/bin/pushary-session-end-hook.js +22 -22
  32. package/dist/bin/pushary-session-start-hook.js +24 -24
  33. package/dist/bin/pushary-setup.js +39 -60
  34. package/dist/bin/pushary-stats.js +4 -4
  35. package/dist/bin/pushary-status.js +13 -13
  36. package/dist/bin/pushary-stop-hook.js +22 -22
  37. package/dist/bin/pushary-stopfailure-hook.js +22 -22
  38. package/dist/bin/pushary-suggestions.js +2 -2
  39. package/dist/bin/pushary-transcript-register.js +18 -18
  40. package/dist/bin/pushary-transcripts.js +8 -8
  41. package/dist/bin/pushary-upgrade.js +17 -17
  42. package/dist/bin/pushary-wait.js +5 -5
  43. package/dist/bin/pushary.js +1 -1
  44. package/dist/{chunk-3AIFWTVB.js → chunk-2TOTCUJC.js} +2 -2
  45. package/dist/{chunk-X3KIGNIP.js → chunk-43Z3RLS5.js} +83 -24
  46. package/dist/{chunk-YF4JDCU4.js → chunk-5F5QQMCA.js} +1 -1
  47. package/dist/{chunk-ZHL62DRA.js → chunk-5IBVIMWT.js} +3 -3
  48. package/dist/{chunk-BYGEZI7B.js → chunk-5MSBI6NP.js} +1 -1
  49. package/dist/{chunk-WPJS3OZH.js → chunk-7IEB2GV4.js} +100 -9
  50. package/dist/{chunk-2PLEJ7SY.js → chunk-7PQZYCEP.js} +1 -1
  51. package/dist/{chunk-OE6U2AXM.js → chunk-A7XIL2WD.js} +3 -3
  52. package/dist/{chunk-A2URMNFM.js → chunk-BYAXXBNO.js} +1 -1
  53. package/dist/{chunk-DON24PNT.js → chunk-D4TNSUDY.js} +1 -1
  54. package/dist/{chunk-EU6JH7KU.js → chunk-ESLLVHDY.js} +8 -8
  55. package/dist/{chunk-LZQVAEWP.js → chunk-FAH3D74E.js} +2 -1
  56. package/dist/{chunk-MU275U5A.js → chunk-GIXLZFEY.js} +2 -2
  57. package/dist/{chunk-4V24U44H.js → chunk-GLPWCQ6V.js} +2 -2
  58. package/dist/{chunk-BL4SQNO6.js → chunk-GS5EI2YL.js} +1 -1
  59. package/dist/{chunk-EKQAKSGI.js → chunk-H25UB3CG.js} +1 -1
  60. package/dist/{chunk-BNDHXPTQ.js → chunk-HGN6VZTY.js} +20 -13
  61. package/dist/{chunk-WXXDHGTZ.js → chunk-IF46POXX.js} +5 -4
  62. package/dist/{chunk-A5ZWYQFV.js → chunk-JCWCTBYH.js} +4 -4
  63. package/dist/{chunk-IAEVVUXO.js → chunk-KZIXHLPQ.js} +1 -1
  64. package/dist/{chunk-FGVNU7XD.js → chunk-NBCTKC4Z.js} +34 -3
  65. package/dist/{chunk-XBO42HKH.js → chunk-NYHNLMQO.js} +12 -3
  66. package/dist/{chunk-K4TAUYUJ.js → chunk-O5L2OJBB.js} +3 -3
  67. package/dist/{chunk-DVO4SZMJ.js → chunk-O6RN46OS.js} +3 -3
  68. package/dist/{chunk-POAFXBNH.js → chunk-P4JVVJ2D.js} +1 -1
  69. package/dist/{chunk-E7WBNGJS.js → chunk-Q4UAMWXK.js} +1 -1
  70. package/dist/{chunk-KS4P6F3R.js → chunk-RGZY77CD.js} +4 -4
  71. package/dist/{chunk-K7UIAE7I.js → chunk-RQM5SMMR.js} +2 -2
  72. package/dist/{chunk-UVXNYYHG.js → chunk-S3CLZSV3.js} +6 -6
  73. package/dist/{chunk-B3S4SF3D.js → chunk-S3EUZZJB.js} +1 -1
  74. package/dist/{chunk-XBJW4MV5.js → chunk-SGZ3IAWF.js} +1 -1
  75. package/dist/{chunk-YKLCWWEI.js → chunk-SGZJ5LYH.js} +1 -1
  76. package/dist/{chunk-4LL6LG5X.js → chunk-T7E75EKA.js} +4 -3
  77. package/dist/{chunk-GOQHN2UH.js → chunk-TWWLVOPB.js} +1 -1
  78. package/dist/{chunk-RVDHN2TJ.js → chunk-U4FB623J.js} +2 -2
  79. package/dist/{chunk-RHLUJE4H.js → chunk-UCGQEAKZ.js} +1 -1
  80. package/dist/{chunk-KPNGPRYU.js → chunk-UIN3RD2U.js} +4 -4
  81. package/dist/{chunk-XEXOHUJ6.js → chunk-UTYTPDWN.js} +4 -4
  82. package/dist/{chunk-EJZCV5G4.js → chunk-VENQSVKA.js} +1 -1
  83. package/dist/{chunk-EYMMU5HC.js → chunk-XJWMPQ6X.js} +1 -1
  84. package/dist/{chunk-JYEGOYQC.js → chunk-YYRZPEMA.js} +1 -1
  85. package/dist/{chunk-N43EQPJK.js → chunk-ZIGGEKZW.js} +41 -7
  86. package/dist/{reapply-FUCAKR6Z.js → reapply-B3S2LRMT.js} +9 -9
  87. package/dist/src/index.d.ts +20 -17
  88. package/dist/src/index.js +13 -13
  89. package/package.json +1 -1
  90. /package/dist/{chunk-KBITFQST.js → chunk-P32LPT2L.js} +0 -0
@@ -2,17 +2,19 @@
2
2
  import {
3
3
  HERMES_CONFIG_SCRIPT,
4
4
  HERMES_PIP_PACKAGE,
5
+ PUSHARY_SKILL_NAME,
6
+ PUSHARY_SKILL_SOURCE,
5
7
  createGuards,
6
8
  resolveHermesPython,
7
9
  runHermesPythonScript
8
- } from "../chunk-FGVNU7XD.js";
9
- import "../chunk-KBITFQST.js";
10
+ } from "../chunk-NBCTKC4Z.js";
11
+ import "../chunk-P32LPT2L.js";
10
12
  import {
11
13
  removeInstructionBlock,
12
14
  shortenHome,
13
15
  unregisterPluginLocation,
14
16
  vscodeSettingsTargets
15
- } from "../chunk-K4TAUYUJ.js";
17
+ } from "../chunk-O5L2OJBB.js";
16
18
  import {
17
19
  OPENCODE_PLUGIN_SIBLING,
18
20
  OPENCODE_PLUGIN_SIBLING_CONTENTS,
@@ -22,7 +24,7 @@ import {
22
24
  openCodePluginDir,
23
25
  openCodePluginFile,
24
26
  removeOpenCodeMcpServer
25
- } from "../chunk-E7WBNGJS.js";
27
+ } from "../chunk-Q4UAMWXK.js";
26
28
  import {
27
29
  compatibleConfigFile,
28
30
  compatibleInstructionsFile,
@@ -30,37 +32,40 @@ import {
30
32
  compatiblePermissionRuleFile,
31
33
  removeCompatibleHooks,
32
34
  removeCompatibleMcpServer
33
- } from "../chunk-BNDHXPTQ.js";
35
+ } from "../chunk-HGN6VZTY.js";
34
36
  import {
35
37
  isClaudeMcpSnapshot,
36
38
  removeClaudeMcpServers,
37
39
  removePusharySettings,
38
40
  removePusharyToolPermissions,
39
41
  restoreClaudeMcpServer
40
- } from "../chunk-ZHL62DRA.js";
42
+ } from "../chunk-5IBVIMWT.js";
41
43
  import {
42
44
  hookOwnerOf,
43
45
  isHookTakeoverReceipt,
44
46
  restoreHookTakeover,
45
47
  restoreMcpServer
46
- } from "../chunk-XEXOHUJ6.js";
47
- import "../chunk-BYGEZI7B.js";
48
- import "../chunk-JYEGOYQC.js";
48
+ } from "../chunk-UTYTPDWN.js";
49
+ import "../chunk-5MSBI6NP.js";
50
+ import {
51
+ liveBridgeProbe,
52
+ resolveBridgeStub
53
+ } from "../chunk-YYRZPEMA.js";
49
54
  import {
50
55
  pusharyHookExecutable
51
- } from "../chunk-DVO4SZMJ.js";
56
+ } from "../chunk-O6RN46OS.js";
52
57
  import {
53
58
  disableDaemonService
54
- } from "../chunk-KS4P6F3R.js";
59
+ } from "../chunk-RGZY77CD.js";
55
60
  import {
56
61
  removeGeminiSettings
57
- } from "../chunk-OE6U2AXM.js";
58
- import "../chunk-2PLEJ7SY.js";
62
+ } from "../chunk-A7XIL2WD.js";
63
+ import "../chunk-7PQZYCEP.js";
59
64
  import "../chunk-XHKBHWLX.js";
60
- import "../chunk-WXXDHGTZ.js";
61
- import "../chunk-IAEVVUXO.js";
65
+ import "../chunk-IF46POXX.js";
66
+ import "../chunk-KZIXHLPQ.js";
62
67
  import "../chunk-LGXUGYUH.js";
63
- import "../chunk-K7UIAE7I.js";
68
+ import "../chunk-RQM5SMMR.js";
64
69
  import {
65
70
  bridgeAgentKey,
66
71
  claudeJson,
@@ -89,41 +94,244 @@ import {
89
94
  restoreCodexHookTrust,
90
95
  vscodePluginDir,
91
96
  vscodePluginMcp
92
- } from "../chunk-RHLUJE4H.js";
97
+ } from "../chunk-UCGQEAKZ.js";
93
98
  import {
94
99
  exitOnHelpFlag
95
- } from "../chunk-GOQHN2UH.js";
96
- import "../chunk-LZQVAEWP.js";
100
+ } from "../chunk-TWWLVOPB.js";
101
+ import "../chunk-FAH3D74E.js";
97
102
  import {
98
103
  rejectUnknownFlags
99
104
  } from "../chunk-GMXKITVA.js";
100
105
  import "../chunk-DINDL2CF.js";
101
106
  import {
107
+ CANDIDATE_RCS,
108
+ isDisabledExport,
102
109
  removeClaudeAlias
103
- } from "../chunk-XBO42HKH.js";
110
+ } from "../chunk-NYHNLMQO.js";
104
111
  import "../chunk-EYRGBYZG.js";
105
112
  import "../chunk-SAF6HGAA.js";
106
113
  import "../chunk-7QLSKOSU.js";
107
114
  import {
108
115
  EXIT
109
116
  } from "../chunk-KZERVKTD.js";
110
- import "../chunk-POAFXBNH.js";
111
- import "../chunk-4LL6LG5X.js";
117
+ import "../chunk-P4JVVJ2D.js";
118
+ import "../chunk-T7E75EKA.js";
112
119
  import "../chunk-2UMNXADU.js";
113
120
  import {
114
121
  CURSOR_GATE_EVENTS,
115
122
  HOOK_COMPATIBLE_PROFILES,
116
123
  HOOK_LOCATOR_BINARY,
124
+ SUPPORT_DISCORD,
125
+ SUPPORT_EMAIL,
117
126
  isValidApiKey
118
- } from "../chunk-WPJS3OZH.js";
127
+ } from "../chunk-7IEB2GV4.js";
119
128
 
120
129
  // bin/pushary-clean.ts
121
- import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
122
- import { join } from "path";
123
- import { homedir, tmpdir } from "os";
130
+ import { existsSync as existsSync5, readFileSync as readFileSync4, copyFileSync, readdirSync as readdirSync3 } from "fs";
131
+ import { dirname, join as join5 } from "path";
132
+ import { homedir as homedir5, tmpdir } from "os";
124
133
  import { confirm } from "@inquirer/prompts";
125
134
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
126
135
 
136
+ // src/cli/mac-leftovers.ts
137
+ import { execSync } from "child_process";
138
+ import { existsSync } from "fs";
139
+ import { homedir } from "os";
140
+ import { join } from "path";
141
+ var MAC_APP_BUNDLE = "/Applications/Pushary.app";
142
+ var installedPusharyBundles = (home = homedir()) => [MAC_APP_BUNDLE, join(home, "Applications", "Pushary.app")];
143
+ var LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
144
+ var TEST_DEFAULTS_PREFIXES = [
145
+ "com.pushary.mac.tests.",
146
+ "com.pushary.tests.",
147
+ "pushary.tests.",
148
+ "pushary.quiet-mode-test.",
149
+ "pushary-optin-"
150
+ ];
151
+ var isTestDefaultsDomain = (domain) => TEST_DEFAULTS_PREFIXES.some((prefix) => domain.startsWith(prefix));
152
+ var leakedTestDefaultsDomains = () => {
153
+ try {
154
+ return execSync("defaults domains", { encoding: "utf-8", timeout: 2e4 }).split(",").map((entry) => entry.trim()).filter(isTestDefaultsDomain);
155
+ } catch {
156
+ return [];
157
+ }
158
+ };
159
+ var BUNDLE_NAME = "Pushary.app";
160
+ var DUMP_PATH_LINE = /^\s*path:\s+(.+?)(?: \(0x[0-9a-f]+\))?\s*$/;
161
+ var bundlesInLaunchServicesDump = (dump) => {
162
+ const found = /* @__PURE__ */ new Set();
163
+ for (const line of dump.split("\n")) {
164
+ const path = DUMP_PATH_LINE.exec(line)?.[1];
165
+ const bundle = path?.indexOf(`/${BUNDLE_NAME}`);
166
+ if (path === void 0 || bundle === void 0 || bundle === -1) continue;
167
+ found.add(path.slice(0, bundle + BUNDLE_NAME.length + 1));
168
+ }
169
+ return [...found];
170
+ };
171
+ var registeredPusharyBundles = (installed = installedPusharyBundles()) => {
172
+ if (!existsSync(LSREGISTER)) return [];
173
+ try {
174
+ const dump = execSync(`${LSREGISTER} -dump`, {
175
+ encoding: "utf-8",
176
+ timeout: 6e4,
177
+ maxBuffer: 256 * 1024 * 1024
178
+ });
179
+ return bundlesInLaunchServicesDump(dump).filter((path) => !installed.includes(path));
180
+ } catch {
181
+ return [];
182
+ }
183
+ };
184
+ var MAC_KEYCHAIN_SERVICE = "com.pushary.app.mac";
185
+ var MAC_KEYCHAIN_ACCOUNTS = [
186
+ "pushary.site-api-key",
187
+ "pushary.site-id",
188
+ "clerk.device-token",
189
+ "clerk.session-id"
190
+ ];
191
+ var MAC_DEFAULTS_DOMAINS = ["com.pushary.app", "pushary-bridge", "pushary.render"];
192
+ var macLibraryState = (home = homedir()) => [
193
+ join(home, "Library", "Application Support", "Pushary"),
194
+ join(home, "Library", "Caches", "com.pushary.app"),
195
+ join(home, "Library", "Caches", "com.pushary.app.sparkle"),
196
+ join(home, "Library", "Saved Application State", "com.pushary.app.savedState"),
197
+ join(home, "Library", "Caches", "pushary-bridge"),
198
+ join(home, "Library", "HTTPStorages", "com.pushary.app"),
199
+ join(home, "Library", "HTTPStorages", "com.pushary.app.binarycookies"),
200
+ join(home, "Library", "HTTPStorages", "pushary-bridge")
201
+ ];
202
+ var DUMP_SERVICE_LINE = /^\s*"svce"<blob>="(.+)"\s*$/;
203
+ var pusharyKeychainServices = (dump) => {
204
+ const found = /* @__PURE__ */ new Set();
205
+ for (const line of dump.split("\n")) {
206
+ const service = DUMP_SERVICE_LINE.exec(line)?.[1];
207
+ if (service?.startsWith(MAC_KEYCHAIN_SERVICE)) found.add(service);
208
+ }
209
+ return [...found];
210
+ };
211
+ var storedPusharyKeychainServices = () => {
212
+ try {
213
+ const dump = execSync("security dump-keychain", {
214
+ encoding: "utf-8",
215
+ timeout: 3e4,
216
+ maxBuffer: 64 * 1024 * 1024,
217
+ stdio: ["ignore", "pipe", "ignore"]
218
+ });
219
+ const stored = pusharyKeychainServices(dump);
220
+ return stored.includes(MAC_KEYCHAIN_SERVICE) ? stored : [MAC_KEYCHAIN_SERVICE, ...stored];
221
+ } catch {
222
+ return [MAC_KEYCHAIN_SERVICE];
223
+ }
224
+ };
225
+ var MAC_APP_BUNDLE_ID = "com.pushary.app";
226
+ var QUIT_MAC_APP = `osascript -e 'tell application id "${MAC_APP_BUNDLE_ID}" to quit'`;
227
+ var runningMacAppPIDs = () => {
228
+ if (process.platform !== "darwin") return [];
229
+ try {
230
+ return execSync('pgrep -f "Pushary.app/Contents/MacOS/Pushary"', {
231
+ encoding: "utf-8",
232
+ timeout: 1e4,
233
+ stdio: ["ignore", "pipe", "ignore"]
234
+ }).split("\n").map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0);
235
+ } catch {
236
+ return [];
237
+ }
238
+ };
239
+ var waitForMacAppExit = (attempts = 20, everyMs = 250) => {
240
+ const idle = new Int32Array(new SharedArrayBuffer(4));
241
+ for (let attempt = 0; attempt < attempts; attempt++) {
242
+ if (runningMacAppPIDs().length === 0) return true;
243
+ Atomics.wait(idle, 0, 0, everyMs);
244
+ }
245
+ return runningMacAppPIDs().length === 0;
246
+ };
247
+
248
+ // src/cli/editor-extension.ts
249
+ import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
250
+ import { homedir as homedir2 } from "os";
251
+ import { join as join2 } from "path";
252
+ var EDITOR_EXTENSION_ID = "pushary.pushary";
253
+ var EDITOR_EXTENSION_DIRS = [
254
+ join2(".cursor", "extensions"),
255
+ join2(".vscode", "extensions")
256
+ ];
257
+ var EXTENSIONS_JSON = "extensions.json";
258
+ var isOurEntry = (entry) => {
259
+ if (typeof entry !== "object" || entry === null) return false;
260
+ const identifier = entry.identifier;
261
+ if (typeof identifier !== "object" || identifier === null) return false;
262
+ const id = identifier.id;
263
+ return typeof id === "string" && id.toLowerCase() === EDITOR_EXTENSION_ID;
264
+ };
265
+ var recordWithoutUs = (directory) => {
266
+ const path = join2(directory, EXTENSIONS_JSON);
267
+ try {
268
+ const entries = JSON.parse(readFileSync(path, "utf-8"));
269
+ if (!Array.isArray(entries)) return void 0;
270
+ const kept = entries.filter((entry) => !isOurEntry(entry));
271
+ return kept.length === entries.length ? void 0 : { path, contents: JSON.stringify(kept) };
272
+ } catch {
273
+ return void 0;
274
+ }
275
+ };
276
+ var editorExtensionRemovals = (home = homedir2()) => EDITOR_EXTENSION_DIRS.map((relative) => join2(home, relative)).filter(existsSync2).map((directory) => {
277
+ const folders = readdirSync(directory).filter((name) => name.startsWith(`${EDITOR_EXTENSION_ID}-`)).map((name) => join2(directory, name));
278
+ return { directory, folders, record: recordWithoutUs(directory) };
279
+ }).filter((removal) => removal.folders.length > 0 || removal.record !== void 0);
280
+
281
+ // src/cli/skills-store.ts
282
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
283
+ import { homedir as homedir3 } from "os";
284
+ import { join as join3 } from "path";
285
+ var SKILLS_STORE_DIR = ".agents";
286
+ var LOCK_FILE = ".skill-lock.json";
287
+ var lockWithoutUs = (path) => {
288
+ try {
289
+ const lock = JSON.parse(readFileSync2(path, "utf-8"));
290
+ const entry = lock.skills?.[PUSHARY_SKILL_NAME];
291
+ if (entry?.source !== PUSHARY_SKILL_SOURCE) return void 0;
292
+ const { [PUSHARY_SKILL_NAME]: _ours, ...skills } = lock.skills ?? {};
293
+ return { path, contents: `${JSON.stringify({ ...lock, skills }, null, 2)}
294
+ ` };
295
+ } catch {
296
+ return void 0;
297
+ }
298
+ };
299
+ var skillsStoreRemoval = (home = homedir3()) => {
300
+ const store = join3(home, SKILLS_STORE_DIR);
301
+ const skillDir = join3(store, "skills", PUSHARY_SKILL_NAME);
302
+ return {
303
+ skillDir: existsSync3(skillDir) ? skillDir : void 0,
304
+ lock: lockWithoutUs(join3(store, LOCK_FILE))
305
+ };
306
+ };
307
+
308
+ // src/cli/claude-backups.ts
309
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
310
+ import { homedir as homedir4 } from "os";
311
+ import { join as join4 } from "path";
312
+ var BACKUP_PREFIX = ".claude.json.backup";
313
+ var claudeJsonBackups = (home = homedir4()) => {
314
+ const rollingDir = join4(home, ".claude", "backups");
315
+ const legacy = join4(home, BACKUP_PREFIX);
316
+ const rolling = existsSync4(rollingDir) ? readdirSync2(rollingDir).filter((name) => name.startsWith(BACKUP_PREFIX)).sort().map((name) => join4(rollingDir, name)) : [];
317
+ return existsSync4(legacy) ? [...rolling, legacy] : rolling;
318
+ };
319
+ var parsed = (path) => {
320
+ try {
321
+ const value = JSON.parse(readFileSync3(path, "utf-8"));
322
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
323
+ } catch {
324
+ return null;
325
+ }
326
+ };
327
+ var claudeBackupRewrites = (receipt, home = homedir4()) => claudeJsonBackups(home).flatMap((path) => {
328
+ const backup = parsed(path);
329
+ if (!backup) return [];
330
+ const changed = receipt ? restoreClaudeMcpServer(backup, receipt) : removeClaudeMcpServers(backup);
331
+ return changed ? [{ path, contents: `${JSON.stringify(backup, null, 2)}
332
+ ` }] : [];
333
+ });
334
+
127
335
  // src/app-owned-paths.ts
128
336
  var APP_OWNED_ENTRIES = [
129
337
  // PusharyPaths.bin: bridgeStub, bridgeCache
@@ -176,10 +384,15 @@ var CURSOR_PLUGIN_DIR = cursorPluginDir();
176
384
  var CURSOR_USER_HOOKS = cursorUserHooks();
177
385
  var VSCODE_PLUGIN_DIR = vscodePluginDir();
178
386
  var PUSHARY_DIR = pusharyDir();
179
- var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
180
- var macAppInstalled = () => existsSync(join(PUSHARY_DIR, "bin", HOOK_LOCATOR_BINARY));
181
- var removalScope = (keepMcpServer) => ({
182
- keepAppWiring: true,
387
+ var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join5(homedir5(), f));
388
+ var PRESENCE_BY_RESOLUTION = {
389
+ bundle: "present",
390
+ missing: "gone",
391
+ "no-stub": "absent"
392
+ };
393
+ var macAppPresence = () => PRESENCE_BY_RESOLUTION[resolveBridgeStub(join5(PUSHARY_DIR, "bin", HOOK_LOCATOR_BINARY), liveBridgeProbe()).kind];
394
+ var removalScope = (keepMcpServer, keepAppWiring) => ({
395
+ keepAppWiring,
183
396
  keepMcpServer
184
397
  });
185
398
  var codexMcpKey = (config) => {
@@ -188,7 +401,7 @@ var codexMcpKey = (config) => {
188
401
  };
189
402
  var readJson = (path) => {
190
403
  try {
191
- return JSON.parse(readFileSync(path, "utf-8"));
404
+ return JSON.parse(readFileSync4(path, "utf-8"));
192
405
  } catch {
193
406
  return null;
194
407
  }
@@ -202,6 +415,78 @@ var guards = createGuards(() => dryRun, wouldNote);
202
415
  var guardedWrite = (path, contents) => guards.write(path, contents);
203
416
  var guardedRemove = (path, options) => guards.remove(path, options);
204
417
  var guardedExec = (command, options, describe) => guards.exec(command, options ?? {}, describe);
418
+ var removeDeveloperLeftovers = () => {
419
+ if (process.platform !== "darwin") return 0;
420
+ let removed = 0;
421
+ for (const domain of leakedTestDefaultsDomains()) {
422
+ const command = `defaults delete ${JSON.stringify(domain)}`;
423
+ try {
424
+ guardedExec(command, { stdio: "ignore", timeout: 1e4 }, command);
425
+ removed++;
426
+ } catch {
427
+ continue;
428
+ }
429
+ }
430
+ for (const bundle of registeredPusharyBundles()) {
431
+ const command = `${LSREGISTER} -u ${JSON.stringify(bundle)}`;
432
+ try {
433
+ guardedExec(command, { stdio: "ignore", timeout: 2e4 }, command);
434
+ removed++;
435
+ } catch {
436
+ continue;
437
+ }
438
+ }
439
+ return removed;
440
+ };
441
+ var removeMacAppState = () => {
442
+ if (process.platform !== "darwin") return 0;
443
+ let removed = 0;
444
+ for (const service of storedPusharyKeychainServices()) {
445
+ for (const account of MAC_KEYCHAIN_ACCOUNTS) {
446
+ const command = `security delete-generic-password -s ${JSON.stringify(service)} -a ${account}`;
447
+ try {
448
+ guardedExec(command, { stdio: "ignore", timeout: 1e4 }, command);
449
+ removed++;
450
+ } catch {
451
+ continue;
452
+ }
453
+ }
454
+ }
455
+ for (const domain of MAC_DEFAULTS_DOMAINS) {
456
+ const command = `defaults delete ${JSON.stringify(domain)}`;
457
+ try {
458
+ guardedExec(command, { stdio: "ignore", timeout: 1e4 }, command);
459
+ removed++;
460
+ } catch {
461
+ continue;
462
+ }
463
+ }
464
+ for (const path of macLibraryState(homedir5())) {
465
+ if (!existsSync5(path)) continue;
466
+ guardedRemove(path, { force: true });
467
+ removed++;
468
+ }
469
+ return removed;
470
+ };
471
+ var quitMacApp = () => {
472
+ if (process.platform !== "darwin") return "not-running";
473
+ if (runningMacAppPIDs().length === 0) return "not-running";
474
+ try {
475
+ guardedExec(QUIT_MAC_APP, { stdio: "ignore", timeout: 3e4 }, QUIT_MAC_APP);
476
+ } catch {
477
+ }
478
+ return dryRun || waitForMacAppExit() ? "quit" : "running";
479
+ };
480
+ var CURSOR_PLUGIN_STAGING = ".pushary-staging-";
481
+ var cursorPluginLeftovers = () => {
482
+ const parent = dirname(CURSOR_PLUGIN_DIR);
483
+ const backupPrefix = `${CURSOR_PLUGIN_DIR.split("/").pop()}.pushary-backup-`;
484
+ try {
485
+ return readdirSync3(parent).filter((name) => name.startsWith(backupPrefix) || name.startsWith(CURSOR_PLUGIN_STAGING)).map((name) => join5(parent, name));
486
+ } catch {
487
+ return [];
488
+ }
489
+ };
205
490
  var guardedDaemonRemoval = async () => {
206
491
  if (dryRun) {
207
492
  wouldNote("stop and uninstall", "Pushary background service");
@@ -211,14 +496,14 @@ var guardedDaemonRemoval = async () => {
211
496
  };
212
497
  var guardedInstructionBlockRemoval = (path) => {
213
498
  if (!dryRun) return removeInstructionBlock(path);
214
- if (!existsSync(path)) return false;
215
- const hasBlock = readFileSync(path, "utf-8").includes("pushary");
499
+ if (!existsSync5(path)) return false;
500
+ const hasBlock = readFileSync4(path, "utf-8").includes("pushary");
216
501
  if (hasBlock) wouldNote("rewrite", path);
217
502
  return hasBlock;
218
503
  };
219
504
  var guardedAliasRemoval = () => {
220
505
  if (!dryRun) return removeClaudeAlias();
221
- const shells = [".zshrc", ".bashrc", ".bash_profile", ".profile"].map((name) => join(homedir(), name)).filter((path) => existsSync(path) && readFileSync(path, "utf-8").includes("pushary claude"));
506
+ const shells = CANDIDATE_RCS.map((name) => join5(homedir5(), name)).filter((path) => existsSync5(path) && readFileSync4(path, "utf-8").includes("pushary claude"));
222
507
  for (const path of shells) wouldNote("rewrite", path);
223
508
  return shells;
224
509
  };
@@ -226,15 +511,15 @@ var writeJson = (path, data) => {
226
511
  guardedWrite(path, JSON.stringify(data, null, 2) + "\n");
227
512
  };
228
513
  var removeCursorUserHook = (scope) => {
229
- if (!existsSync(CURSOR_USER_HOOKS)) return "absent";
230
- let parsed;
514
+ if (!existsSync5(CURSOR_USER_HOOKS)) return "absent";
515
+ let parsed2;
231
516
  try {
232
- parsed = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
517
+ parsed2 = JSON.parse(readFileSync4(CURSOR_USER_HOOKS, "utf-8"));
233
518
  } catch {
234
519
  return "failed";
235
520
  }
236
- if (!parsed || typeof parsed !== "object") return "failed";
237
- const hooks = parsed.hooks ?? {};
521
+ if (!parsed2 || typeof parsed2 !== "object") return "failed";
522
+ const hooks = parsed2.hooks ?? {};
238
523
  let removedAny = false;
239
524
  for (const event of CURSOR_GATE_EVENTS) {
240
525
  const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
@@ -248,15 +533,15 @@ var removeCursorUserHook = (scope) => {
248
533
  else hooks[event] = others;
249
534
  }
250
535
  if (!removedAny) return "absent";
251
- parsed.hooks = hooks;
536
+ parsed2.hooks = hooks;
252
537
  try {
253
- writeJson(CURSOR_USER_HOOKS, parsed);
538
+ writeJson(CURSOR_USER_HOOKS, parsed2);
254
539
  } catch {
255
540
  return "failed";
256
541
  }
257
542
  if (dryRun) return "removed";
258
543
  try {
259
- const after = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
544
+ const after = JSON.parse(readFileSync4(CURSOR_USER_HOOKS, "utf-8"));
260
545
  const afterHooks = after?.hooks ?? {};
261
546
  const stillThere = CURSOR_GATE_EVENTS.some((event) => {
262
547
  const entries = afterHooks[event];
@@ -296,15 +581,15 @@ var restoreTakeoverReceipts = () => {
296
581
  ["codex", codexHooksJson(), codexConfigToml(), "mcp_servers", "toml"],
297
582
  ["gemini_cli", geminiSettings(), geminiSettings(), "mcpServers", "json"],
298
583
  ["cursor", CURSOR_USER_HOOKS, null, null, null],
299
- ["vscode", join(VSCODE_PLUGIN_DIR, "hooks", "hooks.json"), vscodePluginMcp(), "mcpServers", "json"]
584
+ ["vscode", join5(VSCODE_PLUGIN_DIR, "hooks", "hooks.json"), vscodePluginMcp(), "mcpServers", "json"]
300
585
  ];
301
586
  for (const [agent, hookFile, mcpFile, mcpKey, mcpFormat] of targets) {
302
587
  const receiptPath = hookTakeoverReceipt(agent);
303
- if (!existsSync(receiptPath)) continue;
588
+ if (!existsSync5(receiptPath)) continue;
304
589
  try {
305
590
  const receipt = readJson(receiptPath);
306
591
  if (!isHookTakeoverReceipt(receipt) || receipt.agent !== agent) throw new Error("invalid receipt");
307
- const document = existsSync(hookFile) ? JSON.parse(readFileSync(hookFile, "utf-8")) : {};
592
+ const document = existsSync5(hookFile) ? JSON.parse(readFileSync4(hookFile, "utf-8")) : {};
308
593
  if (!document || typeof document !== "object" || Array.isArray(document)) throw new Error("invalid hooks file");
309
594
  const appAlreadyOwns = hookOwnerOf(document) === "app";
310
595
  const cliPositions = agent === "codex" ? codexHookPositions(document) : {};
@@ -315,7 +600,7 @@ var restoreTakeoverReceipts = () => {
315
600
  if (mcpFile === hookFile) {
316
601
  mcpDocument = document;
317
602
  } else {
318
- const raw = existsSync(mcpFile) ? readFileSync(mcpFile, "utf-8") : "";
603
+ const raw = existsSync5(mcpFile) ? readFileSync4(mcpFile, "utf-8") : "";
319
604
  mcpDocument = raw.trim() ? mcpFormat === "toml" ? parseTOML(raw) : JSON.parse(raw) : {};
320
605
  }
321
606
  mcpChanged = restoreMcpServer(mcpDocument, mcpKey, receipt.mcpServer);
@@ -325,7 +610,7 @@ var restoreTakeoverReceipts = () => {
325
610
  if (!appAlreadyOwns && agent === "codex") {
326
611
  if (!mcpDocument) {
327
612
  const configPath = codexConfigToml();
328
- const raw = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
613
+ const raw = existsSync5(configPath) ? readFileSync4(configPath, "utf-8") : "";
329
614
  mcpDocument = raw.trim() ? parseTOML(raw) : {};
330
615
  }
331
616
  codexConfig = mcpDocument;
@@ -358,7 +643,7 @@ var main = async () => {
358
643
  try {
359
644
  rejectUnknownFlags(
360
645
  process.argv.slice(2).filter((a) => a !== "clean"),
361
- ["--dry-run", "--yes", "-y", "--help", "-h"]
646
+ ["--dry-run", "--everything", "--yes", "-y", "--help", "-h"]
362
647
  );
363
648
  } catch (err) {
364
649
  process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
@@ -366,6 +651,7 @@ var main = async () => {
366
651
  process.exit(EXIT.USAGE);
367
652
  }
368
653
  dryRun = process.argv.includes("--dry-run");
654
+ const everything = process.argv.includes("--everything");
369
655
  if (dryRun) {
370
656
  console.log(` ${bold("Dry run")} ${dim("- nothing will be changed")}`);
371
657
  console.log();
@@ -381,14 +667,30 @@ var main = async () => {
381
667
  process.exit(0);
382
668
  }
383
669
  console.log();
384
- const keepShared = macAppInstalled();
385
- const scope = removalScope(keepShared);
670
+ const presence = macAppPresence();
671
+ const keepShared = presence === "present" && !everything;
672
+ const scope = removalScope(keepShared, keepShared);
386
673
  if (keepShared) {
387
674
  console.log(` ${dim("The Mac app is installed, so its wiring and the state it owns are left alone.")}`);
388
- console.log();
675
+ console.log(` ${dim("To remove that too, re-run with")} --everything`);
676
+ } else if (presence === "gone") {
677
+ console.log(` ${dim("The Mac app is gone, so the wiring it left behind is removed too.")}`);
678
+ } else if (everything && presence === "present") {
679
+ console.log(` ${dim("Removing the Mac app's wiring and state as well.")}`);
680
+ }
681
+ if (everything) {
682
+ const quit = quitMacApp();
683
+ if (quit === "quit") {
684
+ console.log(` ${check} Pushary ${dim(did("quit"))} ${dim("(running, and its watcher puts back what this removes)")}`);
685
+ } else if (quit === "running") {
686
+ incompleteClean = true;
687
+ console.log(` ${cross} Pushary ${dim("would not quit, and it puts back what this removes")}`);
688
+ console.log(` ${dim("Quit it from the notch menu, then re-run.")}`);
689
+ }
389
690
  }
691
+ if (presence !== "absent") console.log();
390
692
  const restoredByTakeover = new Set(
391
- ["claude_code", "codex", "gemini_cli"].filter((agent) => existsSync(hookTakeoverReceipt(agent)))
693
+ ["claude_code", "codex", "gemini_cli"].filter((agent) => existsSync5(hookTakeoverReceipt(agent)))
392
694
  );
393
695
  if (keepShared) restoreTakeoverReceipts();
394
696
  const sharedMcpKeys = {
@@ -414,12 +716,13 @@ var main = async () => {
414
716
  const keepClaudeMcp = macAppKeeps("claude_code", mcpServerBearer(claudeJson2?.mcpServers));
415
717
  const claudeHasPusharyEntry = () => Boolean(claudeJson2?.mcpServers?.pushary);
416
718
  const claudeHadPusharyEntry = claudeHasPusharyEntry();
719
+ const rawReceipt = readJson(CLAUDE_MCP_RECEIPT);
720
+ const receipt = isClaudeMcpSnapshot(rawReceipt) ? rawReceipt : null;
721
+ const receiptUnreadable = existsSync5(CLAUDE_MCP_RECEIPT) && !receipt;
417
722
  if (keepClaudeMcp) {
418
723
  console.log(` ${skip} Claude Code MCP servers ${dim("(kept: the Mac app uses it)")}`);
419
724
  } else if (claudeJson2) {
420
- const rawReceipt = readJson(CLAUDE_MCP_RECEIPT);
421
- const receipt = isClaudeMcpSnapshot(rawReceipt) ? rawReceipt : null;
422
- if (existsSync(CLAUDE_MCP_RECEIPT) && !receipt) {
725
+ if (receiptUnreadable) {
423
726
  if (!keepShared) incompleteClean = true;
424
727
  console.log(` ${skip} Claude Code MCP servers ${dim("(setup receipt is unreadable; left unchanged)")}`);
425
728
  } else if (receipt && restoreClaudeMcpServer(claudeJson2, receipt) || !receipt && removeClaudeMcpServers(claudeJson2)) {
@@ -430,13 +733,20 @@ var main = async () => {
430
733
  if (receipt && !keepShared) incompleteClean = true;
431
734
  console.log(` ${skip} Claude Code MCP servers ${dim(receipt ? "(changed after setup; left unchanged)" : "(no pushary entries)")}`);
432
735
  }
433
- } else if (existsSync(CLAUDE_MCP_RECEIPT)) {
736
+ } else if (existsSync5(CLAUDE_MCP_RECEIPT)) {
434
737
  incompleteClean = true;
435
738
  console.log(` ${skip} Claude Code MCP servers ${dim("(config missing; setup receipt kept for retry)")}`);
436
739
  } else {
437
740
  console.log(` ${skip} Claude Code MCP servers ${dim("(not found)")}`);
438
741
  }
439
- const claudeScope = removalScope(keepShared && !(claudeHadPusharyEntry && !claudeHasPusharyEntry()));
742
+ if (!keepClaudeMcp && !receiptUnreadable) {
743
+ const rewrites = claudeBackupRewrites(receipt);
744
+ for (const rewrite of rewrites) guardedWrite(rewrite.path, rewrite.contents);
745
+ if (rewrites.length > 0) {
746
+ console.log(` ${check} Claude Code backups ${dim(did("cleaned"))} ${dim(`(the Pushary entry and its key, in ${rewrites.length})`)}`);
747
+ }
748
+ }
749
+ const claudeScope = removalScope(keepShared && !(claudeHadPusharyEntry && !claudeHasPusharyEntry()), keepShared);
440
750
  cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings", claudeScope);
441
751
  cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local", claudeScope);
442
752
  const cursorData = keepShared ? null : readJson(CURSOR_MCP);
@@ -467,22 +777,29 @@ var main = async () => {
467
777
  }
468
778
  if (cursorGate === "failed") {
469
779
  console.log(` ${skip} Cursor plugin ${dim("(kept: removing it now would block matched commands)")}`);
470
- } else if (existsSync(CURSOR_PLUGIN_DIR)) {
780
+ } else if (existsSync5(CURSOR_PLUGIN_DIR)) {
471
781
  guardedRemove(CURSOR_PLUGIN_DIR);
472
782
  console.log(` ${check} Cursor plugin ${dim("(removed from ~/.cursor/plugins/local)")}`);
473
783
  } else {
474
784
  console.log(` ${skip} Cursor plugin ${dim("(not installed)")}`);
475
785
  }
786
+ if (cursorGate !== "failed") {
787
+ const stale = cursorPluginLeftovers();
788
+ for (const path of stale) guardedRemove(path);
789
+ if (stale.length > 0) {
790
+ console.log(` ${check} Cursor plugin copies ${dim(did("removed"))} ${dim(`(${stale.length}, each holding the key of an earlier install)`)}`);
791
+ }
792
+ }
476
793
  if (keepShared) {
477
794
  console.log(` ${skip} VS Code plugin ${dim("(kept: the Mac app writes the same plugin directory)")}`);
478
795
  }
479
- for (const settingsPath of keepShared ? [] : vscodeSettingsTargets(existsSync)) {
796
+ for (const settingsPath of keepShared ? [] : vscodeSettingsTargets(existsSync5)) {
480
797
  const label = `VS Code plugin ${dim(`(${shortenHome(settingsPath)})`)}`;
481
- if (!existsSync(settingsPath)) {
798
+ if (!existsSync5(settingsPath)) {
482
799
  console.log(` ${skip} ${label} ${dim("(no settings.json)")}`);
483
800
  continue;
484
801
  }
485
- const result = unregisterPluginLocation(readFileSync(settingsPath, "utf-8"), VSCODE_PLUGIN_DIR);
802
+ const result = unregisterPluginLocation(readFileSync4(settingsPath, "utf-8"), VSCODE_PLUGIN_DIR);
486
803
  if (result.kind === "removed") {
487
804
  guardedWrite(settingsPath, result.content);
488
805
  console.log(` ${check} ${label} ${dim(did("unregistered"))}`);
@@ -492,13 +809,26 @@ var main = async () => {
492
809
  console.log(` ${skip} ${label} ${dim("(no pushary entry)")}`);
493
810
  }
494
811
  }
495
- if (!keepShared && existsSync(VSCODE_PLUGIN_DIR)) {
812
+ if (!keepShared && existsSync5(VSCODE_PLUGIN_DIR)) {
496
813
  guardedRemove(VSCODE_PLUGIN_DIR);
497
814
  console.log(` ${check} VS Code plugin files ${dim(did("removed"))}`);
498
815
  }
816
+ if (keepShared) {
817
+ console.log(` ${skip} Editor extension ${dim("(kept: installed and updated by the Mac app)")}`);
818
+ } else {
819
+ const removals = editorExtensionRemovals();
820
+ for (const removal of removals) {
821
+ if (removal.record) guardedWrite(removal.record.path, removal.record.contents);
822
+ for (const folder of removal.folders) guardedRemove(folder);
823
+ console.log(` ${check} Editor extension ${dim(`(${did("removed")} from ${shortenHome(removal.directory)})`)}`);
824
+ }
825
+ if (removals.length === 0) {
826
+ console.log(` ${skip} Editor extension ${dim("(not installed in Cursor or VS Code)")}`);
827
+ }
828
+ }
499
829
  if (keepShared) {
500
830
  console.log(` ${skip} Skill directory ${dim("(kept: the Mac app uses it)")}`);
501
- } else if (existsSync(SKILL_DIR)) {
831
+ } else if (existsSync5(SKILL_DIR)) {
502
832
  guardedRemove(SKILL_DIR);
503
833
  console.log(` ${check} Skill directory ${dim(did("removed"))}`);
504
834
  } else {
@@ -509,26 +839,26 @@ var main = async () => {
509
839
  const codexHooksData = readJson(codexHooksJson2);
510
840
  const codexPositions = codexHookPositions(codexHooksData ?? void 0);
511
841
  try {
512
- const config = readFileSync(codexConfig, "utf-8");
513
- const parsed = parseTOML(config);
514
- const trustRemoved = hookOwnerOf(codexHooksData) === "app" ? false : removeCodexHookTrust(parsed, codexHooksJson2, codexPositions);
842
+ const config = readFileSync4(codexConfig, "utf-8");
843
+ const parsed2 = parseTOML(config);
844
+ const trustRemoved = hookOwnerOf(codexHooksData) === "app" && keepShared ? false : removeCodexHookTrust(parsed2, codexHooksJson2, codexPositions);
515
845
  const hadPushary = config.includes("pushary");
516
846
  if (hadPushary || trustRemoved) {
517
- const macAppKeepsCodexMcp = macAppKeeps("codex", codexMcpKey(parsed));
847
+ const macAppKeepsCodexMcp = macAppKeeps("codex", codexMcpKey(parsed2));
518
848
  if (!macAppKeepsCodexMcp) {
519
- const mcpServers = parsed.mcp_servers ?? {};
849
+ const mcpServers = parsed2.mcp_servers ?? {};
520
850
  delete mcpServers.pushary;
521
- if (Object.keys(mcpServers).length === 0) delete parsed.mcp_servers;
522
- else parsed.mcp_servers = mcpServers;
851
+ if (Object.keys(mcpServers).length === 0) delete parsed2.mcp_servers;
852
+ else parsed2.mcp_servers = mcpServers;
523
853
  }
524
- if (Array.isArray(parsed.notify)) {
525
- parsed.notify = parsed.notify.filter((n) => typeof n !== "string" || !n.includes("pushary-codex"));
526
- if (parsed.notify.length === 0) delete parsed.notify;
854
+ if (Array.isArray(parsed2.notify)) {
855
+ parsed2.notify = parsed2.notify.filter((n) => typeof n !== "string" || !n.includes("pushary-codex"));
856
+ if (parsed2.notify.length === 0) delete parsed2.notify;
527
857
  }
528
- guardedWrite(codexConfig, stringifyTOML(parsed));
858
+ guardedWrite(codexConfig, stringifyTOML(parsed2));
529
859
  const what = trustRemoved ? "cleaned, hook trust removed" : "cleaned";
530
860
  console.log(` ${check} Codex config ${dim(did(what))}`);
531
- if (macAppKeepsCodexMcp && parsed.mcp_servers?.pushary) {
861
+ if (macAppKeepsCodexMcp && parsed2.mcp_servers?.pushary) {
532
862
  console.log(` ${skip} Codex MCP server ${dim("(kept: the Mac app uses it)")}`);
533
863
  }
534
864
  } else {
@@ -558,7 +888,7 @@ var main = async () => {
558
888
  const codexSkillDir2 = codexSkillDir();
559
889
  if (keepShared) {
560
890
  console.log(` ${skip} Codex skill directory ${dim("(kept: the Mac app uses it)")}`);
561
- } else if (existsSync(codexSkillDir2)) {
891
+ } else if (existsSync5(codexSkillDir2)) {
562
892
  guardedRemove(codexSkillDir2);
563
893
  console.log(` ${check} Codex skill directory ${dim(did("removed"))}`);
564
894
  } else {
@@ -596,7 +926,7 @@ var main = async () => {
596
926
  const geminiSettingsPath = geminiSettings();
597
927
  const geminiSettings2 = readJson(geminiSettingsPath);
598
928
  if (geminiSettings2) {
599
- if (removeGeminiSettings(geminiSettings2, removalScope(macAppKeeps("gemini_cli", mcpServerBearer(geminiSettings2.mcpServers))))) {
929
+ if (removeGeminiSettings(geminiSettings2, removalScope(macAppKeeps("gemini_cli", mcpServerBearer(geminiSettings2.mcpServers)), keepShared))) {
600
930
  writeJson(geminiSettingsPath, geminiSettings2);
601
931
  console.log(` ${check} Gemini CLI settings ${dim(did("cleaned"))}`);
602
932
  } else {
@@ -614,25 +944,25 @@ var main = async () => {
614
944
  console.log(` ${skip} Gemini GEMINI.md ${dim("(no pushary block)")}`);
615
945
  }
616
946
  const openCodePlugin = openCodePluginFile();
617
- const openCodePluginContents = existsSync(openCodePlugin) ? readFileSync(openCodePlugin, "utf-8") : null;
947
+ const openCodePluginContents = existsSync5(openCodePlugin) ? readFileSync4(openCodePlugin, "utf-8") : null;
618
948
  if (openCodePluginContents === null) {
619
949
  console.log(` ${skip} OpenCode plugin ${dim("(not installed)")}`);
620
950
  } else if (!isOpenCodePluginOurs(openCodePluginContents)) {
621
951
  console.log(` ${skip} OpenCode plugin ${dim("(a pushary.js we did not write)")}`);
622
- } else if (openCodePluginContents.includes(HOOK_LOCATOR_BINARY)) {
952
+ } else if (openCodePluginContents.includes(HOOK_LOCATOR_BINARY) && keepShared) {
623
953
  console.log(` ${skip} OpenCode plugin ${dim("(kept: written by the Mac app)")}`);
624
954
  } else {
625
955
  guardedRemove(openCodePlugin, { force: true });
626
956
  console.log(` ${check} OpenCode plugin ${dim(did("removed"))}`);
627
- const sibling = join(openCodePluginDir(), OPENCODE_PLUGIN_SIBLING);
957
+ const sibling = join5(openCodePluginDir(), OPENCODE_PLUGIN_SIBLING);
628
958
  const remaining = (() => {
629
959
  try {
630
- return readdirSync(openCodePluginDir()).filter((name) => name !== OPENCODE_PLUGIN_SIBLING);
960
+ return readdirSync3(openCodePluginDir()).filter((name) => name !== OPENCODE_PLUGIN_SIBLING);
631
961
  } catch {
632
962
  return [];
633
963
  }
634
964
  })();
635
- const siblingIsOurs = existsSync(sibling) && readFileSync(sibling, "utf-8") === OPENCODE_PLUGIN_SIBLING_CONTENTS;
965
+ const siblingIsOurs = existsSync5(sibling) && readFileSync4(sibling, "utf-8") === OPENCODE_PLUGIN_SIBLING_CONTENTS;
636
966
  if (siblingIsOurs && !hasForeignPlugins(remaining)) {
637
967
  guardedRemove(sibling, { force: true });
638
968
  console.log(` ${check} OpenCode plugin sibling ${dim(did("removed"))}`);
@@ -667,7 +997,7 @@ var main = async () => {
667
997
  console.log(` ${skip} Hermes plugin ${dim("(could not uninstall; setup receipt kept for retry)")}`);
668
998
  }
669
999
  }
670
- } else if (existsSync(HERMES_CONFIG_RECEIPT)) {
1000
+ } else if (existsSync5(HERMES_CONFIG_RECEIPT)) {
671
1001
  incompleteClean = true;
672
1002
  console.log(` ${skip} Hermes ${dim("(runtime not found; setup receipt kept for retry)")}`);
673
1003
  } else {
@@ -675,20 +1005,31 @@ var main = async () => {
675
1005
  }
676
1006
  for (const shellFile of SHELL_FILES) {
677
1007
  try {
678
- const content = readFileSync(shellFile, "utf-8");
1008
+ const content = readFileSync4(shellFile, "utf-8");
679
1009
  if (content.includes("PUSHARY_API_KEY")) {
680
- const isOurExport = (line) => /^\s*export\s+PUSHARY_API_KEY=['"]?pk_[A-Za-z0-9]+\.[A-Za-z0-9]+['"]?\s*$/.test(line);
1010
+ const isOurExport = (line) => /^\s*export\s+PUSHARY_API_KEY=['"]?pk_[A-Za-z0-9]+\.[A-Za-z0-9]+['"]?\s*$/.test(line) || isDisabledExport(line);
681
1011
  const lines = content.split("\n");
682
1012
  const declined = lines.filter((l) => l.includes("PUSHARY_API_KEY") && !isOurExport(l));
683
1013
  const cleaned = lines.filter((l) => !isOurExport(l)).join("\n");
1014
+ const name = shellFile.split("/").pop();
684
1015
  if (cleaned !== content) {
685
1016
  const backup = `${shellFile}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
686
1017
  if (!dryRun) copyFileSync(shellFile, backup);
687
1018
  guardedWrite(shellFile, cleaned);
688
- console.log(` ${check} ${shellFile.split("/").pop()} ${dim(`(removed the export, backup at ${backup})`)}`);
1019
+ const confirmed = !dryRun && readFileSync4(shellFile, "utf-8") === cleaned;
1020
+ if (confirmed) guardedRemove(backup, { force: true });
1021
+ const kept = confirmed ? "" : `, backup at ${backup}`;
1022
+ console.log(` ${check} ${name} ${dim(`(removed the export${kept})`)}`);
689
1023
  }
690
1024
  for (const line of declined) {
691
- console.log(` ${skip} ${shellFile.split("/").pop()} ${dim(`left a line referencing PUSHARY_API_KEY: ${line.trim()}`)}`);
1025
+ if ((line.match(/pk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g) ?? []).some(isValidApiKey)) {
1026
+ incompleteClean = true;
1027
+ console.log(` ${cross} ${name} ${dim("still sets PUSHARY_API_KEY, in a form this command will not rewrite:")}`);
1028
+ console.log(` ${dim(line.trim())}`);
1029
+ console.log(` ${dim("Remove it yourself. A setup run would sign this machine in as that account.")}`);
1030
+ } else {
1031
+ console.log(` ${skip} ${name} ${dim(`left a line referencing PUSHARY_API_KEY: ${line.trim()}`)}`);
1032
+ }
692
1033
  }
693
1034
  }
694
1035
  } catch {
@@ -700,15 +1041,15 @@ var main = async () => {
700
1041
  } else {
701
1042
  console.log(` ${skip} claude alias ${dim("(not set)")}`);
702
1043
  }
703
- if (existsSync(PUSHARY_DIR)) {
1044
+ if (existsSync5(PUSHARY_DIR)) {
704
1045
  let removed = 0;
705
1046
  const kept = [];
706
- for (const entry of readdirSync(PUSHARY_DIR)) {
1047
+ for (const entry of readdirSync3(PUSHARY_DIR)) {
707
1048
  if (keepShared && isAppOwned(entry) || entry === "setup" && (keepShared || incompleteClean)) {
708
1049
  kept.push(entry);
709
1050
  continue;
710
1051
  }
711
- guardedRemove(join(PUSHARY_DIR, entry), { force: true });
1052
+ guardedRemove(join5(PUSHARY_DIR, entry), { force: true });
712
1053
  removed++;
713
1054
  }
714
1055
  if (removed > 0) {
@@ -725,10 +1066,10 @@ var main = async () => {
725
1066
  }
726
1067
  try {
727
1068
  let swept = 0;
728
- for (const f of readdirSync(tmpdir())) {
1069
+ for (const f of readdirSync3(tmpdir())) {
729
1070
  if (!f.startsWith("pushary-")) continue;
730
1071
  try {
731
- guardedRemove(join(tmpdir(), f), { force: true });
1072
+ guardedRemove(join5(tmpdir(), f), { force: true });
732
1073
  swept++;
733
1074
  } catch {
734
1075
  }
@@ -736,6 +1077,24 @@ var main = async () => {
736
1077
  if (swept > 0) console.log(` ${check} Temp state ${dim(`(${swept} pushary entries swept)`)}`);
737
1078
  } catch {
738
1079
  }
1080
+ if (everything) {
1081
+ const store = skillsStoreRemoval();
1082
+ if (store.skillDir) guardedRemove(store.skillDir);
1083
+ if (store.lock) guardedWrite(store.lock.path, store.lock.contents);
1084
+ if (store.skillDir || store.lock) {
1085
+ console.log(` ${check} skills.sh registration ${dim(did("removed"))} ${dim("(~/.agents)")}`);
1086
+ }
1087
+ const leftovers = removeDeveloperLeftovers();
1088
+ if (leftovers > 0) {
1089
+ console.log(` ${check} Developer leftovers ${dim(did("removed"))} ${dim(`(${leftovers} leaked test preferences and stale app registrations)`)}`);
1090
+ }
1091
+ const removed = removeMacAppState();
1092
+ if (removed > 0) {
1093
+ console.log(` ${check} Mac app state ${dim(did("removed"))} ${dim(`(${removed} keychain items, preferences and caches)`)}`);
1094
+ } else {
1095
+ console.log(` ${skip} Mac app state ${dim("(nothing of ours in the keychain, preferences or caches)")}`);
1096
+ }
1097
+ }
739
1098
  try {
740
1099
  if (dryRun) {
741
1100
  wouldNote("run ", "npm uninstall -g @pushary/agent-hooks");
@@ -750,13 +1109,32 @@ var main = async () => {
750
1109
  if (incompleteClean) {
751
1110
  console.log(` ${yellow(bold("Clean finished with something left behind."))}`);
752
1111
  console.log(` ${dim("See the \u2717 above. Fix it, then re-run")} npx @pushary/agent-hooks@latest clean`);
1112
+ console.log(` ${dim("Still stuck? Send this output to")} ${SUPPORT_EMAIL} ${dim("or")} ${SUPPORT_DISCORD}`);
1113
+ } else if (everything) {
1114
+ console.log(` ${green(bold("Clean complete. Nothing of ours is left in any agent config."))}`);
1115
+ const bundle = installedPusharyBundles().find(existsSync5);
1116
+ if (bundle) {
1117
+ console.log(` ${dim("The app itself is still installed. Drag")} ${bundle} ${dim("to the Trash to finish.")}`);
1118
+ }
1119
+ console.log(` ${dim("Signing in as someone else? Sign out at pushary.com in your browser first.")}`);
1120
+ console.log(` ${dim("The app and")} pushary login ${dim("both sign in through it, and would pick the old account back up.")}`);
753
1121
  } else {
754
1122
  console.log(` ${green(bold("Clean complete."))}`);
1123
+ if (keepShared) {
1124
+ console.log(` ${dim("The Mac app's wiring and its stored key are still here. Re-run with")} --everything ${dim("to remove them.")}`);
1125
+ }
755
1126
  }
756
1127
  console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
757
- if (aliasRemovedFrom.length > 0) {
1128
+ const exportedKey = process.env.PUSHARY_API_KEY?.trim();
1129
+ if (aliasRemovedFrom.length > 0 || exportedKey) {
758
1130
  console.log();
759
- console.log(` ${yellow("!")} ${bold("One thing left in your current terminal.")}`);
1131
+ console.log(` ${yellow("!")} ${bold("In your current terminal")}`);
1132
+ }
1133
+ if (exportedKey) {
1134
+ console.log(` ${dim("PUSHARY_API_KEY is still exported here. Run")} ${bold("unset PUSHARY_API_KEY")}`);
1135
+ console.log(` ${dim("(or open a new terminal) before setup, or it signs back in as the old account.")}`);
1136
+ }
1137
+ if (aliasRemovedFrom.length > 0) {
760
1138
  console.log(` ${dim("It still has the")} ${bold("claude")} ${dim("alias loaded from before. Run")} ${bold("unalias claude")}`);
761
1139
  console.log(` ${dim("(or open a new terminal) so")} ${bold("claude")} ${dim("runs Claude Code again.")}`);
762
1140
  }