@pushary/agent-hooks 0.2.5 → 0.2.7
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-setup.js +166 -93
- package/package.json +1 -1
|
@@ -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,38 +29,56 @@ 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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|
|
77
|
+
};
|
|
78
|
+
var addToolPermissions = (settings) => {
|
|
58
79
|
const permissions = settings.permissions ?? {};
|
|
59
80
|
const allow = permissions.allow ?? [];
|
|
60
|
-
const
|
|
81
|
+
const tools = [
|
|
61
82
|
"mcp__pushary__send_notification",
|
|
62
83
|
"mcp__pushary__ask_user",
|
|
63
84
|
"mcp__pushary__ask_user_yes_no",
|
|
@@ -67,52 +88,96 @@ ${bold("Claude Code Setup")}
|
|
|
67
88
|
"mcp__pushary__get_subscriber",
|
|
68
89
|
"mcp__pushary__count_subscribers"
|
|
69
90
|
];
|
|
70
|
-
for (const tool of
|
|
91
|
+
for (const tool of tools) {
|
|
71
92
|
if (!allow.includes(tool)) allow.push(tool);
|
|
72
93
|
}
|
|
73
94
|
permissions.allow = allow;
|
|
74
95
|
settings.permissions = permissions;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log(`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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`);
|
|
81
147
|
};
|
|
82
148
|
var setupCursor = async (apiKey) => {
|
|
83
149
|
console.log(`
|
|
84
|
-
${bold("Cursor
|
|
150
|
+
${bold("Setting up Cursor")}
|
|
85
151
|
`);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
});
|
|
95
162
|
};
|
|
96
|
-
var saveApiKey = (apiKey) => {
|
|
97
|
-
|
|
163
|
+
var saveApiKey = async (apiKey) => {
|
|
164
|
+
await spinner("Saving API key to shell profile", async () => {
|
|
165
|
+
const exportLine = `
|
|
98
166
|
export PUSHARY_API_KEY="${apiKey}"
|
|
99
167
|
`;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
appendFileSync(shellFile, exportLine, "utf-8");
|
|
107
|
-
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
|
+
}
|
|
108
174
|
}
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
process.env.PUSHARY_API_KEY = apiKey;
|
|
176
|
+
});
|
|
111
177
|
};
|
|
112
178
|
var sendTestNotification = async (apiKey) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try {
|
|
179
|
+
let notifResult = "";
|
|
180
|
+
await spinner("Sending test notification", async () => {
|
|
116
181
|
const response = await fetch("https://pushary.com/api/v1/server/send", {
|
|
117
182
|
method: "POST",
|
|
118
183
|
headers: {
|
|
@@ -124,72 +189,80 @@ ${bold("Sending test notification...")}`);
|
|
|
124
189
|
body: "Your AI agent can now send you push notifications."
|
|
125
190
|
})
|
|
126
191
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
console.log(` ${dim("Notification sent but no subscribers found. Enable notifications on your phone at pushary.com first.")}`);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
const err = await response.json().catch(() => ({}));
|
|
136
|
-
console.log(` ${dim(`Could not send: ${err.error ?? response.statusText}. Make sure you enabled notifications at pushary.com`)}`);
|
|
192
|
+
const data = await response.json().catch(() => ({}));
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
notifResult = `Could not send: ${data.error ?? response.statusText}`;
|
|
195
|
+
return;
|
|
137
196
|
}
|
|
138
|
-
|
|
139
|
-
|
|
197
|
+
notifResult = "sent";
|
|
198
|
+
});
|
|
199
|
+
if (notifResult === "sent") {
|
|
200
|
+
console.log(` ${dim("Check your phone!")}`);
|
|
201
|
+
} else if (notifResult) {
|
|
202
|
+
console.log(` ${dim(notifResult)}`);
|
|
203
|
+
console.log(` ${dim("Make sure you enabled notifications at")} ${cyan("pushary.com")}`);
|
|
140
204
|
}
|
|
141
205
|
};
|
|
142
206
|
var main = async () => {
|
|
143
|
-
console.log(
|
|
144
|
-
${bold("Pushary
|
|
145
|
-
`);
|
|
146
|
-
console.log(
|
|
147
|
-
`);
|
|
148
|
-
|
|
207
|
+
console.log();
|
|
208
|
+
console.log(` ${bold("Pushary")} ${dim("v" + (process.env.npm_package_version ?? "0.2"))}`);
|
|
209
|
+
console.log(` ${dim("Push notifications for AI coding agents")}`);
|
|
210
|
+
console.log();
|
|
211
|
+
console.log(` ${dim("Get your API key at")} ${cyan("pushary.com/sign-up")}`);
|
|
212
|
+
console.log();
|
|
213
|
+
const apiKey = await ask(` API key: `);
|
|
149
214
|
if (!apiKey.trim() || !apiKey.includes(".")) {
|
|
150
|
-
console.log(
|
|
215
|
+
console.log(`
|
|
216
|
+
${yellow("!")} Invalid key format. Expected: pk_xxx.sk_xxx`);
|
|
217
|
+
console.log(` ${dim("Get yours at")} ${cyan("https://pushary.com/sign-up?from=ai-coding")}
|
|
218
|
+
`);
|
|
151
219
|
rl.close();
|
|
152
220
|
process.exit(1);
|
|
153
221
|
}
|
|
154
222
|
const trimmedKey = apiKey.trim();
|
|
155
|
-
console.log(`
|
|
156
|
-
Which agents do you use?
|
|
157
|
-
`);
|
|
158
|
-
console.log(` 1. ${cyan("Claude Code")}`);
|
|
159
|
-
console.log(` 2. ${cyan("Cursor")}`);
|
|
160
|
-
console.log(` 3. ${cyan("Both")}`);
|
|
161
|
-
console.log(` 4. ${cyan("Other")} ${dim("(just save the API key)")}`);
|
|
162
223
|
console.log();
|
|
163
|
-
|
|
164
|
-
|
|
224
|
+
console.log(` ${bold("Which agent do you use?")}`);
|
|
225
|
+
console.log();
|
|
226
|
+
console.log(` ${cyan("1.")} Claude Code ${dim("MCP + permission hooks + auto-allowed tools")}`);
|
|
227
|
+
console.log(` ${cyan("2.")} Hermes ${dim("native plugin + auto-error notifications")}`);
|
|
228
|
+
console.log(` ${cyan("3.")} Cursor ${dim("MCP server")}`);
|
|
229
|
+
console.log(` ${cyan("4.")} All ${dim("configure everything")}`);
|
|
230
|
+
console.log(` ${cyan("5.")} Other ${dim("just save the API key")}`);
|
|
231
|
+
console.log();
|
|
232
|
+
const choice = await ask(` Choice ${dim("[1-5]")}: `);
|
|
233
|
+
await saveApiKey(trimmedKey);
|
|
165
234
|
switch (choice.trim()) {
|
|
166
235
|
case "1":
|
|
167
236
|
await setupClaudeCode(trimmedKey);
|
|
168
237
|
break;
|
|
169
238
|
case "2":
|
|
170
|
-
await
|
|
239
|
+
await setupHermes(trimmedKey);
|
|
171
240
|
break;
|
|
172
241
|
case "3":
|
|
173
|
-
await setupClaudeCode(trimmedKey);
|
|
174
242
|
await setupCursor(trimmedKey);
|
|
175
243
|
break;
|
|
176
244
|
case "4":
|
|
245
|
+
await setupClaudeCode(trimmedKey);
|
|
246
|
+
await setupHermes(trimmedKey);
|
|
247
|
+
await setupCursor(trimmedKey);
|
|
248
|
+
break;
|
|
249
|
+
case "5":
|
|
177
250
|
default:
|
|
178
251
|
break;
|
|
179
252
|
}
|
|
180
|
-
|
|
181
|
-
Send a test notification? ${dim("[Y/n]")} `);
|
|
253
|
+
console.log();
|
|
254
|
+
const test = await ask(` Send a test notification? ${dim("[Y/n]")} `);
|
|
182
255
|
if (test.toLowerCase() !== "n") {
|
|
183
256
|
await sendTestNotification(trimmedKey);
|
|
184
257
|
}
|
|
185
|
-
console.log(
|
|
186
|
-
${green("Setup complete.")}
|
|
187
|
-
|
|
188
|
-
console.log(
|
|
258
|
+
console.log();
|
|
259
|
+
console.log(` ${green(bold("Setup complete."))}`);
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(` ${dim("Next:")}`);
|
|
189
262
|
console.log(` ${dim("1.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
|
|
190
|
-
console.log(` ${dim("2.")}
|
|
191
|
-
console.log(` ${dim("3.")} Start coding
|
|
192
|
-
|
|
263
|
+
console.log(` ${dim("2.")} Restart your agent to load the new config`);
|
|
264
|
+
console.log(` ${dim("3.")} Start coding \u2014 your agent will notify you automatically`);
|
|
265
|
+
console.log();
|
|
193
266
|
rl.close();
|
|
194
267
|
};
|
|
195
268
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|