@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.
- package/dist/bin/pushary-setup.js +175 -89
- 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,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
|
|
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
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
150
|
+
${bold("Setting up Cursor")}
|
|
67
151
|
`);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
175
|
+
process.env.PUSHARY_API_KEY = apiKey;
|
|
176
|
+
});
|
|
93
177
|
};
|
|
94
178
|
var sendTestNotification = async (apiKey) => {
|
|
95
|
-
|
|
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
|
-
|
|
193
|
+
throw new Error(err.error ?? response.statusText);
|
|
119
194
|
}
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
127
|
-
`);
|
|
128
|
-
console.log(
|
|
129
|
-
`);
|
|
130
|
-
|
|
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(
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
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.")}
|
|
169
|
-
|
|
170
|
-
console.log(
|
|
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.")}
|
|
173
|
-
console.log(` ${dim("3.")} Start coding
|
|
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.
|
|
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",
|