@pushary/agent-hooks 1.3.1 → 1.4.1

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 (84) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/dist/bin/pushary-bell-hook.js +18 -18
  4. package/dist/bin/pushary-bell.js +8 -8
  5. package/dist/bin/pushary-claude.js +18 -18
  6. package/dist/bin/pushary-clean.js +490 -95
  7. package/dist/bin/pushary-codex-bridge.js +10 -10
  8. package/dist/bin/pushary-codex-hook.js +28 -26
  9. package/dist/bin/pushary-codex.js +6 -6
  10. package/dist/bin/pushary-connect.js +8 -8
  11. package/dist/bin/pushary-cowork.js +5 -5
  12. package/dist/bin/pushary-daemon.js +24 -24
  13. package/dist/bin/pushary-disconnect.js +12 -12
  14. package/dist/bin/pushary-doctor.js +75 -26
  15. package/dist/bin/pushary-elicitation-hook.js +17 -17
  16. package/dist/bin/pushary-gemini-bridge.js +9 -9
  17. package/dist/bin/pushary-gemini-hook.js +19 -19
  18. package/dist/bin/pushary-hook.js +27 -27
  19. package/dist/bin/pushary-login.js +9 -9
  20. package/dist/bin/pushary-logout.js +6 -6
  21. package/dist/bin/pushary-mode.js +5 -5
  22. package/dist/bin/pushary-notification-hook.js +20 -20
  23. package/dist/bin/pushary-opencode-hook.js +19 -19
  24. package/dist/bin/pushary-permission-denied-hook.js +24 -24
  25. package/dist/bin/pushary-permission-hook.js +25 -25
  26. package/dist/bin/pushary-post-hook.js +20 -20
  27. package/dist/bin/pushary-prompt-hook.js +20 -20
  28. package/dist/bin/pushary-session-end-hook.js +20 -20
  29. package/dist/bin/pushary-session-start-hook.js +22 -22
  30. package/dist/bin/pushary-setup.js +38 -59
  31. package/dist/bin/pushary-stats.js +4 -4
  32. package/dist/bin/pushary-status.js +12 -12
  33. package/dist/bin/pushary-stop-hook.js +20 -20
  34. package/dist/bin/pushary-stopfailure-hook.js +20 -20
  35. package/dist/bin/pushary-suggestions.js +2 -2
  36. package/dist/bin/pushary-transcript-register.js +16 -16
  37. package/dist/bin/pushary-transcripts.js +6 -6
  38. package/dist/bin/pushary-upgrade.js +16 -16
  39. package/dist/bin/pushary-wait.js +5 -5
  40. package/dist/bin/pushary.js +1 -1
  41. package/dist/{chunk-RERJ2ZDV.js → chunk-2GMI6JH6.js} +5 -5
  42. package/dist/{chunk-AFOTOA24.js → chunk-37VLIYLI.js} +35 -4
  43. package/dist/{chunk-6HH4IH4K.js → chunk-3BPB2HYZ.js} +3 -3
  44. package/dist/{chunk-465UENKO.js → chunk-3T52B2OF.js} +1 -1
  45. package/dist/{chunk-PEV6ULSM.js → chunk-3UA6FPRC.js} +3 -3
  46. package/dist/{chunk-QDYJQLXT.js → chunk-43JKGLZZ.js} +2 -2
  47. package/dist/{chunk-CKLWJ6EL.js → chunk-4FN6VV56.js} +33 -7
  48. package/dist/{chunk-HCTSTMDD.js → chunk-ALFT7H5B.js} +3 -3
  49. package/dist/{chunk-35RPUNYU.js → chunk-BZBUPZC6.js} +1 -1
  50. package/dist/{chunk-CDSL3LLO.js → chunk-CRNAXFQ6.js} +1 -1
  51. package/dist/{chunk-NDSOOJDX.js → chunk-D7ERHWYB.js} +1 -1
  52. package/dist/{chunk-KHGIM2BN.js → chunk-ED5TA2FJ.js} +3 -3
  53. package/dist/{chunk-LZQVAEWP.js → chunk-FAH3D74E.js} +2 -1
  54. package/dist/{chunk-5UWDYOZX.js → chunk-FVKFDTN6.js} +81 -8
  55. package/dist/{chunk-I2OLM3DM.js → chunk-GOFDGTEI.js} +62 -12
  56. package/dist/{chunk-JZA2JOGO.js → chunk-HK3IXIQJ.js} +1 -1
  57. package/dist/{chunk-LWT34GSY.js → chunk-HQLAZWJJ.js} +7 -7
  58. package/dist/{chunk-5XKMOZO6.js → chunk-I6HRQOMT.js} +4 -4
  59. package/dist/{chunk-EAGMQMRM.js → chunk-JOVWYVSR.js} +1 -1
  60. package/dist/{chunk-5WLH2ENB.js → chunk-K6I5M2ME.js} +1 -1
  61. package/dist/{chunk-REIZ5GXG.js → chunk-KEMEBTCJ.js} +4 -4
  62. package/dist/{chunk-PHXU4NOP.js → chunk-KI36MAT6.js} +2 -2
  63. package/dist/{chunk-WOBHHXW5.js → chunk-LNGYW6G4.js} +5 -4
  64. package/dist/{chunk-AFKOWCMQ.js → chunk-LUT2EZLG.js} +1 -1
  65. package/dist/{chunk-KLZJEMSE.js → chunk-LYXX5NSB.js} +1 -1
  66. package/dist/{chunk-YXWM4TNX.js → chunk-MN3XOVRK.js} +1 -1
  67. package/dist/{chunk-XOUQUZ43.js → chunk-PHHGHHOW.js} +1 -1
  68. package/dist/{chunk-TW7KA27C.js → chunk-QBS6LGEK.js} +1 -1
  69. package/dist/{chunk-FGNABUPN.js → chunk-R3AMKNIW.js} +1 -1
  70. package/dist/{chunk-TGG4FWWB.js → chunk-S3EP4B35.js} +2 -2
  71. package/dist/{chunk-34NNTYK6.js → chunk-SAF6NNXY.js} +1 -1
  72. package/dist/{chunk-R5CU7ED6.js → chunk-SKXLIVFM.js} +1 -1
  73. package/dist/{chunk-OMP3KDDU.js → chunk-SWFF5U2N.js} +1 -1
  74. package/dist/{chunk-QOCWB4Q6.js → chunk-TF4IOINR.js} +3 -3
  75. package/dist/{chunk-5SALHATY.js → chunk-TI3ZWXZG.js} +11 -2
  76. package/dist/{chunk-P232YODQ.js → chunk-TNS3YLFE.js} +6 -6
  77. package/dist/{chunk-GOQHN2UH.js → chunk-TWWLVOPB.js} +1 -1
  78. package/dist/{chunk-XVE52YFZ.js → chunk-VYUDGQM7.js} +1 -1
  79. package/dist/{chunk-XP2KRTZD.js → chunk-WEO5JCB4.js} +1 -1
  80. package/dist/{chunk-SGZE2JBF.js → chunk-WIOXWUUH.js} +3 -3
  81. package/dist/{reapply-ADFRVH5N.js → reapply-BAWDJTG7.js} +8 -8
  82. package/dist/src/index.d.ts +19 -16
  83. package/dist/src/index.js +12 -12
  84. package/package.json +1 -1
@@ -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-AFOTOA24.js";
10
+ } from "../chunk-37VLIYLI.js";
9
11
  import "../chunk-P32LPT2L.js";
10
12
  import {
11
13
  removeInstructionBlock,
12
14
  shortenHome,
13
15
  unregisterPluginLocation,
14
16
  vscodeSettingsTargets
15
- } from "../chunk-PEV6ULSM.js";
17
+ } from "../chunk-3UA6FPRC.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-XVE52YFZ.js";
27
+ } from "../chunk-VYUDGQM7.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-LWT34GSY.js";
35
+ } from "../chunk-HQLAZWJJ.js";
34
36
  import {
35
37
  isClaudeMcpSnapshot,
36
38
  removeClaudeMcpServers,
37
39
  removePusharySettings,
38
40
  removePusharyToolPermissions,
39
41
  restoreClaudeMcpServer
40
- } from "../chunk-SGZE2JBF.js";
42
+ } from "../chunk-WIOXWUUH.js";
41
43
  import {
42
44
  hookOwnerOf,
43
45
  isHookTakeoverReceipt,
44
46
  restoreHookTakeover,
45
47
  restoreMcpServer
46
- } from "../chunk-REIZ5GXG.js";
47
- import "../chunk-FGNABUPN.js";
48
- import "../chunk-KLZJEMSE.js";
48
+ } from "../chunk-KEMEBTCJ.js";
49
+ import "../chunk-R3AMKNIW.js";
50
+ import {
51
+ liveBridgeProbe,
52
+ resolveBridgeStub
53
+ } from "../chunk-LYXX5NSB.js";
49
54
  import {
50
55
  pusharyHookExecutable
51
- } from "../chunk-QOCWB4Q6.js";
56
+ } from "../chunk-TF4IOINR.js";
52
57
  import {
53
58
  disableDaemonService
54
- } from "../chunk-6HH4IH4K.js";
59
+ } from "../chunk-3BPB2HYZ.js";
55
60
  import {
56
61
  removeGeminiSettings
57
- } from "../chunk-KHGIM2BN.js";
58
- import "../chunk-EAGMQMRM.js";
62
+ } from "../chunk-ED5TA2FJ.js";
63
+ import "../chunk-JOVWYVSR.js";
59
64
  import "../chunk-XHKBHWLX.js";
60
- import "../chunk-WOBHHXW5.js";
61
- import "../chunk-XP2KRTZD.js";
65
+ import "../chunk-LNGYW6G4.js";
66
+ import "../chunk-WEO5JCB4.js";
62
67
  import "../chunk-LGXUGYUH.js";
63
- import "../chunk-465UENKO.js";
68
+ import "../chunk-3T52B2OF.js";
64
69
  import {
65
70
  bridgeAgentKey,
66
71
  claudeJson,
@@ -89,41 +94,319 @@ import {
89
94
  restoreCodexHookTrust,
90
95
  vscodePluginDir,
91
96
  vscodePluginMcp
92
- } from "../chunk-34NNTYK6.js";
97
+ } from "../chunk-SAF6NNXY.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-5SALHATY.js";
110
+ } from "../chunk-TI3ZWXZG.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-5WLH2ENB.js";
117
+ import "../chunk-K6I5M2ME.js";
111
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,
117
- isValidApiKey
118
- } from "../chunk-5UWDYOZX.js";
124
+ SUPPORT_DISCORD,
125
+ SUPPORT_EMAIL,
126
+ isValidApiKey,
127
+ redactSecrets
128
+ } from "../chunk-FVKFDTN6.js";
119
129
 
120
130
  // bin/pushary-clean.ts
121
- import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
122
- import { join } from "path";
123
- import { homedir, tmpdir } from "os";
131
+ import { existsSync as existsSync6, readFileSync as readFileSync4, copyFileSync, readdirSync as readdirSync3 } from "fs";
132
+ import { dirname, join as join5 } from "path";
133
+ import { homedir as homedir5, tmpdir } from "os";
124
134
  import { confirm } from "@inquirer/prompts";
125
135
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
126
136
 
137
+ // src/cli/mac-leftovers.ts
138
+ import { execSync, execFileSync } from "child_process";
139
+ import { existsSync } from "fs";
140
+ import { homedir } from "os";
141
+ import { join } from "path";
142
+ var MAC_APP_BUNDLE = "/Applications/Pushary.app";
143
+ var installedPusharyBundles = (home = homedir()) => [MAC_APP_BUNDLE, join(home, "Applications", "Pushary.app")];
144
+ var LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
145
+ var TEST_DEFAULTS_PREFIXES = [
146
+ "com.pushary.mac.tests.",
147
+ "com.pushary.tests.",
148
+ "pushary.tests.",
149
+ "pushary.quiet-mode-test.",
150
+ "pushary-optin-"
151
+ ];
152
+ var isTestDefaultsDomain = (domain) => TEST_DEFAULTS_PREFIXES.some((prefix) => domain.startsWith(prefix));
153
+ var leakedTestDefaultsDomains = () => {
154
+ try {
155
+ return execSync("defaults domains", { encoding: "utf-8", timeout: 2e4 }).split(",").map((entry) => entry.trim()).filter(isTestDefaultsDomain);
156
+ } catch {
157
+ return [];
158
+ }
159
+ };
160
+ var BUNDLE_NAME = "Pushary.app";
161
+ var DUMP_PATH_LINE = /^\s*path:\s+(.+?)(?: \(0x[0-9a-f]+\))?\s*$/;
162
+ var bundlesInLaunchServicesDump = (dump) => {
163
+ const found = /* @__PURE__ */ new Set();
164
+ for (const line of dump.split("\n")) {
165
+ const path = DUMP_PATH_LINE.exec(line)?.[1];
166
+ const bundle = path?.indexOf(`/${BUNDLE_NAME}`);
167
+ if (path === void 0 || bundle === void 0 || bundle === -1) continue;
168
+ found.add(path.slice(0, bundle + BUNDLE_NAME.length + 1));
169
+ }
170
+ return [...found];
171
+ };
172
+ var registeredPusharyBundles = (installed = installedPusharyBundles()) => {
173
+ if (!existsSync(LSREGISTER)) return [];
174
+ try {
175
+ const dump = execSync(`${LSREGISTER} -dump`, {
176
+ encoding: "utf-8",
177
+ timeout: 6e4,
178
+ maxBuffer: 256 * 1024 * 1024
179
+ });
180
+ return bundlesInLaunchServicesDump(dump).filter((path) => !installed.includes(path));
181
+ } catch {
182
+ return [];
183
+ }
184
+ };
185
+ var MAC_KEYCHAIN_SERVICE = "com.pushary.app.mac";
186
+ var MAC_KEYCHAIN_ACCOUNTS = [
187
+ "pushary.site-api-key",
188
+ "pushary.site-id",
189
+ "clerk.device-token",
190
+ "clerk.session-id"
191
+ ];
192
+ var MAC_DEFAULTS_DOMAINS = ["com.pushary.app", "pushary-bridge", "pushary.render"];
193
+ var macLibraryState = (home = homedir()) => [
194
+ join(home, "Library", "Application Support", "Pushary"),
195
+ join(home, "Library", "Caches", "com.pushary.app"),
196
+ join(home, "Library", "Caches", "com.pushary.app.sparkle"),
197
+ join(home, "Library", "Saved Application State", "com.pushary.app.savedState"),
198
+ join(home, "Library", "Caches", "pushary-bridge"),
199
+ join(home, "Library", "HTTPStorages", "com.pushary.app"),
200
+ join(home, "Library", "HTTPStorages", "com.pushary.app.binarycookies"),
201
+ join(home, "Library", "HTTPStorages", "pushary-bridge")
202
+ ];
203
+ var DUMP_SERVICE_LINE = /^\s*"svce"<blob>="(.+)"\s*$/;
204
+ var pusharyKeychainServices = (dump) => {
205
+ const found = /* @__PURE__ */ new Set();
206
+ for (const line of dump.split("\n")) {
207
+ const service = DUMP_SERVICE_LINE.exec(line)?.[1];
208
+ if (service?.startsWith(MAC_KEYCHAIN_SERVICE)) found.add(service);
209
+ }
210
+ return [...found];
211
+ };
212
+ var storedPusharyKeychainServices = () => {
213
+ const dump = execFileSync("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
+ };
222
+ var MAC_APP_BUNDLE_ID = "com.pushary.app";
223
+ var runningMacAppPIDs = () => {
224
+ if (process.platform !== "darwin") return [];
225
+ try {
226
+ return execSync('pgrep -f "Pushary.app/Contents/MacOS/Pushary"', {
227
+ encoding: "utf-8",
228
+ timeout: 1e4,
229
+ stdio: ["ignore", "pipe", "ignore"]
230
+ }).split("\n").map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0);
231
+ } catch {
232
+ return [];
233
+ }
234
+ };
235
+ var waitForMacAppExit = (attempts = 20, everyMs = 250) => {
236
+ const idle = new Int32Array(new SharedArrayBuffer(4));
237
+ for (let attempt = 0; attempt < attempts; attempt++) {
238
+ if (runningMacAppPIDs().length === 0) return true;
239
+ Atomics.wait(idle, 0, 0, everyMs);
240
+ }
241
+ return runningMacAppPIDs().length === 0;
242
+ };
243
+
244
+ // src/cli/clean-mac.ts
245
+ import { existsSync as existsSync2 } from "fs";
246
+ var removeDeveloperLeftovers = (guards2, domains, bundles) => {
247
+ let removed = 0;
248
+ for (const domain of domains) {
249
+ try {
250
+ guards2.execFile("defaults", ["delete", domain], { stdio: "ignore", timeout: 1e4 }, `delete preferences ${domain}`);
251
+ removed++;
252
+ } catch {
253
+ continue;
254
+ }
255
+ }
256
+ for (const bundle of bundles) {
257
+ try {
258
+ guards2.execFile(LSREGISTER, ["-u", bundle], { stdio: "ignore", timeout: 2e4 }, `unregister ${bundle}`);
259
+ removed++;
260
+ } catch {
261
+ continue;
262
+ }
263
+ }
264
+ return removed;
265
+ };
266
+ var removeMacAppState = (guards2, readServices, home) => {
267
+ let removed = 0;
268
+ let failed = 0;
269
+ let services;
270
+ try {
271
+ services = readServices();
272
+ } catch {
273
+ return { removed, failed: 1 };
274
+ }
275
+ for (const service of services) {
276
+ for (const account of MAC_KEYCHAIN_ACCOUNTS) {
277
+ try {
278
+ guards2.execFile(
279
+ "security",
280
+ ["delete-generic-password", "-s", service, "-a", account],
281
+ { stdio: "ignore" },
282
+ `remove keychain item ${service} (${account})`
283
+ );
284
+ removed++;
285
+ } catch (error) {
286
+ if (!(error instanceof Error && "status" in error && error.status === 44)) failed++;
287
+ }
288
+ }
289
+ }
290
+ for (const domain of MAC_DEFAULTS_DOMAINS) {
291
+ try {
292
+ guards2.execFile("defaults", ["delete", domain], { stdio: "ignore", timeout: 1e4 }, `delete preferences ${domain}`);
293
+ removed++;
294
+ } catch {
295
+ continue;
296
+ }
297
+ }
298
+ for (const path of macLibraryState(home)) {
299
+ if (!existsSync2(path)) continue;
300
+ guards2.remove(path, { force: true });
301
+ removed++;
302
+ }
303
+ return { removed, failed };
304
+ };
305
+ var quitMacApp = (guards2, state) => {
306
+ if (state.platform !== "darwin" || state.running().length === 0) return "not-running";
307
+ try {
308
+ guards2.execFile(
309
+ "osascript",
310
+ ["-e", `tell application id "${MAC_APP_BUNDLE_ID}" to quit`],
311
+ { stdio: "ignore", timeout: 3e4 },
312
+ "quit Pushary"
313
+ );
314
+ } catch {
315
+ }
316
+ return state.dryRun || state.waitForExit() ? "quit" : "running";
317
+ };
318
+
319
+ // src/cli/editor-extension.ts
320
+ import { existsSync as existsSync3, readFileSync, readdirSync } from "fs";
321
+ import { homedir as homedir2 } from "os";
322
+ import { join as join2 } from "path";
323
+ var EDITOR_EXTENSION_ID = "pushary.pushary";
324
+ var EDITOR_EXTENSION_DIRS = [
325
+ join2(".cursor", "extensions"),
326
+ join2(".vscode", "extensions")
327
+ ];
328
+ var EXTENSIONS_JSON = "extensions.json";
329
+ var isOurEntry = (entry) => {
330
+ if (typeof entry !== "object" || entry === null) return false;
331
+ const identifier = entry.identifier;
332
+ if (typeof identifier !== "object" || identifier === null) return false;
333
+ const id = identifier.id;
334
+ return typeof id === "string" && id.toLowerCase() === EDITOR_EXTENSION_ID;
335
+ };
336
+ var recordWithoutUs = (directory) => {
337
+ const path = join2(directory, EXTENSIONS_JSON);
338
+ try {
339
+ const entries = JSON.parse(readFileSync(path, "utf-8"));
340
+ if (!Array.isArray(entries)) return void 0;
341
+ const kept = entries.filter((entry) => !isOurEntry(entry));
342
+ return kept.length === entries.length ? void 0 : { path, contents: JSON.stringify(kept) };
343
+ } catch {
344
+ return void 0;
345
+ }
346
+ };
347
+ var editorExtensionRemovals = (home = homedir2()) => EDITOR_EXTENSION_DIRS.map((relative) => join2(home, relative)).filter(existsSync3).map((directory) => {
348
+ const folders = readdirSync(directory).filter((name) => name.startsWith(`${EDITOR_EXTENSION_ID}-`)).map((name) => join2(directory, name));
349
+ return { directory, folders, record: recordWithoutUs(directory) };
350
+ }).filter((removal) => removal.folders.length > 0 || removal.record !== void 0);
351
+
352
+ // src/cli/skills-store.ts
353
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
354
+ import { homedir as homedir3 } from "os";
355
+ import { join as join3 } from "path";
356
+ var SKILLS_STORE_DIR = ".agents";
357
+ var LOCK_FILE = ".skill-lock.json";
358
+ var lockWithoutUs = (path) => {
359
+ try {
360
+ const lock = JSON.parse(readFileSync2(path, "utf-8"));
361
+ if (!lock || typeof lock !== "object" || Array.isArray(lock) || !("skills" in lock)) return void 0;
362
+ const entries = lock.skills;
363
+ if (!entries || typeof entries !== "object" || Array.isArray(entries) || !(PUSHARY_SKILL_NAME in entries)) return void 0;
364
+ const entry = entries[PUSHARY_SKILL_NAME];
365
+ if (!entry || typeof entry !== "object" || !("source" in entry) || entry.source !== PUSHARY_SKILL_SOURCE) return void 0;
366
+ const { [PUSHARY_SKILL_NAME]: _ours, ...skills } = entries;
367
+ return { path, contents: `${JSON.stringify({ ...lock, skills }, null, 2)}
368
+ ` };
369
+ } catch {
370
+ return void 0;
371
+ }
372
+ };
373
+ var skillsStoreRemoval = (home = homedir3()) => {
374
+ const store = join3(home, SKILLS_STORE_DIR);
375
+ const skillDir = join3(store, "skills", PUSHARY_SKILL_NAME);
376
+ const lock = lockWithoutUs(join3(store, LOCK_FILE));
377
+ return {
378
+ skillDir: lock && existsSync4(skillDir) ? skillDir : void 0,
379
+ lock
380
+ };
381
+ };
382
+
383
+ // src/cli/claude-backups.ts
384
+ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
385
+ import { homedir as homedir4 } from "os";
386
+ import { join as join4 } from "path";
387
+ var BACKUP_PREFIX = ".claude.json.backup";
388
+ var claudeJsonBackups = (home = homedir4()) => {
389
+ const rollingDir = join4(home, ".claude", "backups");
390
+ const legacy = join4(home, BACKUP_PREFIX);
391
+ const rolling = existsSync5(rollingDir) ? readdirSync2(rollingDir).filter((name) => name.startsWith(BACKUP_PREFIX)).sort().map((name) => join4(rollingDir, name)) : [];
392
+ return existsSync5(legacy) ? [...rolling, legacy] : rolling;
393
+ };
394
+ var parsed = (path) => {
395
+ try {
396
+ const value = JSON.parse(readFileSync3(path, "utf-8"));
397
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
398
+ } catch {
399
+ return null;
400
+ }
401
+ };
402
+ var claudeBackupRewrites = (receipt, home = homedir4()) => claudeJsonBackups(home).flatMap((path) => {
403
+ const backup = parsed(path);
404
+ if (!backup) return [];
405
+ const changed = receipt ? restoreClaudeMcpServer(backup, receipt) : removeClaudeMcpServers(backup);
406
+ return changed ? [{ path, contents: `${JSON.stringify(backup, null, 2)}
407
+ ` }] : [];
408
+ });
409
+
127
410
  // src/app-owned-paths.ts
128
411
  var APP_OWNED_ENTRIES = [
129
412
  // PusharyPaths.bin: bridgeStub, bridgeCache
@@ -176,10 +459,15 @@ var CURSOR_PLUGIN_DIR = cursorPluginDir();
176
459
  var CURSOR_USER_HOOKS = cursorUserHooks();
177
460
  var VSCODE_PLUGIN_DIR = vscodePluginDir();
178
461
  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,
462
+ var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join5(homedir5(), f));
463
+ var PRESENCE_BY_RESOLUTION = {
464
+ bundle: "present",
465
+ missing: "gone",
466
+ "no-stub": "absent"
467
+ };
468
+ var macAppPresence = () => PRESENCE_BY_RESOLUTION[resolveBridgeStub(join5(PUSHARY_DIR, "bin", HOOK_LOCATOR_BINARY), liveBridgeProbe()).kind];
469
+ var removalScope = (keepMcpServer, keepAppWiring) => ({
470
+ keepAppWiring,
183
471
  keepMcpServer
184
472
  });
185
473
  var codexMcpKey = (config) => {
@@ -188,7 +476,7 @@ var codexMcpKey = (config) => {
188
476
  };
189
477
  var readJson = (path) => {
190
478
  try {
191
- return JSON.parse(readFileSync(path, "utf-8"));
479
+ return JSON.parse(readFileSync4(path, "utf-8"));
192
480
  } catch {
193
481
  return null;
194
482
  }
@@ -201,7 +489,16 @@ var wouldNote = (action, path) => {
201
489
  var guards = createGuards(() => dryRun, wouldNote);
202
490
  var guardedWrite = (path, contents) => guards.write(path, contents);
203
491
  var guardedRemove = (path, options) => guards.remove(path, options);
204
- var guardedExec = (command, options, describe) => guards.exec(command, options ?? {}, describe);
492
+ var CURSOR_PLUGIN_STAGING = ".pushary-staging-";
493
+ var cursorPluginLeftovers = () => {
494
+ const parent = dirname(CURSOR_PLUGIN_DIR);
495
+ const backupPrefix = `${CURSOR_PLUGIN_DIR.split("/").pop()}.pushary-backup-`;
496
+ try {
497
+ return readdirSync3(parent).filter((name) => name.startsWith(backupPrefix) || name.startsWith(CURSOR_PLUGIN_STAGING)).map((name) => join5(parent, name));
498
+ } catch {
499
+ return [];
500
+ }
501
+ };
205
502
  var guardedDaemonRemoval = async () => {
206
503
  if (dryRun) {
207
504
  wouldNote("stop and uninstall", "Pushary background service");
@@ -211,14 +508,14 @@ var guardedDaemonRemoval = async () => {
211
508
  };
212
509
  var guardedInstructionBlockRemoval = (path) => {
213
510
  if (!dryRun) return removeInstructionBlock(path);
214
- if (!existsSync(path)) return false;
215
- const hasBlock = readFileSync(path, "utf-8").includes("pushary");
511
+ if (!existsSync6(path)) return false;
512
+ const hasBlock = readFileSync4(path, "utf-8").includes("pushary");
216
513
  if (hasBlock) wouldNote("rewrite", path);
217
514
  return hasBlock;
218
515
  };
219
516
  var guardedAliasRemoval = () => {
220
517
  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"));
518
+ const shells = CANDIDATE_RCS.map((name) => join5(homedir5(), name)).filter((path) => existsSync6(path) && readFileSync4(path, "utf-8").includes("pushary claude"));
222
519
  for (const path of shells) wouldNote("rewrite", path);
223
520
  return shells;
224
521
  };
@@ -226,15 +523,15 @@ var writeJson = (path, data) => {
226
523
  guardedWrite(path, JSON.stringify(data, null, 2) + "\n");
227
524
  };
228
525
  var removeCursorUserHook = (scope) => {
229
- if (!existsSync(CURSOR_USER_HOOKS)) return "absent";
230
- let parsed;
526
+ if (!existsSync6(CURSOR_USER_HOOKS)) return "absent";
527
+ let parsed2;
231
528
  try {
232
- parsed = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
529
+ parsed2 = JSON.parse(readFileSync4(CURSOR_USER_HOOKS, "utf-8"));
233
530
  } catch {
234
531
  return "failed";
235
532
  }
236
- if (!parsed || typeof parsed !== "object") return "failed";
237
- const hooks = parsed.hooks ?? {};
533
+ if (!parsed2 || typeof parsed2 !== "object") return "failed";
534
+ const hooks = parsed2.hooks ?? {};
238
535
  let removedAny = false;
239
536
  for (const event of CURSOR_GATE_EVENTS) {
240
537
  const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
@@ -248,15 +545,15 @@ var removeCursorUserHook = (scope) => {
248
545
  else hooks[event] = others;
249
546
  }
250
547
  if (!removedAny) return "absent";
251
- parsed.hooks = hooks;
548
+ parsed2.hooks = hooks;
252
549
  try {
253
- writeJson(CURSOR_USER_HOOKS, parsed);
550
+ writeJson(CURSOR_USER_HOOKS, parsed2);
254
551
  } catch {
255
552
  return "failed";
256
553
  }
257
554
  if (dryRun) return "removed";
258
555
  try {
259
- const after = JSON.parse(readFileSync(CURSOR_USER_HOOKS, "utf-8"));
556
+ const after = JSON.parse(readFileSync4(CURSOR_USER_HOOKS, "utf-8"));
260
557
  const afterHooks = after?.hooks ?? {};
261
558
  const stillThere = CURSOR_GATE_EVENTS.some((event) => {
262
559
  const entries = afterHooks[event];
@@ -296,15 +593,15 @@ var restoreTakeoverReceipts = () => {
296
593
  ["codex", codexHooksJson(), codexConfigToml(), "mcp_servers", "toml"],
297
594
  ["gemini_cli", geminiSettings(), geminiSettings(), "mcpServers", "json"],
298
595
  ["cursor", CURSOR_USER_HOOKS, null, null, null],
299
- ["vscode", join(VSCODE_PLUGIN_DIR, "hooks", "hooks.json"), vscodePluginMcp(), "mcpServers", "json"]
596
+ ["vscode", join5(VSCODE_PLUGIN_DIR, "hooks", "hooks.json"), vscodePluginMcp(), "mcpServers", "json"]
300
597
  ];
301
598
  for (const [agent, hookFile, mcpFile, mcpKey, mcpFormat] of targets) {
302
599
  const receiptPath = hookTakeoverReceipt(agent);
303
- if (!existsSync(receiptPath)) continue;
600
+ if (!existsSync6(receiptPath)) continue;
304
601
  try {
305
602
  const receipt = readJson(receiptPath);
306
603
  if (!isHookTakeoverReceipt(receipt) || receipt.agent !== agent) throw new Error("invalid receipt");
307
- const document = existsSync(hookFile) ? JSON.parse(readFileSync(hookFile, "utf-8")) : {};
604
+ const document = existsSync6(hookFile) ? JSON.parse(readFileSync4(hookFile, "utf-8")) : {};
308
605
  if (!document || typeof document !== "object" || Array.isArray(document)) throw new Error("invalid hooks file");
309
606
  const appAlreadyOwns = hookOwnerOf(document) === "app";
310
607
  const cliPositions = agent === "codex" ? codexHookPositions(document) : {};
@@ -315,7 +612,7 @@ var restoreTakeoverReceipts = () => {
315
612
  if (mcpFile === hookFile) {
316
613
  mcpDocument = document;
317
614
  } else {
318
- const raw = existsSync(mcpFile) ? readFileSync(mcpFile, "utf-8") : "";
615
+ const raw = existsSync6(mcpFile) ? readFileSync4(mcpFile, "utf-8") : "";
319
616
  mcpDocument = raw.trim() ? mcpFormat === "toml" ? parseTOML(raw) : JSON.parse(raw) : {};
320
617
  }
321
618
  mcpChanged = restoreMcpServer(mcpDocument, mcpKey, receipt.mcpServer);
@@ -325,7 +622,7 @@ var restoreTakeoverReceipts = () => {
325
622
  if (!appAlreadyOwns && agent === "codex") {
326
623
  if (!mcpDocument) {
327
624
  const configPath = codexConfigToml();
328
- const raw = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
625
+ const raw = existsSync6(configPath) ? readFileSync4(configPath, "utf-8") : "";
329
626
  mcpDocument = raw.trim() ? parseTOML(raw) : {};
330
627
  }
331
628
  codexConfig = mcpDocument;
@@ -358,7 +655,7 @@ var main = async () => {
358
655
  try {
359
656
  rejectUnknownFlags(
360
657
  process.argv.slice(2).filter((a) => a !== "clean"),
361
- ["--dry-run", "--yes", "-y", "--help", "-h"]
658
+ ["--dry-run", "--everything", "--yes", "-y", "--help", "-h"]
362
659
  );
363
660
  } catch (err) {
364
661
  process.stderr.write(` ! ${err instanceof Error ? err.message : String(err)}
@@ -366,6 +663,7 @@ var main = async () => {
366
663
  process.exit(EXIT.USAGE);
367
664
  }
368
665
  dryRun = process.argv.includes("--dry-run");
666
+ const everything = process.argv.includes("--everything");
369
667
  if (dryRun) {
370
668
  console.log(` ${bold("Dry run")} ${dim("- nothing will be changed")}`);
371
669
  console.log();
@@ -381,14 +679,31 @@ var main = async () => {
381
679
  process.exit(0);
382
680
  }
383
681
  console.log();
384
- const keepShared = macAppInstalled();
385
- const scope = removalScope(keepShared);
682
+ const presence = macAppPresence();
683
+ const keepShared = presence === "present" && !everything;
684
+ const scope = removalScope(keepShared, keepShared);
386
685
  if (keepShared) {
387
686
  console.log(` ${dim("The Mac app is installed, so its wiring and the state it owns are left alone.")}`);
388
- console.log();
687
+ console.log(` ${dim("To remove that too, re-run with")} --everything`);
688
+ } else if (presence === "gone") {
689
+ console.log(` ${dim("The Mac app is gone, so the wiring it left behind is removed too.")}`);
690
+ } else if (everything && presence === "present") {
691
+ console.log(` ${dim("Removing the Mac app's wiring and state as well.")}`);
389
692
  }
693
+ if (everything) {
694
+ const quit = quitMacApp(guards, { platform: process.platform, dryRun, running: runningMacAppPIDs, waitForExit: waitForMacAppExit });
695
+ if (quit === "quit") {
696
+ console.log(` ${check} Pushary ${dim(did("quit"))} ${dim("(running, and its watcher puts back what this removes)")}`);
697
+ } else if (quit === "running") {
698
+ incompleteClean = true;
699
+ console.log(` ${cross} Pushary ${dim("would not quit, and it puts back what this removes")}`);
700
+ console.log(` ${dim("Quit it from the notch menu, then re-run.")}`);
701
+ process.exit(EXIT.FAILED);
702
+ }
703
+ }
704
+ if (presence !== "absent") console.log();
390
705
  const restoredByTakeover = new Set(
391
- ["claude_code", "codex", "gemini_cli"].filter((agent) => existsSync(hookTakeoverReceipt(agent)))
706
+ ["claude_code", "codex", "gemini_cli"].filter((agent) => existsSync6(hookTakeoverReceipt(agent)))
392
707
  );
393
708
  if (keepShared) restoreTakeoverReceipts();
394
709
  const sharedMcpKeys = {
@@ -414,12 +729,13 @@ var main = async () => {
414
729
  const keepClaudeMcp = macAppKeeps("claude_code", mcpServerBearer(claudeJson2?.mcpServers));
415
730
  const claudeHasPusharyEntry = () => Boolean(claudeJson2?.mcpServers?.pushary);
416
731
  const claudeHadPusharyEntry = claudeHasPusharyEntry();
732
+ const rawReceipt = readJson(CLAUDE_MCP_RECEIPT);
733
+ const receipt = isClaudeMcpSnapshot(rawReceipt) ? rawReceipt : null;
734
+ const receiptUnreadable = existsSync6(CLAUDE_MCP_RECEIPT) && !receipt;
417
735
  if (keepClaudeMcp) {
418
736
  console.log(` ${skip} Claude Code MCP servers ${dim("(kept: the Mac app uses it)")}`);
419
737
  } else if (claudeJson2) {
420
- const rawReceipt = readJson(CLAUDE_MCP_RECEIPT);
421
- const receipt = isClaudeMcpSnapshot(rawReceipt) ? rawReceipt : null;
422
- if (existsSync(CLAUDE_MCP_RECEIPT) && !receipt) {
738
+ if (receiptUnreadable) {
423
739
  if (!keepShared) incompleteClean = true;
424
740
  console.log(` ${skip} Claude Code MCP servers ${dim("(setup receipt is unreadable; left unchanged)")}`);
425
741
  } else if (receipt && restoreClaudeMcpServer(claudeJson2, receipt) || !receipt && removeClaudeMcpServers(claudeJson2)) {
@@ -430,13 +746,20 @@ var main = async () => {
430
746
  if (receipt && !keepShared) incompleteClean = true;
431
747
  console.log(` ${skip} Claude Code MCP servers ${dim(receipt ? "(changed after setup; left unchanged)" : "(no pushary entries)")}`);
432
748
  }
433
- } else if (existsSync(CLAUDE_MCP_RECEIPT)) {
749
+ } else if (existsSync6(CLAUDE_MCP_RECEIPT)) {
434
750
  incompleteClean = true;
435
751
  console.log(` ${skip} Claude Code MCP servers ${dim("(config missing; setup receipt kept for retry)")}`);
436
752
  } else {
437
753
  console.log(` ${skip} Claude Code MCP servers ${dim("(not found)")}`);
438
754
  }
439
- const claudeScope = removalScope(keepShared && !(claudeHadPusharyEntry && !claudeHasPusharyEntry()));
755
+ if (!keepClaudeMcp && !receiptUnreadable) {
756
+ const rewrites = claudeBackupRewrites(receipt);
757
+ for (const rewrite of rewrites) guardedWrite(rewrite.path, rewrite.contents);
758
+ if (rewrites.length > 0) {
759
+ console.log(` ${check} Claude Code backups ${dim(did("cleaned"))} ${dim(`(the Pushary entry and its key, in ${rewrites.length})`)}`);
760
+ }
761
+ }
762
+ const claudeScope = removalScope(keepShared && !(claudeHadPusharyEntry && !claudeHasPusharyEntry()), keepShared);
440
763
  cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings", claudeScope);
441
764
  cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local", claudeScope);
442
765
  const cursorData = keepShared ? null : readJson(CURSOR_MCP);
@@ -467,22 +790,29 @@ var main = async () => {
467
790
  }
468
791
  if (cursorGate === "failed") {
469
792
  console.log(` ${skip} Cursor plugin ${dim("(kept: removing it now would block matched commands)")}`);
470
- } else if (existsSync(CURSOR_PLUGIN_DIR)) {
793
+ } else if (existsSync6(CURSOR_PLUGIN_DIR)) {
471
794
  guardedRemove(CURSOR_PLUGIN_DIR);
472
795
  console.log(` ${check} Cursor plugin ${dim("(removed from ~/.cursor/plugins/local)")}`);
473
796
  } else {
474
797
  console.log(` ${skip} Cursor plugin ${dim("(not installed)")}`);
475
798
  }
799
+ if (cursorGate !== "failed") {
800
+ const stale = cursorPluginLeftovers();
801
+ for (const path of stale) guardedRemove(path);
802
+ if (stale.length > 0) {
803
+ console.log(` ${check} Cursor plugin copies ${dim(did("removed"))} ${dim(`(${stale.length}, each holding the key of an earlier install)`)}`);
804
+ }
805
+ }
476
806
  if (keepShared) {
477
807
  console.log(` ${skip} VS Code plugin ${dim("(kept: the Mac app writes the same plugin directory)")}`);
478
808
  }
479
- for (const settingsPath of keepShared ? [] : vscodeSettingsTargets(existsSync)) {
809
+ for (const settingsPath of keepShared ? [] : vscodeSettingsTargets(existsSync6)) {
480
810
  const label = `VS Code plugin ${dim(`(${shortenHome(settingsPath)})`)}`;
481
- if (!existsSync(settingsPath)) {
811
+ if (!existsSync6(settingsPath)) {
482
812
  console.log(` ${skip} ${label} ${dim("(no settings.json)")}`);
483
813
  continue;
484
814
  }
485
- const result = unregisterPluginLocation(readFileSync(settingsPath, "utf-8"), VSCODE_PLUGIN_DIR);
815
+ const result = unregisterPluginLocation(readFileSync4(settingsPath, "utf-8"), VSCODE_PLUGIN_DIR);
486
816
  if (result.kind === "removed") {
487
817
  guardedWrite(settingsPath, result.content);
488
818
  console.log(` ${check} ${label} ${dim(did("unregistered"))}`);
@@ -492,13 +822,26 @@ var main = async () => {
492
822
  console.log(` ${skip} ${label} ${dim("(no pushary entry)")}`);
493
823
  }
494
824
  }
495
- if (!keepShared && existsSync(VSCODE_PLUGIN_DIR)) {
825
+ if (!keepShared && existsSync6(VSCODE_PLUGIN_DIR)) {
496
826
  guardedRemove(VSCODE_PLUGIN_DIR);
497
827
  console.log(` ${check} VS Code plugin files ${dim(did("removed"))}`);
498
828
  }
829
+ if (keepShared) {
830
+ console.log(` ${skip} Editor extension ${dim("(kept: installed and updated by the Mac app)")}`);
831
+ } else {
832
+ const removals = editorExtensionRemovals();
833
+ for (const removal of removals) {
834
+ if (removal.record) guardedWrite(removal.record.path, removal.record.contents);
835
+ for (const folder of removal.folders) guardedRemove(folder);
836
+ console.log(` ${check} Editor extension ${dim(`(${did("removed")} from ${shortenHome(removal.directory)})`)}`);
837
+ }
838
+ if (removals.length === 0) {
839
+ console.log(` ${skip} Editor extension ${dim("(not installed in Cursor or VS Code)")}`);
840
+ }
841
+ }
499
842
  if (keepShared) {
500
843
  console.log(` ${skip} Skill directory ${dim("(kept: the Mac app uses it)")}`);
501
- } else if (existsSync(SKILL_DIR)) {
844
+ } else if (existsSync6(SKILL_DIR)) {
502
845
  guardedRemove(SKILL_DIR);
503
846
  console.log(` ${check} Skill directory ${dim(did("removed"))}`);
504
847
  } else {
@@ -509,26 +852,26 @@ var main = async () => {
509
852
  const codexHooksData = readJson(codexHooksJson2);
510
853
  const codexPositions = codexHookPositions(codexHooksData ?? void 0);
511
854
  try {
512
- const config = readFileSync(codexConfig, "utf-8");
513
- const parsed = parseTOML(config);
514
- const trustRemoved = hookOwnerOf(codexHooksData) === "app" ? false : removeCodexHookTrust(parsed, codexHooksJson2, codexPositions);
855
+ const config = readFileSync4(codexConfig, "utf-8");
856
+ const parsed2 = parseTOML(config);
857
+ const trustRemoved = hookOwnerOf(codexHooksData) === "app" && keepShared ? false : removeCodexHookTrust(parsed2, codexHooksJson2, codexPositions);
515
858
  const hadPushary = config.includes("pushary");
516
859
  if (hadPushary || trustRemoved) {
517
- const macAppKeepsCodexMcp = macAppKeeps("codex", codexMcpKey(parsed));
860
+ const macAppKeepsCodexMcp = macAppKeeps("codex", codexMcpKey(parsed2));
518
861
  if (!macAppKeepsCodexMcp) {
519
- const mcpServers = parsed.mcp_servers ?? {};
862
+ const mcpServers = parsed2.mcp_servers ?? {};
520
863
  delete mcpServers.pushary;
521
- if (Object.keys(mcpServers).length === 0) delete parsed.mcp_servers;
522
- else parsed.mcp_servers = mcpServers;
864
+ if (Object.keys(mcpServers).length === 0) delete parsed2.mcp_servers;
865
+ else parsed2.mcp_servers = mcpServers;
523
866
  }
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;
867
+ if (Array.isArray(parsed2.notify)) {
868
+ parsed2.notify = parsed2.notify.filter((n) => typeof n !== "string" || !n.includes("pushary-codex"));
869
+ if (parsed2.notify.length === 0) delete parsed2.notify;
527
870
  }
528
- guardedWrite(codexConfig, stringifyTOML(parsed));
871
+ guardedWrite(codexConfig, stringifyTOML(parsed2));
529
872
  const what = trustRemoved ? "cleaned, hook trust removed" : "cleaned";
530
873
  console.log(` ${check} Codex config ${dim(did(what))}`);
531
- if (macAppKeepsCodexMcp && parsed.mcp_servers?.pushary) {
874
+ if (macAppKeepsCodexMcp && parsed2.mcp_servers?.pushary) {
532
875
  console.log(` ${skip} Codex MCP server ${dim("(kept: the Mac app uses it)")}`);
533
876
  }
534
877
  } else {
@@ -558,7 +901,7 @@ var main = async () => {
558
901
  const codexSkillDir2 = codexSkillDir();
559
902
  if (keepShared) {
560
903
  console.log(` ${skip} Codex skill directory ${dim("(kept: the Mac app uses it)")}`);
561
- } else if (existsSync(codexSkillDir2)) {
904
+ } else if (existsSync6(codexSkillDir2)) {
562
905
  guardedRemove(codexSkillDir2);
563
906
  console.log(` ${check} Codex skill directory ${dim(did("removed"))}`);
564
907
  } else {
@@ -596,7 +939,7 @@ var main = async () => {
596
939
  const geminiSettingsPath = geminiSettings();
597
940
  const geminiSettings2 = readJson(geminiSettingsPath);
598
941
  if (geminiSettings2) {
599
- if (removeGeminiSettings(geminiSettings2, removalScope(macAppKeeps("gemini_cli", mcpServerBearer(geminiSettings2.mcpServers))))) {
942
+ if (removeGeminiSettings(geminiSettings2, removalScope(macAppKeeps("gemini_cli", mcpServerBearer(geminiSettings2.mcpServers)), keepShared))) {
600
943
  writeJson(geminiSettingsPath, geminiSettings2);
601
944
  console.log(` ${check} Gemini CLI settings ${dim(did("cleaned"))}`);
602
945
  } else {
@@ -614,25 +957,25 @@ var main = async () => {
614
957
  console.log(` ${skip} Gemini GEMINI.md ${dim("(no pushary block)")}`);
615
958
  }
616
959
  const openCodePlugin = openCodePluginFile();
617
- const openCodePluginContents = existsSync(openCodePlugin) ? readFileSync(openCodePlugin, "utf-8") : null;
960
+ const openCodePluginContents = existsSync6(openCodePlugin) ? readFileSync4(openCodePlugin, "utf-8") : null;
618
961
  if (openCodePluginContents === null) {
619
962
  console.log(` ${skip} OpenCode plugin ${dim("(not installed)")}`);
620
963
  } else if (!isOpenCodePluginOurs(openCodePluginContents)) {
621
964
  console.log(` ${skip} OpenCode plugin ${dim("(a pushary.js we did not write)")}`);
622
- } else if (openCodePluginContents.includes(HOOK_LOCATOR_BINARY)) {
965
+ } else if (openCodePluginContents.includes(HOOK_LOCATOR_BINARY) && keepShared) {
623
966
  console.log(` ${skip} OpenCode plugin ${dim("(kept: written by the Mac app)")}`);
624
967
  } else {
625
968
  guardedRemove(openCodePlugin, { force: true });
626
969
  console.log(` ${check} OpenCode plugin ${dim(did("removed"))}`);
627
- const sibling = join(openCodePluginDir(), OPENCODE_PLUGIN_SIBLING);
970
+ const sibling = join5(openCodePluginDir(), OPENCODE_PLUGIN_SIBLING);
628
971
  const remaining = (() => {
629
972
  try {
630
- return readdirSync(openCodePluginDir()).filter((name) => name !== OPENCODE_PLUGIN_SIBLING);
973
+ return readdirSync3(openCodePluginDir()).filter((name) => name !== OPENCODE_PLUGIN_SIBLING);
631
974
  } catch {
632
975
  return [];
633
976
  }
634
977
  })();
635
- const siblingIsOurs = existsSync(sibling) && readFileSync(sibling, "utf-8") === OPENCODE_PLUGIN_SIBLING_CONTENTS;
978
+ const siblingIsOurs = existsSync6(sibling) && readFileSync4(sibling, "utf-8") === OPENCODE_PLUGIN_SIBLING_CONTENTS;
636
979
  if (siblingIsOurs && !hasForeignPlugins(remaining)) {
637
980
  guardedRemove(sibling, { force: true });
638
981
  console.log(` ${check} OpenCode plugin sibling ${dim(did("removed"))}`);
@@ -660,14 +1003,14 @@ var main = async () => {
660
1003
  } else {
661
1004
  console.log(` ${check} Hermes config ${dim("(setup-owned changes restored)")}`);
662
1005
  try {
663
- guardedExec(`"${hermesPython}" -m pip uninstall -y ${HERMES_PIP_PACKAGE}`, { stdio: "pipe", timeout: 6e4 }, `pip uninstall ${HERMES_PIP_PACKAGE}`);
1006
+ guards.execFile(hermesPython, ["-m", "pip", "uninstall", "-y", HERMES_PIP_PACKAGE], { stdio: "pipe", timeout: 6e4 }, `pip uninstall ${HERMES_PIP_PACKAGE}`);
664
1007
  console.log(` ${check} Hermes plugin ${dim("(pip uninstalled)")}`);
665
1008
  } catch {
666
1009
  incompleteClean = true;
667
1010
  console.log(` ${skip} Hermes plugin ${dim("(could not uninstall; setup receipt kept for retry)")}`);
668
1011
  }
669
1012
  }
670
- } else if (existsSync(HERMES_CONFIG_RECEIPT)) {
1013
+ } else if (existsSync6(HERMES_CONFIG_RECEIPT)) {
671
1014
  incompleteClean = true;
672
1015
  console.log(` ${skip} Hermes ${dim("(runtime not found; setup receipt kept for retry)")}`);
673
1016
  } else {
@@ -675,20 +1018,31 @@ var main = async () => {
675
1018
  }
676
1019
  for (const shellFile of SHELL_FILES) {
677
1020
  try {
678
- const content = readFileSync(shellFile, "utf-8");
1021
+ const content = readFileSync4(shellFile, "utf-8");
679
1022
  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);
1023
+ const isOurExport = (line) => /^\s*export\s+PUSHARY_API_KEY=['"]?pk_[A-Za-z0-9]+\.[A-Za-z0-9]+['"]?\s*$/.test(line) || isDisabledExport(line);
681
1024
  const lines = content.split("\n");
682
1025
  const declined = lines.filter((l) => l.includes("PUSHARY_API_KEY") && !isOurExport(l));
683
1026
  const cleaned = lines.filter((l) => !isOurExport(l)).join("\n");
1027
+ const name = shellFile.split("/").pop();
684
1028
  if (cleaned !== content) {
685
1029
  const backup = `${shellFile}.pushary-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
686
1030
  if (!dryRun) copyFileSync(shellFile, backup);
687
1031
  guardedWrite(shellFile, cleaned);
688
- console.log(` ${check} ${shellFile.split("/").pop()} ${dim(`(removed the export, backup at ${backup})`)}`);
1032
+ const confirmed = !dryRun && readFileSync4(shellFile, "utf-8") === cleaned;
1033
+ if (confirmed) guardedRemove(backup, { force: true });
1034
+ const kept = confirmed ? "" : `, backup at ${backup}`;
1035
+ console.log(` ${check} ${name} ${dim(`(removed the export${kept})`)}`);
689
1036
  }
690
1037
  for (const line of declined) {
691
- console.log(` ${skip} ${shellFile.split("/").pop()} ${dim(`left a line referencing PUSHARY_API_KEY: ${line.trim()}`)}`);
1038
+ if ((line.match(/pk_[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g) ?? []).some(isValidApiKey)) {
1039
+ incompleteClean = true;
1040
+ console.log(` ${cross} ${name} ${dim("still sets PUSHARY_API_KEY, in a form this command will not rewrite:")}`);
1041
+ console.log(` ${dim(redactSecrets(line.trim()))}`);
1042
+ console.log(` ${dim("Remove it yourself. A setup run would sign this machine in as that account.")}`);
1043
+ } else {
1044
+ console.log(` ${skip} ${name} ${dim(`left a line referencing PUSHARY_API_KEY: ${redactSecrets(line.trim())}`)}`);
1045
+ }
692
1046
  }
693
1047
  }
694
1048
  } catch {
@@ -700,15 +1054,15 @@ var main = async () => {
700
1054
  } else {
701
1055
  console.log(` ${skip} claude alias ${dim("(not set)")}`);
702
1056
  }
703
- if (existsSync(PUSHARY_DIR)) {
1057
+ if (existsSync6(PUSHARY_DIR)) {
704
1058
  let removed = 0;
705
1059
  const kept = [];
706
- for (const entry of readdirSync(PUSHARY_DIR)) {
1060
+ for (const entry of readdirSync3(PUSHARY_DIR)) {
707
1061
  if (keepShared && isAppOwned(entry) || entry === "setup" && (keepShared || incompleteClean)) {
708
1062
  kept.push(entry);
709
1063
  continue;
710
1064
  }
711
- guardedRemove(join(PUSHARY_DIR, entry), { force: true });
1065
+ guardedRemove(join5(PUSHARY_DIR, entry), { force: true });
712
1066
  removed++;
713
1067
  }
714
1068
  if (removed > 0) {
@@ -725,10 +1079,10 @@ var main = async () => {
725
1079
  }
726
1080
  try {
727
1081
  let swept = 0;
728
- for (const f of readdirSync(tmpdir())) {
1082
+ for (const f of readdirSync3(tmpdir())) {
729
1083
  if (!f.startsWith("pushary-")) continue;
730
1084
  try {
731
- guardedRemove(join(tmpdir(), f), { force: true });
1085
+ guardedRemove(join5(tmpdir(), f), { force: true });
732
1086
  swept++;
733
1087
  } catch {
734
1088
  }
@@ -736,6 +1090,28 @@ var main = async () => {
736
1090
  if (swept > 0) console.log(` ${check} Temp state ${dim(`(${swept} pushary entries swept)`)}`);
737
1091
  } catch {
738
1092
  }
1093
+ if (everything) {
1094
+ const store = skillsStoreRemoval();
1095
+ if (store.skillDir) guardedRemove(store.skillDir);
1096
+ if (store.lock) guardedWrite(store.lock.path, store.lock.contents);
1097
+ if (store.skillDir || store.lock) {
1098
+ console.log(` ${check} skills.sh registration ${dim(did("removed"))} ${dim("(~/.agents)")}`);
1099
+ }
1100
+ const leftovers = process.platform === "darwin" ? removeDeveloperLeftovers(guards, leakedTestDefaultsDomains(), registeredPusharyBundles()) : 0;
1101
+ if (leftovers > 0) {
1102
+ console.log(` ${check} Developer leftovers ${dim(did("removed"))} ${dim(`(${leftovers} leaked test preferences and stale app registrations)`)}`);
1103
+ }
1104
+ const { removed, failed } = process.platform === "darwin" ? removeMacAppState(guards, storedPusharyKeychainServices, homedir5()) : { removed: 0, failed: 0 };
1105
+ if (failed > 0) {
1106
+ incompleteClean = true;
1107
+ console.log(` ${cross} Mac keychain ${dim(`(${failed} operations failed; unlock your keychain and re-run)`)}`);
1108
+ }
1109
+ if (removed > 0) {
1110
+ console.log(` ${check} Mac app state ${dim(did("removed"))} ${dim(`(${removed} keychain items, preferences and caches)`)}`);
1111
+ } else if (failed === 0) {
1112
+ console.log(` ${skip} Mac app state ${dim("(nothing of ours in the keychain, preferences or caches)")}`);
1113
+ }
1114
+ }
739
1115
  try {
740
1116
  if (dryRun) {
741
1117
  wouldNote("run ", "npm uninstall -g @pushary/agent-hooks");
@@ -750,13 +1126,32 @@ var main = async () => {
750
1126
  if (incompleteClean) {
751
1127
  console.log(` ${yellow(bold("Clean finished with something left behind."))}`);
752
1128
  console.log(` ${dim("See the \u2717 above. Fix it, then re-run")} npx @pushary/agent-hooks@latest clean`);
1129
+ console.log(` ${dim("Still stuck? Send this output to")} ${SUPPORT_EMAIL} ${dim("or")} ${SUPPORT_DISCORD}`);
1130
+ } else if (everything) {
1131
+ console.log(` ${green(bold("Clean complete. Nothing of ours is left in any agent config."))}`);
1132
+ const bundle = installedPusharyBundles().find(existsSync6);
1133
+ if (bundle) {
1134
+ console.log(` ${dim("The app itself is still installed. Drag")} ${bundle} ${dim("to the Trash to finish.")}`);
1135
+ }
1136
+ console.log(` ${dim("Signing in as someone else? Sign out at pushary.com in your browser first.")}`);
1137
+ console.log(` ${dim("The app and")} pushary login ${dim("both sign in through it, and would pick the old account back up.")}`);
753
1138
  } else {
754
1139
  console.log(` ${green(bold("Clean complete."))}`);
1140
+ if (keepShared) {
1141
+ console.log(` ${dim("The Mac app's wiring and its stored key are still here. Re-run with")} --everything ${dim("to remove them.")}`);
1142
+ }
755
1143
  }
756
1144
  console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
757
- if (aliasRemovedFrom.length > 0) {
1145
+ const exportedKey = process.env.PUSHARY_API_KEY?.trim();
1146
+ if (aliasRemovedFrom.length > 0 || exportedKey) {
758
1147
  console.log();
759
- console.log(` ${yellow("!")} ${bold("One thing left in your current terminal.")}`);
1148
+ console.log(` ${yellow("!")} ${bold("In your current terminal")}`);
1149
+ }
1150
+ if (exportedKey) {
1151
+ console.log(` ${dim("PUSHARY_API_KEY is still exported here. Run")} ${bold("unset PUSHARY_API_KEY")}`);
1152
+ console.log(` ${dim("(or open a new terminal) before setup, or it signs back in as the old account.")}`);
1153
+ }
1154
+ if (aliasRemovedFrom.length > 0) {
760
1155
  console.log(` ${dim("It still has the")} ${bold("claude")} ${dim("alias loaded from before. Run")} ${bold("unalias claude")}`);
761
1156
  console.log(` ${dim("(or open a new terminal) so")} ${bold("claude")} ${dim("runs Claude Code again.")}`);
762
1157
  }