@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.
- package/dist/bin/pushary-clean.d.ts +1 -0
- package/dist/bin/pushary-clean.js +154 -0
- package/dist/bin/pushary-doctor.d.ts +1 -0
- package/dist/bin/pushary-doctor.js +252 -0
- package/dist/bin/pushary-setup.js +103 -69
- package/dist/bin/pushary.js +10 -3
- package/dist/pushary-clean-M5RW2DG6.js +154 -0
- package/dist/pushary-clean-RM6TBJ3H.js +147 -0
- package/dist/pushary-doctor-EHLTPBD3.js +252 -0
- package/dist/pushary-doctor-LYMEFIZN.js +254 -0
- package/package.json +10 -5
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
|
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 (!
|
|
106
|
-
permissions.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")}
|
|
131
|
-
console.log(` ${dim("\u2022")}
|
|
132
|
-
console.log(` ${dim("\u2022")}
|
|
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 (
|
|
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=
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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" +
|
|
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
|
|
277
|
-
if (!apiKey.trim() ||
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
await
|
|
304
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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.")}
|
|
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();
|
package/dist/bin/pushary.js
CHANGED
|
@@ -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
|
|
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();
|