@pushary/agent-hooks 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/pushary-doctor.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { confirm } from "@inquirer/prompts";
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 red = (s) => `\x1B[31m${s}\x1B[0m`;
12
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
13
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
14
+ var pass = green("\u2713");
15
+ var fail = red("\u2717");
16
+ var warn = yellow("!");
17
+ var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
18
+ var SKILL_PATH = join(homedir(), ".claude", "skills", "pushary", "SKILL.md");
19
+ var MCP_URL = "https://pushary.com/api/mcp/mcp";
20
+ var readJson = (path) => {
21
+ try {
22
+ return JSON.parse(readFileSync(path, "utf-8"));
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+ var results = [];
28
+ var check = (passed, label, detail) => {
29
+ results.push({ passed, label, detail });
30
+ const icon = passed ? pass : fail;
31
+ const suffix = detail ? ` ${dim(`(${detail})`)}` : "";
32
+ console.log(` ${icon} ${label}${suffix}`);
33
+ };
34
+ var main = async () => {
35
+ console.log();
36
+ console.log(` ${bold("Pushary Doctor")}`);
37
+ console.log();
38
+ console.log(` ${dim("Configuration")}`);
39
+ const apiKey = process.env.PUSHARY_API_KEY;
40
+ check(!!apiKey, "API key in environment", apiKey ? `pk_${apiKey.split(".")[0]?.slice(3, 7)}...` : "PUSHARY_API_KEY not set");
41
+ const settings = readJson(CLAUDE_SETTINGS);
42
+ if (settings) {
43
+ const mcpServers = settings.mcpServers;
44
+ const pusharyServer = mcpServers?.pushary;
45
+ check(!!pusharyServer, "Claude Code: MCP server configured");
46
+ if (pusharyServer) {
47
+ check(pusharyServer.type === "http", "Claude Code: MCP server type", pusharyServer.type ? String(pusharyServer.type) : 'missing \u2014 add type: "http"');
48
+ }
49
+ const hooks = settings.hooks;
50
+ const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
51
+ const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
52
+ const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
53
+ check(hasPreHook, "Claude Code: PreToolUse hook");
54
+ check(hasPostHook, "Claude Code: PostToolUse hook");
55
+ check(hasStopHook, "Claude Code: Stop hook");
56
+ const permissions = settings.permissions;
57
+ const hasWildcard = permissions?.allow?.some((r) => r === "MCP(pushary:*)") ?? false;
58
+ check(hasWildcard, "Claude Code: Pushary tools auto-allowed", hasWildcard ? "MCP(pushary:*)" : "missing");
59
+ const hasLegacyPerms = permissions?.allow?.some((r) => r.startsWith("mcp__pushary__")) ?? false;
60
+ if (hasLegacyPerms) {
61
+ console.log(` ${warn} Legacy individual permissions detected ${dim("(run pushary clean, then setup again)")}`);
62
+ }
63
+ } else {
64
+ check(false, "Claude Code: settings.json", "not found");
65
+ }
66
+ check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
67
+ let globalVersion = "";
68
+ try {
69
+ const { execSync } = await import("child_process");
70
+ globalVersion = execSync("npm list -g @pushary/agent-hooks --depth=0 2>/dev/null", { encoding: "utf-8" }).match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
71
+ } catch {
72
+ }
73
+ check(!!globalVersion, "Global package installed", globalVersion || "not found");
74
+ console.log();
75
+ console.log(` ${dim("Connectivity")}`);
76
+ if (!apiKey) {
77
+ check(false, "MCP server reachable", "skipped \u2014 no API key");
78
+ check(false, "API key valid", "skipped");
79
+ check(false, "MCP handshake", "skipped");
80
+ } else {
81
+ let sessionId = "";
82
+ let toolCount = 0;
83
+ try {
84
+ const initRes = await fetch(MCP_URL, {
85
+ method: "POST",
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ "Accept": "application/json, text/event-stream",
89
+ "Authorization": `Bearer ${apiKey}`
90
+ },
91
+ body: JSON.stringify({
92
+ jsonrpc: "2.0",
93
+ id: 1,
94
+ method: "initialize",
95
+ params: {
96
+ protocolVersion: "2025-03-26",
97
+ capabilities: {},
98
+ clientInfo: { name: "pushary-doctor", version: "1.0" }
99
+ }
100
+ }),
101
+ signal: AbortSignal.timeout(1e4)
102
+ });
103
+ check(initRes.ok, "MCP server reachable", `${initRes.status} ${initRes.statusText}`);
104
+ sessionId = initRes.headers.get("mcp-session-id") ?? "";
105
+ check(!!sessionId, "Session ID returned", sessionId ? `${sessionId.slice(0, 8)}...` : "missing \u2014 update MCP SDK");
106
+ const initBody = await initRes.text();
107
+ const initMatch = initBody.match(/data: (.+)/);
108
+ if (initMatch) {
109
+ const initData = JSON.parse(initMatch[1]);
110
+ check(!!initData.result?.serverInfo, "API key valid", initData.result?.serverInfo?.name ?? "unknown server");
111
+ }
112
+ const toolsRes = await fetch(MCP_URL, {
113
+ method: "POST",
114
+ headers: {
115
+ "Content-Type": "application/json",
116
+ "Accept": "application/json, text/event-stream",
117
+ "Authorization": `Bearer ${apiKey}`,
118
+ ...sessionId ? { "Mcp-Session-Id": sessionId } : {}
119
+ },
120
+ body: JSON.stringify({
121
+ jsonrpc: "2.0",
122
+ id: 2,
123
+ method: "tools/list",
124
+ params: {}
125
+ }),
126
+ signal: AbortSignal.timeout(1e4)
127
+ });
128
+ const toolsBody = await toolsRes.text();
129
+ const toolsMatch = toolsBody.match(/data: (.+)/);
130
+ if (toolsMatch) {
131
+ const toolsData = JSON.parse(toolsMatch[1]);
132
+ toolCount = toolsData.result?.tools?.length ?? 0;
133
+ }
134
+ check(toolCount > 0, "MCP tools discovered", `${toolCount} tools`);
135
+ } catch (err) {
136
+ const msg = err instanceof Error ? err.message : "unknown error";
137
+ check(false, "MCP server reachable", msg);
138
+ }
139
+ console.log();
140
+ console.log(` ${dim("Notification Delivery")}`);
141
+ try {
142
+ const notifRes = await fetch("https://pushary.com/api/v1/server/send", {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ "Authorization": `Bearer ${apiKey}`
147
+ },
148
+ body: JSON.stringify({
149
+ title: "Pushary Doctor",
150
+ body: "If you see this, push notifications are working."
151
+ }),
152
+ signal: AbortSignal.timeout(1e4)
153
+ });
154
+ check(notifRes.ok, "Push notification sent", notifRes.ok ? "check your phone" : `${notifRes.status}`);
155
+ } catch (err) {
156
+ const msg = err instanceof Error ? err.message : "network error";
157
+ check(false, "Push notification sent", msg);
158
+ }
159
+ const testQuestion = await confirm({ message: "Test question roundtrip? (sends a push notification)", default: false });
160
+ if (testQuestion) {
161
+ console.log();
162
+ console.log(` ${dim("Question Roundtrip")}`);
163
+ try {
164
+ const askRes = await fetch(MCP_URL, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/json",
168
+ "Accept": "application/json, text/event-stream",
169
+ "Authorization": `Bearer ${apiKey}`
170
+ },
171
+ body: JSON.stringify({
172
+ jsonrpc: "2.0",
173
+ id: 3,
174
+ method: "tools/call",
175
+ params: {
176
+ name: "ask_user",
177
+ arguments: {
178
+ question: "Pushary Doctor: tap Yes to verify the roundtrip works.",
179
+ type: "confirm",
180
+ agentName: "Pushary Doctor"
181
+ }
182
+ }
183
+ }),
184
+ signal: AbortSignal.timeout(15e3)
185
+ });
186
+ const askBody = await askRes.text();
187
+ const askMatch = askBody.match(/data: (.+)/);
188
+ let correlationId = "";
189
+ if (askMatch) {
190
+ const askData = JSON.parse(askMatch[1]);
191
+ const content = askData.result?.content?.[0]?.text;
192
+ if (content) {
193
+ const parsed = JSON.parse(content);
194
+ correlationId = parsed.correlationId;
195
+ }
196
+ }
197
+ if (correlationId) {
198
+ console.log(` ${dim("\u2192")} Question sent, waiting for your answer...`);
199
+ const start = Date.now();
200
+ const waitRes = await fetch(MCP_URL, {
201
+ method: "POST",
202
+ headers: {
203
+ "Content-Type": "application/json",
204
+ "Accept": "application/json, text/event-stream",
205
+ "Authorization": `Bearer ${apiKey}`
206
+ },
207
+ body: JSON.stringify({
208
+ jsonrpc: "2.0",
209
+ id: 4,
210
+ method: "tools/call",
211
+ params: {
212
+ name: "wait_for_answer",
213
+ arguments: { correlationId, timeoutMs: 55e3 }
214
+ }
215
+ }),
216
+ signal: AbortSignal.timeout(6e4)
217
+ });
218
+ const waitBody = await waitRes.text();
219
+ const waitMatch = waitBody.match(/data: (.+)/);
220
+ if (waitMatch) {
221
+ const waitData = JSON.parse(waitMatch[1]);
222
+ const content = waitData.result?.content?.[0]?.text;
223
+ if (content) {
224
+ const parsed = JSON.parse(content);
225
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
226
+ check(parsed.answered === true, "Answer received", `"${parsed.value}" (${elapsed}s roundtrip)`);
227
+ } else {
228
+ check(false, "Answer received", "no response within timeout");
229
+ }
230
+ }
231
+ } else {
232
+ check(false, "Question sent", "failed to get correlationId");
233
+ }
234
+ } catch (err) {
235
+ const msg = err instanceof Error ? err.message : "unknown error";
236
+ check(false, "Question roundtrip", msg);
237
+ }
238
+ }
239
+ }
240
+ console.log();
241
+ const failed = results.filter((r) => !r.passed);
242
+ if (failed.length === 0) {
243
+ console.log(` ${green(bold("All checks passed."))}`);
244
+ } else {
245
+ console.log(` ${red(bold(`${failed.length} check${failed.length === 1 ? "" : "s"} failed:`))}`);
246
+ for (const f of failed) {
247
+ console.log(` ${fail} ${f.label}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
248
+ }
249
+ console.log();
250
+ console.log(` ${dim("Run")} ${cyan("npx @pushary/agent-hooks@latest clean")} ${dim("then")} ${cyan("setup")} ${dim("to fix.")}`);
251
+ }
252
+ console.log();
253
+ };
254
+ main();
@@ -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,28 @@ 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 checkForUpdates = async () => {
50
+ try {
51
+ const res = await fetch("https://registry.npmjs.org/@pushary/agent-hooks/latest", {
52
+ signal: AbortSignal.timeout(3e3)
53
+ });
54
+ const data = await res.json();
55
+ const latest = data.version;
56
+ const current = process.env.npm_package_version;
57
+ if (latest && current && latest !== current) {
58
+ console.log(` ${yellow("!")} Update available: ${dim(current)} \u2192 ${green(latest)}`);
59
+ console.log(` ${dim("Run:")} npx @pushary/agent-hooks@${latest} setup`);
60
+ console.log();
61
+ }
62
+ } catch {
63
+ }
64
+ };
49
65
  var installGlobally = async () => {
50
66
  await spinner("Installing pushary-hook globally", async () => {
51
67
  execSync("npm install -g @pushary/agent-hooks@latest", { stdio: "ignore" });
@@ -54,6 +70,7 @@ var installGlobally = async () => {
54
70
  var addMcpServer = (settings, apiKey) => {
55
71
  const mcpServers = settings.mcpServers ?? {};
56
72
  mcpServers.pushary = {
73
+ type: "http",
57
74
  url: "https://pushary.com/api/mcp/mcp",
58
75
  headers: { Authorization: `Bearer ${apiKey}` }
59
76
  };
@@ -101,17 +118,36 @@ var addPermissionHooks = (settings) => {
101
118
  var addToolPermissions = (settings) => {
102
119
  const permissions = settings.permissions ?? {};
103
120
  const allow = permissions.allow ?? [];
121
+ const filtered = allow.filter((r) => !r.includes("pushary"));
104
122
  const rule = "MCP(pushary:*)";
105
- if (!allow.includes(rule)) allow.push(rule);
106
- permissions.allow = allow;
123
+ if (!filtered.includes(rule)) filtered.push(rule);
124
+ permissions.allow = filtered;
107
125
  settings.permissions = permissions;
108
126
  };
127
+ var installSkill = async () => {
128
+ await spinner("Installing Pushary skill", async () => {
129
+ const __dirname = dirname(fileURLToPath(import.meta.url));
130
+ const skillSource = join(__dirname, "..", "data", "SKILL.md");
131
+ let content;
132
+ if (existsSync(skillSource)) {
133
+ content = readFileSync(skillSource, "utf-8");
134
+ } else {
135
+ const res = await fetch("https://raw.githubusercontent.com/pushary/pushary-skill/main/skills/pushary/SKILL.md", {
136
+ signal: AbortSignal.timeout(5e3)
137
+ });
138
+ if (!res.ok) throw new Error("Failed to fetch skill");
139
+ content = await res.text();
140
+ }
141
+ if (!existsSync(SKILL_DIR)) mkdirSync(SKILL_DIR, { recursive: true });
142
+ writeFileSync(join(SKILL_DIR, "SKILL.md"), content, "utf-8");
143
+ });
144
+ };
109
145
  var setupClaudeCode = async (apiKey) => {
110
146
  console.log(`
111
147
  ${bold("Setting up Claude Code")}
112
148
  `);
113
149
  const settings = readJson(CLAUDE_SETTINGS);
114
- await spinner("Adding MCP server", async () => {
150
+ await spinner("Adding MCP server (type: http)", async () => {
115
151
  addMcpServer(settings, apiKey);
116
152
  });
117
153
  await spinner("Auto-allowing Pushary tools", async () => {
@@ -124,15 +160,15 @@ var setupClaudeCode = async (apiKey) => {
124
160
  await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
125
161
  writeJson(CLAUDE_SETTINGS, settings);
126
162
  });
163
+ await installSkill();
127
164
  console.log();
128
165
  console.log(` ${dim("What this configured:")}`);
129
166
  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`);
167
+ console.log(` ${dim("\u2022")} Skill: teaches your agent when and how to use Pushary`);
168
+ console.log(` ${dim("\u2022")} Hooks: route permission approvals through push notifications`);
169
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
134
170
  };
135
- var setupHermes = async (apiKey) => {
171
+ var setupHermes = async (_apiKey) => {
136
172
  console.log(`
137
173
  ${bold("Setting up Hermes Agent")}
138
174
  `);
@@ -157,7 +193,6 @@ var setupHermes = async (apiKey) => {
157
193
  console.log(` ${dim("What this configured:")}`);
158
194
  console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
159
195
  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
196
  };
162
197
  var setupCodex = async (_apiKey) => {
163
198
  console.log(`
@@ -206,7 +241,6 @@ notify = ["${pusharyCodexPath}"]
206
241
  console.log(` ${dim("What this configured:")}`);
207
242
  console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
208
243
  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
244
  };
211
245
  var setupCursor = async (apiKey) => {
212
246
  console.log(`
@@ -216,12 +250,14 @@ var setupCursor = async (apiKey) => {
216
250
  const config = readJson(CURSOR_MCP);
217
251
  const mcpServers = config.mcpServers ?? {};
218
252
  mcpServers.pushary = {
253
+ type: "http",
219
254
  url: "https://pushary.com/api/mcp/mcp",
220
255
  headers: { Authorization: `Bearer ${apiKey}` }
221
256
  };
222
257
  config.mcpServers = mcpServers;
223
258
  writeJson(CURSOR_MCP, config);
224
259
  });
260
+ await installSkill();
225
261
  };
226
262
  var saveApiKey = async (apiKey) => {
227
263
  await spinner("Saving API key to shell profile", async () => {
@@ -239,8 +275,12 @@ export PUSHARY_API_KEY="${apiKey}"
239
275
  });
240
276
  };
241
277
  var sendTestNotification = async (apiKey) => {
242
- let notifResult = "";
243
- await spinner("Sending test notification", async () => {
278
+ const frames = [" ", ". ", ".. ", "..."];
279
+ let i = 0;
280
+ const interval = setInterval(() => {
281
+ process.stdout.write(`\r ${dim(frames[i++ % frames.length])} Sending test notification`);
282
+ }, 200);
283
+ try {
244
284
  const response = await fetch("https://pushary.com/api/v1/server/send", {
245
285
  method: "POST",
246
286
  headers: {
@@ -253,74 +293,68 @@ var sendTestNotification = async (apiKey) => {
253
293
  })
254
294
  });
255
295
  const data = await response.json().catch(() => ({}));
296
+ clearInterval(interval);
256
297
  if (!response.ok) {
257
- notifResult = `Could not send: ${data.error ?? response.statusText}`;
258
- return;
298
+ const reason = data.error ?? response.statusText;
299
+ process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${reason})`)}
300
+ `);
301
+ console.log(` ${dim("Make sure you enabled notifications at")} ${cyan("pushary.com")}`);
302
+ } else {
303
+ process.stdout.write(`\r ${check} Sending test notification
304
+ `);
305
+ console.log(` ${dim("Check your phone!")}`);
259
306
  }
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")}`);
307
+ } catch (err) {
308
+ clearInterval(interval);
309
+ const msg = err instanceof Error ? err.message : "network error";
310
+ process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${msg})`)}
311
+ `);
267
312
  }
268
313
  };
314
+ var AGENT_SETUP = {
315
+ claude_code: setupClaudeCode,
316
+ codex: setupCodex,
317
+ hermes: setupHermes,
318
+ cursor: setupCursor
319
+ };
269
320
  var main = async () => {
321
+ const version = process.env.npm_package_version ?? "0.3";
270
322
  console.log();
271
- console.log(` ${bold("Pushary")} ${dim("v" + (process.env.npm_package_version ?? "0.2"))}`);
323
+ console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
272
324
  console.log(` ${dim("Push notifications for AI coding agents")}`);
273
325
  console.log();
326
+ await checkForUpdates();
274
327
  console.log(` ${dim("Get your API key at")} ${cyan("pushary.com/sign-up")}`);
275
328
  console.log();
276
- const apiKey = await ask(` API key: `);
329
+ const apiKey = await input({ message: "API key:" });
277
330
  if (!apiKey.trim() || !apiKey.includes(".")) {
278
331
  console.log(`
279
332
  ${yellow("!")} Invalid key format. Expected: pk_xxx.sk_xxx`);
280
333
  console.log(` ${dim("Get yours at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
281
334
  `);
282
- rl.close();
283
335
  process.exit(1);
284
336
  }
285
337
  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]")}: `);
338
+ const agents = await checkbox({
339
+ message: "Which agents do you use? " + dim("(space = toggle, enter = confirm)"),
340
+ choices: [
341
+ { name: `Claude Code ${dim("MCP + hooks + auto-allowed tools")}`, value: "claude_code" },
342
+ { name: `Codex ${dim("MCP server via codex mcp add")}`, value: "codex" },
343
+ { name: `Hermes ${dim("native plugin + auto-error notifications")}`, value: "hermes" },
344
+ { name: `Cursor ${dim("MCP server")}`, value: "cursor" }
345
+ ]
346
+ });
297
347
  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;
348
+ if (agents.length === 0) {
349
+ console.log(`
350
+ ${dim("No agents selected. API key saved.")}`);
351
+ } else {
352
+ for (const agent of agents) {
353
+ await AGENT_SETUP[agent](trimmedKey);
354
+ }
320
355
  }
321
- console.log();
322
- const test = await ask(` Send a test notification? ${dim("[Y/n]")} `);
323
- if (test.toLowerCase() !== "n") {
356
+ const sendTest = await confirm({ message: "Send a test notification?", default: true });
357
+ if (sendTest) {
324
358
  await sendTestNotification(trimmedKey);
325
359
  }
326
360
  console.log();
@@ -329,8 +363,7 @@ var main = async () => {
329
363
  console.log(` ${dim("Next:")}`);
330
364
  console.log(` ${dim("1.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
331
365
  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`);
366
+ console.log(` ${dim("3.")} Run ${cyan("npx @pushary/agent-hooks doctor")} to verify`);
333
367
  console.log();
334
- rl.close();
335
368
  };
336
369
  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
  }