@pushary/agent-hooks 0.5.1 → 0.6.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 +13 -19
- package/dist/bin/pushary-codex.js +3 -3
- package/dist/bin/pushary-doctor.js +2 -2
- package/dist/bin/pushary-hook.js +11 -6
- package/dist/bin/pushary-post-hook.js +3 -3
- package/dist/bin/pushary-setup.js +44 -54
- package/dist/bin/pushary-stop-hook.js +3 -3
- package/dist/chunk-4Z4MB37G.js +96 -0
- package/dist/chunk-KYARP7KP.js +97 -0
- package/dist/chunk-O6A5RHWY.js +122 -0
- package/dist/chunk-WNXGIEX7.js +219 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +4 -4
- package/package.json +4 -3
|
@@ -10,6 +10,7 @@ import { join } from "path";
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { execSync } from "child_process";
|
|
12
12
|
import { confirm } from "@inquirer/prompts";
|
|
13
|
+
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
13
14
|
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`;
|
|
@@ -21,7 +22,7 @@ var CLAUDE_SETTINGS_LOCAL = join(homedir(), ".claude", "settings.local.json");
|
|
|
21
22
|
var CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
22
23
|
var SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
23
24
|
var CURSOR_MCP = join(".cursor", "mcp.json");
|
|
24
|
-
var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
|
|
25
|
+
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
25
26
|
var readJson = (path) => {
|
|
26
27
|
try {
|
|
27
28
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -91,26 +92,19 @@ var main = async () => {
|
|
|
91
92
|
}
|
|
92
93
|
const codexConfig = join(homedir(), ".codex", "config.toml");
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
+
const config = readFileSync(codexConfig, "utf-8");
|
|
95
96
|
const hadPushary = config.includes("pushary");
|
|
96
97
|
if (hadPushary) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (skipping && line.startsWith("[") && !/^\[mcp_servers\.pushary/.test(line.trim())) {
|
|
106
|
-
skipping = false;
|
|
107
|
-
}
|
|
108
|
-
if (skipping) continue;
|
|
109
|
-
if (line.includes("pushary-codex")) continue;
|
|
110
|
-
cleaned.push(line);
|
|
98
|
+
const parsed = parseTOML(config);
|
|
99
|
+
const mcpServers = parsed.mcp_servers ?? {};
|
|
100
|
+
delete mcpServers.pushary;
|
|
101
|
+
if (Object.keys(mcpServers).length === 0) delete parsed.mcp_servers;
|
|
102
|
+
else parsed.mcp_servers = mcpServers;
|
|
103
|
+
if (Array.isArray(parsed.notify)) {
|
|
104
|
+
parsed.notify = parsed.notify.filter((n) => typeof n !== "string" || !n.includes("pushary-codex"));
|
|
105
|
+
if (parsed.notify.length === 0) delete parsed.notify;
|
|
111
106
|
}
|
|
112
|
-
|
|
113
|
-
writeFileSync(codexConfig, config, "utf-8");
|
|
107
|
+
writeFileSync(codexConfig, stringifyTOML(parsed), "utf-8");
|
|
114
108
|
console.log(` ${check} Codex config ${dim("(cleaned)")}`);
|
|
115
109
|
} else {
|
|
116
110
|
console.log(` ${skip} Codex config ${dim("(no pushary entries)")}`);
|
|
@@ -130,7 +124,7 @@ var main = async () => {
|
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
126
|
try {
|
|
133
|
-
execSync("npm uninstall -g @pushary/agent-hooks", { stdio: "ignore" });
|
|
127
|
+
execSync("npm uninstall -g @pushary/agent-hooks", { stdio: "ignore", timeout: 3e4 });
|
|
134
128
|
console.log(` ${check} Global package ${dim("(uninstalled)")}`);
|
|
135
129
|
} catch {
|
|
136
130
|
console.log(` ${skip} Global package ${dim("(not installed)")}`);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
reportEvent
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-KYARP7KP.js";
|
|
5
5
|
import {
|
|
6
6
|
askUser,
|
|
7
7
|
waitForAnswer
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-4Z4MB37G.js";
|
|
9
9
|
import {
|
|
10
10
|
getApiKey
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-O6A5RHWY.js";
|
|
12
12
|
|
|
13
13
|
// bin/pushary-codex.ts
|
|
14
14
|
import { hostname } from "os";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
callMcpTool,
|
|
4
4
|
sendMcpRequest
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-O6A5RHWY.js";
|
|
6
6
|
|
|
7
7
|
// bin/pushary-doctor.ts
|
|
8
8
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -89,7 +89,7 @@ var main = async () => {
|
|
|
89
89
|
let globalVersion = "";
|
|
90
90
|
try {
|
|
91
91
|
const { execSync } = await import("child_process");
|
|
92
|
-
globalVersion = execSync("npm list -g @pushary/agent-hooks --depth=0 2>/dev/null", { encoding: "utf-8" }).match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
|
|
92
|
+
globalVersion = execSync("npm list -g @pushary/agent-hooks --depth=0 2>/dev/null", { encoding: "utf-8", timeout: 1e4 }).match(/@pushary\/agent-hooks@([\d.]+)/)?.[1] ?? "";
|
|
93
93
|
} catch {
|
|
94
94
|
}
|
|
95
95
|
check(!!globalVersion, "Global package installed", globalVersion || "not found");
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePreToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-WNXGIEX7.js";
|
|
5
|
+
import "../chunk-4Z4MB37G.js";
|
|
6
|
+
import "../chunk-O6A5RHWY.js";
|
|
7
7
|
|
|
8
8
|
// bin/pushary-hook.ts
|
|
9
9
|
var main = async () => {
|
|
@@ -25,14 +25,19 @@ var main = async () => {
|
|
|
25
25
|
}
|
|
26
26
|
try {
|
|
27
27
|
const output = await handlePreToolUse(input);
|
|
28
|
-
|
|
29
|
-
process.stdout.write(JSON.stringify(output));
|
|
30
|
-
}
|
|
28
|
+
process.stdout.write(JSON.stringify(output));
|
|
31
29
|
} catch (err) {
|
|
32
30
|
process.stderr.write(
|
|
33
31
|
`[pushary-hook] Error: ${err instanceof Error ? err.message : String(err)}
|
|
34
32
|
`
|
|
35
33
|
);
|
|
34
|
+
process.stdout.write(JSON.stringify({
|
|
35
|
+
hookSpecificOutput: {
|
|
36
|
+
hookEventName: "PreToolUse",
|
|
37
|
+
permissionDecision: "ask",
|
|
38
|
+
permissionDecisionReason: "Pushary hook error, falling back to terminal"
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
36
41
|
}
|
|
37
42
|
};
|
|
38
43
|
main();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePostToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-KYARP7KP.js";
|
|
5
|
+
import "../chunk-4Z4MB37G.js";
|
|
6
|
+
import "../chunk-O6A5RHWY.js";
|
|
7
7
|
|
|
8
8
|
// bin/pushary-post-hook.ts
|
|
9
9
|
var main = async () => {
|
|
@@ -12,13 +12,14 @@ import { homedir } from "os";
|
|
|
12
12
|
import { execSync } from "child_process";
|
|
13
13
|
import { checkbox, input, confirm } from "@inquirer/prompts";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
15
16
|
var CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
16
17
|
var CLAUDE_JSON = join(homedir(), ".claude.json");
|
|
17
18
|
var CURSOR_MCP = join(".cursor", "mcp.json");
|
|
18
19
|
var CURSOR_RULES_DIR = join(".cursor", "rules");
|
|
19
20
|
var CLAUDE_SKILL_DIR = join(homedir(), ".claude", "skills", "pushary");
|
|
20
21
|
var CODEX_SKILL_DIR = join(homedir(), ".codex", "skills", "pushary");
|
|
21
|
-
var SHELL_FILES = [".zshrc", ".bashrc"].map((f) => join(homedir(), f));
|
|
22
|
+
var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
|
|
22
23
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
23
24
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
24
25
|
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
@@ -37,7 +38,16 @@ var writeJson = (path, data) => {
|
|
|
37
38
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
38
39
|
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
39
40
|
};
|
|
40
|
-
var formatError = (err) =>
|
|
41
|
+
var formatError = (err) => {
|
|
42
|
+
if (err instanceof Error) {
|
|
43
|
+
const errWithSignal = err;
|
|
44
|
+
if (errWithSignal.killed && errWithSignal.signal === "SIGTERM") {
|
|
45
|
+
return "timed out \u2014 check your network connection";
|
|
46
|
+
}
|
|
47
|
+
return err.message;
|
|
48
|
+
}
|
|
49
|
+
return String(err);
|
|
50
|
+
};
|
|
41
51
|
var spinner = async (label, fn, options = {}) => {
|
|
42
52
|
const frames = [" ", ". ", ".. ", "..."];
|
|
43
53
|
let i = 0;
|
|
@@ -82,7 +92,7 @@ var checkForUpdates = async (current) => {
|
|
|
82
92
|
};
|
|
83
93
|
var installGlobally = async () => {
|
|
84
94
|
await spinner("Installing pushary-hook globally", async () => {
|
|
85
|
-
execSync("npm install -g @pushary/agent-hooks@latest", { stdio: "ignore" });
|
|
95
|
+
execSync("npm install -g @pushary/agent-hooks@latest", { stdio: "ignore", timeout: 12e4 });
|
|
86
96
|
});
|
|
87
97
|
};
|
|
88
98
|
var _cachedSkillContent = null;
|
|
@@ -150,7 +160,7 @@ var findPython310Plus = () => {
|
|
|
150
160
|
const candidates = ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
|
|
151
161
|
for (const py of candidates) {
|
|
152
162
|
try {
|
|
153
|
-
const version = execSync(`${py} --version 2>&1`, { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
163
|
+
const version = execSync(`${py} --version 2>&1`, { encoding: "utf-8", stdio: "pipe", timeout: 5e3 }).trim();
|
|
154
164
|
const match = version.match(/Python (\d+)\.(\d+)/);
|
|
155
165
|
if (match && (Number(match[1]) > 3 || Number(match[1]) === 3 && Number(match[2]) >= 10)) {
|
|
156
166
|
return py;
|
|
@@ -161,7 +171,7 @@ var findPython310Plus = () => {
|
|
|
161
171
|
return null;
|
|
162
172
|
};
|
|
163
173
|
var installPythonPlugin = (pythonBin) => {
|
|
164
|
-
execSync(`${pythonBin} -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe" });
|
|
174
|
+
execSync(`${pythonBin} -m pip install --upgrade hermes-plugin-pushary`, { stdio: "pipe", timeout: 12e4 });
|
|
165
175
|
};
|
|
166
176
|
var setupHermes = async (_apiKey) => {
|
|
167
177
|
console.log(`
|
|
@@ -170,7 +180,7 @@ var setupHermes = async (_apiKey) => {
|
|
|
170
180
|
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
171
181
|
const hasHermes = (() => {
|
|
172
182
|
try {
|
|
173
|
-
execSync(`${whichCmd} hermes`, { stdio: "ignore" });
|
|
183
|
+
execSync(`${whichCmd} hermes`, { stdio: "ignore", timeout: 5e3 });
|
|
174
184
|
return true;
|
|
175
185
|
} catch {
|
|
176
186
|
return false;
|
|
@@ -183,7 +193,7 @@ var setupHermes = async (_apiKey) => {
|
|
|
183
193
|
}
|
|
184
194
|
await spinner("Installing hermes-plugin-pushary", async () => {
|
|
185
195
|
try {
|
|
186
|
-
execSync("uv pip install hermes-plugin-pushary", { stdio: "pipe" });
|
|
196
|
+
execSync("uv pip install hermes-plugin-pushary", { stdio: "pipe", timeout: 12e4 });
|
|
187
197
|
return;
|
|
188
198
|
} catch {
|
|
189
199
|
}
|
|
@@ -191,8 +201,8 @@ var setupHermes = async (_apiKey) => {
|
|
|
191
201
|
if (!python) {
|
|
192
202
|
if (process.platform === "darwin") {
|
|
193
203
|
try {
|
|
194
|
-
execSync("which brew", { stdio: "ignore" });
|
|
195
|
-
execSync("brew install python@3.12", { stdio: "pipe" });
|
|
204
|
+
execSync("which brew", { stdio: "ignore", timeout: 5e3 });
|
|
205
|
+
execSync("brew install python@3.12", { stdio: "pipe", timeout: 3e5 });
|
|
196
206
|
python = findPython310Plus();
|
|
197
207
|
} catch {
|
|
198
208
|
}
|
|
@@ -204,8 +214,8 @@ var setupHermes = async (_apiKey) => {
|
|
|
204
214
|
["which pacman", "sudo pacman -S --noconfirm python python-pip"]
|
|
205
215
|
]) {
|
|
206
216
|
try {
|
|
207
|
-
execSync(check2, { stdio: "ignore" });
|
|
208
|
-
execSync(install, { stdio: "pipe" });
|
|
217
|
+
execSync(check2, { stdio: "ignore", timeout: 5e3 });
|
|
218
|
+
execSync(install, { stdio: "pipe", timeout: 3e5 });
|
|
209
219
|
python = findPython310Plus();
|
|
210
220
|
if (python) break;
|
|
211
221
|
} catch {
|
|
@@ -219,7 +229,7 @@ var setupHermes = async (_apiKey) => {
|
|
|
219
229
|
}
|
|
220
230
|
for (const pip of ["pip3", "pip"]) {
|
|
221
231
|
try {
|
|
222
|
-
execSync(`${pip} install hermes-plugin-pushary`, { stdio: "pipe" });
|
|
232
|
+
execSync(`${pip} install hermes-plugin-pushary`, { stdio: "pipe", timeout: 12e4 });
|
|
223
233
|
return;
|
|
224
234
|
} catch {
|
|
225
235
|
}
|
|
@@ -227,7 +237,7 @@ var setupHermes = async (_apiKey) => {
|
|
|
227
237
|
throw new Error("Python 3.10+ not found and could not be installed");
|
|
228
238
|
});
|
|
229
239
|
await spinner("Enabling plugin", async () => {
|
|
230
|
-
execSync("hermes plugins enable pushary", { stdio: "ignore" });
|
|
240
|
+
execSync("hermes plugins enable pushary", { stdio: "ignore", timeout: 1e4 });
|
|
231
241
|
});
|
|
232
242
|
console.log();
|
|
233
243
|
console.log(` ${dim("What this configured:")}`);
|
|
@@ -241,7 +251,7 @@ var setupCodex = async (_apiKey) => {
|
|
|
241
251
|
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
242
252
|
const hasCodex = (() => {
|
|
243
253
|
try {
|
|
244
|
-
execSync(`${whichCmd} codex`, { stdio: "ignore" });
|
|
254
|
+
execSync(`${whichCmd} codex`, { stdio: "ignore", timeout: 5e3 });
|
|
245
255
|
return true;
|
|
246
256
|
} catch {
|
|
247
257
|
return false;
|
|
@@ -258,61 +268,41 @@ var setupCodex = async (_apiKey) => {
|
|
|
258
268
|
try {
|
|
259
269
|
execSync(
|
|
260
270
|
"codex mcp add pushary --url https://pushary.com/api/mcp/mcp --bearer-token-env-var PUSHARY_API_KEY",
|
|
261
|
-
{ stdio: "ignore" }
|
|
271
|
+
{ stdio: "ignore", timeout: 15e3 }
|
|
262
272
|
);
|
|
263
273
|
} catch {
|
|
264
274
|
}
|
|
265
275
|
});
|
|
266
276
|
await spinner("Auto-allowing all Pushary tools", async () => {
|
|
267
|
-
let
|
|
277
|
+
let raw = "";
|
|
268
278
|
try {
|
|
269
|
-
|
|
279
|
+
raw = readFileSync(codexConfig, "utf-8");
|
|
270
280
|
} catch {
|
|
271
281
|
}
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (skippingToolSection) {
|
|
281
|
-
if (line.startsWith("[") || line.trim() === "") {
|
|
282
|
-
skippingToolSection = false;
|
|
283
|
-
if (line.startsWith("[")) {
|
|
284
|
-
cleaned.push(line);
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
} else {
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
cleaned.push(line);
|
|
292
|
-
}
|
|
293
|
-
config = cleaned.join("\n");
|
|
294
|
-
if (!config.includes("default_tools_approval_mode")) {
|
|
295
|
-
config = config.replace(
|
|
296
|
-
/(\[mcp_servers\.pushary\]\n)/,
|
|
297
|
-
'$1default_tools_approval_mode = "approve"\n'
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
writeFileSync(codexConfig, config, "utf-8");
|
|
282
|
+
const config = raw ? parseTOML(raw) : {};
|
|
283
|
+
const mcpServers = config.mcp_servers ?? {};
|
|
284
|
+
const pushary = mcpServers.pushary ?? {};
|
|
285
|
+
pushary.default_tools_approval_mode = "approve";
|
|
286
|
+
delete pushary.tools;
|
|
287
|
+
mcpServers.pushary = pushary;
|
|
288
|
+
config.mcp_servers = mcpServers;
|
|
289
|
+
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
301
290
|
});
|
|
302
291
|
await spinner("Adding notify handler for Codex events", async () => {
|
|
303
|
-
const globalPrefix = execSync("npm prefix -g", { encoding: "utf-8" }).trim();
|
|
292
|
+
const globalPrefix = execSync("npm prefix -g", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
304
293
|
const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
|
|
305
294
|
if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
|
|
306
|
-
let
|
|
295
|
+
let raw = "";
|
|
307
296
|
try {
|
|
308
|
-
|
|
297
|
+
raw = readFileSync(codexConfig, "utf-8");
|
|
309
298
|
} catch {
|
|
310
299
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
notify
|
|
314
|
-
|
|
315
|
-
|
|
300
|
+
const config = raw ? parseTOML(raw) : {};
|
|
301
|
+
const notify = Array.isArray(config.notify) ? config.notify : [];
|
|
302
|
+
if (!notify.some((n) => typeof n === "string" && n.includes("pushary-codex"))) {
|
|
303
|
+
notify.push(pusharyCodexPath);
|
|
304
|
+
config.notify = notify;
|
|
305
|
+
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
316
306
|
}
|
|
317
307
|
});
|
|
318
308
|
await installSkillToDir(CODEX_SKILL_DIR, "Installing Pushary skill");
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleStop
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-KYARP7KP.js";
|
|
5
|
+
import "../chunk-4Z4MB37G.js";
|
|
6
|
+
import "../chunk-O6A5RHWY.js";
|
|
7
7
|
|
|
8
8
|
// bin/pushary-stop-hook.ts
|
|
9
9
|
var main = async () => {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callMcpTool
|
|
3
|
+
} from "./chunk-O6A5RHWY.js";
|
|
4
|
+
|
|
5
|
+
// src/validate.ts
|
|
6
|
+
var isPolicyConfig = (data) => {
|
|
7
|
+
if (!data || typeof data !== "object") return false;
|
|
8
|
+
const d = data;
|
|
9
|
+
return Array.isArray(d.policies) && typeof d.defaultTimeoutSeconds === "number" && typeof d.defaultTimeoutAction === "string";
|
|
10
|
+
};
|
|
11
|
+
var isAskUserResponse = (data) => {
|
|
12
|
+
if (!data || typeof data !== "object") return false;
|
|
13
|
+
const d = data;
|
|
14
|
+
return typeof d.correlationId === "string" && typeof d.status === "string";
|
|
15
|
+
};
|
|
16
|
+
var isWaitForAnswerResponse = (data) => {
|
|
17
|
+
if (!data || typeof data !== "object") return false;
|
|
18
|
+
const d = data;
|
|
19
|
+
return typeof d.answered === "boolean";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/api.ts
|
|
23
|
+
var askUser = async (apiKey, params) => {
|
|
24
|
+
const result = await callMcpTool(apiKey, "ask_user", { ...params });
|
|
25
|
+
if (!isAskUserResponse(result)) throw new Error("Invalid ask_user response");
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
|
|
29
|
+
const result = await callMcpTool(apiKey, "wait_for_answer", {
|
|
30
|
+
correlationId,
|
|
31
|
+
timeoutMs
|
|
32
|
+
});
|
|
33
|
+
if (!isWaitForAnswerResponse(result)) throw new Error("Invalid wait_for_answer response");
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
var cancelQuestion = async (apiKey, correlationId) => {
|
|
37
|
+
await callMcpTool(apiKey, "cancel_question", { correlationId });
|
|
38
|
+
};
|
|
39
|
+
var sendNotification = async (apiKey, params) => {
|
|
40
|
+
await callMcpTool(apiKey, "send_notification", { ...params });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/describe.ts
|
|
44
|
+
var hookPrefixes = {
|
|
45
|
+
Bash: (input) => `bash: ${input.command ?? "(no command)"}`,
|
|
46
|
+
Write: (input) => `write file: ${input.file_path ?? "(unknown path)"}`,
|
|
47
|
+
Edit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
|
|
48
|
+
Read: (input) => `read file: ${input.file_path ?? "(unknown path)"}`
|
|
49
|
+
};
|
|
50
|
+
var eventPrefixes = {
|
|
51
|
+
Bash: (input) => `ran: ${String(input.command ?? "").slice(0, 120)}`,
|
|
52
|
+
Write: (input) => `wrote: ${input.file_path ?? "unknown"}`,
|
|
53
|
+
Edit: (input) => `edited: ${input.file_path ?? "unknown"}`,
|
|
54
|
+
Read: (input) => `read: ${input.file_path ?? "unknown"}`
|
|
55
|
+
};
|
|
56
|
+
var describeToolCall = (toolName, toolInput, format = "hook") => {
|
|
57
|
+
const prefixes = format === "hook" ? hookPrefixes : eventPrefixes;
|
|
58
|
+
const builder = prefixes[toolName];
|
|
59
|
+
if (builder) return builder(toolInput);
|
|
60
|
+
return format === "hook" ? `${toolName}: ${JSON.stringify(toolInput).slice(0, 200)}` : `${toolName}: done`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/pending.ts
|
|
64
|
+
import { join } from "path";
|
|
65
|
+
import { tmpdir } from "os";
|
|
66
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync } from "fs";
|
|
67
|
+
var PENDING_DIR = join(tmpdir(), "pushary-pending");
|
|
68
|
+
var savePendingQuestion = (correlationId) => {
|
|
69
|
+
if (!existsSync(PENDING_DIR)) mkdirSync(PENDING_DIR, { recursive: true });
|
|
70
|
+
writeFileSync(join(PENDING_DIR, correlationId), "", "utf-8");
|
|
71
|
+
};
|
|
72
|
+
var listPendingQuestions = () => {
|
|
73
|
+
try {
|
|
74
|
+
return readdirSync(PENDING_DIR);
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var removePendingQuestion = (correlationId) => {
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(join(PENDING_DIR, correlationId));
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
isPolicyConfig,
|
|
88
|
+
askUser,
|
|
89
|
+
waitForAnswer,
|
|
90
|
+
cancelQuestion,
|
|
91
|
+
sendNotification,
|
|
92
|
+
describeToolCall,
|
|
93
|
+
savePendingQuestion,
|
|
94
|
+
listPendingQuestions,
|
|
95
|
+
removePendingQuestion
|
|
96
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancelQuestion,
|
|
3
|
+
describeToolCall,
|
|
4
|
+
listPendingQuestions,
|
|
5
|
+
removePendingQuestion
|
|
6
|
+
} from "./chunk-4Z4MB37G.js";
|
|
7
|
+
import {
|
|
8
|
+
getApiKey,
|
|
9
|
+
getBaseUrl,
|
|
10
|
+
withRetry
|
|
11
|
+
} from "./chunk-O6A5RHWY.js";
|
|
12
|
+
|
|
13
|
+
// src/events.ts
|
|
14
|
+
import { hostname } from "os";
|
|
15
|
+
import { basename } from "path";
|
|
16
|
+
var cleanupPendingQuestions = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const files = listPendingQuestions();
|
|
19
|
+
const apiKey = getApiKey();
|
|
20
|
+
for (const correlationId of files) {
|
|
21
|
+
try {
|
|
22
|
+
await cancelQuestion(apiKey, correlationId);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
removePendingQuestion(correlationId);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var reportEvent = async (event) => {
|
|
31
|
+
const apiKey = getApiKey();
|
|
32
|
+
const baseUrl = getBaseUrl();
|
|
33
|
+
await withRetry(async () => {
|
|
34
|
+
await fetch(`${baseUrl}/api/agent/event`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${apiKey}`
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
...event,
|
|
42
|
+
machineId: event.machineId ?? hostname()
|
|
43
|
+
}),
|
|
44
|
+
signal: AbortSignal.timeout(1e4)
|
|
45
|
+
});
|
|
46
|
+
}, { maxAttempts: 2, baseDelayMs: 300 });
|
|
47
|
+
};
|
|
48
|
+
var handlePostToolUse = async (input) => {
|
|
49
|
+
try {
|
|
50
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
51
|
+
const action = describeToolCall(input.tool_name, input.tool_input, "event");
|
|
52
|
+
const isError = input.tool_result && ("error" in input.tool_result || "is_error" in input.tool_result);
|
|
53
|
+
await Promise.allSettled([
|
|
54
|
+
cleanupPendingQuestions(),
|
|
55
|
+
reportEvent({
|
|
56
|
+
event: isError ? "tool_error" : "tool_complete",
|
|
57
|
+
agentType: "claude_code",
|
|
58
|
+
agentName: `Claude Code - ${projectName}`,
|
|
59
|
+
action,
|
|
60
|
+
error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0
|
|
61
|
+
})
|
|
62
|
+
]);
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var handleStop = async (input) => {
|
|
67
|
+
try {
|
|
68
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
69
|
+
await reportEvent({
|
|
70
|
+
event: "session_end",
|
|
71
|
+
agentType: "claude_code",
|
|
72
|
+
agentName: `Claude Code - ${projectName}`,
|
|
73
|
+
action: "Session ended"
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var handleNotification = async (input) => {
|
|
79
|
+
try {
|
|
80
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
81
|
+
await reportEvent({
|
|
82
|
+
event: input.type === "error" ? "error" : "notification",
|
|
83
|
+
agentType: "claude_code",
|
|
84
|
+
agentName: `Claude Code - ${projectName}`,
|
|
85
|
+
action: input.title ?? input.message ?? "Notification",
|
|
86
|
+
error: input.type === "error" ? input.message : void 0
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
reportEvent,
|
|
94
|
+
handlePostToolUse,
|
|
95
|
+
handleStop,
|
|
96
|
+
handleNotification
|
|
97
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var getApiKey = () => {
|
|
3
|
+
const key = process.env.PUSHARY_API_KEY;
|
|
4
|
+
if (!key) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"PUSHARY_API_KEY environment variable is not set. Get your API key at https://pushary.com/sign-up?from=ai-coding"
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return key;
|
|
10
|
+
};
|
|
11
|
+
var getBaseUrl = () => process.env.PUSHARY_BASE_URL ?? "https://pushary.com";
|
|
12
|
+
|
|
13
|
+
// src/retry.ts
|
|
14
|
+
var isRetryable = (err) => {
|
|
15
|
+
if (!(err instanceof Error)) return false;
|
|
16
|
+
const msg = err.message;
|
|
17
|
+
return /\b(502|503|429)\b/.test(msg) || /ECONNRESET|ECONNREFUSED|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|fetch failed/i.test(msg) || msg.includes("AbortError");
|
|
18
|
+
};
|
|
19
|
+
var withRetry = async (fn, options = {}) => {
|
|
20
|
+
const {
|
|
21
|
+
maxAttempts = 3,
|
|
22
|
+
baseDelayMs = 500,
|
|
23
|
+
maxDelayMs = 5e3,
|
|
24
|
+
shouldRetry = isRetryable
|
|
25
|
+
} = options;
|
|
26
|
+
let lastError;
|
|
27
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return await fn();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
lastError = err;
|
|
32
|
+
if (attempt + 1 >= maxAttempts || !shouldRetry(err)) throw err;
|
|
33
|
+
const jitter = Math.random() * 0.3 + 0.85;
|
|
34
|
+
const delay = Math.min(baseDelayMs * 2 ** attempt * jitter, maxDelayMs);
|
|
35
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw lastError;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/mcp-http.ts
|
|
42
|
+
var parseSseJson = (body) => {
|
|
43
|
+
const messages = [];
|
|
44
|
+
for (const event of body.split(/\r?\n\r?\n/)) {
|
|
45
|
+
const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n").trim();
|
|
46
|
+
if (!data) continue;
|
|
47
|
+
try {
|
|
48
|
+
messages.push(JSON.parse(data));
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const message = messages.at(-1);
|
|
53
|
+
if (!message) throw new Error("Empty response from Pushary");
|
|
54
|
+
return message;
|
|
55
|
+
};
|
|
56
|
+
var parseMcpResponse = (body, contentType) => {
|
|
57
|
+
if (contentType?.includes("text/event-stream")) {
|
|
58
|
+
return parseSseJson(body);
|
|
59
|
+
}
|
|
60
|
+
return JSON.parse(body);
|
|
61
|
+
};
|
|
62
|
+
var sendMcpRequest = async (apiKey, message, options = {}) => {
|
|
63
|
+
return withRetry(async () => {
|
|
64
|
+
const baseUrl = options.baseUrl ?? getBaseUrl();
|
|
65
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
66
|
+
const headers = {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"Accept": "application/json, text/event-stream",
|
|
69
|
+
"Authorization": `Bearer ${apiKey}`
|
|
70
|
+
};
|
|
71
|
+
if (options.sessionId) {
|
|
72
|
+
headers["Mcp-Session-Id"] = options.sessionId;
|
|
73
|
+
}
|
|
74
|
+
const response = await fetchFn(`${baseUrl}/api/mcp/mcp`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers,
|
|
77
|
+
body: JSON.stringify(message),
|
|
78
|
+
signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : void 0
|
|
79
|
+
});
|
|
80
|
+
const body = await response.text();
|
|
81
|
+
const contentType = response.headers.get("content-type");
|
|
82
|
+
let data = null;
|
|
83
|
+
if (body.trim()) {
|
|
84
|
+
try {
|
|
85
|
+
data = parseMcpResponse(body, contentType);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (response.ok) throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const msg = data?.error?.message ?? (body.trim() || response.statusText);
|
|
92
|
+
throw new Error(`Pushary MCP error: ${response.status} ${msg}`);
|
|
93
|
+
}
|
|
94
|
+
if (!data) throw new Error("Empty response from Pushary");
|
|
95
|
+
if (data.error) throw new Error(data.error.message ?? "Pushary MCP error");
|
|
96
|
+
return {
|
|
97
|
+
data,
|
|
98
|
+
sessionId: response.headers.get("mcp-session-id") ?? "",
|
|
99
|
+
status: response.status,
|
|
100
|
+
statusText: response.statusText
|
|
101
|
+
};
|
|
102
|
+
}, { maxAttempts: options.maxRetries ?? 1 });
|
|
103
|
+
};
|
|
104
|
+
var callMcpTool = async (apiKey, toolName, params, options = {}) => {
|
|
105
|
+
const { data } = await sendMcpRequest(apiKey, {
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id: options.id ?? Date.now(),
|
|
108
|
+
method: "tools/call",
|
|
109
|
+
params: { name: toolName, arguments: params }
|
|
110
|
+
}, options);
|
|
111
|
+
const text = data.result?.content?.[0]?.text;
|
|
112
|
+
if (!text) throw new Error("Empty response from Pushary");
|
|
113
|
+
return JSON.parse(text);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
getApiKey,
|
|
118
|
+
getBaseUrl,
|
|
119
|
+
withRetry,
|
|
120
|
+
sendMcpRequest,
|
|
121
|
+
callMcpTool
|
|
122
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
askUser,
|
|
3
|
+
describeToolCall,
|
|
4
|
+
isPolicyConfig,
|
|
5
|
+
savePendingQuestion,
|
|
6
|
+
sendNotification,
|
|
7
|
+
waitForAnswer
|
|
8
|
+
} from "./chunk-4Z4MB37G.js";
|
|
9
|
+
import {
|
|
10
|
+
getApiKey,
|
|
11
|
+
getBaseUrl,
|
|
12
|
+
withRetry
|
|
13
|
+
} from "./chunk-O6A5RHWY.js";
|
|
14
|
+
|
|
15
|
+
// src/policy.ts
|
|
16
|
+
import { createHash } from "crypto";
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { tmpdir } from "os";
|
|
20
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
21
|
+
var cacheFile = (apiKey) => {
|
|
22
|
+
const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
23
|
+
return join(tmpdir(), `pushary-policy-${hash}.json`);
|
|
24
|
+
};
|
|
25
|
+
var fetchPolicy = async (apiKey) => {
|
|
26
|
+
return withRetry(async () => {
|
|
27
|
+
const baseUrl = getBaseUrl();
|
|
28
|
+
const response = await fetch(`${baseUrl}/api/mcp/policy`, {
|
|
29
|
+
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
30
|
+
signal: AbortSignal.timeout(1e4)
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(`Failed to fetch policy: ${response.status}`);
|
|
34
|
+
}
|
|
35
|
+
const raw = await response.json();
|
|
36
|
+
if (!isPolicyConfig(raw)) throw new Error("Invalid policy response");
|
|
37
|
+
return raw;
|
|
38
|
+
}, { maxAttempts: 2 });
|
|
39
|
+
};
|
|
40
|
+
var getPolicy = async (apiKey) => {
|
|
41
|
+
const path = cacheFile(apiKey);
|
|
42
|
+
let staleCache = null;
|
|
43
|
+
if (existsSync(path)) {
|
|
44
|
+
try {
|
|
45
|
+
const stat = readFileSync(path, "utf-8");
|
|
46
|
+
const cached = JSON.parse(stat);
|
|
47
|
+
if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
|
|
48
|
+
if (!cached._cachedAt || Date.now() - cached._cachedAt < CACHE_TTL_MS) {
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
51
|
+
staleCache = cached;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const policy = await fetchPolicy(apiKey);
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now() }), "utf-8");
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
return policy;
|
|
62
|
+
} catch {
|
|
63
|
+
if (staleCache) return staleCache;
|
|
64
|
+
throw new Error("Failed to fetch policy and no cached policy available");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var resolvePolicy = (config, toolName) => {
|
|
68
|
+
const exact = config.policies.find((p) => p.tool === toolName);
|
|
69
|
+
if (exact) return exact;
|
|
70
|
+
const wildcard = config.policies.find((p) => p.tool === "*");
|
|
71
|
+
if (wildcard) return wildcard;
|
|
72
|
+
return {
|
|
73
|
+
tool: toolName,
|
|
74
|
+
timeoutSeconds: config.defaultTimeoutSeconds,
|
|
75
|
+
timeoutAction: config.defaultTimeoutAction,
|
|
76
|
+
mode: config.defaultMode ?? "push_first",
|
|
77
|
+
pushFirstSeconds: config.defaultPushFirstSeconds ?? 10
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/hook.ts
|
|
82
|
+
import { basename } from "path";
|
|
83
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
+
var allow = () => ({
|
|
85
|
+
hookSpecificOutput: {
|
|
86
|
+
hookEventName: "PreToolUse",
|
|
87
|
+
permissionDecision: "allow"
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
var deny = (reason) => ({
|
|
91
|
+
hookSpecificOutput: {
|
|
92
|
+
hookEventName: "PreToolUse",
|
|
93
|
+
permissionDecision: "deny",
|
|
94
|
+
permissionDecisionReason: reason
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
var ask = (reason) => ({
|
|
98
|
+
hookSpecificOutput: {
|
|
99
|
+
hookEventName: "PreToolUse",
|
|
100
|
+
permissionDecision: "ask",
|
|
101
|
+
...reason ? { permissionDecisionReason: reason } : {}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
|
|
105
|
+
while (Date.now() < deadlineMs) {
|
|
106
|
+
const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
|
|
107
|
+
let answer;
|
|
108
|
+
try {
|
|
109
|
+
answer = await waitForAnswer(apiKey, correlationId, remaining);
|
|
110
|
+
} catch {
|
|
111
|
+
if (Date.now() + pollInterval >= deadlineMs) break;
|
|
112
|
+
await sleep(pollInterval);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (answer.answered) return answer;
|
|
116
|
+
if (Date.now() + pollInterval >= deadlineMs) break;
|
|
117
|
+
await sleep(pollInterval);
|
|
118
|
+
}
|
|
119
|
+
return { answered: false };
|
|
120
|
+
};
|
|
121
|
+
var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction) => {
|
|
122
|
+
let result;
|
|
123
|
+
try {
|
|
124
|
+
result = await askUser(apiKey, {
|
|
125
|
+
question: `Allow ${description}?`,
|
|
126
|
+
type: "confirm",
|
|
127
|
+
context: `Agent wants to run this in ${projectName}`,
|
|
128
|
+
agentName: `Claude Code - ${projectName}`
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
switch (timeoutAction) {
|
|
132
|
+
case "approve":
|
|
133
|
+
return allow();
|
|
134
|
+
case "deny":
|
|
135
|
+
return deny("Push notification failed, denying per policy");
|
|
136
|
+
default:
|
|
137
|
+
return ask("Push notification failed, asking in terminal");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
141
|
+
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
142
|
+
if (answer.answered) {
|
|
143
|
+
return answer.value === "yes" ? allow() : deny("Denied via push notification");
|
|
144
|
+
}
|
|
145
|
+
switch (timeoutAction) {
|
|
146
|
+
case "approve":
|
|
147
|
+
return allow();
|
|
148
|
+
case "deny":
|
|
149
|
+
return deny("No response within timeout");
|
|
150
|
+
default:
|
|
151
|
+
return ask("No push response, asking in terminal");
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var handleTerminalOnly = () => {
|
|
155
|
+
return ask();
|
|
156
|
+
};
|
|
157
|
+
var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds) => {
|
|
158
|
+
let result;
|
|
159
|
+
try {
|
|
160
|
+
result = await askUser(apiKey, {
|
|
161
|
+
question: `Allow ${description}?`,
|
|
162
|
+
type: "confirm",
|
|
163
|
+
context: `Agent wants to run this in ${projectName}`,
|
|
164
|
+
agentName: `Claude Code - ${projectName}`
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
return ask("Push notification failed, asking in terminal");
|
|
168
|
+
}
|
|
169
|
+
const deadline = Date.now() + pushFirstSeconds * 1e3;
|
|
170
|
+
const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
|
|
171
|
+
if (answer.answered) {
|
|
172
|
+
return answer.value === "yes" ? allow() : deny("Denied via push notification");
|
|
173
|
+
}
|
|
174
|
+
savePendingQuestion(result.correlationId);
|
|
175
|
+
return ask("Sent as push notification. You can also approve here.");
|
|
176
|
+
};
|
|
177
|
+
var handleNotifyOnly = async (apiKey, description, projectName) => {
|
|
178
|
+
try {
|
|
179
|
+
await sendNotification(apiKey, {
|
|
180
|
+
title: "Agent needs approval",
|
|
181
|
+
body: description,
|
|
182
|
+
agentName: `Claude Code - ${projectName}`
|
|
183
|
+
});
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
return ask();
|
|
187
|
+
};
|
|
188
|
+
var handlePreToolUse = async (input) => {
|
|
189
|
+
try {
|
|
190
|
+
const apiKey = getApiKey();
|
|
191
|
+
const policy = await getPolicy(apiKey);
|
|
192
|
+
const toolPolicy = resolvePolicy(policy, input.tool_name);
|
|
193
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
|
|
194
|
+
return allow();
|
|
195
|
+
}
|
|
196
|
+
const description = describeToolCall(input.tool_name, input.tool_input, "hook");
|
|
197
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
198
|
+
switch (toolPolicy.mode) {
|
|
199
|
+
case "push_only":
|
|
200
|
+
return handlePushOnly(apiKey, description, projectName, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction);
|
|
201
|
+
case "terminal_only":
|
|
202
|
+
return handleTerminalOnly();
|
|
203
|
+
case "push_first":
|
|
204
|
+
return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds);
|
|
205
|
+
case "notify_only":
|
|
206
|
+
return handleNotifyOnly(apiKey, description, projectName);
|
|
207
|
+
default:
|
|
208
|
+
return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
return ask("Pushary unavailable, falling back to terminal approval");
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
getPolicy,
|
|
217
|
+
resolvePolicy,
|
|
218
|
+
handlePreToolUse
|
|
219
|
+
};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ interface HookOutput {
|
|
|
11
11
|
permissionDecisionReason?: string;
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
-
declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput
|
|
14
|
+
declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput>;
|
|
15
15
|
|
|
16
16
|
interface AgentEvent {
|
|
17
17
|
event: string;
|
package/dist/src/index.js
CHANGED
|
@@ -2,22 +2,22 @@ import {
|
|
|
2
2
|
getPolicy,
|
|
3
3
|
handlePreToolUse,
|
|
4
4
|
resolvePolicy
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-WNXGIEX7.js";
|
|
6
6
|
import {
|
|
7
7
|
handleNotification,
|
|
8
8
|
handlePostToolUse,
|
|
9
9
|
handleStop,
|
|
10
10
|
reportEvent
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-KYARP7KP.js";
|
|
12
12
|
import {
|
|
13
13
|
askUser,
|
|
14
14
|
cancelQuestion,
|
|
15
15
|
waitForAnswer
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-4Z4MB37G.js";
|
|
17
17
|
import {
|
|
18
18
|
getApiKey,
|
|
19
19
|
getBaseUrl
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-O6A5RHWY.js";
|
|
21
21
|
export {
|
|
22
22
|
askUser,
|
|
23
23
|
cancelQuestion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup src/index.ts bin/pushary.ts bin/pushary-hook.ts bin/pushary-post-hook.ts bin/pushary-stop-hook.ts bin/pushary-codex.ts bin/pushary-setup.ts bin/pushary-clean.ts bin/pushary-doctor.ts --format esm --dts --outDir dist",
|
|
33
33
|
"dev": "tsup src/index.ts bin/pushary.ts bin/pushary-hook.ts bin/pushary-post-hook.ts bin/pushary-stop-hook.ts bin/pushary-codex.ts bin/pushary-setup.ts bin/pushary-clean.ts bin/pushary-doctor.ts --format esm --watch",
|
|
34
|
-
"test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/mcp-http.test.ts"
|
|
34
|
+
"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/events.test.ts && bun test src/hook.test.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@inquirer/prompts": "^8.4.2"
|
|
37
|
+
"@inquirer/prompts": "^8.4.2",
|
|
38
|
+
"smol-toml": "^1.6.1"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"tsup": "^8.0.0",
|