@pushary/agent-hooks 0.2.8 → 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();
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-5ZMTG7GF.js";
4
+ } from "../chunk-KINE5LNQ.js";
5
+ import "../chunk-4TWRLEOX.js";
6
+ import "../chunk-VUNL35KE.js";
5
7
 
6
8
  // bin/pushary-hook.ts
7
9
  var main = async () => {
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ handlePostToolUse
4
+ } from "../chunk-EQE6Z4YQ.js";
5
+ import "../chunk-VUNL35KE.js";
6
+
7
+ // bin/pushary-post-hook.ts
8
+ var main = async () => {
9
+ let rawInput = "";
10
+ for await (const chunk of process.stdin) {
11
+ rawInput += chunk;
12
+ }
13
+ if (!rawInput.trim()) {
14
+ process.exit(0);
15
+ }
16
+ try {
17
+ const input = JSON.parse(rawInput);
18
+ await handlePostToolUse(input);
19
+ } catch {
20
+ }
21
+ };
22
+ 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
  };
@@ -72,42 +89,86 @@ var addPermissionHooks = (settings) => {
72
89
  }]
73
90
  });
74
91
  hooks.PreToolUse = preToolUse;
75
- settings.hooks = hooks;
76
92
  }
93
+ const postToolUse = hooks.PostToolUse ?? [];
94
+ if (!JSON.stringify(postToolUse).includes("pushary-post-hook")) {
95
+ postToolUse.push({
96
+ matcher: "Bash|Write|Edit",
97
+ hooks: [{
98
+ type: "command",
99
+ command: "pushary-post-hook",
100
+ timeout: 10
101
+ }]
102
+ });
103
+ hooks.PostToolUse = postToolUse;
104
+ }
105
+ const stop = hooks.Stop ?? [];
106
+ if (!JSON.stringify(stop).includes("pushary-stop-hook")) {
107
+ stop.push({
108
+ hooks: [{
109
+ type: "command",
110
+ command: "pushary-stop-hook",
111
+ timeout: 10
112
+ }]
113
+ });
114
+ hooks.Stop = stop;
115
+ }
116
+ settings.hooks = hooks;
77
117
  };
78
118
  var addToolPermissions = (settings) => {
79
119
  const permissions = settings.permissions ?? {};
80
120
  const allow = permissions.allow ?? [];
121
+ const filtered = allow.filter((r) => !r.includes("pushary"));
81
122
  const rule = "MCP(pushary:*)";
82
- if (!allow.includes(rule)) allow.push(rule);
83
- permissions.allow = allow;
123
+ if (!filtered.includes(rule)) filtered.push(rule);
124
+ permissions.allow = filtered;
84
125
  settings.permissions = permissions;
85
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
+ };
86
145
  var setupClaudeCode = async (apiKey) => {
87
146
  console.log(`
88
147
  ${bold("Setting up Claude Code")}
89
148
  `);
90
149
  const settings = readJson(CLAUDE_SETTINGS);
91
- await spinner("Adding MCP server", async () => {
150
+ await spinner("Adding MCP server (type: http)", async () => {
92
151
  addMcpServer(settings, apiKey);
93
152
  });
94
153
  await spinner("Auto-allowing Pushary tools", async () => {
95
154
  addToolPermissions(settings);
96
155
  });
97
156
  await installGlobally();
98
- await spinner("Adding permission hooks (Bash, Write, Edit)", async () => {
157
+ await spinner("Adding hooks (PreToolUse, PostToolUse, Stop)", async () => {
99
158
  addPermissionHooks(settings);
100
159
  });
101
160
  await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
102
161
  writeJson(CLAUDE_SETTINGS, settings);
103
162
  });
163
+ await installSkill();
104
164
  console.log();
105
165
  console.log(` ${dim("What this configured:")}`);
106
166
  console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
107
- console.log(` ${dim("\u2022")} Permission hooks: approve Bash/Write/Edit from your phone`);
108
- 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`);
109
170
  };
110
- var setupHermes = async (apiKey) => {
171
+ var setupHermes = async (_apiKey) => {
111
172
  console.log(`
112
173
  ${bold("Setting up Hermes Agent")}
113
174
  `);
@@ -132,7 +193,54 @@ var setupHermes = async (apiKey) => {
132
193
  console.log(` ${dim("What this configured:")}`);
133
194
  console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
134
195
  console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
135
- console.log(` ${dim("\u2022")} Session alerts: opt-in with PUSHARY_AUTO_NOTIFY_SESSION_END=1`);
196
+ };
197
+ var setupCodex = async (_apiKey) => {
198
+ console.log(`
199
+ ${bold("Setting up Codex")}
200
+ `);
201
+ const hasCodex = (() => {
202
+ try {
203
+ execSync("which codex", { stdio: "ignore" });
204
+ return true;
205
+ } catch {
206
+ return false;
207
+ }
208
+ })();
209
+ if (!hasCodex) {
210
+ console.log(` ${yellow("!")} Codex CLI not found. Skipping.`);
211
+ console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
212
+ return;
213
+ }
214
+ await installGlobally();
215
+ await spinner("Adding Pushary MCP server to Codex", async () => {
216
+ try {
217
+ execSync(
218
+ "codex mcp add pushary --url https://pushary.com/api/mcp/mcp --bearer-token-env-var PUSHARY_API_KEY",
219
+ { stdio: "ignore" }
220
+ );
221
+ } catch {
222
+ }
223
+ });
224
+ const codexConfig = join(homedir(), ".codex", "config.toml");
225
+ await spinner("Adding notify handler for Codex events", async () => {
226
+ const pusharyCodexPath = execSync("which pushary-codex", { encoding: "utf-8" }).trim();
227
+ if (!pusharyCodexPath) throw new Error("pushary-codex not found");
228
+ let config = "";
229
+ try {
230
+ config = readFileSync(codexConfig, "utf-8");
231
+ } catch {
232
+ }
233
+ if (!config.includes("pushary-codex")) {
234
+ const notifyLine = `
235
+ notify = ["${pusharyCodexPath}"]
236
+ `;
237
+ appendFileSync(codexConfig, notifyLine, "utf-8");
238
+ }
239
+ });
240
+ console.log();
241
+ console.log(` ${dim("What this configured:")}`);
242
+ console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
243
+ console.log(` ${dim("\u2022")} Notify handler: captures turn completions and approval requests`);
136
244
  };
137
245
  var setupCursor = async (apiKey) => {
138
246
  console.log(`
@@ -142,12 +250,14 @@ var setupCursor = async (apiKey) => {
142
250
  const config = readJson(CURSOR_MCP);
143
251
  const mcpServers = config.mcpServers ?? {};
144
252
  mcpServers.pushary = {
253
+ type: "http",
145
254
  url: "https://pushary.com/api/mcp/mcp",
146
255
  headers: { Authorization: `Bearer ${apiKey}` }
147
256
  };
148
257
  config.mcpServers = mcpServers;
149
258
  writeJson(CURSOR_MCP, config);
150
259
  });
260
+ await installSkill();
151
261
  };
152
262
  var saveApiKey = async (apiKey) => {
153
263
  await spinner("Saving API key to shell profile", async () => {
@@ -165,8 +275,12 @@ export PUSHARY_API_KEY="${apiKey}"
165
275
  });
166
276
  };
167
277
  var sendTestNotification = async (apiKey) => {
168
- let notifResult = "";
169
- 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 {
170
284
  const response = await fetch("https://pushary.com/api/v1/server/send", {
171
285
  method: "POST",
172
286
  headers: {
@@ -179,69 +293,68 @@ var sendTestNotification = async (apiKey) => {
179
293
  })
180
294
  });
181
295
  const data = await response.json().catch(() => ({}));
296
+ clearInterval(interval);
182
297
  if (!response.ok) {
183
- notifResult = `Could not send: ${data.error ?? response.statusText}`;
184
- 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!")}`);
185
306
  }
186
- notifResult = "sent";
187
- });
188
- if (notifResult === "sent") {
189
- console.log(` ${dim("Check your phone!")}`);
190
- } else if (notifResult) {
191
- console.log(` ${dim(notifResult)}`);
192
- 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
+ `);
193
312
  }
194
313
  };
314
+ var AGENT_SETUP = {
315
+ claude_code: setupClaudeCode,
316
+ codex: setupCodex,
317
+ hermes: setupHermes,
318
+ cursor: setupCursor
319
+ };
195
320
  var main = async () => {
321
+ const version = process.env.npm_package_version ?? "0.3";
196
322
  console.log();
197
- console.log(` ${bold("Pushary")} ${dim("v" + (process.env.npm_package_version ?? "0.2"))}`);
323
+ console.log(` ${bold("Pushary")} ${dim("v" + version)}`);
198
324
  console.log(` ${dim("Push notifications for AI coding agents")}`);
199
325
  console.log();
326
+ await checkForUpdates();
200
327
  console.log(` ${dim("Get your API key at")} ${cyan("pushary.com/sign-up")}`);
201
328
  console.log();
202
- const apiKey = await ask(` API key: `);
329
+ const apiKey = await input({ message: "API key:" });
203
330
  if (!apiKey.trim() || !apiKey.includes(".")) {
204
331
  console.log(`
205
332
  ${yellow("!")} Invalid key format. Expected: pk_xxx.sk_xxx`);
206
333
  console.log(` ${dim("Get yours at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
207
334
  `);
208
- rl.close();
209
335
  process.exit(1);
210
336
  }
211
337
  const trimmedKey = apiKey.trim();
212
- console.log();
213
- console.log(` ${bold("Which agent do you use?")}`);
214
- console.log();
215
- console.log(` ${cyan("1.")} Claude Code ${dim("MCP + permission hooks + auto-allowed tools")}`);
216
- console.log(` ${cyan("2.")} Hermes ${dim("native plugin + auto-error notifications")}`);
217
- console.log(` ${cyan("3.")} Cursor ${dim("MCP server")}`);
218
- console.log(` ${cyan("4.")} All ${dim("configure everything")}`);
219
- console.log(` ${cyan("5.")} Other ${dim("just save the API key")}`);
220
- console.log();
221
- const choice = await ask(` Choice ${dim("[1-5]")}: `);
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
+ });
222
347
  await saveApiKey(trimmedKey);
223
- switch (choice.trim()) {
224
- case "1":
225
- await setupClaudeCode(trimmedKey);
226
- break;
227
- case "2":
228
- await setupHermes(trimmedKey);
229
- break;
230
- case "3":
231
- await setupCursor(trimmedKey);
232
- break;
233
- case "4":
234
- await setupClaudeCode(trimmedKey);
235
- await setupHermes(trimmedKey);
236
- await setupCursor(trimmedKey);
237
- break;
238
- case "5":
239
- default:
240
- 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
+ }
241
355
  }
242
- console.log();
243
- const test = await ask(` Send a test notification? ${dim("[Y/n]")} `);
244
- if (test.toLowerCase() !== "n") {
356
+ const sendTest = await confirm({ message: "Send a test notification?", default: true });
357
+ if (sendTest) {
245
358
  await sendTestNotification(trimmedKey);
246
359
  }
247
360
  console.log();
@@ -250,8 +363,7 @@ var main = async () => {
250
363
  console.log(` ${dim("Next:")}`);
251
364
  console.log(` ${dim("1.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
252
365
  console.log(` ${dim("2.")} Restart your agent to load the new config`);
253
- console.log(` ${dim("3.")} Start coding \u2014 your agent will notify you automatically`);
366
+ console.log(` ${dim("3.")} Run ${cyan("npx @pushary/agent-hooks doctor")} to verify`);
254
367
  console.log();
255
- rl.close();
256
368
  };
257
369
  main();
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ handleStop
4
+ } from "../chunk-EQE6Z4YQ.js";
5
+ import "../chunk-VUNL35KE.js";
6
+
7
+ // bin/pushary-stop-hook.ts
8
+ var main = async () => {
9
+ let rawInput = "";
10
+ for await (const chunk of process.stdin) {
11
+ rawInput += chunk;
12
+ }
13
+ try {
14
+ const input = rawInput.trim() ? JSON.parse(rawInput) : {};
15
+ await handleStop(input);
16
+ } catch {
17
+ }
18
+ };
19
+ main();