@pushary/agent-hooks 0.9.0 → 0.10.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.
- package/dist/bin/pushary-clean.js +4 -2
- package/dist/bin/pushary-doctor.js +4 -1
- package/dist/bin/pushary-setup.js +249 -104
- package/dist/chunk-RSHN2AQ7.js +29 -0
- package/package.json +3 -2
|
@@ -3,12 +3,14 @@ import {
|
|
|
3
3
|
removeClaudeMcpServers,
|
|
4
4
|
removePusharySettings
|
|
5
5
|
} from "../chunk-5GFUI5N6.js";
|
|
6
|
+
import {
|
|
7
|
+
execNpm
|
|
8
|
+
} from "../chunk-RSHN2AQ7.js";
|
|
6
9
|
|
|
7
10
|
// bin/pushary-clean.ts
|
|
8
11
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
9
12
|
import { join } from "path";
|
|
10
13
|
import { homedir } from "os";
|
|
11
|
-
import { execSync } from "child_process";
|
|
12
14
|
import { confirm } from "@inquirer/prompts";
|
|
13
15
|
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
14
16
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
@@ -124,7 +126,7 @@ var main = async () => {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
try {
|
|
127
|
-
|
|
129
|
+
execNpm("uninstall -g --no-workspaces @pushary/agent-hooks", { stdio: "ignore", timeout: 3e4 });
|
|
128
130
|
console.log(` ${check} Global package ${dim("(uninstalled)")}`);
|
|
129
131
|
} catch {
|
|
130
132
|
console.log(` ${skip} Global package ${dim("(not installed)")}`);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
execNpm
|
|
4
|
+
} from "../chunk-RSHN2AQ7.js";
|
|
2
5
|
import {
|
|
3
6
|
callMcpTool,
|
|
4
7
|
sendMcpRequest
|
|
@@ -143,7 +146,7 @@ var main = async () => {
|
|
|
143
146
|
check(existsSync(SKILL_PATH), "Skill installed", existsSync(SKILL_PATH) ? SKILL_PATH : "not found");
|
|
144
147
|
let globalVersion = "";
|
|
145
148
|
try {
|
|
146
|
-
globalVersion =
|
|
149
|
+
globalVersion = execNpm("list -g --no-workspaces @pushary/agent-hooks --depth=0", { timeout: 1e4 }).toString().match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
|
|
147
150
|
} catch {
|
|
148
151
|
globalVersion = "";
|
|
149
152
|
}
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
addPusharyHooks,
|
|
5
5
|
addPusharyToolPermissions
|
|
6
6
|
} from "../chunk-5GFUI5N6.js";
|
|
7
|
+
import {
|
|
8
|
+
execNpm,
|
|
9
|
+
npmErrorMessage
|
|
10
|
+
} from "../chunk-RSHN2AQ7.js";
|
|
7
11
|
import {
|
|
8
12
|
isValidApiKey
|
|
9
13
|
} from "../chunk-IBWCHA5M.js";
|
|
@@ -16,6 +20,156 @@ import { execSync } from "child_process";
|
|
|
16
20
|
import { checkbox, input, confirm } from "@inquirer/prompts";
|
|
17
21
|
import { fileURLToPath } from "url";
|
|
18
22
|
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
23
|
+
|
|
24
|
+
// src/onboarding.ts
|
|
25
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
26
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
27
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
28
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
29
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
30
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
31
|
+
var check = green("\u2713");
|
|
32
|
+
var API_BASE = process.env.PUSHARY_API_URL?.trim() || "https://pushary.com";
|
|
33
|
+
var CONNECT_POLL_INTERVAL_MS = 2500;
|
|
34
|
+
var CONNECT_TIMEOUT_MS = 18e4;
|
|
35
|
+
var DELIVERY_POLL_INTERVAL_MS = 1500;
|
|
36
|
+
var DELIVERY_TIMEOUT_MS = 2e4;
|
|
37
|
+
var authHeaders = (apiKey) => ({
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
Authorization: `Bearer ${apiKey}`
|
|
40
|
+
});
|
|
41
|
+
var getJson = async (url, apiKey) => {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, { headers: authHeaders(apiKey), signal: AbortSignal.timeout(1e4) });
|
|
44
|
+
if (!res.ok) return null;
|
|
45
|
+
return await res.json().catch(() => null);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var fetchSite = async (apiKey) => {
|
|
51
|
+
const data = await getJson(`${API_BASE}/api/v1/server/site`, apiKey);
|
|
52
|
+
if (!data || typeof data.slug !== "string" || typeof data.subscribeUrl !== "string") return null;
|
|
53
|
+
return { slug: data.slug, name: typeof data.name === "string" ? data.name : data.slug, subscribeUrl: data.subscribeUrl };
|
|
54
|
+
};
|
|
55
|
+
var activeSubscriberCount = async (apiKey) => {
|
|
56
|
+
const data = await getJson(`${API_BASE}/api/v1/server/subscribers/count`, apiKey);
|
|
57
|
+
return data && typeof data.active === "number" ? data.active : 0;
|
|
58
|
+
};
|
|
59
|
+
var sendTestNotification = async (apiKey) => {
|
|
60
|
+
const res = await fetch(`${API_BASE}/api/v1/server/send`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: authHeaders(apiKey),
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
title: "Pushary is connected",
|
|
65
|
+
body: "Your AI agent can now reach you right here.",
|
|
66
|
+
metadata: { source: "cli-onboarding" }
|
|
67
|
+
}),
|
|
68
|
+
signal: AbortSignal.timeout(15e3)
|
|
69
|
+
});
|
|
70
|
+
const data = await res.json().catch(() => ({}));
|
|
71
|
+
if (!res.ok) throw new Error(typeof data.error === "string" ? data.error : res.statusText);
|
|
72
|
+
return Array.isArray(data.notificationIds) ? data.notificationIds : [];
|
|
73
|
+
};
|
|
74
|
+
var isDelivered = async (apiKey, id) => {
|
|
75
|
+
const data = await getJson(`${API_BASE}/api/v1/server/notifications/${id}`, apiKey);
|
|
76
|
+
return !!data && (data.status === "delivered" || data.deliveredAt != null);
|
|
77
|
+
};
|
|
78
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
79
|
+
var startSpinner = (getLabel) => {
|
|
80
|
+
const frames = [" ", ". ", ".. ", "..."];
|
|
81
|
+
let i = 0;
|
|
82
|
+
const interval = setInterval(() => {
|
|
83
|
+
process.stdout.write(`\r ${dim(frames[i++ % frames.length])} ${getLabel()}`);
|
|
84
|
+
}, 200);
|
|
85
|
+
return (finalGlyph, finalLabel) => {
|
|
86
|
+
clearInterval(interval);
|
|
87
|
+
process.stdout.write(`\r ${finalGlyph} ${finalLabel}\x1B[K
|
|
88
|
+
`);
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
var printQr = (url) => new Promise((resolve) => {
|
|
92
|
+
qrcodeTerminal.generate(url, { small: true }, (qr) => {
|
|
93
|
+
process.stdout.write("\n" + qr.split("\n").map((line) => " " + line).join("\n") + "\n");
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
var waitForDevice = async (apiKey) => {
|
|
98
|
+
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
|
|
99
|
+
const stop = startSpinner(() => {
|
|
100
|
+
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
|
|
101
|
+
return `Waiting for your phone ${dim(`(${left}s)`)}`;
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
while (Date.now() < deadline) {
|
|
105
|
+
if (await activeSubscriberCount(apiKey) > 0) {
|
|
106
|
+
stop(check, "Phone connected");
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
await sleep(CONNECT_POLL_INTERVAL_MS);
|
|
110
|
+
}
|
|
111
|
+
stop(yellow("!"), "Didn't detect a connection yet");
|
|
112
|
+
return false;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
stop(yellow("!"), `Couldn't check connection ${dim(`(${err instanceof Error ? err.message : "network error"})`)}`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var sendAndConfirm = async (apiKey) => {
|
|
119
|
+
let notificationIds = [];
|
|
120
|
+
const stopSend = startSpinner(() => "Sending a test notification");
|
|
121
|
+
try {
|
|
122
|
+
notificationIds = await sendTestNotification(apiKey);
|
|
123
|
+
stopSend(check, "Test notification sent");
|
|
124
|
+
} catch (err) {
|
|
125
|
+
stopSend(yellow("!"), `Couldn't send test notification ${dim(`(${err instanceof Error ? err.message : "error"})`)}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (notificationIds.length === 0) return;
|
|
129
|
+
const deadline = Date.now() + DELIVERY_TIMEOUT_MS;
|
|
130
|
+
const stop = startSpinner(() => "Confirming it reached your phone");
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
const results = await Promise.all(notificationIds.map((id) => isDelivered(apiKey, id)));
|
|
133
|
+
if (results.some(Boolean)) {
|
|
134
|
+
stop(check, "Received on your phone");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await sleep(DELIVERY_POLL_INTERVAL_MS);
|
|
138
|
+
}
|
|
139
|
+
stop(yellow("!"), "Sent \u2014 couldn't confirm receipt; check your phone");
|
|
140
|
+
};
|
|
141
|
+
var connectDevice = async (apiKey) => {
|
|
142
|
+
const site = await fetchSite(apiKey);
|
|
143
|
+
if (!site) {
|
|
144
|
+
console.log(` ${yellow("!")} Couldn't load your subscribe link. Connect your phone later at ${cyan("pushary.com")}.`);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (await activeSubscriberCount(apiKey) > 0) {
|
|
148
|
+
console.log(` ${check} Phone already connected`);
|
|
149
|
+
await sendAndConfirm(apiKey);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(` ${bold("Connect your phone")}`);
|
|
154
|
+
console.log(` ${dim("Scan this with your phone camera to get push notifications:")}`);
|
|
155
|
+
await printQr(site.subscribeUrl);
|
|
156
|
+
console.log(` ${dim("or open")} ${cyan(site.subscribeUrl)}`);
|
|
157
|
+
console.log();
|
|
158
|
+
const connected = await waitForDevice(apiKey);
|
|
159
|
+
if (!connected) {
|
|
160
|
+
console.log(` ${dim("No rush \u2014 open")} ${cyan(site.subscribeUrl)} ${dim("on your phone whenever you're ready.")}`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
await sendAndConfirm(apiKey);
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
var printConnectInstructions = async (apiKey) => {
|
|
167
|
+
const site = await fetchSite(apiKey);
|
|
168
|
+
const target = site?.subscribeUrl ?? "https://pushary.com";
|
|
169
|
+
console.log(` ${dim("Connect your phone for push notifications at")} ${cyan(target)}`);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// bin/pushary-setup.ts
|
|
19
173
|
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
20
174
|
var CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
21
175
|
var CURSOR_MCP = join(".cursor", "mcp.json");
|
|
@@ -23,12 +177,12 @@ var CURSOR_RULES_DIR = join(".cursor", "rules");
|
|
|
23
177
|
var CLAUDE_SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
24
178
|
var CODEX_SKILL_DIR = join(homedir(), ".codex", "skills", "pushary");
|
|
25
179
|
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var
|
|
180
|
+
var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
181
|
+
var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
182
|
+
var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
183
|
+
var cyan2 = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
184
|
+
var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
185
|
+
var check2 = green2("\u2713");
|
|
32
186
|
var parseKeyFlag = () => {
|
|
33
187
|
const args = process.argv.slice(2);
|
|
34
188
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -72,16 +226,16 @@ var spinner = async (label, fn, options = {}) => {
|
|
|
72
226
|
const frames = [" ", ". ", ".. ", "..."];
|
|
73
227
|
let i = 0;
|
|
74
228
|
const interval = setInterval(() => {
|
|
75
|
-
process.stdout.write(`\r ${
|
|
229
|
+
process.stdout.write(`\r ${dim2(frames[i++ % frames.length])} ${label}`);
|
|
76
230
|
}, 200);
|
|
77
231
|
try {
|
|
78
232
|
await fn();
|
|
79
233
|
clearInterval(interval);
|
|
80
|
-
process.stdout.write(`\r ${
|
|
234
|
+
process.stdout.write(`\r ${check2} ${label}
|
|
81
235
|
`);
|
|
82
236
|
} catch (err) {
|
|
83
237
|
clearInterval(interval);
|
|
84
|
-
process.stdout.write(`\r ${
|
|
238
|
+
process.stdout.write(`\r ${yellow2("!")} ${label} ${dim2(`(${formatError(err)})`)}
|
|
85
239
|
`);
|
|
86
240
|
if (!options.optional) throw err;
|
|
87
241
|
}
|
|
@@ -103,8 +257,8 @@ var checkForUpdates = async (current) => {
|
|
|
103
257
|
const data = await res.json();
|
|
104
258
|
const latest = data.version;
|
|
105
259
|
if (latest && latest !== current) {
|
|
106
|
-
console.log(` ${
|
|
107
|
-
console.log(` ${
|
|
260
|
+
console.log(` ${yellow2("!")} Update available: ${dim2(current)} \u2192 ${green2(latest)}`);
|
|
261
|
+
console.log(` ${dim2("Run:")} npx @pushary/agent-hooks@${latest} setup`);
|
|
108
262
|
console.log();
|
|
109
263
|
}
|
|
110
264
|
} catch {
|
|
@@ -112,7 +266,11 @@ var checkForUpdates = async (current) => {
|
|
|
112
266
|
};
|
|
113
267
|
var installGlobally = async () => {
|
|
114
268
|
await spinner("Installing pushary-hook globally", async () => {
|
|
115
|
-
|
|
269
|
+
try {
|
|
270
|
+
execNpm("install -g --no-workspaces @pushary/agent-hooks@latest", { timeout: 12e4 });
|
|
271
|
+
} catch (err) {
|
|
272
|
+
throw new Error(npmErrorMessage(err));
|
|
273
|
+
}
|
|
116
274
|
});
|
|
117
275
|
};
|
|
118
276
|
var _cachedSkillContent = null;
|
|
@@ -150,7 +308,7 @@ ${body}`;
|
|
|
150
308
|
};
|
|
151
309
|
var setupClaudeCode = async (apiKey) => {
|
|
152
310
|
console.log(`
|
|
153
|
-
${
|
|
311
|
+
${bold2("Setting up Claude Code")}
|
|
154
312
|
`);
|
|
155
313
|
const settings = readJson(CLAUDE_SETTINGS);
|
|
156
314
|
await spinner("Adding MCP server (type: http)", async () => {
|
|
@@ -165,7 +323,7 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
165
323
|
await spinner("Adding hooks (PreToolUse, PostToolUse, Stop)", async () => {
|
|
166
324
|
let binDir;
|
|
167
325
|
try {
|
|
168
|
-
binDir = join(
|
|
326
|
+
binDir = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin");
|
|
169
327
|
} catch {
|
|
170
328
|
binDir = void 0;
|
|
171
329
|
}
|
|
@@ -176,11 +334,11 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
176
334
|
});
|
|
177
335
|
await installSkillToDir(CLAUDE_SKILL_DIR, "Installing Pushary skill");
|
|
178
336
|
console.log();
|
|
179
|
-
console.log(` ${
|
|
180
|
-
console.log(` ${
|
|
181
|
-
console.log(` ${
|
|
182
|
-
console.log(` ${
|
|
183
|
-
console.log(` ${
|
|
337
|
+
console.log(` ${dim2("What this configured:")}`);
|
|
338
|
+
console.log(` ${dim2("\u2022")} MCP server: your agent can send notifications and ask questions`);
|
|
339
|
+
console.log(` ${dim2("\u2022")} Skill: teaches your agent when and how to use Pushary`);
|
|
340
|
+
console.log(` ${dim2("\u2022")} Hooks: route permission approvals through push notifications`);
|
|
341
|
+
console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
|
|
184
342
|
};
|
|
185
343
|
var findPython310Plus = () => {
|
|
186
344
|
const candidates = ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
|
|
@@ -197,15 +355,24 @@ var findPython310Plus = () => {
|
|
|
197
355
|
return null;
|
|
198
356
|
};
|
|
199
357
|
var installPythonPlugin = (pythonBin) => {
|
|
200
|
-
|
|
358
|
+
try {
|
|
359
|
+
execSync(`${pythonBin} -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 12e4 });
|
|
360
|
+
} catch (err) {
|
|
361
|
+
const msg = npmErrorMessage(err);
|
|
362
|
+
if (!msg.includes("externally-managed-environment")) throw err;
|
|
363
|
+
execSync(
|
|
364
|
+
`${pythonBin} -m pip install --upgrade --user --break-system-packages hermes-plugin-pushary`,
|
|
365
|
+
{ stdio: "pipe", timeout: 12e4 }
|
|
366
|
+
);
|
|
367
|
+
}
|
|
201
368
|
};
|
|
202
369
|
var setupHermes = async (_apiKey) => {
|
|
203
370
|
console.log(`
|
|
204
|
-
${
|
|
371
|
+
${bold2("Setting up Hermes Agent")}
|
|
205
372
|
`);
|
|
206
373
|
if (!isInstalled("hermes")) {
|
|
207
|
-
console.log(` ${
|
|
208
|
-
console.log(` ${
|
|
374
|
+
console.log(` ${yellow2("!")} Hermes CLI not found. Skipping.`);
|
|
375
|
+
console.log(` ${dim2("Install Hermes and re-run setup to configure.")}`);
|
|
209
376
|
return;
|
|
210
377
|
}
|
|
211
378
|
await spinner("Installing hermes-plugin-pushary", async () => {
|
|
@@ -214,6 +381,13 @@ var setupHermes = async (_apiKey) => {
|
|
|
214
381
|
return;
|
|
215
382
|
} catch {
|
|
216
383
|
}
|
|
384
|
+
if (isInstalled("pipx")) {
|
|
385
|
+
try {
|
|
386
|
+
execSync("pipx inject hermes hermes-plugin-pushary", { stdio: "pipe", timeout: 12e4 });
|
|
387
|
+
return;
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
217
391
|
let python = findPython310Plus();
|
|
218
392
|
if (!python) {
|
|
219
393
|
if (process.platform === "darwin") {
|
|
@@ -224,14 +398,14 @@ var setupHermes = async (_apiKey) => {
|
|
|
224
398
|
} catch {
|
|
225
399
|
}
|
|
226
400
|
} else if (process.platform === "linux") {
|
|
227
|
-
for (const [
|
|
401
|
+
for (const [check3, install] of [
|
|
228
402
|
["which apt-get", "sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-pip"],
|
|
229
403
|
["which dnf", "sudo dnf install -y -q python3 python3-pip"],
|
|
230
404
|
["which yum", "sudo yum install -y -q python3 python3-pip"],
|
|
231
405
|
["which pacman", "sudo pacman -S --noconfirm python python-pip"]
|
|
232
406
|
]) {
|
|
233
407
|
try {
|
|
234
|
-
execSync(
|
|
408
|
+
execSync(check3, { stdio: "ignore", timeout: 5e3 });
|
|
235
409
|
execSync(install, { stdio: "pipe", timeout: 3e5 });
|
|
236
410
|
python = findPython310Plus();
|
|
237
411
|
if (python) break;
|
|
@@ -248,7 +422,14 @@ var setupHermes = async (_apiKey) => {
|
|
|
248
422
|
try {
|
|
249
423
|
execSync(`${pip} install hermes-plugin-pushary`, { stdio: "pipe", timeout: 12e4 });
|
|
250
424
|
return;
|
|
251
|
-
} catch {
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (npmErrorMessage(err).includes("externally-managed-environment")) {
|
|
427
|
+
try {
|
|
428
|
+
execSync(`${pip} install --user --break-system-packages hermes-plugin-pushary`, { stdio: "pipe", timeout: 12e4 });
|
|
429
|
+
return;
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
}
|
|
252
433
|
}
|
|
253
434
|
}
|
|
254
435
|
throw new Error("Python 3.10+ not found and could not be installed");
|
|
@@ -257,17 +438,17 @@ var setupHermes = async (_apiKey) => {
|
|
|
257
438
|
execSync("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
|
|
258
439
|
});
|
|
259
440
|
console.log();
|
|
260
|
-
console.log(` ${
|
|
261
|
-
console.log(` ${
|
|
262
|
-
console.log(` ${
|
|
441
|
+
console.log(` ${dim2("What this configured:")}`);
|
|
442
|
+
console.log(` ${dim2("\u2022")} Native tools: pushary_notify, pushary_ask, pushary_wait, pushary_cancel`);
|
|
443
|
+
console.log(` ${dim2("\u2022")} Auto-notifications: push alert when tools return errors`);
|
|
263
444
|
};
|
|
264
445
|
var setupCodex = async (_apiKey) => {
|
|
265
446
|
console.log(`
|
|
266
|
-
${
|
|
447
|
+
${bold2("Setting up Codex")}
|
|
267
448
|
`);
|
|
268
449
|
if (!isInstalled("codex")) {
|
|
269
|
-
console.log(` ${
|
|
270
|
-
console.log(` ${
|
|
450
|
+
console.log(` ${yellow2("!")} Codex CLI not found. Skipping.`);
|
|
451
|
+
console.log(` ${dim2("Install Codex and re-run setup to configure.")}`);
|
|
271
452
|
return;
|
|
272
453
|
}
|
|
273
454
|
await installGlobally();
|
|
@@ -297,7 +478,7 @@ var setupCodex = async (_apiKey) => {
|
|
|
297
478
|
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
298
479
|
});
|
|
299
480
|
await spinner("Adding notify handler for Codex events", async () => {
|
|
300
|
-
const globalPrefix =
|
|
481
|
+
const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
|
|
301
482
|
const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
|
|
302
483
|
if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
|
|
303
484
|
let raw = "";
|
|
@@ -315,14 +496,14 @@ var setupCodex = async (_apiKey) => {
|
|
|
315
496
|
});
|
|
316
497
|
await installSkillToDir(CODEX_SKILL_DIR, "Installing Pushary skill");
|
|
317
498
|
console.log();
|
|
318
|
-
console.log(` ${
|
|
319
|
-
console.log(` ${
|
|
320
|
-
console.log(` ${
|
|
321
|
-
console.log(` ${
|
|
499
|
+
console.log(` ${dim2("What this configured:")}`);
|
|
500
|
+
console.log(` ${dim2("\u2022")} MCP server: Codex can send notifications and ask questions`);
|
|
501
|
+
console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
|
|
502
|
+
console.log(` ${dim2("\u2022")} Notify handler: captures turn completions and approval requests`);
|
|
322
503
|
};
|
|
323
504
|
var setupCursor = async (apiKey) => {
|
|
324
505
|
console.log(`
|
|
325
|
-
${
|
|
506
|
+
${bold2("Setting up Cursor")}
|
|
326
507
|
`);
|
|
327
508
|
await spinner("Adding MCP server to .cursor/mcp.json", async () => {
|
|
328
509
|
const config = readJson(CURSOR_MCP);
|
|
@@ -352,43 +533,6 @@ export PUSHARY_API_KEY='${apiKey}'
|
|
|
352
533
|
process.env.PUSHARY_API_KEY = apiKey;
|
|
353
534
|
});
|
|
354
535
|
};
|
|
355
|
-
var sendTestNotification = async (apiKey) => {
|
|
356
|
-
const frames = [" ", ". ", ".. ", "..."];
|
|
357
|
-
let i = 0;
|
|
358
|
-
const interval = setInterval(() => {
|
|
359
|
-
process.stdout.write(`\r ${dim(frames[i++ % frames.length])} Sending test notification`);
|
|
360
|
-
}, 200);
|
|
361
|
-
try {
|
|
362
|
-
const response = await fetch("https://pushary.com/api/v1/server/send", {
|
|
363
|
-
method: "POST",
|
|
364
|
-
headers: {
|
|
365
|
-
"Content-Type": "application/json",
|
|
366
|
-
"Authorization": `Bearer ${apiKey}`
|
|
367
|
-
},
|
|
368
|
-
body: JSON.stringify({
|
|
369
|
-
title: "Pushary is working",
|
|
370
|
-
body: "Your AI agent can now send you push notifications."
|
|
371
|
-
})
|
|
372
|
-
});
|
|
373
|
-
const data = await response.json().catch(() => ({}));
|
|
374
|
-
clearInterval(interval);
|
|
375
|
-
if (!response.ok) {
|
|
376
|
-
const reason = data.error ?? response.statusText;
|
|
377
|
-
process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${reason})`)}
|
|
378
|
-
`);
|
|
379
|
-
console.log(` ${dim("Make sure you enabled notifications at")} ${cyan("pushary.com")}`);
|
|
380
|
-
} else {
|
|
381
|
-
process.stdout.write(`\r ${check} Sending test notification
|
|
382
|
-
`);
|
|
383
|
-
console.log(` ${dim("Check your phone!")}`);
|
|
384
|
-
}
|
|
385
|
-
} catch (err) {
|
|
386
|
-
clearInterval(interval);
|
|
387
|
-
const msg = err instanceof Error ? err.message : "network error";
|
|
388
|
-
process.stdout.write(`\r ${yellow("!")} Sending test notification ${dim(`(${msg})`)}
|
|
389
|
-
`);
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
536
|
var AGENT_SETUP = {
|
|
393
537
|
claude_code: setupClaudeCode,
|
|
394
538
|
codex: setupCodex,
|
|
@@ -398,8 +542,8 @@ var AGENT_SETUP = {
|
|
|
398
542
|
var main = async () => {
|
|
399
543
|
const version = getPackageVersion();
|
|
400
544
|
console.log();
|
|
401
|
-
console.log(` ${
|
|
402
|
-
console.log(` ${
|
|
545
|
+
console.log(` ${bold2("Pushary")} ${dim2("v" + version)}`);
|
|
546
|
+
console.log(` ${dim2("Push notifications for AI coding agents")}`);
|
|
403
547
|
console.log();
|
|
404
548
|
await checkForUpdates(version);
|
|
405
549
|
const flagKey = parseKeyFlag();
|
|
@@ -407,12 +551,12 @@ var main = async () => {
|
|
|
407
551
|
let trimmedKey;
|
|
408
552
|
if (flagKey && isValidApiKey(flagKey)) {
|
|
409
553
|
const masked = `${flagKey.slice(0, 8)}...${flagKey.slice(-4)}`;
|
|
410
|
-
console.log(` ${
|
|
554
|
+
console.log(` ${check2} Using API key: ${dim2(masked)}`);
|
|
411
555
|
console.log();
|
|
412
556
|
trimmedKey = flagKey;
|
|
413
557
|
} else if (envKey && isValidApiKey(envKey)) {
|
|
414
558
|
const masked = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`;
|
|
415
|
-
console.log(` ${
|
|
559
|
+
console.log(` ${check2} Found API key in environment: ${dim2(masked)}`);
|
|
416
560
|
console.log();
|
|
417
561
|
const useExisting = await confirm({ message: "Use this key?", default: true });
|
|
418
562
|
if (useExisting) {
|
|
@@ -422,17 +566,17 @@ var main = async () => {
|
|
|
422
566
|
trimmedKey = apiKey.trim();
|
|
423
567
|
}
|
|
424
568
|
} else {
|
|
425
|
-
console.log(` ${
|
|
426
|
-
console.log(` ${
|
|
569
|
+
console.log(` ${dim2("Paste your API key from the onboarding page.")}`);
|
|
570
|
+
console.log(` ${dim2("Can't find it? Copy it from:")} ${cyan2("pushary.com/dashboard/agent/settings")}`);
|
|
427
571
|
console.log();
|
|
428
572
|
const apiKey = await input({ message: "API key:" });
|
|
429
573
|
trimmedKey = apiKey.trim();
|
|
430
574
|
}
|
|
431
575
|
if (!trimmedKey || !isValidApiKey(trimmedKey)) {
|
|
432
576
|
console.log(`
|
|
433
|
-
${
|
|
434
|
-
console.log(` ${
|
|
435
|
-
console.log(` ${
|
|
577
|
+
${yellow2("!")} Invalid key format. Expected: ${dim2("pk_xxx.xxx")}`);
|
|
578
|
+
console.log(` ${dim2("Copy your key from")} ${cyan2("https://pushary.com/dashboard/agent/settings")}`);
|
|
579
|
+
console.log(` ${dim2("Or sign up at")} ${cyan2("https://pushary.com/sign-up?from=ai-coding")}
|
|
436
580
|
`);
|
|
437
581
|
process.exit(1);
|
|
438
582
|
}
|
|
@@ -444,18 +588,18 @@ var main = async () => {
|
|
|
444
588
|
};
|
|
445
589
|
const hint = Object.values(detected).some(Boolean) ? "(detected agents pre-selected)" : "(space = toggle, enter = confirm)";
|
|
446
590
|
const agents = await checkbox({
|
|
447
|
-
message: "Which agents do you use? " +
|
|
591
|
+
message: "Which agents do you use? " + dim2(hint),
|
|
448
592
|
choices: [
|
|
449
|
-
{ name: `Claude Code ${
|
|
450
|
-
{ name: `Codex ${
|
|
451
|
-
{ name: `Hermes ${
|
|
452
|
-
{ name: `Cursor ${
|
|
593
|
+
{ name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
|
|
594
|
+
{ name: `Codex ${dim2("MCP + notify handler + auto-allowed tools")}`, value: "codex", checked: detected.codex },
|
|
595
|
+
{ name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
|
|
596
|
+
{ name: `Cursor ${dim2("MCP server")}`, value: "cursor", checked: detected.cursor }
|
|
453
597
|
]
|
|
454
598
|
});
|
|
455
599
|
await saveApiKey(trimmedKey);
|
|
456
600
|
if (agents.length === 0) {
|
|
457
601
|
console.log(`
|
|
458
|
-
${
|
|
602
|
+
${dim2("No agents selected. API key saved.")}`);
|
|
459
603
|
} else {
|
|
460
604
|
const failed = [];
|
|
461
605
|
for (const agent of agents) {
|
|
@@ -463,33 +607,34 @@ var main = async () => {
|
|
|
463
607
|
await AGENT_SETUP[agent](trimmedKey);
|
|
464
608
|
} catch (err) {
|
|
465
609
|
failed.push(agent.replace("_", " "));
|
|
466
|
-
console.log(` ${
|
|
467
|
-
console.log(` ${
|
|
610
|
+
console.log(` ${yellow2("!")} ${agent.replace("_", " ")} setup failed: ${formatError(err)}`);
|
|
611
|
+
console.log(` ${dim2("Other agents will continue. Re-run setup to retry.")}`);
|
|
468
612
|
}
|
|
469
613
|
}
|
|
470
614
|
if (failed.length > 0) {
|
|
471
615
|
console.log();
|
|
472
|
-
console.log(` ${
|
|
616
|
+
console.log(` ${yellow2("!")} Failed: ${failed.join(", ")} ${dim2("(others completed successfully)")}`);
|
|
473
617
|
}
|
|
474
618
|
}
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
477
|
-
await
|
|
619
|
+
const skipPhone = process.argv.includes("--skip-phone");
|
|
620
|
+
if (skipPhone || !process.stdout.isTTY) {
|
|
621
|
+
await printConnectInstructions(trimmedKey);
|
|
622
|
+
} else {
|
|
623
|
+
await connectDevice(trimmedKey);
|
|
478
624
|
}
|
|
479
625
|
console.log();
|
|
480
|
-
console.log(` ${
|
|
626
|
+
console.log(` ${green2(bold2("Setup complete."))}`);
|
|
481
627
|
console.log();
|
|
482
|
-
console.log(` ${
|
|
483
|
-
console.log(` ${
|
|
484
|
-
console.log(` ${
|
|
485
|
-
console.log(` ${
|
|
486
|
-
console.log(` ${dim("4.")} Run ${cyan("npx @pushary/agent-hooks doctor")} to verify`);
|
|
628
|
+
console.log(` ${dim2("Next:")}`);
|
|
629
|
+
console.log(` ${dim2("1.")} Load your API key: ${cyan2("source ~/.zshrc")} ${dim2("(or open a new terminal)")}`);
|
|
630
|
+
console.log(` ${dim2("2.")} Restart your agent to load the new config`);
|
|
631
|
+
console.log(` ${dim2("3.")} Run ${cyan2("npx @pushary/agent-hooks doctor")} to verify`);
|
|
487
632
|
console.log();
|
|
488
633
|
};
|
|
489
634
|
main().catch((err) => {
|
|
490
635
|
console.log();
|
|
491
|
-
console.log(` ${
|
|
492
|
-
console.log(` ${
|
|
636
|
+
console.log(` ${yellow2("!")} Setup failed: ${formatError(err)}`);
|
|
637
|
+
console.log(` ${dim2("Run")} ${cyan2("npx @pushary/agent-hooks doctor")} ${dim2("after fixing the issue, then rerun setup.")}`);
|
|
493
638
|
console.log();
|
|
494
639
|
process.exit(1);
|
|
495
640
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/npm.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
var cleanNpmEnv = () => {
|
|
4
|
+
const env = {};
|
|
5
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
6
|
+
if (key.toLowerCase().startsWith("npm_config_workspace")) continue;
|
|
7
|
+
env[key] = value;
|
|
8
|
+
}
|
|
9
|
+
return env;
|
|
10
|
+
};
|
|
11
|
+
var npmErrorMessage = (err) => {
|
|
12
|
+
const e = err;
|
|
13
|
+
const text = [e?.stderr, e?.stdout].map((part) => part ? part.toString() : "").join("\n");
|
|
14
|
+
const line = text.split("\n").map((l) => l.replace(/^npm error\s*/i, "").trim()).find((l) => l && !l.startsWith("A complete log") && !/^code\s/i.test(l));
|
|
15
|
+
return line || e?.message || String(err);
|
|
16
|
+
};
|
|
17
|
+
var execNpm = (args, options = {}) => {
|
|
18
|
+
return execSync(`npm ${args}`, {
|
|
19
|
+
timeout: 12e4,
|
|
20
|
+
stdio: "pipe",
|
|
21
|
+
...options,
|
|
22
|
+
env: { ...cleanNpmEnv(), ...options.env ?? {} }
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
npmErrorMessage,
|
|
28
|
+
execNpm
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsup",
|
|
34
34
|
"dev": "tsup --watch",
|
|
35
|
-
"test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/hook.test.ts"
|
|
35
|
+
"test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/hook.test.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@inquirer/prompts": "^8.4.2",
|
|
39
|
+
"qrcode-terminal": "^0.12.0",
|
|
39
40
|
"smol-toml": "^1.6.1"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|