@pushary/agent-hooks 0.13.0 → 0.14.1
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 +15 -2
- package/dist/bin/pushary-codex-hook.d.ts +1 -0
- package/dist/bin/pushary-codex-hook.js +262 -0
- package/dist/bin/pushary-codex.js +10 -4
- package/dist/bin/pushary-doctor.js +37 -9
- package/dist/bin/pushary-hook.js +3 -3
- package/dist/bin/pushary-post-hook.js +3 -3
- package/dist/bin/pushary-prompt-hook.js +3 -3
- package/dist/bin/pushary-setup.js +97 -20
- package/dist/bin/pushary-stop-hook.js +3 -3
- package/dist/chunk-2HMNOZPY.js +109 -0
- package/dist/chunk-HJMFYDWY.js +222 -0
- package/dist/chunk-QRXWPZKN.js +308 -0
- package/dist/chunk-SH26ZOHU.js +159 -0
- package/dist/chunk-TRLBBLSS.js +176 -0
- package/dist/chunk-V4GLWPD7.js +174 -0
- package/dist/chunk-XPVSZIA3.js +315 -0
- package/dist/src/index.d.ts +16 -3
- package/dist/src/index.js +4 -4
- package/package.json +3 -2
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
removePusharySettings
|
|
5
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,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CODEX_AGENT,
|
|
4
|
+
codexAllow,
|
|
5
|
+
codexDeny,
|
|
6
|
+
codexPass,
|
|
7
|
+
handlePostToolUse,
|
|
8
|
+
handleStop,
|
|
9
|
+
handleUserPrompt,
|
|
10
|
+
permissionTimeoutDecision,
|
|
11
|
+
preToolUseTimeoutDecision,
|
|
12
|
+
reportEvent,
|
|
13
|
+
toCodexWire,
|
|
14
|
+
toPolicyLookup
|
|
15
|
+
} from "../chunk-HJMFYDWY.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_SESSION,
|
|
18
|
+
askUser,
|
|
19
|
+
deriveToolTarget,
|
|
20
|
+
describeToolCall,
|
|
21
|
+
fetchModeState,
|
|
22
|
+
getMachineId,
|
|
23
|
+
getPolicy,
|
|
24
|
+
resolvePolicy,
|
|
25
|
+
savePendingQuestion,
|
|
26
|
+
sendNotification,
|
|
27
|
+
waitForAnswer
|
|
28
|
+
} from "../chunk-XPVSZIA3.js";
|
|
29
|
+
import "../chunk-22CV7V7A.js";
|
|
30
|
+
import "../chunk-3MIR7ODJ.js";
|
|
31
|
+
import {
|
|
32
|
+
getApiKey
|
|
33
|
+
} from "../chunk-VUNL35KE.js";
|
|
34
|
+
|
|
35
|
+
// bin/pushary-codex-hook.ts
|
|
36
|
+
import { basename, join } from "path";
|
|
37
|
+
import { tmpdir, userInfo } from "os";
|
|
38
|
+
import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
39
|
+
var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
|
|
40
|
+
var MAX_WAIT_SECONDS = 170;
|
|
41
|
+
var APPROVAL_TTL_MS = 10 * 60 * 1e3;
|
|
42
|
+
var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
|
|
43
|
+
var userScope = () => {
|
|
44
|
+
try {
|
|
45
|
+
const info = userInfo();
|
|
46
|
+
const id = typeof info.uid === "number" && info.uid >= 0 ? String(info.uid) : info.username;
|
|
47
|
+
return sanitizeId(id) || "default";
|
|
48
|
+
} catch {
|
|
49
|
+
return "default";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var APPROVAL_DIR = join(tmpdir(), `pushary-codex-approvals-${userScope()}`);
|
|
53
|
+
var markApproved = (toolUseId) => {
|
|
54
|
+
if (!toolUseId) return;
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(APPROVAL_DIR)) mkdirSync(APPROVAL_DIR, { recursive: true, mode: 448 });
|
|
57
|
+
writeFileSync(join(APPROVAL_DIR, sanitizeId(toolUseId)), "", { encoding: "utf-8", mode: 384 });
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var consumeApproval = (toolUseId) => {
|
|
62
|
+
if (!toolUseId) return false;
|
|
63
|
+
const path = join(APPROVAL_DIR, sanitizeId(toolUseId));
|
|
64
|
+
try {
|
|
65
|
+
const fresh = Date.now() - statSync(path).mtimeMs < APPROVAL_TTL_MS;
|
|
66
|
+
unlinkSync(path);
|
|
67
|
+
return fresh;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
|
|
74
|
+
while (Date.now() < deadlineMs) {
|
|
75
|
+
const remaining = Math.min(Math.max(deadlineMs - Date.now(), 1e3), 3e4);
|
|
76
|
+
let answer;
|
|
77
|
+
try {
|
|
78
|
+
answer = await waitForAnswer(apiKey, correlationId, remaining);
|
|
79
|
+
} catch {
|
|
80
|
+
if (Date.now() + pollInterval >= deadlineMs) break;
|
|
81
|
+
await sleep(pollInterval);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (answer.answered) return answer;
|
|
85
|
+
if (Date.now() + pollInterval >= deadlineMs) break;
|
|
86
|
+
await sleep(pollInterval);
|
|
87
|
+
}
|
|
88
|
+
return { answered: false };
|
|
89
|
+
};
|
|
90
|
+
var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
|
|
91
|
+
var pushQuestion = async (apiKey, input, waitSeconds) => {
|
|
92
|
+
const toolName = input.tool_name ?? "";
|
|
93
|
+
const toolInput = input.tool_input ?? {};
|
|
94
|
+
const projectName = basename(input.cwd ?? process.cwd());
|
|
95
|
+
const description = describeToolCall(toolName, toolInput, "hook");
|
|
96
|
+
const result = await askUser(apiKey, {
|
|
97
|
+
question: `Allow ${description}?`,
|
|
98
|
+
type: "confirm",
|
|
99
|
+
context: `Agent wants to run this in ${projectName}`,
|
|
100
|
+
agentName: agentNameFor(input),
|
|
101
|
+
sessionId: input.session_id,
|
|
102
|
+
machineId: getMachineId(),
|
|
103
|
+
toolName,
|
|
104
|
+
toolTarget: deriveToolTarget(toolName, toolInput)
|
|
105
|
+
});
|
|
106
|
+
const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
|
|
107
|
+
const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
|
|
108
|
+
return { answer, correlationId: result.correlationId };
|
|
109
|
+
};
|
|
110
|
+
var notifyApprovalNeeded = async (apiKey, input) => {
|
|
111
|
+
try {
|
|
112
|
+
await sendNotification(apiKey, {
|
|
113
|
+
title: "Agent needs approval",
|
|
114
|
+
body: describeToolCall(input.tool_name ?? "", input.tool_input ?? {}, "hook"),
|
|
115
|
+
agentName: agentNameFor(input),
|
|
116
|
+
sessionId: input.session_id,
|
|
117
|
+
machineId: getMachineId()
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var decidePermissionRequest = async (input) => {
|
|
123
|
+
try {
|
|
124
|
+
const apiKey = getApiKey();
|
|
125
|
+
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
126
|
+
if (modeState.kill) return codexDeny(KILL_REASON);
|
|
127
|
+
const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
|
|
128
|
+
const policy = await getPolicy(apiKey, modeState.policyVersion);
|
|
129
|
+
const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
|
|
130
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
|
|
131
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
|
|
132
|
+
return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
|
|
133
|
+
}
|
|
134
|
+
if (toolPolicy.mode === "terminal_only") return codexPass();
|
|
135
|
+
if (toolPolicy.mode === "notify_only") {
|
|
136
|
+
await notifyApprovalNeeded(apiKey, input);
|
|
137
|
+
return codexPass();
|
|
138
|
+
}
|
|
139
|
+
const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : toolPolicy.timeoutSeconds;
|
|
140
|
+
const { answer, correlationId } = await pushQuestion(apiKey, input, waitSeconds);
|
|
141
|
+
if (answer.answered) {
|
|
142
|
+
if (answer.value === "yes") {
|
|
143
|
+
if (toolPolicy.mode === "push_only") markApproved(input.tool_use_id);
|
|
144
|
+
return codexAllow();
|
|
145
|
+
}
|
|
146
|
+
return codexDeny("Denied via push notification");
|
|
147
|
+
}
|
|
148
|
+
savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
|
|
149
|
+
if (toolPolicy.mode === "push_first") return codexPass();
|
|
150
|
+
const decision = permissionTimeoutDecision(toolPolicy.timeoutAction);
|
|
151
|
+
if (decision.kind === "allow") markApproved(input.tool_use_id);
|
|
152
|
+
return decision;
|
|
153
|
+
} catch {
|
|
154
|
+
return codexPass();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var decidePreToolUse = async (input) => {
|
|
158
|
+
try {
|
|
159
|
+
const apiKey = getApiKey();
|
|
160
|
+
const modeState = await fetchModeState(apiKey, input.session_id);
|
|
161
|
+
if (modeState.kill) return codexDeny(KILL_REASON);
|
|
162
|
+
const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
|
|
163
|
+
const policy = await getPolicy(apiKey, modeState.policyVersion);
|
|
164
|
+
const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
|
|
165
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
|
|
166
|
+
if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "deny") {
|
|
167
|
+
return codexDeny(`Denied by policy for ${toolPolicy.tool}`);
|
|
168
|
+
}
|
|
169
|
+
if (toolPolicy.mode === "push_only") {
|
|
170
|
+
if (consumeApproval(input.tool_use_id)) return codexPass();
|
|
171
|
+
let pushed;
|
|
172
|
+
try {
|
|
173
|
+
pushed = await pushQuestion(apiKey, input, toolPolicy.timeoutSeconds);
|
|
174
|
+
} catch {
|
|
175
|
+
return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
|
|
176
|
+
}
|
|
177
|
+
const { answer, correlationId } = pushed;
|
|
178
|
+
if (answer.answered) {
|
|
179
|
+
return answer.value === "yes" ? codexPass() : codexDeny("Denied via push notification");
|
|
180
|
+
}
|
|
181
|
+
savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
|
|
182
|
+
return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
|
|
183
|
+
}
|
|
184
|
+
if (toolPolicy.mode === "notify_only") {
|
|
185
|
+
await notifyApprovalNeeded(apiKey, input);
|
|
186
|
+
return codexPass();
|
|
187
|
+
}
|
|
188
|
+
return codexPass();
|
|
189
|
+
} catch {
|
|
190
|
+
return codexPass();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var reportSessionStart = async (input) => {
|
|
194
|
+
try {
|
|
195
|
+
await reportEvent({
|
|
196
|
+
event: "session_begin",
|
|
197
|
+
agentType: CODEX_AGENT.type,
|
|
198
|
+
agentName: agentNameFor(input),
|
|
199
|
+
action: "Session started",
|
|
200
|
+
sessionId: input.session_id
|
|
201
|
+
}, { maxAttempts: 1, timeoutMs: 5e3 });
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var asToolResult = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
206
|
+
var emit = (wire) => {
|
|
207
|
+
if (wire) process.stdout.write(JSON.stringify(wire));
|
|
208
|
+
};
|
|
209
|
+
var main = async () => {
|
|
210
|
+
let rawInput = "";
|
|
211
|
+
for await (const chunk of process.stdin) {
|
|
212
|
+
rawInput += chunk;
|
|
213
|
+
}
|
|
214
|
+
if (!rawInput.trim()) {
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
let input;
|
|
218
|
+
try {
|
|
219
|
+
input = JSON.parse(rawInput);
|
|
220
|
+
} catch {
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
switch (input.hook_event_name) {
|
|
225
|
+
case "PermissionRequest":
|
|
226
|
+
emit(toCodexWire("PermissionRequest", await decidePermissionRequest(input)));
|
|
227
|
+
break;
|
|
228
|
+
case "PreToolUse":
|
|
229
|
+
emit(toCodexWire("PreToolUse", await decidePreToolUse(input)));
|
|
230
|
+
break;
|
|
231
|
+
case "PostToolUse":
|
|
232
|
+
await handlePostToolUse({
|
|
233
|
+
tool_name: input.tool_name ?? "",
|
|
234
|
+
tool_input: input.tool_input ?? {},
|
|
235
|
+
tool_result: asToolResult(input.tool_response),
|
|
236
|
+
cwd: input.cwd,
|
|
237
|
+
session_id: input.session_id
|
|
238
|
+
}, CODEX_AGENT);
|
|
239
|
+
break;
|
|
240
|
+
case "UserPromptSubmit":
|
|
241
|
+
await handleUserPrompt({
|
|
242
|
+
prompt: input.prompt,
|
|
243
|
+
cwd: input.cwd,
|
|
244
|
+
session_id: input.session_id
|
|
245
|
+
}, CODEX_AGENT);
|
|
246
|
+
break;
|
|
247
|
+
case "Stop":
|
|
248
|
+
await handleStop({
|
|
249
|
+
cwd: input.cwd,
|
|
250
|
+
session_id: input.session_id
|
|
251
|
+
}, CODEX_AGENT);
|
|
252
|
+
break;
|
|
253
|
+
case "SessionStart":
|
|
254
|
+
await reportSessionStart(input);
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
main();
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
reportEvent
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-HJMFYDWY.js";
|
|
5
5
|
import {
|
|
6
6
|
askUser,
|
|
7
7
|
getMachineId,
|
|
8
8
|
waitForAnswer
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-XPVSZIA3.js";
|
|
10
|
+
import "../chunk-22CV7V7A.js";
|
|
10
11
|
import "../chunk-3MIR7ODJ.js";
|
|
11
12
|
import {
|
|
12
13
|
getApiKey
|
|
13
14
|
} from "../chunk-VUNL35KE.js";
|
|
14
|
-
import "../chunk-22CV7V7A.js";
|
|
15
15
|
|
|
16
16
|
// bin/pushary-codex.ts
|
|
17
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";
|
|
18
19
|
var readStdin = async () => {
|
|
19
20
|
let raw = "";
|
|
20
21
|
for await (const chunk of process.stdin) raw += chunk;
|
|
21
22
|
return raw;
|
|
22
23
|
};
|
|
23
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
|
+
}
|
|
24
29
|
const argvPayload = process.argv.slice(2).find((a) => a.trim().startsWith("{"));
|
|
25
30
|
let rawInput = argvPayload ?? "";
|
|
26
|
-
if (!rawInput.trim()) {
|
|
31
|
+
if (!rawInput.trim() && !process.stdin.isTTY) {
|
|
27
32
|
rawInput = await readStdin();
|
|
28
33
|
}
|
|
29
34
|
if (!rawInput.trim()) {
|
|
35
|
+
if (process.stdin.isTTY) process.stderr.write(DEPRECATION_NOTICE);
|
|
30
36
|
process.exit(0);
|
|
31
37
|
}
|
|
32
38
|
let event;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
execNpm,
|
|
4
|
+
hasCodexHooks,
|
|
5
|
+
missingCodexHookEvents
|
|
6
|
+
} from "../chunk-2HMNOZPY.js";
|
|
2
7
|
import {
|
|
3
8
|
callMcpTool,
|
|
4
9
|
sendMcpRequest
|
|
5
10
|
} from "../chunk-3MIR7ODJ.js";
|
|
6
11
|
import "../chunk-VUNL35KE.js";
|
|
7
|
-
import {
|
|
8
|
-
execNpm
|
|
9
|
-
} from "../chunk-RSHN2AQ7.js";
|
|
10
12
|
|
|
11
13
|
// bin/pushary-doctor.ts
|
|
12
14
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -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`;
|
|
@@ -124,8 +127,10 @@ var main = async () => {
|
|
|
124
127
|
check(false, "Claude Code: settings.json", "not found");
|
|
125
128
|
}
|
|
126
129
|
const codexConfigPath = join(homedir(), ".codex", "config.toml");
|
|
127
|
-
|
|
128
|
-
|
|
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") : "";
|
|
129
134
|
const hasPusharyMcp = codexConfig.includes("[mcp_servers.pushary]");
|
|
130
135
|
check(hasPusharyMcp, "Codex: MCP server configured");
|
|
131
136
|
if (hasPusharyMcp) {
|
|
@@ -137,10 +142,33 @@ var main = async () => {
|
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
const
|
|
143
|
-
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: trust the hooks inside Codex ${dim("(type /hooks in Codex and trust them, once; cannot be checked 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
|
+
}
|
|
144
172
|
}
|
|
145
173
|
const codexSkillPath = join(homedir(), ".codex", "skills", "pushary", "SKILL.md");
|
|
146
174
|
check(existsSync(codexSkillPath), "Codex: skill installed");
|
package/dist/bin/pushary-hook.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePreToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-V4GLWPD7.js";
|
|
5
|
+
import "../chunk-XPVSZIA3.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
6
7
|
import "../chunk-3MIR7ODJ.js";
|
|
7
8
|
import "../chunk-VUNL35KE.js";
|
|
8
|
-
import "../chunk-22CV7V7A.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-hook.ts
|
|
11
11
|
var main = async () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handlePostToolUse
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-HJMFYDWY.js";
|
|
5
|
+
import "../chunk-XPVSZIA3.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
6
7
|
import "../chunk-3MIR7ODJ.js";
|
|
7
8
|
import "../chunk-VUNL35KE.js";
|
|
8
|
-
import "../chunk-22CV7V7A.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-post-hook.ts
|
|
11
11
|
var main = async () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleUserPrompt
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-HJMFYDWY.js";
|
|
5
|
+
import "../chunk-XPVSZIA3.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
6
7
|
import "../chunk-3MIR7ODJ.js";
|
|
7
8
|
import "../chunk-VUNL35KE.js";
|
|
8
|
-
import "../chunk-22CV7V7A.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-prompt-hook.ts
|
|
11
11
|
var main = async () => {
|
|
@@ -5,9 +5,10 @@ import {
|
|
|
5
5
|
addPusharyToolPermissions
|
|
6
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
14
|
} from "../chunk-22CV7V7A.js";
|
|
@@ -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,48 @@ 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("One step left, and it happens inside Codex")}`);
|
|
522
|
+
console.log(` ${dim2("Codex makes you trust any hook before it runs. This is Codex's own")}`);
|
|
523
|
+
console.log(` ${dim2("security check, not a Pushary thing, and you only do it once.")}`);
|
|
524
|
+
console.log();
|
|
525
|
+
console.log(` ${cyan2("1.")} Open Codex and type ${cyan2("/hooks")}`);
|
|
526
|
+
console.log(` ${cyan2("2.")} You will see the Pushary hook commands. Trust them.`);
|
|
527
|
+
console.log();
|
|
528
|
+
console.log(` ${dim2("Until you do, Codex just runs as usual without the phone approvals.")}`);
|
|
529
|
+
console.log(` ${dim2("To confirm:")} run a command that needs approval and watch your phone.`);
|
|
530
|
+
console.log(` ${dim2("Pushary updates keep the same trust, so Codex will not ask again.")}`);
|
|
531
|
+
} else {
|
|
532
|
+
console.log(` ${dim2("\u2022")} Notify handler (deprecated): captures turn completions and approval requests`);
|
|
533
|
+
}
|
|
457
534
|
};
|
|
458
535
|
var resolveBundledPlugin = () => {
|
|
459
536
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -568,7 +645,7 @@ var main = async () => {
|
|
|
568
645
|
message: "Which agents do you use? " + dim2(hint),
|
|
569
646
|
choices: [
|
|
570
647
|
{ name: `Claude Code ${dim2("MCP + hooks + auto-allowed tools")}`, value: "claude_code", checked: detected.claude_code },
|
|
571
|
-
{ name: `Codex ${dim2("MCP +
|
|
648
|
+
{ name: `Codex ${dim2("MCP + native hooks + auto-allowed tools")}`, value: "codex", checked: detected.codex },
|
|
572
649
|
{ name: `Hermes ${dim2("native plugin + auto-error notifications")}`, value: "hermes", checked: detected.hermes },
|
|
573
650
|
{ name: `Cursor ${dim2("MCP server")}`, value: "cursor", checked: detected.cursor }
|
|
574
651
|
]
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleStop
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-HJMFYDWY.js";
|
|
5
|
+
import "../chunk-XPVSZIA3.js";
|
|
6
|
+
import "../chunk-22CV7V7A.js";
|
|
6
7
|
import "../chunk-3MIR7ODJ.js";
|
|
7
8
|
import "../chunk-VUNL35KE.js";
|
|
8
|
-
import "../chunk-22CV7V7A.js";
|
|
9
9
|
|
|
10
10
|
// bin/pushary-stop-hook.ts
|
|
11
11
|
var main = async () => {
|