@pushary/agent-hooks 0.2.4 → 0.2.6

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.
@@ -5,6 +5,7 @@ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } fr
5
5
  import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { createInterface } from "readline";
8
+ import { execSync } from "child_process";
8
9
  var rl = createInterface({ input: process.stdin, output: process.stdout });
9
10
  var ask = (q) => new Promise((r) => rl.question(q, r));
10
11
  var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
@@ -14,6 +15,8 @@ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
14
15
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
15
16
  var green = (s) => `\x1B[32m${s}\x1B[0m`;
16
17
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
18
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
19
+ var check = green("\u2713");
17
20
  var readJson = (path) => {
18
21
  try {
19
22
  return JSON.parse(readFileSync(path, "utf-8"));
@@ -26,75 +29,154 @@ var writeJson = (path, data) => {
26
29
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
27
30
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
28
31
  };
29
- var setupClaudeCode = async (apiKey) => {
30
- console.log(`
31
- ${bold("Claude Code Setup")}
32
+ var spinner = async (label, fn) => {
33
+ const frames = [" ", ". ", ".. ", "..."];
34
+ let i = 0;
35
+ const interval = setInterval(() => {
36
+ process.stdout.write(`\r ${dim(frames[i++ % frames.length])} ${label}`);
37
+ }, 200);
38
+ try {
39
+ await fn();
40
+ clearInterval(interval);
41
+ process.stdout.write(`\r ${check} ${label}
32
42
  `);
33
- const settings = readJson(CLAUDE_SETTINGS);
43
+ } catch (err) {
44
+ clearInterval(interval);
45
+ process.stdout.write(`\r ${yellow("!")} ${label} ${dim("(skipped)")}
46
+ `);
47
+ }
48
+ };
49
+ var installGlobally = async () => {
50
+ await spinner("Installing pushary-hook globally", async () => {
51
+ execSync("npm install -g @pushary/agent-hooks@latest", { stdio: "ignore" });
52
+ });
53
+ };
54
+ var addMcpServer = (settings, apiKey) => {
34
55
  const mcpServers = settings.mcpServers ?? {};
35
56
  mcpServers.pushary = {
36
57
  url: "https://pushary.com/api/mcp/mcp",
37
58
  headers: { Authorization: `Bearer ${apiKey}` }
38
59
  };
39
60
  settings.mcpServers = mcpServers;
40
- const addHooks = await ask(`Add permission hooks? (approve/deny tools from your phone) ${dim("[Y/n]")} `);
41
- if (addHooks.toLowerCase() !== "n") {
42
- const hooks = settings.hooks ?? {};
43
- const preToolUse = hooks.PreToolUse ?? [];
44
- const alreadyHasPushary = JSON.stringify(preToolUse).includes("pushary-hook");
45
- if (!alreadyHasPushary) {
46
- preToolUse.push({
47
- matcher: "Bash|Write|Edit",
48
- hooks: [{
49
- type: "command",
50
- command: "pushary-hook",
51
- timeout: 120
52
- }]
53
- });
54
- hooks.PreToolUse = preToolUse;
55
- settings.hooks = hooks;
56
- }
61
+ };
62
+ var addPermissionHooks = (settings) => {
63
+ const hooks = settings.hooks ?? {};
64
+ const preToolUse = hooks.PreToolUse ?? [];
65
+ if (!JSON.stringify(preToolUse).includes("pushary-hook")) {
66
+ preToolUse.push({
67
+ matcher: "Bash|Write|Edit",
68
+ hooks: [{
69
+ type: "command",
70
+ command: "pushary-hook",
71
+ timeout: 120
72
+ }]
73
+ });
74
+ hooks.PreToolUse = preToolUse;
75
+ settings.hooks = hooks;
57
76
  }
58
- writeJson(CLAUDE_SETTINGS, settings);
59
- console.log(` ${green("done")} MCP server added to ${dim(CLAUDE_SETTINGS)}`);
60
- if (settings.hooks) {
61
- console.log(` ${green("done")} Permission hooks configured`);
77
+ };
78
+ var addToolPermissions = (settings) => {
79
+ const permissions = settings.permissions ?? {};
80
+ const allow = permissions.allow ?? [];
81
+ const tools = [
82
+ "mcp__pushary__send_notification",
83
+ "mcp__pushary__ask_user",
84
+ "mcp__pushary__ask_user_yes_no",
85
+ "mcp__pushary__wait_for_answer",
86
+ "mcp__pushary__cancel_question",
87
+ "mcp__pushary__list_subscribers",
88
+ "mcp__pushary__get_subscriber",
89
+ "mcp__pushary__count_subscribers"
90
+ ];
91
+ for (const tool of tools) {
92
+ if (!allow.includes(tool)) allow.push(tool);
62
93
  }
94
+ permissions.allow = allow;
95
+ settings.permissions = permissions;
96
+ };
97
+ var setupClaudeCode = async (apiKey) => {
98
+ console.log(`
99
+ ${bold("Setting up Claude Code")}
100
+ `);
101
+ const settings = readJson(CLAUDE_SETTINGS);
102
+ await spinner("Adding MCP server", async () => {
103
+ addMcpServer(settings, apiKey);
104
+ });
105
+ await spinner("Auto-allowing Pushary tools", async () => {
106
+ addToolPermissions(settings);
107
+ });
108
+ await installGlobally();
109
+ await spinner("Adding permission hooks (Bash, Write, Edit)", async () => {
110
+ addPermissionHooks(settings);
111
+ });
112
+ await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
113
+ writeJson(CLAUDE_SETTINGS, settings);
114
+ });
115
+ console.log();
116
+ console.log(` ${dim("What this configured:")}`);
117
+ console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
118
+ console.log(` ${dim("\u2022")} Permission hooks: approve Bash/Write/Edit from your phone`);
119
+ console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary`);
120
+ };
121
+ var setupHermes = async (apiKey) => {
122
+ console.log(`
123
+ ${bold("Setting up Hermes Agent")}
124
+ `);
125
+ await spinner("Installing hermes-plugin-pushary", async () => {
126
+ try {
127
+ execSync("pip install hermes-plugin-pushary", { stdio: "ignore" });
128
+ } catch {
129
+ try {
130
+ execSync("pip3 install hermes-plugin-pushary", { stdio: "ignore" });
131
+ } catch {
132
+ throw new Error("pip not found");
133
+ }
134
+ }
135
+ });
136
+ await spinner("Enabling plugin", async () => {
137
+ try {
138
+ execSync("hermes plugins enable pushary", { stdio: "ignore" });
139
+ } catch {
140
+ }
141
+ });
142
+ console.log();
143
+ console.log(` ${dim("What this configured:")}`);
144
+ console.log(` ${dim("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
145
+ console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
146
+ console.log(` ${dim("\u2022")} Session alerts: opt-in with PUSHARY_AUTO_NOTIFY_SESSION_END=1`);
63
147
  };
64
148
  var setupCursor = async (apiKey) => {
65
149
  console.log(`
66
- ${bold("Cursor Setup")}
150
+ ${bold("Setting up Cursor")}
67
151
  `);
68
- const config = readJson(CURSOR_MCP);
69
- const mcpServers = config.mcpServers ?? {};
70
- mcpServers.pushary = {
71
- url: "https://pushary.com/api/mcp/mcp",
72
- headers: { Authorization: `Bearer ${apiKey}` }
73
- };
74
- config.mcpServers = mcpServers;
75
- writeJson(CURSOR_MCP, config);
76
- console.log(` ${green("done")} MCP server added to ${dim(CURSOR_MCP)}`);
152
+ await spinner("Adding MCP server to .cursor/mcp.json", async () => {
153
+ const config = readJson(CURSOR_MCP);
154
+ const mcpServers = config.mcpServers ?? {};
155
+ mcpServers.pushary = {
156
+ url: "https://pushary.com/api/mcp/mcp",
157
+ headers: { Authorization: `Bearer ${apiKey}` }
158
+ };
159
+ config.mcpServers = mcpServers;
160
+ writeJson(CURSOR_MCP, config);
161
+ });
77
162
  };
78
- var saveApiKey = (apiKey) => {
79
- const exportLine = `
163
+ var saveApiKey = async (apiKey) => {
164
+ await spinner("Saving API key to shell profile", async () => {
165
+ const exportLine = `
80
166
  export PUSHARY_API_KEY="${apiKey}"
81
167
  `;
82
- const shellFile = SHELL_FILES.find((f) => existsSync(f));
83
- if (shellFile) {
84
- const content = readFileSync(shellFile, "utf-8");
85
- if (content.includes("PUSHARY_API_KEY")) {
86
- console.log(` ${dim("PUSHARY_API_KEY already in")} ${shellFile}`);
87
- } else {
88
- appendFileSync(shellFile, exportLine, "utf-8");
89
- console.log(` ${green("done")} Added PUSHARY_API_KEY to ${dim(shellFile)}`);
168
+ const shellFile = SHELL_FILES.find((f) => existsSync(f));
169
+ if (shellFile) {
170
+ const content = readFileSync(shellFile, "utf-8");
171
+ if (!content.includes("PUSHARY_API_KEY")) {
172
+ appendFileSync(shellFile, exportLine, "utf-8");
173
+ }
90
174
  }
91
- }
92
- process.env.PUSHARY_API_KEY = apiKey;
175
+ process.env.PUSHARY_API_KEY = apiKey;
176
+ });
93
177
  };
94
178
  var sendTestNotification = async (apiKey) => {
95
- console.log(`
96
- ${bold("Sending test notification...")}`);
97
- try {
179
+ await spinner("Sending test notification", async () => {
98
180
  const response = await fetch("https://pushary.com/api/v1/server/send", {
99
181
  method: "POST",
100
182
  headers: {
@@ -106,72 +188,76 @@ ${bold("Sending test notification...")}`);
106
188
  body: "Your AI agent can now send you push notifications."
107
189
  })
108
190
  });
109
- if (response.ok) {
110
- const data = await response.json();
111
- if (data.delivered && data.delivered > 0) {
112
- console.log(` ${green("done")} Check your phone!`);
113
- } else {
114
- console.log(` ${dim("Notification sent but no subscribers found. Enable notifications on your phone at pushary.com first.")}`);
115
- }
116
- } else {
191
+ if (!response.ok) {
117
192
  const err = await response.json().catch(() => ({}));
118
- console.log(` ${dim(`Could not send: ${err.error ?? response.statusText}. Make sure you enabled notifications at pushary.com`)}`);
193
+ throw new Error(err.error ?? response.statusText);
119
194
  }
120
- } catch {
121
- console.log(` ${dim("Could not reach Pushary API. Check your internet connection.")}`);
122
- }
195
+ const data = await response.json();
196
+ if (!data.delivered || data.delivered === 0) {
197
+ throw new Error("no subscribers");
198
+ }
199
+ });
123
200
  };
124
201
  var main = async () => {
125
- console.log(`
126
- ${bold("Pushary Setup")}
127
- `);
128
- console.log(`Push notifications for your AI coding agents.
129
- `);
130
- const apiKey = await ask(`Paste your API key ${dim("(from pushary.com/dashboard/agent/settings)")}: `);
202
+ console.log();
203
+ console.log(` ${bold("Pushary")} ${dim("v" + (process.env.npm_package_version ?? "0.2"))}`);
204
+ console.log(` ${dim("Push notifications for AI coding agents")}`);
205
+ console.log();
206
+ console.log(` ${dim("Get your API key at")} ${cyan("pushary.com/sign-up")}`);
207
+ console.log();
208
+ const apiKey = await ask(` API key: `);
131
209
  if (!apiKey.trim() || !apiKey.includes(".")) {
132
- console.log("\nInvalid API key. Get yours at https://pushary.com/sign-up?from=ai-coding\n");
210
+ console.log(`
211
+ ${yellow("!")} Invalid key format. Expected: pk_xxx.sk_xxx`);
212
+ console.log(` ${dim("Get yours at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
213
+ `);
133
214
  rl.close();
134
215
  process.exit(1);
135
216
  }
136
217
  const trimmedKey = apiKey.trim();
137
- console.log(`
138
- Which agents do you use?
139
- `);
140
- console.log(` 1. ${cyan("Claude Code")}`);
141
- console.log(` 2. ${cyan("Cursor")}`);
142
- console.log(` 3. ${cyan("Both")}`);
143
- console.log(` 4. ${cyan("Other")} ${dim("(just save the API key)")}`);
144
218
  console.log();
145
- const choice = await ask(`Choice ${dim("[1-4]")}: `);
146
- saveApiKey(trimmedKey);
219
+ console.log(` ${bold("Which agent do you use?")}`);
220
+ console.log();
221
+ console.log(` ${cyan("1.")} Claude Code ${dim("MCP + permission hooks + auto-allowed tools")}`);
222
+ console.log(` ${cyan("2.")} Hermes ${dim("native plugin + auto-error notifications")}`);
223
+ console.log(` ${cyan("3.")} Cursor ${dim("MCP server")}`);
224
+ console.log(` ${cyan("4.")} All ${dim("configure everything")}`);
225
+ console.log(` ${cyan("5.")} Other ${dim("just save the API key")}`);
226
+ console.log();
227
+ const choice = await ask(` Choice ${dim("[1-5]")}: `);
228
+ await saveApiKey(trimmedKey);
147
229
  switch (choice.trim()) {
148
230
  case "1":
149
231
  await setupClaudeCode(trimmedKey);
150
232
  break;
151
233
  case "2":
152
- await setupCursor(trimmedKey);
234
+ await setupHermes(trimmedKey);
153
235
  break;
154
236
  case "3":
155
- await setupClaudeCode(trimmedKey);
156
237
  await setupCursor(trimmedKey);
157
238
  break;
158
239
  case "4":
240
+ await setupClaudeCode(trimmedKey);
241
+ await setupHermes(trimmedKey);
242
+ await setupCursor(trimmedKey);
243
+ break;
244
+ case "5":
159
245
  default:
160
246
  break;
161
247
  }
162
- const test = await ask(`
163
- Send a test notification? ${dim("[Y/n]")} `);
248
+ console.log();
249
+ const test = await ask(` Send a test notification? ${dim("[Y/n]")} `);
164
250
  if (test.toLowerCase() !== "n") {
165
251
  await sendTestNotification(trimmedKey);
166
252
  }
167
- console.log(`
168
- ${green("Setup complete.")} Your agents will now send you push notifications.
169
- `);
170
- console.log(`${dim("Next steps:")}`);
253
+ console.log();
254
+ console.log(` ${green(bold("Setup complete."))}`);
255
+ console.log();
256
+ console.log(` ${dim("Next:")}`);
171
257
  console.log(` ${dim("1.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
172
- console.log(` ${dim("2.")} Configure timeout policies at ${cyan("pushary.com/dashboard/agent/policies")}`);
173
- console.log(` ${dim("3.")} Start coding with your agent
174
- `);
258
+ console.log(` ${dim("2.")} Restart your agent to load the new config`);
259
+ console.log(` ${dim("3.")} Start coding \u2014 your agent will notify you automatically`);
260
+ console.log();
175
261
  rl.close();
176
262
  };
177
263
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Permission hooks for AI coding agents — route tool approvals through Pushary push notifications",
5
5
  "author": "Pushary <business@pushary.com>",
6
6
  "homepage": "https://pushary.com",