@pushary/agent-hooks 0.12.0 → 0.14.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 +16 -3
- package/dist/bin/pushary-codex-hook.d.ts +1 -0
- package/dist/bin/pushary-codex-hook.js +298 -0
- package/dist/bin/pushary-codex.js +10 -3
- package/dist/bin/pushary-doctor.js +38 -8
- package/dist/bin/pushary-hook.js +3 -3
- package/dist/bin/pushary-post-hook.js +3 -2
- package/dist/bin/pushary-prompt-hook.d.ts +1 -0
- package/dist/bin/pushary-prompt-hook.js +25 -0
- package/dist/bin/pushary-setup.js +94 -23
- package/dist/bin/pushary-stop-hook.js +3 -2
- package/dist/chunk-22CV7V7A.js +38 -0
- package/dist/chunk-2HMNOZPY.js +109 -0
- package/dist/chunk-5MA3CPZB.js +141 -0
- package/dist/chunk-CH53PBQN.js +265 -0
- package/dist/chunk-QRXWPZKN.js +308 -0
- package/dist/chunk-RNWPCELY.js +176 -0
- package/dist/chunk-SH26ZOHU.js +159 -0
- package/dist/chunk-TRLBBLSS.js +176 -0
- package/dist/chunk-WCGKLHCL.js +154 -0
- package/dist/src/index.d.ts +24 -4
- package/dist/src/index.js +9 -9
- package/package.json +4 -2
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
removeClaudeMcpServers,
|
|
4
4
|
removePusharySettings
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-5MA3CPZB.js";
|
|
6
6
|
import {
|
|
7
|
-
execNpm
|
|
8
|
-
|
|
7
|
+
execNpm,
|
|
8
|
+
removeCodexHooks
|
|
9
|
+
} from "../chunk-2HMNOZPY.js";
|
|
9
10
|
|
|
10
11
|
// bin/pushary-clean.ts
|
|
11
12
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
@@ -121,6 +122,18 @@ var main = async () => {
|
|
|
121
122
|
} catch {
|
|
122
123
|
console.log(` ${skip} Codex config ${dim("(not found)")}`);
|
|
123
124
|
}
|
|
125
|
+
const codexHooksJson = join(homedir(), ".codex", "hooks.json");
|
|
126
|
+
const codexHooksData = readJson(codexHooksJson);
|
|
127
|
+
if (codexHooksData) {
|
|
128
|
+
if (removeCodexHooks(codexHooksData)) {
|
|
129
|
+
writeJson(codexHooksJson, codexHooksData);
|
|
130
|
+
console.log(` ${check} Codex hooks ${dim("(removed from ~/.codex/hooks.json)")}`);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` ${skip} Codex hooks ${dim("(no pushary entries)")}`);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
console.log(` ${skip} Codex hooks ${dim("(not found)")}`);
|
|
136
|
+
}
|
|
124
137
|
for (const shellFile of SHELL_FILES) {
|
|
125
138
|
try {
|
|
126
139
|
const content = readFileSync(shellFile, "utf-8");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
handlePostToolUse,
|
|
4
|
+
handleStop,
|
|
5
|
+
handleUserPrompt,
|
|
6
|
+
reportEvent
|
|
7
|
+
} from "../chunk-SH26ZOHU.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_SESSION,
|
|
10
|
+
askUser,
|
|
11
|
+
deriveToolTarget,
|
|
12
|
+
describeToolCall,
|
|
13
|
+
fetchModeState,
|
|
14
|
+
getMachineId,
|
|
15
|
+
getPolicy,
|
|
16
|
+
resolvePolicy,
|
|
17
|
+
savePendingQuestion,
|
|
18
|
+
sendNotification,
|
|
19
|
+
waitForAnswer
|
|
20
|
+
} from "../chunk-QRXWPZKN.js";
|
|
21
|
+
import "../chunk-22CV7V7A.js";
|
|
22
|
+
import "../chunk-3MIR7ODJ.js";
|
|
23
|
+
import {
|
|
24
|
+
getApiKey
|
|
25
|
+
} from "../chunk-VUNL35KE.js";
|
|
26
|
+
|
|
27
|
+
// bin/pushary-codex-hook.ts
|
|
28
|
+
import { basename, join } from "path";
|
|
29
|
+
import { tmpdir } from "os";
|
|
30
|
+
import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
31
|
+
|
|
32
|
+
// src/codex-adapter.ts
|
|
33
|
+
var CODEX_AGENT = { type: "codex", label: "Codex" };
|
|
34
|
+
var codexAllow = () => ({ kind: "allow" });
|
|
35
|
+
var codexDeny = (reason) => ({ kind: "deny", reason });
|
|
36
|
+
var codexPass = () => ({ kind: "pass" });
|
|
37
|
+
var toCodexWire = (event, decision) => {
|
|
38
|
+
if (event === "PermissionRequest") {
|
|
39
|
+
if (decision.kind === "allow") {
|
|
40
|
+
return {
|
|
41
|
+
hookSpecificOutput: {
|
|
42
|
+
hookEventName: "PermissionRequest",
|
|
43
|
+
decision: { behavior: "allow" }
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (decision.kind === "deny") {
|
|
48
|
+
return {
|
|
49
|
+
hookSpecificOutput: {
|
|
50
|
+
hookEventName: "PermissionRequest",
|
|
51
|
+
decision: { behavior: "deny", message: decision.reason }
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (event === "PreToolUse" && decision.kind === "deny") {
|
|
58
|
+
return {
|
|
59
|
+
hookSpecificOutput: {
|
|
60
|
+
hookEventName: "PreToolUse",
|
|
61
|
+
permissionDecision: "deny",
|
|
62
|
+
permissionDecisionReason: decision.reason
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
var toPolicyLookup = (toolName, toolInput) => {
|
|
69
|
+
if (toolName !== "apply_patch") return { tool: toolName, input: toolInput };
|
|
70
|
+
const command = toolInput.command;
|
|
71
|
+
return {
|
|
72
|
+
tool: "Edit",
|
|
73
|
+
input: typeof command === "string" ? { file_path: command } : {}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
var permissionTimeoutDecision = (timeoutAction) => {
|
|
77
|
+
if (timeoutAction === "approve") return codexAllow();
|
|
78
|
+
if (timeoutAction === "deny") return codexDeny("No response within timeout");
|
|
79
|
+
return codexPass();
|
|
80
|
+
};
|
|
81
|
+
var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
|
|
82
|
+
|
|
83
|
+
// bin/pushary-codex-hook.ts
|
|
84
|
+
var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
|
|
85
|
+
var MAX_WAIT_SECONDS = 170;
|
|
86
|
+
var APPROVAL_DIR = join(tmpdir(), "pushary-codex-approvals");
|
|
87
|
+
var APPROVAL_TTL_MS = 10 * 60 * 1e3;
|
|
88
|
+
var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
|
|
89
|
+
var markApproved = (toolUseId) => {
|
|
90
|
+
if (!toolUseId) return;
|
|
91
|
+
try {
|
|
92
|
+
if (!existsSync(APPROVAL_DIR)) mkdirSync(APPROVAL_DIR, { recursive: true });
|
|
93
|
+
writeFileSync(join(APPROVAL_DIR, sanitizeId(toolUseId)), "", "utf-8");
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var consumeApproval = (toolUseId) => {
|
|
98
|
+
if (!toolUseId) return false;
|
|
99
|
+
const path = join(APPROVAL_DIR, sanitizeId(toolUseId));
|
|
100
|
+
try {
|
|
101
|
+
const fresh = Date.now() - statSync(path).mtimeMs < APPROVAL_TTL_MS;
|
|
102
|
+
unlinkSync(path);
|
|
103
|
+
return fresh;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
|
|
110
|
+
while (Date.now() < deadlineMs) {
|
|
111
|
+
const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
|
|
112
|
+
let answer;
|
|
113
|
+
try {
|
|
114
|
+
answer = await waitForAnswer(apiKey, correlationId, remaining);
|
|
115
|
+
} catch {
|
|
116
|
+
if (Date.now() + pollInterval >= deadlineMs) break;
|
|
117
|
+
await sleep(pollInterval);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (answer.answered) return answer;
|
|
121
|
+
if (Date.now() + pollInterval >= deadlineMs) break;
|
|
122
|
+
await sleep(pollInterval);
|
|
123
|
+
}
|
|
124
|
+
return { answered: false };
|
|
125
|
+
};
|
|
126
|
+
var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
|
|
127
|
+
var pushQuestion = async (apiKey, input, waitSeconds) => {
|
|
128
|
+
const toolName = input.tool_name ?? "";
|
|
129
|
+
const toolInput = input.tool_input ?? {};
|
|
130
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
131
|
+
const description = describeToolCall(toolName, toolInput, "hook");
|
|
132
|
+
const result = await askUser(apiKey, {
|
|
133
|
+
question: `Allow ${description}?`,
|
|
134
|
+
type: "confirm",
|
|
135
|
+
context: `Agent wants to run this in ${projectName}`,
|
|
136
|
+
agentName: agentNameFor(input),
|
|
137
|
+
sessionId: input.session_id,
|
|
138
|
+
machineId: getMachineId(),
|
|
139
|
+
toolName,
|
|
140
|
+
toolTarget: deriveToolTarget(toolName, toolInput)
|
|
141
|
+
});
|
|
142
|
+
const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
|
|
143
|
+
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
144
|
+
return { answer, correlationId: result.correlationId };
|
|
145
|
+
};
|
|
146
|
+
var notifyApprovalNeeded = async (apiKey, input) => {
|
|
147
|
+
try {
|
|
148
|
+
await sendNotification(apiKey, {
|
|
149
|
+
title: "Agent needs approval",
|
|
150
|
+
body: describeToolCall(input.tool_name ?? "", input.tool_input ?? {}, "hook"),
|
|
151
|
+
agentName: agentNameFor(input),
|
|
152
|
+
sessionId: input.session_id,
|
|
153
|
+
machineId: getMachineId()
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var decidePermissionRequest = async (input) => {
|
|
159
|
+
try {
|
|
160
|
+
const apiKey = getApiKey();
|
|
161
|
+
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
162
|
+
if (modeState.kill) return codexDeny(KILL_REASON);
|
|
163
|
+
const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
|
|
164
|
+
const policy = await getPolicy(apiKey);
|
|
165
|
+
const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
|
|
166
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
|
|
167
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
|
|
168
|
+
return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
|
|
169
|
+
}
|
|
170
|
+
if (toolPolicy.mode === "terminal_only") return codexPass();
|
|
171
|
+
if (toolPolicy.mode === "notify_only") {
|
|
172
|
+
await notifyApprovalNeeded(apiKey, input);
|
|
173
|
+
return codexPass();
|
|
174
|
+
}
|
|
175
|
+
const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : toolPolicy.timeoutSeconds;
|
|
176
|
+
const { answer, correlationId } = await pushQuestion(apiKey, input, waitSeconds);
|
|
177
|
+
if (answer.answered) {
|
|
178
|
+
if (answer.value === "yes") {
|
|
179
|
+
if (toolPolicy.mode === "push_only") markApproved(input.tool_use_id);
|
|
180
|
+
return codexAllow();
|
|
181
|
+
}
|
|
182
|
+
return codexDeny("Denied via push notification");
|
|
183
|
+
}
|
|
184
|
+
savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
|
|
185
|
+
if (toolPolicy.mode === "push_first") return codexPass();
|
|
186
|
+
const decision = permissionTimeoutDecision(toolPolicy.timeoutAction);
|
|
187
|
+
if (decision.kind === "allow") markApproved(input.tool_use_id);
|
|
188
|
+
return decision;
|
|
189
|
+
} catch {
|
|
190
|
+
return codexPass();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var decidePreToolUse = async (input) => {
|
|
194
|
+
try {
|
|
195
|
+
const apiKey = getApiKey();
|
|
196
|
+
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
197
|
+
if (modeState.kill) return codexDeny(KILL_REASON);
|
|
198
|
+
const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
|
|
199
|
+
const policy = await getPolicy(apiKey);
|
|
200
|
+
const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
|
|
201
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
|
|
202
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
|
|
203
|
+
return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
|
|
204
|
+
}
|
|
205
|
+
if (toolPolicy.mode === "push_only") {
|
|
206
|
+
if (consumeApproval(input.tool_use_id)) return codexPass();
|
|
207
|
+
let pushed;
|
|
208
|
+
try {
|
|
209
|
+
pushed = await pushQuestion(apiKey, input, toolPolicy.timeoutSeconds);
|
|
210
|
+
} catch {
|
|
211
|
+
return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
|
|
212
|
+
}
|
|
213
|
+
const { answer, correlationId } = pushed;
|
|
214
|
+
if (answer.answered) {
|
|
215
|
+
return answer.value === "yes" ? codexPass() : codexDeny("Denied via push notification");
|
|
216
|
+
}
|
|
217
|
+
savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
|
|
218
|
+
return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
|
|
219
|
+
}
|
|
220
|
+
if (toolPolicy.mode === "notify_only") {
|
|
221
|
+
await notifyApprovalNeeded(apiKey, input);
|
|
222
|
+
return codexPass();
|
|
223
|
+
}
|
|
224
|
+
return codexPass();
|
|
225
|
+
} catch {
|
|
226
|
+
return codexPass();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var reportSessionStart = async (input) => {
|
|
230
|
+
try {
|
|
231
|
+
await reportEvent({
|
|
232
|
+
event: "session_begin",
|
|
233
|
+
agentType: CODEX_AGENT.type,
|
|
234
|
+
agentName: agentNameFor(input),
|
|
235
|
+
action: "Session started",
|
|
236
|
+
sessionId: input.session_id
|
|
237
|
+
}, { maxAttempts: 1, timeoutMs: 5e3 });
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var asToolResult = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
242
|
+
var emit = (wire) => {
|
|
243
|
+
if (wire) process.stdout.write(JSON.stringify(wire));
|
|
244
|
+
};
|
|
245
|
+
var main = async () => {
|
|
246
|
+
let rawInput = "";
|
|
247
|
+
for await (const chunk of process.stdin) {
|
|
248
|
+
rawInput += chunk;
|
|
249
|
+
}
|
|
250
|
+
if (!rawInput.trim()) {
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
let input;
|
|
254
|
+
try {
|
|
255
|
+
input = JSON.parse(rawInput);
|
|
256
|
+
} catch {
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
switch (input.hook_event_name) {
|
|
261
|
+
case "PermissionRequest":
|
|
262
|
+
emit(toCodexWire("PermissionRequest", await decidePermissionRequest(input)));
|
|
263
|
+
break;
|
|
264
|
+
case "PreToolUse":
|
|
265
|
+
emit(toCodexWire("PreToolUse", await decidePreToolUse(input)));
|
|
266
|
+
break;
|
|
267
|
+
case "PostToolUse":
|
|
268
|
+
await handlePostToolUse({
|
|
269
|
+
tool_name: input.tool_name ?? "",
|
|
270
|
+
tool_input: input.tool_input ?? {},
|
|
271
|
+
tool_result: asToolResult(input.tool_response),
|
|
272
|
+
cwd: input.cwd,
|
|
273
|
+
session_id: input.session_id
|
|
274
|
+
}, CODEX_AGENT);
|
|
275
|
+
break;
|
|
276
|
+
case "UserPromptSubmit":
|
|
277
|
+
await handleUserPrompt({
|
|
278
|
+
prompt: input.prompt,
|
|
279
|
+
cwd: input.cwd,
|
|
280
|
+
session_id: input.session_id
|
|
281
|
+
}, CODEX_AGENT);
|
|
282
|
+
break;
|
|
283
|
+
case "Stop":
|
|
284
|
+
await handleStop({
|
|
285
|
+
cwd: input.cwd,
|
|
286
|
+
session_id: input.session_id
|
|
287
|
+
}, CODEX_AGENT);
|
|
288
|
+
break;
|
|
289
|
+
case "SessionStart":
|
|
290
|
+
await reportSessionStart(input);
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
main();
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
reportEvent
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-SH26ZOHU.js";
|
|
5
5
|
import {
|
|
6
6
|
askUser,
|
|
7
7
|
getMachineId,
|
|
8
8
|
waitForAnswer
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-QRXWPZKN.js";
|
|
10
|
+
import "../chunk-22CV7V7A.js";
|
|
10
11
|
import "../chunk-3MIR7ODJ.js";
|
|
11
12
|
import {
|
|
12
13
|
getApiKey
|
|
@@ -14,18 +15,24 @@ import {
|
|
|
14
15
|
|
|
15
16
|
// bin/pushary-codex.ts
|
|
16
17
|
import { basename } from "path";
|
|
18
|
+
var DEPRECATION_NOTICE = "[pushary-codex] Deprecated: this is the legacy Codex notify handler. Native Codex hooks via pushary-codex-hook replace it. Run npx @pushary/agent-hooks setup to migrate.\n";
|
|
17
19
|
var readStdin = async () => {
|
|
18
20
|
let raw = "";
|
|
19
21
|
for await (const chunk of process.stdin) raw += chunk;
|
|
20
22
|
return raw;
|
|
21
23
|
};
|
|
22
24
|
var main = async () => {
|
|
25
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
26
|
+
process.stderr.write(DEPRECATION_NOTICE);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
23
29
|
const argvPayload = process.argv.slice(2).find((a) => a.trim().startsWith("{"));
|
|
24
30
|
let rawInput = argvPayload ?? "";
|
|
25
|
-
if (!rawInput.trim()) {
|
|
31
|
+
if (!rawInput.trim() && !process.stdin.isTTY) {
|
|
26
32
|
rawInput = await readStdin();
|
|
27
33
|
}
|
|
28
34
|
if (!rawInput.trim()) {
|
|
35
|
+
if (process.stdin.isTTY) process.stderr.write(DEPRECATION_NOTICE);
|
|
29
36
|
process.exit(0);
|
|
30
37
|
}
|
|
31
38
|
let event;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
execNpm
|
|
4
|
-
|
|
3
|
+
execNpm,
|
|
4
|
+
hasCodexHooks,
|
|
5
|
+
missingCodexHookEvents
|
|
6
|
+
} from "../chunk-2HMNOZPY.js";
|
|
5
7
|
import {
|
|
6
8
|
callMcpTool,
|
|
7
9
|
sendMcpRequest
|
|
@@ -14,6 +16,7 @@ import { join } from "path";
|
|
|
14
16
|
import { homedir } from "os";
|
|
15
17
|
import { execSync } from "child_process";
|
|
16
18
|
import { confirm } from "@inquirer/prompts";
|
|
19
|
+
import { parse as parseTOML } from "smol-toml";
|
|
17
20
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
18
21
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
19
22
|
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
@@ -103,9 +106,11 @@ var main = async () => {
|
|
|
103
106
|
const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
|
|
104
107
|
const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
|
|
105
108
|
const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
|
|
109
|
+
const hasPromptHook = JSON.stringify(hooks?.UserPromptSubmit ?? []).includes("pushary-prompt-hook");
|
|
106
110
|
check(hasPreHook, "Claude Code: PreToolUse hook");
|
|
107
111
|
check(hasPostHook, "Claude Code: PostToolUse hook");
|
|
108
112
|
check(hasStopHook, "Claude Code: Stop hook");
|
|
113
|
+
check(hasPromptHook, "Claude Code: UserPromptSubmit hook", hasPromptHook ? void 0 : "missing, re-run setup to register it");
|
|
109
114
|
const preHookCommand = extractHookCommand(hooks?.PreToolUse, "pushary-hook");
|
|
110
115
|
if (preHookCommand) {
|
|
111
116
|
const resolves = commandResolves(preHookCommand);
|
|
@@ -122,8 +127,10 @@ var main = async () => {
|
|
|
122
127
|
check(false, "Claude Code: settings.json", "not found");
|
|
123
128
|
}
|
|
124
129
|
const codexConfigPath = join(homedir(), ".codex", "config.toml");
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
const codexHooksPath = join(homedir(), ".codex", "hooks.json");
|
|
131
|
+
const codexHooksJson = readJson(codexHooksPath);
|
|
132
|
+
if (existsSync(codexConfigPath) || codexHooksJson) {
|
|
133
|
+
const codexConfig = existsSync(codexConfigPath) ? readFileSync(codexConfigPath, "utf-8") : "";
|
|
127
134
|
const hasPusharyMcp = codexConfig.includes("[mcp_servers.pushary]");
|
|
128
135
|
check(hasPusharyMcp, "Codex: MCP server configured");
|
|
129
136
|
if (hasPusharyMcp) {
|
|
@@ -135,10 +142,33 @@ var main = async () => {
|
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
144
|
const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
const
|
|
141
|
-
check(
|
|
145
|
+
const hooksInstalled = !!codexHooksJson && hasCodexHooks(codexHooksJson);
|
|
146
|
+
if (hooksInstalled) {
|
|
147
|
+
const missingEvents = missingCodexHookEvents(codexHooksJson);
|
|
148
|
+
check(missingEvents.length === 0, "Codex: native hooks installed", missingEvents.length === 0 ? "all 6 events" : `missing ${missingEvents.join(", ")}, re-run setup`);
|
|
149
|
+
const hookCommand = extractHookCommand(codexHooksJson.hooks?.PreToolUse, "pushary-codex-hook") ?? extractHookCommand(codexHooksJson.hooks?.PermissionRequest, "pushary-codex-hook");
|
|
150
|
+
if (hookCommand) {
|
|
151
|
+
const resolves = commandResolves(hookCommand);
|
|
152
|
+
check(resolves, "Codex: hook command resolves", resolves ? hookCommand : `not on PATH: ${hookCommand}`);
|
|
153
|
+
}
|
|
154
|
+
let hooksFeatureDisabled = false;
|
|
155
|
+
try {
|
|
156
|
+
const parsed = parseTOML(codexConfig);
|
|
157
|
+
const features = parsed.features;
|
|
158
|
+
hooksFeatureDisabled = !!features && typeof features === "object" && features.hooks === false;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
check(!hooksFeatureDisabled, "Codex: hooks feature enabled", hooksFeatureDisabled ? "[features].hooks = false in config.toml" : void 0);
|
|
162
|
+
if (codexNotifyPath) {
|
|
163
|
+
console.log(` ${warn} Codex: stale legacy notify entry ${dim("(double-push risk, re-run setup to remove it)")}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(` ${warn} Codex: hooks must be trusted inside Codex ${dim("(run /hooks in Codex, cannot be verified from here)")}`);
|
|
166
|
+
} else {
|
|
167
|
+
check(!!codexNotifyPath, "Codex: notify handler configured (deprecated)", codexNotifyPath ? "upgrade Codex and re-run setup for native hooks" : "missing, re-run setup");
|
|
168
|
+
if (codexNotifyPath) {
|
|
169
|
+
const resolves = commandResolves(codexNotifyPath);
|
|
170
|
+
check(resolves, "Codex: notify handler resolves", resolves ? codexNotifyPath : `not found: ${codexNotifyPath}`);
|
|
171
|
+
}
|
|
142
172
|
}
|
|
143
173
|
const codexSkillPath = join(homedir(), ".codex", "skills", "pushary", "SKILL.md");
|
|
144
174
|
check(existsSync(codexSkillPath), "Codex: skill installed");
|
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-TRLBBLSS.js";
|
|
5
|
+
import "../chunk-QRXWPZKN.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
7
7
|
import "../chunk-3MIR7ODJ.js";
|
|
8
8
|
import "../chunk-VUNL35KE.js";
|
|
9
9
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePostToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-SH26ZOHU.js";
|
|
5
|
+
import "../chunk-QRXWPZKN.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
6
7
|
import "../chunk-3MIR7ODJ.js";
|
|
7
8
|
import "../chunk-VUNL35KE.js";
|
|
8
9
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
handleUserPrompt
|
|
4
|
+
} from "../chunk-SH26ZOHU.js";
|
|
5
|
+
import "../chunk-QRXWPZKN.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
7
|
+
import "../chunk-3MIR7ODJ.js";
|
|
8
|
+
import "../chunk-VUNL35KE.js";
|
|
9
|
+
|
|
10
|
+
// bin/pushary-prompt-hook.ts
|
|
11
|
+
var main = async () => {
|
|
12
|
+
let rawInput = "";
|
|
13
|
+
for await (const chunk of process.stdin) {
|
|
14
|
+
rawInput += chunk;
|
|
15
|
+
}
|
|
16
|
+
if (!rawInput.trim()) {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const input = JSON.parse(rawInput);
|
|
21
|
+
await handleUserPrompt(input);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
main();
|
|
@@ -3,14 +3,15 @@ import {
|
|
|
3
3
|
addClaudeMcpServer,
|
|
4
4
|
addPusharyHooks,
|
|
5
5
|
addPusharyToolPermissions
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-5MA3CPZB.js";
|
|
7
7
|
import {
|
|
8
|
+
addCodexHooks,
|
|
8
9
|
execNpm,
|
|
9
10
|
npmErrorMessage
|
|
10
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-2HMNOZPY.js";
|
|
11
12
|
import {
|
|
12
13
|
isValidApiKey
|
|
13
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-22CV7V7A.js";
|
|
14
15
|
|
|
15
16
|
// bin/pushary-setup.ts
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, cpSync, rmSync } from "fs";
|
|
@@ -310,7 +311,7 @@ var setupClaudeCode = async (apiKey) => {
|
|
|
310
311
|
addPusharyToolPermissions(settings);
|
|
311
312
|
});
|
|
312
313
|
await installGlobally();
|
|
313
|
-
await spinner("Adding hooks (PreToolUse, PostToolUse, Stop)", async () => {
|
|
314
|
+
await spinner("Adding hooks (PreToolUse, PostToolUse, UserPromptSubmit, Stop)", async () => {
|
|
314
315
|
let binDir;
|
|
315
316
|
try {
|
|
316
317
|
binDir = join(execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim(), "bin");
|
|
@@ -396,6 +397,63 @@ var setupHermes = async (_apiKey) => {
|
|
|
396
397
|
console.log(` ${dim2("\u2022")} Permission gating: set ${bold2("PUSHARY_GATE_TOOLS")} to require lock-screen approval for risky tools`);
|
|
397
398
|
console.log(` ${dim2("To re-enable terminal prompts:")} remove ${bold2("clarify")} from ${dim2("agent.disabled_toolsets")} in ~/.hermes/config.yaml`);
|
|
398
399
|
};
|
|
400
|
+
var CODEX_HOOKS_JSON = join(homedir(), ".codex", "hooks.json");
|
|
401
|
+
var CODEX_HOOKS_MIN_VERSION = [0, 122, 0];
|
|
402
|
+
var parseCodexVersion = (raw) => {
|
|
403
|
+
const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
404
|
+
if (!match) return null;
|
|
405
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
406
|
+
};
|
|
407
|
+
var codexSupportsHooks = () => {
|
|
408
|
+
try {
|
|
409
|
+
const raw = execSync("codex --version", { encoding: "utf-8", stdio: "pipe", timeout: 1e4 });
|
|
410
|
+
const version = parseCodexVersion(raw);
|
|
411
|
+
if (!version) return false;
|
|
412
|
+
for (let i = 0; i < 3; i++) {
|
|
413
|
+
if (version[i] > CODEX_HOOKS_MIN_VERSION[i]) return true;
|
|
414
|
+
if (version[i] < CODEX_HOOKS_MIN_VERSION[i]) return false;
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
var removeCodexNotifyEntry = (codexConfig) => {
|
|
422
|
+
let raw = "";
|
|
423
|
+
try {
|
|
424
|
+
raw = readFileSync(codexConfig, "utf-8");
|
|
425
|
+
} catch {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!raw.includes("pushary-codex")) return;
|
|
429
|
+
const config = parseTOML(raw);
|
|
430
|
+
if (!Array.isArray(config.notify)) return;
|
|
431
|
+
const filtered = config.notify.filter((entry) => typeof entry !== "string" || !entry.includes("pushary-codex"));
|
|
432
|
+
if (filtered.length === config.notify.length) return;
|
|
433
|
+
if (filtered.length === 0) {
|
|
434
|
+
delete config.notify;
|
|
435
|
+
} else {
|
|
436
|
+
config.notify = filtered;
|
|
437
|
+
}
|
|
438
|
+
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
439
|
+
};
|
|
440
|
+
var addCodexNotifyEntry = (codexConfig) => {
|
|
441
|
+
const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
|
|
442
|
+
const pusharyCodexPath = join(globalPrefix, "bin", "pushary-codex");
|
|
443
|
+
if (!existsSync(pusharyCodexPath)) throw new Error("pushary-codex not found at " + pusharyCodexPath);
|
|
444
|
+
let raw = "";
|
|
445
|
+
try {
|
|
446
|
+
raw = readFileSync(codexConfig, "utf-8");
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
const config = raw ? parseTOML(raw) : {};
|
|
450
|
+
const notify = Array.isArray(config.notify) ? config.notify : [];
|
|
451
|
+
if (!notify.some((n) => typeof n === "string" && n.includes("pushary-codex"))) {
|
|
452
|
+
notify.push(pusharyCodexPath);
|
|
453
|
+
config.notify = notify;
|
|
454
|
+
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
455
|
+
}
|
|
456
|
+
};
|
|
399
457
|
var setupCodex = async (_apiKey) => {
|
|
400
458
|
console.log(`
|
|
401
459
|
${bold2("Setting up Codex")}
|
|
@@ -431,29 +489,42 @@ var setupCodex = async (_apiKey) => {
|
|
|
431
489
|
config.mcp_servers = mcpServers;
|
|
432
490
|
writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
|
|
433
491
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
});
|
|
492
|
+
const hooksSupported = codexSupportsHooks();
|
|
493
|
+
if (hooksSupported) {
|
|
494
|
+
await spinner("Adding native hooks (~/.codex/hooks.json)", async () => {
|
|
495
|
+
const globalPrefix = execNpm("prefix -g --no-workspaces", { timeout: 5e3 }).toString().trim();
|
|
496
|
+
const hookCommand = join(globalPrefix, "bin", "pushary-codex-hook");
|
|
497
|
+
if (!existsSync(hookCommand)) throw new Error("pushary-codex-hook not found at " + hookCommand);
|
|
498
|
+
const hooksConfig = readJson(CODEX_HOOKS_JSON);
|
|
499
|
+
addCodexHooks(hooksConfig, hookCommand);
|
|
500
|
+
writeJson(CODEX_HOOKS_JSON, hooksConfig);
|
|
501
|
+
});
|
|
502
|
+
await spinner("Removing legacy notify handler", async () => {
|
|
503
|
+
removeCodexNotifyEntry(codexConfig);
|
|
504
|
+
});
|
|
505
|
+
} else {
|
|
506
|
+
console.log(` ${yellow2("!")} This Codex version predates native hooks (needs ${CODEX_HOOKS_MIN_VERSION.join(".")}+).`);
|
|
507
|
+
console.log(` ${dim2("Installing the deprecated notify handler instead. Upgrade Codex and re-run setup")}`);
|
|
508
|
+
console.log(` ${dim2("to get policy enforcement, phone approvals, and session tracking.")}`);
|
|
509
|
+
await spinner("Adding notify handler for Codex events (deprecated)", async () => {
|
|
510
|
+
addCodexNotifyEntry(codexConfig);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
451
513
|
await installSkillToDir(CODEX_SKILL_DIR, "Installing Pushary skill");
|
|
452
514
|
console.log();
|
|
453
515
|
console.log(` ${dim2("What this configured:")}`);
|
|
454
516
|
console.log(` ${dim2("\u2022")} MCP server: Codex can send notifications and ask questions`);
|
|
455
517
|
console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
|
|
456
|
-
|
|
518
|
+
if (hooksSupported) {
|
|
519
|
+
console.log(` ${dim2("\u2022")} Native hooks: phone approvals, policy enforcement, kill switch, session tracking`);
|
|
520
|
+
console.log();
|
|
521
|
+
console.log(` ${bold2("Trust step (required):")}`);
|
|
522
|
+
console.log(` ${dim2("1.")} Open Codex and run ${cyan2("/hooks")}`);
|
|
523
|
+
console.log(` ${dim2("2.")} Review the Pushary hooks and trust them`);
|
|
524
|
+
console.log(` ${dim2("Hooks stay inactive until you trust them inside Codex.")}`);
|
|
525
|
+
} else {
|
|
526
|
+
console.log(` ${dim2("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
|
|
527
|
+
}
|
|
457
528
|
};
|
|
458
529
|
var resolveBundledPlugin = () => {
|
|
459
530
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -568,7 +639,7 @@ var main = async () => {
|
|
|
568
639
|
message: "Which agents do you use? " + dim2(hint),
|
|
569
640
|
choices: [
|
|
570
641
|
{ name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
|
|
571
|
-
{ name: `Codex ${dim2("MCP +
|
|
642
|
+
{ name: `Codex ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "codex", checked: detected.codex },
|
|
572
643
|
{ name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
|
|
573
644
|
{ name: `Cursor ${dim2("MCP server")}`, value: "cursor", checked: detected.cursor }
|
|
574
645
|
]
|