@pushary/agent-hooks 0.3.0 → 0.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.
@@ -2,14 +2,14 @@
2
2
 
3
3
  // bin/pushary-setup.ts
4
4
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "fs";
5
- import { join } from "path";
5
+ import { join, dirname } from "path";
6
6
  import { homedir } from "os";
7
- import { createInterface } from "readline";
8
7
  import { execSync } from "child_process";
9
- var rl = createInterface({ input: process.stdin, output: process.stdout });
10
- var ask = (q) => new Promise((r) => rl.question(q, r));
8
+ import { checkbox, input, confirm } from "@inquirer/prompts";
9
+ import { fileURLToPath } from "url";
11
10
  var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
12
11
  var CURSOR_MCP = join(".cursor", "mcp.json");
12
+ var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
13
13
  var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
14
14
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
15
15
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
@@ -40,12 +40,36 @@ var spinner = async (label, fn) => {
40
40
  clearInterval(interval);
41
41
  process.stdout.write(`\r ${check} ${label}
42
42
  `);
43
- } catch (err) {
43
+ } catch {
44
44
  clearInterval(interval);
45
45
  process.stdout.write(`\r ${yellow("!")} ${label} ${dim("(skipped)")}
46
46
  `);
47
47
  }
48
48
  };
49
+ var getPackageVersion = () => {
50
+ try {
51
+ const __dirname = dirname(fileURLToPath(import.meta.url));
52
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
53
+ return pkg.version ?? "0.0.0";
54
+ } catch {
55
+ return "0.0.0";
56
+ }
57
+ };
58
+ var checkForUpdates = async (current) => {
59
+ try {
60
+ const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
61
+ signal: AbortSignal.timeout(3e3)
62
+ });
63
+ const data = await res.json();
64
+ const latest = data.version;
65
+ if (latest && latest !== current) {
66
+ console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
67
+ console.log(` ${dim("Run:")} npx @pushary/agent-hooks@${latest} setup`);
68
+ console.log();
69
+ }
70
+ } catch {
71
+ }
72
+ };
49
73
  var installGlobally = async () => {
50
74
  await spinner("Installing pushary-hook globally", async () => {
51
75
  execSync("npm install -g @pushary/agent-hooks@latest", { stdio: "ignore" });
@@ -54,6 +78,7 @@ var installGlobally = async () => {
54
78
  var addMcpServer = (settings, apiKey) => {
55
79
  const mcpServers = settings.mcpServers ?? {};
56
80
  mcpServers.pushary = {
81
+ type: "http",
57
82
  url: "https://pushary.com/api/mcp/mcp",
58
83
  headers: { Authorization: `Bearer ${apiKey}` }
59
84
  };
@@ -101,17 +126,29 @@ var addPermissionHooks = (settings) => {
101
126
  var addToolPermissions = (settings) => {
102
127
  const permissions = settings.permissions ?? {};
103
128
  const allow = permissions.allow ?? [];
129
+ const filtered = allow.filter((r) => !r.includes("pushary"));
104
130
  const rule = "MCP(pushary:*)";
105
- if (!allow.includes(rule)) allow.push(rule);
106
- permissions.allow = allow;
131
+ if (!filtered.includes(rule)) filtered.push(rule);
132
+ permissions.allow = filtered;
107
133
  settings.permissions = permissions;
108
134
  };
135
+ var installSkill = async () => {
136
+ await spinner("Installing Pushary skill", async () => {
137
+ const res = await fetch("https://raw.githubusercontent.com/pushary/pushary-skill/main/skills/pushary/SKILL.md", {
138
+ signal: AbortSignal.timeout(1e4)
139
+ });
140
+ if (!res.ok) throw new Error(`Failed to fetch skill (${res.status})`);
141
+ const content = await res.text();
142
+ if (!existsSync(SKILL_DIR)) mkdirSync(SKILL_DIR, { recursive: true });
143
+ writeFileSync(join(SKILL_DIR, "SKILL.md"), content, "utf-8");
144
+ });
145
+ };
109
146
  var setupClaudeCode = async (apiKey) => {
110
147
  console.log(`
111
148
  ${bold("Setting up Claude Code")}
112
149
  `);
113
150
  const settings = readJson(CLAUDE_SETTINGS);
114
- await spinner("Adding MCP server", async () => {
151
+ await spinner("Adding MCP server (type: http)", async () => {
115
152
  addMcpServer(settings, apiKey);
116
153
  });
117
154
  await spinner("Auto-allowing Pushary tools", async () => {
@@ -124,15 +161,15 @@ var setupClaudeCode = async (apiKey) => {
124
161
  await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
125
162
  writeJson(CLAUDE_SETTINGS, settings);
126
163
  });
164
+ await installSkill();
127
165
  console.log();
128
166
  console.log(` ${dim("What this configured:")}`);
129
167
  console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
130
- console.log(` ${dim("\u2022")} PreToolUse: approve Bash/Write/Edit from your phone`);
131
- console.log(` ${dim("\u2022")} PostToolUse: track when tools finish`);
132
- console.log(` ${dim("\u2022")} Stop: detect when the agent session ends`);
133
- console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary`);
168
+ console.log(` ${dim("\u2022")} Skill: teaches your agent when and how to use Pushary`);
169
+ console.log(` ${dim("\u2022")} Hooks: route permission approvals through push notifications`);
170
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
134
171
  };
135
- var setupHermes = async (apiKey) => {
172
+ var setupHermes = async (_apiKey) => {
136
173
  console.log(`
137
174
  ${bold("Setting up Hermes Agent")}
138
175
  `);
@@ -157,7 +194,6 @@ var setupHermes = async (apiKey) => {
157
194
  console.log(` ${dim("What this configured:")}`);
158
195
  console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
159
196
  console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
160
- console.log(` ${dim("\u2022")} Session alerts: opt-in with PUSHARY_AUTO_NOTIFY_SESSION_END=1`);
161
197
  };
162
198
  var setupCodex = async (_apiKey) => {
163
199
  console.log(`
@@ -206,7 +242,6 @@ notify = ["${pusharyCodexPath}"]
206
242
  console.log(` ${dim("What this configured:")}`);
207
243
  console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
208
244
  console.log(` ${dim("\u2022")} Notify handler: captures turn completions and approval requests`);
209
- console.log(` ${dim("\u2022")} Uses PUSHARY_API_KEY env var for auth`);
210
245
  };
211
246
  var setupCursor = async (apiKey) => {
212
247
  console.log(`
@@ -216,17 +251,19 @@ var setupCursor = async (apiKey) => {
216
251
  const config = readJson(CURSOR_MCP);
217
252
  const mcpServers = config.mcpServers ?? {};
218
253
  mcpServers.pushary = {
254
+ type: "http",
219
255
  url: "https://pushary.com/api/mcp/mcp",
220
256
  headers: { Authorization: `Bearer ${apiKey}` }
221
257
  };
222
258
  config.mcpServers = mcpServers;
223
259
  writeJson(CURSOR_MCP, config);
224
260
  });
261
+ await installSkill();
225
262
  };
226
263
  var saveApiKey = async (apiKey) => {
227
264
  await spinner("Saving API key to shell profile", async () => {
228
265
  const exportLine = `
229
- export PUSHARY_API_KEY="${apiKey}"
266
+ export PUSHARY_API_KEY='${apiKey}'
230
267
  `;
231
268
  const shellFile = SHELL_FILES.find((f) => existsSync(f));
232
269
  if (shellFile) {
@@ -239,8 +276,12 @@ export PUSHARY_API_KEY="${apiKey}"
239
276
  });
240
277
  };
241
278
  var sendTestNotification = async (apiKey) => {
242
- let notifResult = "";
243
- await spinner("Sending test notification", async () => {
279
+ const frames = [" ", ". ", ".. ", "..."];
280
+ let i = 0;
281
+ const interval = setInterval(() => {
282
+ process.stdout.write(`\r ${dim(frames[i++ % frames.length])} Sending test notification`);
283
+ }, 200);
284
+ try {
244
285
  const response = await fetch("https://pushary.com/api/v1/server/send", {
245
286
  method: "POST",
246
287
  headers: {
@@ -253,74 +294,68 @@ var sendTestNotification = async (apiKey) => {
253
294
  })
254
295
  });
255
296
  const data = await response.json().catch(() => ({}));
297
+ clearInterval(interval);
256
298
  if (!response.ok) {
257
- notifResult = `Could not send: ${data.error ?? response.statusText}`;
258
- return;
299
+ const reason = data.error ?? response.statusText;
300
+ process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${reason})`)}
301
+ `);
302
+ console.log(` ${dim("Make sure you enabled notifications at")} ${cyan("pushary.com")}`);
303
+ } else {
304
+ process.stdout.write(`\r ${check} Sending test notification
305
+ `);
306
+ console.log(` ${dim("Check your phone!")}`);
259
307
  }
260
- notifResult = "sent";
261
- });
262
- if (notifResult === "sent") {
263
- console.log(` ${dim("Check your phone!")}`);
264
- } else if (notifResult) {
265
- console.log(` ${dim(notifResult)}`);
266
- console.log(` ${dim("Make sure you enabled notifications at")} ${cyan("pushary.com")}`);
308
+ } catch (err) {
309
+ clearInterval(interval);
310
+ const msg = err instanceof Error ? err.message : "network error";
311
+ process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${msg})`)}
312
+ `);
267
313
  }
268
314
  };
315
+ var AGENT_SETUP = {
316
+ claude_code: setupClaudeCode,
317
+ codex: setupCodex,
318
+ hermes: setupHermes,
319
+ cursor: setupCursor
320
+ };
269
321
  var main = async () => {
322
+ const version = getPackageVersion();
270
323
  console.log();
271
- console.log(` ${bold("Pushary")} ${dim("v" + (process.env.npm_package_version ?? "0.2"))}`);
324
+ console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
272
325
  console.log(` ${dim("Push notifications for AI coding agents")}`);
273
326
  console.log();
327
+ await checkForUpdates(version);
274
328
  console.log(` ${dim("Get your API key at")} ${cyan("pushary.com/sign-up")}`);
275
329
  console.log();
276
- const apiKey = await ask(` API key: `);
277
- if (!apiKey.trim() || !apiKey.includes(".")) {
330
+ const apiKey = await input({ message: "API key:" });
331
+ if (!apiKey.trim() || !/^pk_[a-f0-9]+\.[a-f0-9]+$/.test(apiKey.trim())) {
278
332
  console.log(`
279
333
  ${yellow("!")} Invalid key format. Expected: pk_xxx.sk_xxx`);
280
334
  console.log(` ${dim("Get yours at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
281
335
  `);
282
- rl.close();
283
336
  process.exit(1);
284
337
  }
285
338
  const trimmedKey = apiKey.trim();
286
- console.log();
287
- console.log(` ${bold("Which agent do you use?")}`);
288
- console.log();
289
- console.log(` ${cyan("1.")} Claude Code ${dim("MCP + permission hooks + auto-allowed tools")}`);
290
- console.log(` ${cyan("2.")} Codex ${dim("MCP server via codex mcp add")}`);
291
- console.log(` ${cyan("3.")} Hermes ${dim("native plugin + auto-error notifications")}`);
292
- console.log(` ${cyan("4.")} Cursor ${dim("MCP server")}`);
293
- console.log(` ${cyan("5.")} All ${dim("configure everything")}`);
294
- console.log(` ${cyan("6.")} Other ${dim("just save the API key")}`);
295
- console.log();
296
- const choice = await ask(` Choice ${dim("[1-6]")}: `);
339
+ const agents = await checkbox({
340
+ message: "Which agents do you use? " + dim("(space = toggle, enter = confirm)"),
341
+ choices: [
342
+ { name: `Claude Code ${dim("MCP + hooks + auto-allowed tools")}`, value: "claude_code" },
343
+ { name: `Codex ${dim("MCP server via codex mcp add")}`, value: "codex" },
344
+ { name: `Hermes ${dim("native plugin + auto-error notifications")}`, value: "hermes" },
345
+ { name: `Cursor ${dim("MCP server")}`, value: "cursor" }
346
+ ]
347
+ });
297
348
  await saveApiKey(trimmedKey);
298
- switch (choice.trim()) {
299
- case "1":
300
- await setupClaudeCode(trimmedKey);
301
- break;
302
- case "2":
303
- await setupCodex(trimmedKey);
304
- break;
305
- case "3":
306
- await setupHermes(trimmedKey);
307
- break;
308
- case "4":
309
- await setupCursor(trimmedKey);
310
- break;
311
- case "5":
312
- await setupClaudeCode(trimmedKey);
313
- await setupCodex(trimmedKey);
314
- await setupHermes(trimmedKey);
315
- await setupCursor(trimmedKey);
316
- break;
317
- case "6":
318
- default:
319
- break;
349
+ if (agents.length === 0) {
350
+ console.log(`
351
+ ${dim("No agents selected. API key saved.")}`);
352
+ } else {
353
+ for (const agent of agents) {
354
+ await AGENT_SETUP[agent](trimmedKey);
355
+ }
320
356
  }
321
- console.log();
322
- const test = await ask(` Send a test notification? ${dim("[Y/n]")} `);
323
- if (test.toLowerCase() !== "n") {
357
+ const sendTest = await confirm({ message: "Send a test notification?", default: true });
358
+ if (sendTest) {
324
359
  await sendTestNotification(trimmedKey);
325
360
  }
326
361
  console.log();
@@ -329,8 +364,7 @@ var main = async () => {
329
364
  console.log(` ${dim("Next:")}`);
330
365
  console.log(` ${dim("1.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
331
366
  console.log(` ${dim("2.")} Restart your agent to load the new config`);
332
- console.log(` ${dim("3.")} Start coding. Your agent will notify you automatically`);
367
+ console.log(` ${dim("3.")} Run ${cyan("npx @pushary/agent-hooks doctor")} to verify`);
333
368
  console.log();
334
- rl.close();
335
369
  };
336
370
  main();
@@ -6,16 +6,23 @@ if (command === "setup") {
6
6
  await import("./pushary-setup.js");
7
7
  } else if (command === "hook") {
8
8
  await import("./pushary-hook.js");
9
+ } else if (command === "clean") {
10
+ await import("./pushary-clean.js");
11
+ } else if (command === "doctor") {
12
+ await import("./pushary-doctor.js");
9
13
  } else {
10
14
  console.log(`
11
15
  Pushary Agent Hooks
12
16
 
13
17
  Commands:
14
- setup Configure Claude Code or Cursor with Pushary
18
+ setup Configure Claude Code, Codex, Hermes, or Cursor with Pushary
19
+ doctor Verify your Pushary installation is working
20
+ clean Remove all Pushary configuration
15
21
  hook Run as a PreToolUse hook (reads stdin, writes stdout)
16
22
 
17
23
  Usage:
18
- npx pushary setup
19
- npx pushary hook
24
+ npx @pushary/agent-hooks@latest setup
25
+ npx @pushary/agent-hooks@latest doctor
26
+ npx @pushary/agent-hooks@latest clean
20
27
  `);
21
28
  }
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/pushary-clean.ts
4
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { execSync } from "child_process";
8
+ import { confirm } from "@inquirer/prompts";
9
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
10
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
11
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
12
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
13
+ var check = green("\u2713");
14
+ var skip = yellow("\u2013");
15
+ var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
16
+ var CLAUDE_SETTINGS_LOCAL = join(homedir(), ".claude", "settings.local.json");
17
+ var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
18
+ var CURSOR_MCP = join(".cursor", "mcp.json");
19
+ var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
20
+ var readJson = (path) => {
21
+ try {
22
+ return JSON.parse(readFileSync(path, "utf-8"));
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+ var writeJson = (path, data) => {
28
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
29
+ };
30
+ var isPusharyPermission = (rule) => rule.includes("pushary") || rule.includes("MCP(pushary");
31
+ var isPusharyHook = (entry) => {
32
+ const hooks = entry.hooks;
33
+ if (!hooks) return false;
34
+ return hooks.some((h) => {
35
+ const cmd = String(h.command ?? "");
36
+ return cmd.includes("pushary-hook") || cmd.includes("pushary-post-hook") || cmd.includes("pushary-stop-hook");
37
+ });
38
+ };
39
+ var cleanSettingsFile = (path, label) => {
40
+ const data = readJson(path);
41
+ if (!data) {
42
+ console.log(` ${skip} ${label} ${dim("(not found)")}`);
43
+ return;
44
+ }
45
+ let changed = false;
46
+ const mcpServers = data.mcpServers;
47
+ if (mcpServers?.pushary) {
48
+ delete mcpServers.pushary;
49
+ if (Object.keys(mcpServers).length === 0) delete data.mcpServers;
50
+ changed = true;
51
+ }
52
+ const permissions = data.permissions;
53
+ if (permissions?.allow) {
54
+ const allow = permissions.allow;
55
+ const filtered = allow.filter((r) => !isPusharyPermission(r));
56
+ if (filtered.length !== allow.length) {
57
+ permissions.allow = filtered;
58
+ if (filtered.length === 0) delete permissions.allow;
59
+ if (Object.keys(permissions).length === 0) delete data.permissions;
60
+ changed = true;
61
+ }
62
+ }
63
+ const hooks = data.hooks;
64
+ if (hooks) {
65
+ for (const key of ["PreToolUse", "PostToolUse", "Stop"]) {
66
+ const entries = hooks[key];
67
+ if (!entries) continue;
68
+ const filtered = entries.filter((e) => !isPusharyHook(e));
69
+ if (filtered.length !== entries.length) {
70
+ if (filtered.length === 0) {
71
+ delete hooks[key];
72
+ } else {
73
+ hooks[key] = filtered;
74
+ }
75
+ changed = true;
76
+ }
77
+ }
78
+ if (Object.keys(hooks).length === 0) delete data.hooks;
79
+ }
80
+ if (changed) {
81
+ writeJson(path, data);
82
+ console.log(` ${check} ${label} ${dim("(cleaned)")}`);
83
+ } else {
84
+ console.log(` ${skip} ${label} ${dim("(no pushary entries)")}`);
85
+ }
86
+ };
87
+ var main = async () => {
88
+ console.log();
89
+ console.log(` ${bold("Pushary Clean")}`);
90
+ console.log(` ${dim("Removes all Pushary configuration")}`);
91
+ console.log();
92
+ const proceed = await confirm({ message: "Remove all Pushary configuration?", default: false });
93
+ if (!proceed) {
94
+ console.log(` ${dim("Cancelled.")}`);
95
+ process.exit(0);
96
+ }
97
+ console.log();
98
+ cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings");
99
+ cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local");
100
+ const cursorData = readJson(CURSOR_MCP);
101
+ if (cursorData) {
102
+ const mcpServers = cursorData.mcpServers;
103
+ if (mcpServers?.pushary) {
104
+ delete mcpServers.pushary;
105
+ writeJson(CURSOR_MCP, cursorData);
106
+ console.log(` ${check} Cursor MCP config ${dim("(cleaned)")}`);
107
+ } else {
108
+ console.log(` ${skip} Cursor MCP config ${dim("(no pushary entries)")}`);
109
+ }
110
+ } else {
111
+ console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
112
+ }
113
+ if (existsSync(SKILL_DIR)) {
114
+ rmSync(SKILL_DIR, { recursive: true });
115
+ console.log(` ${check} Skill directory ${dim("(removed)")}`);
116
+ } else {
117
+ console.log(` ${skip} Skill directory ${dim("(not found)")}`);
118
+ }
119
+ const codexConfig = join(homedir(), ".codex", "config.toml");
120
+ try {
121
+ let config = readFileSync(codexConfig, "utf-8");
122
+ if (config.includes("pushary-codex")) {
123
+ config = config.split("\n").filter((l) => !l.includes("pushary-codex")).join("\n");
124
+ writeFileSync(codexConfig, config, "utf-8");
125
+ console.log(` ${check} Codex config ${dim("(cleaned)")}`);
126
+ } else {
127
+ console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
128
+ }
129
+ } catch {
130
+ console.log(` ${skip} Codex config ${dim("(not found)")}`);
131
+ }
132
+ for (const shellFile of SHELL_FILES) {
133
+ try {
134
+ const content = readFileSync(shellFile, "utf-8");
135
+ if (content.includes("PUSHARY_API_KEY")) {
136
+ const cleaned = content.split("\n").filter((l) => !l.includes("PUSHARY_API_KEY")).join("\n");
137
+ writeFileSync(shellFile, cleaned, "utf-8");
138
+ console.log(` ${check} ${shellFile.split("/").pop()} ${dim("(removed API key)")}`);
139
+ }
140
+ } catch {
141
+ }
142
+ }
143
+ try {
144
+ execSync("npm uninstall -g @pushary/agent-hooks", { stdio: "ignore" });
145
+ console.log(` ${check} Global package ${dim("(uninstalled)")}`);
146
+ } catch {
147
+ console.log(` ${skip} Global package ${dim("(not installed)")}`);
148
+ }
149
+ console.log();
150
+ console.log(` ${green(bold("Clean complete."))}`);
151
+ console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
152
+ console.log();
153
+ };
154
+ main();
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/pushary-clean.ts
4
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { execSync } from "child_process";
8
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
9
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
10
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
11
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
12
+ var check = green("\u2713");
13
+ var skip = yellow("\u2013");
14
+ var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
15
+ var CLAUDE_SETTINGS_LOCAL = join(homedir(), ".claude", "settings.local.json");
16
+ var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
17
+ var CURSOR_MCP = join(".cursor", "mcp.json");
18
+ var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
19
+ var readJson = (path) => {
20
+ try {
21
+ return JSON.parse(readFileSync(path, "utf-8"));
22
+ } catch {
23
+ return null;
24
+ }
25
+ };
26
+ var writeJson = (path, data) => {
27
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
28
+ };
29
+ var isPusharyPermission = (rule) => rule.includes("pushary") || rule.includes("MCP(pushary");
30
+ var isPusharyHook = (entry) => {
31
+ const hooks = entry.hooks;
32
+ if (!hooks) return false;
33
+ return hooks.some((h) => {
34
+ const cmd = String(h.command ?? "");
35
+ return cmd.includes("pushary-hook") || cmd.includes("pushary-post-hook") || cmd.includes("pushary-stop-hook");
36
+ });
37
+ };
38
+ var cleanSettingsFile = (path, label) => {
39
+ const data = readJson(path);
40
+ if (!data) {
41
+ console.log(` ${skip} ${label} ${dim("(not found)")}`);
42
+ return;
43
+ }
44
+ let changed = false;
45
+ const mcpServers = data.mcpServers;
46
+ if (mcpServers?.pushary) {
47
+ delete mcpServers.pushary;
48
+ if (Object.keys(mcpServers).length === 0) delete data.mcpServers;
49
+ changed = true;
50
+ }
51
+ const permissions = data.permissions;
52
+ if (permissions?.allow) {
53
+ const allow = permissions.allow;
54
+ const filtered = allow.filter((r) => !isPusharyPermission(r));
55
+ if (filtered.length !== allow.length) {
56
+ permissions.allow = filtered;
57
+ if (filtered.length === 0) delete permissions.allow;
58
+ if (Object.keys(permissions).length === 0) delete data.permissions;
59
+ changed = true;
60
+ }
61
+ }
62
+ const hooks = data.hooks;
63
+ if (hooks) {
64
+ for (const key of ["PreToolUse", "PostToolUse", "Stop"]) {
65
+ const entries = hooks[key];
66
+ if (!entries) continue;
67
+ const filtered = entries.filter((e) => !isPusharyHook(e));
68
+ if (filtered.length !== entries.length) {
69
+ if (filtered.length === 0) {
70
+ delete hooks[key];
71
+ } else {
72
+ hooks[key] = filtered;
73
+ }
74
+ changed = true;
75
+ }
76
+ }
77
+ if (Object.keys(hooks).length === 0) delete data.hooks;
78
+ }
79
+ if (changed) {
80
+ writeJson(path, data);
81
+ console.log(` ${check} ${label} ${dim("(cleaned)")}`);
82
+ } else {
83
+ console.log(` ${skip} ${label} ${dim("(no pushary entries)")}`);
84
+ }
85
+ };
86
+ var main = async () => {
87
+ console.log();
88
+ console.log(` ${bold("Pushary Clean")}`);
89
+ console.log(` ${dim("Removes all Pushary configuration")}`);
90
+ console.log();
91
+ cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings");
92
+ cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local");
93
+ const cursorData = readJson(CURSOR_MCP);
94
+ if (cursorData) {
95
+ const mcpServers = cursorData.mcpServers;
96
+ if (mcpServers?.pushary) {
97
+ delete mcpServers.pushary;
98
+ writeJson(CURSOR_MCP, cursorData);
99
+ console.log(` ${check} Cursor MCP config ${dim("(cleaned)")}`);
100
+ } else {
101
+ console.log(` ${skip} Cursor MCP config ${dim("(no pushary entries)")}`);
102
+ }
103
+ } else {
104
+ console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
105
+ }
106
+ if (existsSync(SKILL_DIR)) {
107
+ rmSync(SKILL_DIR, { recursive: true });
108
+ console.log(` ${check} Skill directory ${dim("(removed)")}`);
109
+ } else {
110
+ console.log(` ${skip} Skill directory ${dim("(not found)")}`);
111
+ }
112
+ const codexConfig = join(homedir(), ".codex", "config.toml");
113
+ try {
114
+ let config = readFileSync(codexConfig, "utf-8");
115
+ if (config.includes("pushary-codex")) {
116
+ config = config.split("\n").filter((l) => !l.includes("pushary-codex")).join("\n");
117
+ writeFileSync(codexConfig, config, "utf-8");
118
+ console.log(` ${check} Codex config ${dim("(cleaned)")}`);
119
+ } else {
120
+ console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
121
+ }
122
+ } catch {
123
+ console.log(` ${skip} Codex config ${dim("(not found)")}`);
124
+ }
125
+ for (const shellFile of SHELL_FILES) {
126
+ try {
127
+ const content = readFileSync(shellFile, "utf-8");
128
+ if (content.includes("PUSHARY_API_KEY")) {
129
+ const cleaned = content.split("\n").filter((l) => !l.includes("PUSHARY_API_KEY")).join("\n");
130
+ writeFileSync(shellFile, cleaned, "utf-8");
131
+ console.log(` ${check} ${shellFile.split("/").pop()} ${dim("(removed API key)")}`);
132
+ }
133
+ } catch {
134
+ }
135
+ }
136
+ try {
137
+ execSync("npm uninstall -g @pushary/agent-hooks", { stdio: "ignore" });
138
+ console.log(` ${check} Global package ${dim("(uninstalled)")}`);
139
+ } catch {
140
+ console.log(` ${skip} Global package ${dim("(not installed)")}`);
141
+ }
142
+ console.log();
143
+ console.log(` ${green(bold("Clean complete."))}`);
144
+ console.log(` ${dim("Run")} npx @pushary/agent-hooks@latest setup ${dim("to reinstall.")}`);
145
+ console.log();
146
+ };
147
+ main();