@pushary/agent-hooks 0.2.7 → 0.3.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-codex.d.ts +1 -0
- package/dist/bin/pushary-codex.js +91 -0
- package/dist/bin/pushary-hook.js +3 -1
- package/dist/bin/pushary-post-hook.d.ts +1 -0
- package/dist/bin/pushary-post-hook.js +22 -0
- package/dist/bin/pushary-setup.js +93 -25
- package/dist/bin/pushary-stop-hook.d.ts +1 -0
- package/dist/bin/pushary-stop-hook.js +19 -0
- package/dist/chunk-4TWRLEOX.js +49 -0
- package/dist/chunk-EQE6Z4YQ.js +77 -0
- package/dist/chunk-KINE5LNQ.js +136 -0
- package/dist/chunk-VUNL35KE.js +16 -0
- package/dist/src/index.d.ts +30 -1
- package/dist/src/index.js +20 -6
- package/package.json +7 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
askUser,
|
|
4
|
+
waitForAnswer
|
|
5
|
+
} from "../chunk-4TWRLEOX.js";
|
|
6
|
+
import {
|
|
7
|
+
reportEvent
|
|
8
|
+
} from "../chunk-EQE6Z4YQ.js";
|
|
9
|
+
import {
|
|
10
|
+
getApiKey
|
|
11
|
+
} from "../chunk-VUNL35KE.js";
|
|
12
|
+
|
|
13
|
+
// bin/pushary-codex.ts
|
|
14
|
+
import { hostname } from "os";
|
|
15
|
+
import { basename } from "path";
|
|
16
|
+
var main = async () => {
|
|
17
|
+
let rawInput = "";
|
|
18
|
+
for await (const chunk of process.stdin) {
|
|
19
|
+
rawInput += chunk;
|
|
20
|
+
}
|
|
21
|
+
if (!rawInput.trim()) {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
let event;
|
|
25
|
+
try {
|
|
26
|
+
event = JSON.parse(rawInput);
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
const projectName = basename(event.cwd ?? process.cwd());
|
|
31
|
+
const agentName = `Codex - ${projectName}`;
|
|
32
|
+
try {
|
|
33
|
+
if (event.type === "agent-turn-complete") {
|
|
34
|
+
await reportEvent({
|
|
35
|
+
event: "turn_complete",
|
|
36
|
+
agentType: "codex",
|
|
37
|
+
agentName,
|
|
38
|
+
action: event.message ?? "Turn complete",
|
|
39
|
+
machineId: hostname()
|
|
40
|
+
});
|
|
41
|
+
process.stdout.write(JSON.stringify({ acknowledged: true }));
|
|
42
|
+
}
|
|
43
|
+
if (event.type === "approval-requested") {
|
|
44
|
+
const apiKey = getApiKey();
|
|
45
|
+
const description = event.command ?? event.message ?? "Codex needs approval";
|
|
46
|
+
await reportEvent({
|
|
47
|
+
event: "approval_requested",
|
|
48
|
+
agentType: "codex",
|
|
49
|
+
agentName,
|
|
50
|
+
action: description,
|
|
51
|
+
machineId: hostname()
|
|
52
|
+
});
|
|
53
|
+
const result = await askUser(apiKey, {
|
|
54
|
+
question: `Allow: ${description}?`,
|
|
55
|
+
type: "confirm",
|
|
56
|
+
context: `Codex wants to run this in ${projectName}`,
|
|
57
|
+
agentName
|
|
58
|
+
});
|
|
59
|
+
const deadline = Date.now() + 12e4;
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
const remaining = Math.min(Math.max(deadline - Date.now(), 1e3), 3e4);
|
|
62
|
+
const answer = await waitForAnswer(apiKey, result.correlationId, remaining);
|
|
63
|
+
if (answer.answered) {
|
|
64
|
+
const approved = answer.value === "yes";
|
|
65
|
+
process.stdout.write(JSON.stringify({ approved }));
|
|
66
|
+
await reportEvent({
|
|
67
|
+
event: approved ? "approval_granted" : "approval_denied",
|
|
68
|
+
agentType: "codex",
|
|
69
|
+
agentName,
|
|
70
|
+
action: approved ? `Approved: ${description}` : `Denied: ${description}`,
|
|
71
|
+
machineId: hostname()
|
|
72
|
+
});
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() + 2e3 >= deadline) break;
|
|
76
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write(JSON.stringify({ approved: false }));
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
`[pushary-codex] Error: ${err instanceof Error ? err.message : String(err)}
|
|
84
|
+
`
|
|
85
|
+
);
|
|
86
|
+
if (event.type === "approval-requested") {
|
|
87
|
+
process.stdout.write(JSON.stringify({ approved: false }));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
main();
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
handlePostToolUse
|
|
4
|
+
} from "../chunk-EQE6Z4YQ.js";
|
|
5
|
+
import "../chunk-VUNL35KE.js";
|
|
6
|
+
|
|
7
|
+
// bin/pushary-post-hook.ts
|
|
8
|
+
var main = async () => {
|
|
9
|
+
let rawInput = "";
|
|
10
|
+
for await (const chunk of process.stdin) {
|
|
11
|
+
rawInput += chunk;
|
|
12
|
+
}
|
|
13
|
+
if (!rawInput.trim()) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const input = JSON.parse(rawInput);
|
|
18
|
+
await handlePostToolUse(input);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
main();
|
|
@@ -72,25 +72,37 @@ var addPermissionHooks = (settings) => {
|
|
|
72
72
|
}]
|
|
73
73
|
});
|
|
74
74
|
hooks.PreToolUse = preToolUse;
|
|
75
|
-
settings.hooks = hooks;
|
|
76
75
|
}
|
|
76
|
+
const postToolUse = hooks.PostToolUse ?? [];
|
|
77
|
+
if (!JSON.stringify(postToolUse).includes("pushary-post-hook")) {
|
|
78
|
+
postToolUse.push({
|
|
79
|
+
matcher: "Bash|Write|Edit",
|
|
80
|
+
hooks: [{
|
|
81
|
+
type: "command",
|
|
82
|
+
command: "pushary-post-hook",
|
|
83
|
+
timeout: 10
|
|
84
|
+
}]
|
|
85
|
+
});
|
|
86
|
+
hooks.PostToolUse = postToolUse;
|
|
87
|
+
}
|
|
88
|
+
const stop = hooks.Stop ?? [];
|
|
89
|
+
if (!JSON.stringify(stop).includes("pushary-stop-hook")) {
|
|
90
|
+
stop.push({
|
|
91
|
+
hooks: [{
|
|
92
|
+
type: "command",
|
|
93
|
+
command: "pushary-stop-hook",
|
|
94
|
+
timeout: 10
|
|
95
|
+
}]
|
|
96
|
+
});
|
|
97
|
+
hooks.Stop = stop;
|
|
98
|
+
}
|
|
99
|
+
settings.hooks = hooks;
|
|
77
100
|
};
|
|
78
101
|
var addToolPermissions = (settings) => {
|
|
79
102
|
const permissions = settings.permissions ?? {};
|
|
80
103
|
const allow = permissions.allow ?? [];
|
|
81
|
-
const
|
|
82
|
-
|
|
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);
|
|
93
|
-
}
|
|
104
|
+
const rule = "MCP(pushary:*)";
|
|
105
|
+
if (!allow.includes(rule)) allow.push(rule);
|
|
94
106
|
permissions.allow = allow;
|
|
95
107
|
settings.permissions = permissions;
|
|
96
108
|
};
|
|
@@ -106,7 +118,7 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
106
118
|
addToolPermissions(settings);
|
|
107
119
|
});
|
|
108
120
|
await installGlobally();
|
|
109
|
-
await spinner("Adding
|
|
121
|
+
await spinner("Adding hooks (PreToolUse, PostToolUse, Stop)", async () => {
|
|
110
122
|
addPermissionHooks(settings);
|
|
111
123
|
});
|
|
112
124
|
await spinner(`Writing ${CLAUDE_SETTINGS}`, async () => {
|
|
@@ -115,7 +127,9 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
115
127
|
console.log();
|
|
116
128
|
console.log(` ${dim("What this configured:")}`);
|
|
117
129
|
console.log(` ${dim("\u2022")} MCP server: your agent can send notifications and ask questions`);
|
|
118
|
-
console.log(` ${dim("\u2022")}
|
|
130
|
+
console.log(` ${dim("\u2022")} PreToolUse: approve Bash/Write/Edit from your phone`);
|
|
131
|
+
console.log(` ${dim("\u2022")} PostToolUse: track when tools finish`);
|
|
132
|
+
console.log(` ${dim("\u2022")} Stop: detect when the agent session ends`);
|
|
119
133
|
console.log(` ${dim("\u2022")} Auto-allowed tools: no permission prompts for Pushary`);
|
|
120
134
|
};
|
|
121
135
|
var setupHermes = async (apiKey) => {
|
|
@@ -145,6 +159,55 @@ var setupHermes = async (apiKey) => {
|
|
|
145
159
|
console.log(` ${dim("\u2022")} Auto-notifications: push alert when tools return errors`);
|
|
146
160
|
console.log(` ${dim("\u2022")} Session alerts: opt-in with PUSHARY_AUTO_NOTIFY_SESSION_END=1`);
|
|
147
161
|
};
|
|
162
|
+
var setupCodex = async (_apiKey) => {
|
|
163
|
+
console.log(`
|
|
164
|
+
${bold("Setting up Codex")}
|
|
165
|
+
`);
|
|
166
|
+
const hasCodex = (() => {
|
|
167
|
+
try {
|
|
168
|
+
execSync("which codex", { stdio: "ignore" });
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
if (!hasCodex) {
|
|
175
|
+
console.log(` ${yellow("!")} Codex CLI not found. Skipping.`);
|
|
176
|
+
console.log(` ${dim("Install Codex and re-run setup to configure.")}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await installGlobally();
|
|
180
|
+
await spinner("Adding Pushary MCP server to Codex", async () => {
|
|
181
|
+
try {
|
|
182
|
+
execSync(
|
|
183
|
+
"codex mcp add pushary --url https://pushary.com/api/mcp/mcp --bearer-token-env-var PUSHARY_API_KEY",
|
|
184
|
+
{ stdio: "ignore" }
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const codexConfig = join(homedir(), ".codex", "config.toml");
|
|
190
|
+
await spinner("Adding notify handler for Codex events", async () => {
|
|
191
|
+
const pusharyCodexPath = execSync("which pushary-codex", { encoding: "utf-8" }).trim();
|
|
192
|
+
if (!pusharyCodexPath) throw new Error("pushary-codex not found");
|
|
193
|
+
let config = "";
|
|
194
|
+
try {
|
|
195
|
+
config = readFileSync(codexConfig, "utf-8");
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
if (!config.includes("pushary-codex")) {
|
|
199
|
+
const notifyLine = `
|
|
200
|
+
notify = ["${pusharyCodexPath}"]
|
|
201
|
+
`;
|
|
202
|
+
appendFileSync(codexConfig, notifyLine, "utf-8");
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
console.log();
|
|
206
|
+
console.log(` ${dim("What this configured:")}`);
|
|
207
|
+
console.log(` ${dim("\u2022")} MCP server: Codex can send notifications and ask questions`);
|
|
208
|
+
console.log(` ${dim("\u2022")} Notify handler: captures turn completions and approval requests`);
|
|
209
|
+
console.log(` ${dim("\u2022")} Uses PUSHARY_API_KEY env var for auth`);
|
|
210
|
+
};
|
|
148
211
|
var setupCursor = async (apiKey) => {
|
|
149
212
|
console.log(`
|
|
150
213
|
${bold("Setting up Cursor")}
|
|
@@ -224,29 +287,34 @@ var main = async () => {
|
|
|
224
287
|
console.log(` ${bold("Which agent do you use?")}`);
|
|
225
288
|
console.log();
|
|
226
289
|
console.log(` ${cyan("1.")} Claude Code ${dim("MCP + permission hooks + auto-allowed tools")}`);
|
|
227
|
-
console.log(` ${cyan("2.")}
|
|
228
|
-
console.log(` ${cyan("3.")}
|
|
229
|
-
console.log(` ${cyan("4.")}
|
|
230
|
-
console.log(` ${cyan("5.")}
|
|
290
|
+
console.log(` ${cyan("2.")} Codex ${dim("MCP server via codex mcp add")}`);
|
|
291
|
+
console.log(` ${cyan("3.")} Hermes ${dim("native plugin + auto-error notifications")}`);
|
|
292
|
+
console.log(` ${cyan("4.")} Cursor ${dim("MCP server")}`);
|
|
293
|
+
console.log(` ${cyan("5.")} All ${dim("configure everything")}`);
|
|
294
|
+
console.log(` ${cyan("6.")} Other ${dim("just save the API key")}`);
|
|
231
295
|
console.log();
|
|
232
|
-
const choice = await ask(` Choice ${dim("[1-
|
|
296
|
+
const choice = await ask(` Choice ${dim("[1-6]")}: `);
|
|
233
297
|
await saveApiKey(trimmedKey);
|
|
234
298
|
switch (choice.trim()) {
|
|
235
299
|
case "1":
|
|
236
300
|
await setupClaudeCode(trimmedKey);
|
|
237
301
|
break;
|
|
238
302
|
case "2":
|
|
239
|
-
await
|
|
303
|
+
await setupCodex(trimmedKey);
|
|
240
304
|
break;
|
|
241
305
|
case "3":
|
|
242
|
-
await
|
|
306
|
+
await setupHermes(trimmedKey);
|
|
243
307
|
break;
|
|
244
308
|
case "4":
|
|
309
|
+
await setupCursor(trimmedKey);
|
|
310
|
+
break;
|
|
311
|
+
case "5":
|
|
245
312
|
await setupClaudeCode(trimmedKey);
|
|
313
|
+
await setupCodex(trimmedKey);
|
|
246
314
|
await setupHermes(trimmedKey);
|
|
247
315
|
await setupCursor(trimmedKey);
|
|
248
316
|
break;
|
|
249
|
-
case "
|
|
317
|
+
case "6":
|
|
250
318
|
default:
|
|
251
319
|
break;
|
|
252
320
|
}
|
|
@@ -261,7 +329,7 @@ var main = async () => {
|
|
|
261
329
|
console.log(` ${dim("Next:")}`);
|
|
262
330
|
console.log(` ${dim("1.")} Enable notifications on your phone at ${cyan("pushary.com")}`);
|
|
263
331
|
console.log(` ${dim("2.")} Restart your agent to load the new config`);
|
|
264
|
-
console.log(` ${dim("3.")} Start coding
|
|
332
|
+
console.log(` ${dim("3.")} Start coding. Your agent will notify you automatically`);
|
|
265
333
|
console.log();
|
|
266
334
|
rl.close();
|
|
267
335
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
handleStop
|
|
4
|
+
} from "../chunk-EQE6Z4YQ.js";
|
|
5
|
+
import "../chunk-VUNL35KE.js";
|
|
6
|
+
|
|
7
|
+
// bin/pushary-stop-hook.ts
|
|
8
|
+
var main = async () => {
|
|
9
|
+
let rawInput = "";
|
|
10
|
+
for await (const chunk of process.stdin) {
|
|
11
|
+
rawInput += chunk;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const input = rawInput.trim() ? JSON.parse(rawInput) : {};
|
|
15
|
+
await handleStop(input);
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
main();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBaseUrl
|
|
3
|
+
} from "./chunk-VUNL35KE.js";
|
|
4
|
+
|
|
5
|
+
// src/api.ts
|
|
6
|
+
var mcpToolCall = async (apiKey, toolName, params) => {
|
|
7
|
+
const baseUrl = getBaseUrl();
|
|
8
|
+
const response = await fetch(`${baseUrl}/api/mcp/mcp`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
"Authorization": `Bearer ${apiKey}`
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
jsonrpc: "2.0",
|
|
16
|
+
id: Date.now(),
|
|
17
|
+
method: "tools/call",
|
|
18
|
+
params: { name: toolName, arguments: params }
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error(`Pushary API error: ${response.status} ${response.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
const json = await response.json();
|
|
25
|
+
if (json.error) throw new Error(json.error.message);
|
|
26
|
+
const text = json.result?.content?.[0]?.text;
|
|
27
|
+
if (!text) throw new Error("Empty response from Pushary");
|
|
28
|
+
return JSON.parse(text);
|
|
29
|
+
};
|
|
30
|
+
var askUser = async (apiKey, params) => {
|
|
31
|
+
const result = await mcpToolCall(apiKey, "ask_user", { ...params });
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
var waitForAnswer = async (apiKey, correlationId, timeoutMs = 3e4) => {
|
|
35
|
+
const result = await mcpToolCall(apiKey, "wait_for_answer", {
|
|
36
|
+
correlationId,
|
|
37
|
+
timeoutMs
|
|
38
|
+
});
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
var cancelQuestion = async (apiKey, correlationId) => {
|
|
42
|
+
await mcpToolCall(apiKey, "cancel_question", { correlationId });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
askUser,
|
|
47
|
+
waitForAnswer,
|
|
48
|
+
cancelQuestion
|
|
49
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getApiKey,
|
|
3
|
+
getBaseUrl
|
|
4
|
+
} from "./chunk-VUNL35KE.js";
|
|
5
|
+
|
|
6
|
+
// src/events.ts
|
|
7
|
+
import { hostname } from "os";
|
|
8
|
+
import { basename } from "path";
|
|
9
|
+
var reportEvent = async (event) => {
|
|
10
|
+
const apiKey = getApiKey();
|
|
11
|
+
const baseUrl = getBaseUrl();
|
|
12
|
+
await fetch(`${baseUrl}/api/agent/event`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"Authorization": `Bearer ${apiKey}`
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
...event,
|
|
20
|
+
machineId: event.machineId ?? hostname()
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
var handlePostToolUse = async (input) => {
|
|
25
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
26
|
+
let action;
|
|
27
|
+
switch (input.tool_name) {
|
|
28
|
+
case "Bash":
|
|
29
|
+
action = `ran: ${String(input.tool_input.command ?? "").slice(0, 120)}`;
|
|
30
|
+
break;
|
|
31
|
+
case "Write":
|
|
32
|
+
action = `wrote: ${input.tool_input.file_path ?? "unknown"}`;
|
|
33
|
+
break;
|
|
34
|
+
case "Edit":
|
|
35
|
+
action = `edited: ${input.tool_input.file_path ?? "unknown"}`;
|
|
36
|
+
break;
|
|
37
|
+
case "Read":
|
|
38
|
+
action = `read: ${input.tool_input.file_path ?? "unknown"}`;
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
action = `${input.tool_name}: done`;
|
|
42
|
+
}
|
|
43
|
+
const isError = input.tool_result && ("error" in input.tool_result || "is_error" in input.tool_result);
|
|
44
|
+
await reportEvent({
|
|
45
|
+
event: isError ? "tool_error" : "tool_complete",
|
|
46
|
+
agentType: "claude_code",
|
|
47
|
+
agentName: `Claude Code - ${projectName}`,
|
|
48
|
+
action,
|
|
49
|
+
error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
var handleStop = async (input) => {
|
|
53
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
54
|
+
await reportEvent({
|
|
55
|
+
event: "session_end",
|
|
56
|
+
agentType: "claude_code",
|
|
57
|
+
agentName: `Claude Code - ${projectName}`,
|
|
58
|
+
action: "Session ended"
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
var handleNotification = async (input) => {
|
|
62
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
63
|
+
await reportEvent({
|
|
64
|
+
event: input.type === "error" ? "error" : "notification",
|
|
65
|
+
agentType: "claude_code",
|
|
66
|
+
agentName: `Claude Code - ${projectName}`,
|
|
67
|
+
action: input.title ?? input.message ?? "Notification",
|
|
68
|
+
error: input.type === "error" ? input.message : void 0
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
reportEvent,
|
|
74
|
+
handlePostToolUse,
|
|
75
|
+
handleStop,
|
|
76
|
+
handleNotification
|
|
77
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
askUser,
|
|
3
|
+
waitForAnswer
|
|
4
|
+
} from "./chunk-4TWRLEOX.js";
|
|
5
|
+
import {
|
|
6
|
+
getApiKey,
|
|
7
|
+
getBaseUrl
|
|
8
|
+
} from "./chunk-VUNL35KE.js";
|
|
9
|
+
|
|
10
|
+
// src/policy.ts
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
var cacheFile = (apiKey) => {
|
|
16
|
+
const hash = createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
17
|
+
return join(tmpdir(), `pushary-policy-${hash}.json`);
|
|
18
|
+
};
|
|
19
|
+
var fetchPolicy = async (apiKey) => {
|
|
20
|
+
const baseUrl = getBaseUrl();
|
|
21
|
+
const response = await fetch(`${baseUrl}/api/mcp/policy`, {
|
|
22
|
+
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
23
|
+
signal: AbortSignal.timeout(1e4)
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch policy: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
};
|
|
30
|
+
var getPolicy = async (apiKey) => {
|
|
31
|
+
const path = cacheFile(apiKey);
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const policy = await fetchPolicy(apiKey);
|
|
39
|
+
writeFileSync(path, JSON.stringify(policy), "utf-8");
|
|
40
|
+
return policy;
|
|
41
|
+
};
|
|
42
|
+
var resolvePolicy = (config, toolName) => {
|
|
43
|
+
const exact = config.policies.find((p) => p.tool === toolName);
|
|
44
|
+
if (exact) return exact;
|
|
45
|
+
const wildcard = config.policies.find((p) => p.tool === "*");
|
|
46
|
+
if (wildcard) return wildcard;
|
|
47
|
+
return {
|
|
48
|
+
tool: toolName,
|
|
49
|
+
timeoutSeconds: config.defaultTimeoutSeconds,
|
|
50
|
+
timeoutAction: config.defaultTimeoutAction
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/hook.ts
|
|
55
|
+
import { basename } from "path";
|
|
56
|
+
var describeToolCall = (input) => {
|
|
57
|
+
const { tool_name, tool_input } = input;
|
|
58
|
+
switch (tool_name) {
|
|
59
|
+
case "Bash":
|
|
60
|
+
return `bash: ${tool_input.command ?? "(no command)"}`;
|
|
61
|
+
case "Write":
|
|
62
|
+
return `write file: ${tool_input.file_path ?? "(unknown path)"}`;
|
|
63
|
+
case "Edit":
|
|
64
|
+
return `edit file: ${tool_input.file_path ?? "(unknown path)"}`;
|
|
65
|
+
case "Read":
|
|
66
|
+
return `read file: ${tool_input.file_path ?? "(unknown path)"}`;
|
|
67
|
+
default:
|
|
68
|
+
return `${tool_name}: ${JSON.stringify(tool_input).slice(0, 200)}`;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
72
|
+
var allow = () => ({
|
|
73
|
+
hookSpecificOutput: {
|
|
74
|
+
hookEventName: "PreToolUse",
|
|
75
|
+
permissionDecision: "allow"
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
var deny = (reason) => ({
|
|
79
|
+
hookSpecificOutput: {
|
|
80
|
+
hookEventName: "PreToolUse",
|
|
81
|
+
permissionDecision: "deny",
|
|
82
|
+
permissionDecisionReason: reason
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
var ask = (reason) => ({
|
|
86
|
+
hookSpecificOutput: {
|
|
87
|
+
hookEventName: "PreToolUse",
|
|
88
|
+
permissionDecision: "ask",
|
|
89
|
+
permissionDecisionReason: reason
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
var handlePreToolUse = async (input) => {
|
|
93
|
+
const apiKey = getApiKey();
|
|
94
|
+
const policy = await getPolicy(apiKey);
|
|
95
|
+
const toolPolicy = resolvePolicy(policy, input.tool_name);
|
|
96
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
|
|
97
|
+
return allow();
|
|
98
|
+
}
|
|
99
|
+
const description = describeToolCall(input);
|
|
100
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
101
|
+
const result = await askUser(apiKey, {
|
|
102
|
+
question: `Allow ${description}?`,
|
|
103
|
+
type: "confirm",
|
|
104
|
+
context: `Agent wants to run this in ${projectName}`,
|
|
105
|
+
agentName: `Claude Code - ${projectName}`
|
|
106
|
+
});
|
|
107
|
+
const deadline = Date.now() + toolPolicy.timeoutSeconds * 1e3;
|
|
108
|
+
const pollInterval = 2e3;
|
|
109
|
+
while (Date.now() < deadline) {
|
|
110
|
+
const remaining = Math.min(
|
|
111
|
+
Math.max(deadline - Date.now(), 1e3),
|
|
112
|
+
3e4
|
|
113
|
+
);
|
|
114
|
+
const answer = await waitForAnswer(apiKey, result.correlationId, remaining);
|
|
115
|
+
if (answer.answered) {
|
|
116
|
+
return answer.value === "yes" ? allow() : deny("Denied via Pushary push notification");
|
|
117
|
+
}
|
|
118
|
+
if (Date.now() + pollInterval >= deadline) break;
|
|
119
|
+
await sleep(pollInterval);
|
|
120
|
+
}
|
|
121
|
+
switch (toolPolicy.timeoutAction) {
|
|
122
|
+
case "approve":
|
|
123
|
+
return allow();
|
|
124
|
+
case "deny":
|
|
125
|
+
return deny("No response within timeout");
|
|
126
|
+
case "escalate":
|
|
127
|
+
default:
|
|
128
|
+
return ask("Pushary: no response, asking in terminal");
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
getPolicy,
|
|
134
|
+
resolvePolicy,
|
|
135
|
+
handlePreToolUse
|
|
136
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export {
|
|
14
|
+
getApiKey,
|
|
15
|
+
getBaseUrl
|
|
16
|
+
};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,6 +13,35 @@ interface HookOutput {
|
|
|
13
13
|
}
|
|
14
14
|
declare const handlePreToolUse: (input: ToolInput) => Promise<HookOutput | null>;
|
|
15
15
|
|
|
16
|
+
interface AgentEvent {
|
|
17
|
+
event: string;
|
|
18
|
+
agentType: string;
|
|
19
|
+
agentName?: string;
|
|
20
|
+
action?: string;
|
|
21
|
+
machineId?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
declare const reportEvent: (event: AgentEvent) => Promise<void>;
|
|
25
|
+
declare const handlePostToolUse: (input: {
|
|
26
|
+
tool_name: string;
|
|
27
|
+
tool_input: Record<string, unknown>;
|
|
28
|
+
tool_result?: Record<string, unknown>;
|
|
29
|
+
cwd?: string;
|
|
30
|
+
session_id?: string;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
declare const handleStop: (input: {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
session_id?: string;
|
|
35
|
+
stop_hook_active?: boolean;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
declare const handleNotification: (input: {
|
|
38
|
+
message?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
type?: string;
|
|
41
|
+
cwd?: string;
|
|
42
|
+
session_id?: string;
|
|
43
|
+
}) => Promise<void>;
|
|
44
|
+
|
|
16
45
|
interface AskUserParams {
|
|
17
46
|
question: string;
|
|
18
47
|
type?: 'confirm' | 'select' | 'input';
|
|
@@ -48,4 +77,4 @@ declare const resolvePolicy: (config: PolicyConfig, toolName: string) => ToolPol
|
|
|
48
77
|
declare const getApiKey: () => string;
|
|
49
78
|
declare const getBaseUrl: () => string;
|
|
50
79
|
|
|
51
|
-
export { type PolicyConfig, type ToolPolicy, askUser, cancelQuestion, getApiKey, getBaseUrl, getPolicy, handlePreToolUse, resolvePolicy, waitForAnswer };
|
|
80
|
+
export { type PolicyConfig, type ToolPolicy, askUser, cancelQuestion, getApiKey, getBaseUrl, getPolicy, handleNotification, handlePostToolUse, handlePreToolUse, handleStop, reportEvent, resolvePolicy, waitForAnswer };
|
package/dist/src/index.js
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
-
askUser,
|
|
3
|
-
cancelQuestion,
|
|
4
|
-
getApiKey,
|
|
5
|
-
getBaseUrl,
|
|
6
2
|
getPolicy,
|
|
7
3
|
handlePreToolUse,
|
|
8
|
-
resolvePolicy
|
|
4
|
+
resolvePolicy
|
|
5
|
+
} from "../chunk-KINE5LNQ.js";
|
|
6
|
+
import {
|
|
7
|
+
askUser,
|
|
8
|
+
cancelQuestion,
|
|
9
9
|
waitForAnswer
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-4TWRLEOX.js";
|
|
11
|
+
import {
|
|
12
|
+
handleNotification,
|
|
13
|
+
handlePostToolUse,
|
|
14
|
+
handleStop,
|
|
15
|
+
reportEvent
|
|
16
|
+
} from "../chunk-EQE6Z4YQ.js";
|
|
17
|
+
import {
|
|
18
|
+
getApiKey,
|
|
19
|
+
getBaseUrl
|
|
20
|
+
} from "../chunk-VUNL35KE.js";
|
|
11
21
|
export {
|
|
12
22
|
askUser,
|
|
13
23
|
cancelQuestion,
|
|
14
24
|
getApiKey,
|
|
15
25
|
getBaseUrl,
|
|
16
26
|
getPolicy,
|
|
27
|
+
handleNotification,
|
|
28
|
+
handlePostToolUse,
|
|
17
29
|
handlePreToolUse,
|
|
30
|
+
handleStop,
|
|
31
|
+
reportEvent,
|
|
18
32
|
resolvePolicy,
|
|
19
33
|
waitForAnswer
|
|
20
34
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Permission hooks for AI coding agents
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"repository": {
|
|
@@ -17,14 +17,17 @@
|
|
|
17
17
|
"agent-hooks": "./dist/bin/pushary.js",
|
|
18
18
|
"pushary": "./dist/bin/pushary.js",
|
|
19
19
|
"pushary-hook": "./dist/bin/pushary-hook.js",
|
|
20
|
+
"pushary-post-hook": "./dist/bin/pushary-post-hook.js",
|
|
21
|
+
"pushary-stop-hook": "./dist/bin/pushary-stop-hook.js",
|
|
22
|
+
"pushary-codex": "./dist/bin/pushary-codex.js",
|
|
20
23
|
"pushary-setup": "./dist/bin/pushary-setup.js"
|
|
21
24
|
},
|
|
22
25
|
"files": [
|
|
23
26
|
"dist"
|
|
24
27
|
],
|
|
25
28
|
"scripts": {
|
|
26
|
-
"build": "tsup src/index.ts bin/pushary.ts bin/pushary-hook.ts bin/pushary-setup.ts --format esm --dts --outDir dist",
|
|
27
|
-
"dev": "tsup src/index.ts bin/pushary.ts bin/pushary-hook.ts bin/pushary-setup.ts --format esm --watch"
|
|
29
|
+
"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 --format esm --dts --outDir dist",
|
|
30
|
+
"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 --format esm --watch"
|
|
28
31
|
},
|
|
29
32
|
"devDependencies": {
|
|
30
33
|
"tsup": "^8.0.0",
|