@pushary/agent-hooks 0.21.0 → 0.22.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.
@@ -231,6 +231,25 @@ const fetchModeState = async (apiKey, sessionId) => {
231
231
  }
232
232
  }
233
233
 
234
+ // ── action body capture + redaction (inlined mirror of describe.ts, since this
235
+ // dependency-free hook cannot import the workspace) ──────────────────────────────
236
+ const ACTION_BODY_MAX = 4000
237
+ const ACTION_BODY_TRUNCATION_MARKER = '\n… [truncated]'
238
+ const REDACTION_RULES = [
239
+ [/\bsk-[A-Za-z0-9]{20,}\b/g, '[redacted]'],
240
+ [/\bpk_(?:live|test)_[A-Za-z0-9]+\b/g, '[redacted]'],
241
+ [/\brk_[A-Za-z0-9]+\b/g, '[redacted]'],
242
+ [/\bAKIA[0-9A-Z]{16}\b/g, '[redacted]'],
243
+ [/\bbearer\s+[A-Za-z0-9._-]+/gi, 'bearer [redacted]'],
244
+ [/\bauthorization:\s*\S+/gi, 'authorization: [redacted]'],
245
+ [/((?:secret|token|password|passwd|api[_-]?key|private[_-]?key)\s*[=:]\s*)(\S+)/gi, '$1[redacted]'],
246
+ [/[A-Za-z0-9+/]{40,}={0,2}/g, '[redacted]'],
247
+ ]
248
+ const redactSecrets = (text) => REDACTION_RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), text)
249
+ const capActionBody = (text) =>
250
+ text.length <= ACTION_BODY_MAX ? text : `${text.slice(0, ACTION_BODY_MAX - ACTION_BODY_TRUNCATION_MARKER.length)}${ACTION_BODY_TRUNCATION_MARKER}`
251
+ const deriveActionBody = (command) => capActionBody(redactSecrets(command))
252
+
234
253
  // ── ask / wait ───────────────────────────────────────────────────────────────
235
254
  const askArgs = (command, project, ident) => ({
236
255
  question: `Allow this command?\n\n${command}`,
@@ -240,6 +259,7 @@ const askArgs = (command, project, ident) => ({
240
259
  sessionId: ident.sessionId,
241
260
  machineId: ident.machineId,
242
261
  toolName: 'Bash',
262
+ actionBody: deriveActionBody(command),
243
263
  wait: false,
244
264
  })
245
265
 
@@ -278,7 +298,10 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
278
298
  const realMs = Math.max(timeoutSeconds, 1) * 1000
279
299
  const cap = Math.min(realMs, MAX_BLOCK_MS)
280
300
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
281
- if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(DENIED)
301
+ if (answer.answered) {
302
+ if (answer.value === 'defer') return ask()
303
+ return answer.value === 'yes' ? ALLOW : deny(DENIED)
304
+ }
282
305
 
283
306
  // If Cursor's hook limit cut us off before the configured timeout, hand off to
284
307
  // Cursor's own prompt rather than misapplying the policy's timeout action.
@@ -298,7 +321,10 @@ const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds
298
321
 
299
322
  const cap = Math.min(Math.max(pushFirstSeconds, 1) * 1000, MAX_BLOCK_MS)
300
323
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
301
- if (answer.answered) return answer.value === 'yes' ? ALLOW : deny(DENIED)
324
+ if (answer.answered) {
325
+ if (answer.value === 'defer') return ask()
326
+ return answer.value === 'yes' ? ALLOW : deny(DENIED)
327
+ }
302
328
  return ask('Sent to your phone via Pushary — you can also approve here.')
303
329
  }
304
330
 
@@ -1,35 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- denyReasonFrom
4
- } from "../chunk-N7VXDBQU.js";
3
+ denyReasonFrom,
4
+ isDeferAnswer
5
+ } from "../chunk-KQYIHZ5E.js";
5
6
  import {
6
7
  CODEX_AGENT,
8
+ DEFAULT_SESSION,
9
+ askUser,
7
10
  codexAllow,
8
11
  codexDeny,
9
12
  codexPass,
13
+ deriveActionBody,
14
+ deriveBlocker,
15
+ deriveToolTarget,
16
+ describeApplyPatch,
17
+ describeToolCall,
18
+ fetchModeState,
19
+ getMachineId,
20
+ getPolicy,
10
21
  handlePostToolUse,
11
22
  handleStop,
12
23
  handleUserPrompt,
13
24
  permissionTimeoutDecision,
14
25
  preToolUseTimeoutDecision,
26
+ readLastPrompt,
15
27
  reportEvent,
16
- toCodexWire,
17
- toPolicyLookup
18
- } from "../chunk-QY4L6XHN.js";
19
- import {
20
- DEFAULT_SESSION,
21
- askUser,
22
- deriveToolTarget,
23
- describeToolCall,
24
- fetchModeState,
25
- getMachineId,
26
- getPolicy,
27
28
  resolvePolicy,
28
29
  savePendingQuestion,
29
30
  sendNotification,
31
+ toCodexWire,
32
+ toPolicyLookup,
30
33
  waitForAnswer
31
- } from "../chunk-ACE77TKQ.js";
32
- import "../chunk-USUCPCUC.js";
34
+ } from "../chunk-M4GDBGXF.js";
35
+ import {
36
+ DECISION_LINE_MAX
37
+ } from "../chunk-WPVL6VIT.js";
33
38
  import "../chunk-DWED7BS3.js";
34
39
  import {
35
40
  getApiKey
@@ -41,6 +46,7 @@ import { tmpdir, userInfo } from "os";
41
46
  import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
42
47
  var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
43
48
  var MAX_WAIT_SECONDS = 170;
49
+ var START_MS = Date.now();
44
50
  var APPROVAL_TTL_MS = 10 * 60 * 1e3;
45
51
  var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
46
52
  var userScope = () => {
@@ -104,11 +110,10 @@ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3
104
110
  return { answered: false };
105
111
  };
106
112
  var agentNameFor = (input) => `Codex - ${basename(input.cwd ?? process.cwd())}`;
107
- var pushQuestion = async (apiKey, input, waitSeconds) => {
108
- const toolName = input.tool_name ?? "";
109
- const toolInput = input.tool_input ?? {};
113
+ var describeFor = (input, lookup) => input.tool_name === "apply_patch" ? describeApplyPatch(input.tool_input?.command) ?? describeToolCall(lookup.tool, lookup.input, "hook") : describeToolCall(lookup.tool, lookup.input, "hook");
114
+ var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode) => {
110
115
  const projectName = basename(input.cwd ?? process.cwd());
111
- const description = describeToolCall(toolName, toolInput, "hook");
116
+ const description = describeFor(input, lookup);
112
117
  const result = await askUser(apiKey, {
113
118
  question: `Allow ${description}?`,
114
119
  type: "confirm",
@@ -116,18 +121,25 @@ var pushQuestion = async (apiKey, input, waitSeconds) => {
116
121
  agentName: agentNameFor(input),
117
122
  sessionId: input.session_id,
118
123
  machineId: getMachineId(),
119
- toolName,
120
- toolTarget: deriveToolTarget(toolName, toolInput)
124
+ toolName: lookup.tool,
125
+ toolTarget: deriveToolTarget(lookup.tool, lookup.input),
126
+ intent: readLastPrompt(input.session_id || DEFAULT_SESSION),
127
+ action: description.slice(0, DECISION_LINE_MAX),
128
+ blocker: deriveBlocker(mode),
129
+ actionBody: deriveActionBody(input.tool_name ?? "", input.tool_input ?? {})
121
130
  });
122
- const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
131
+ const deadline = Math.min(
132
+ Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3,
133
+ START_MS + MAX_WAIT_SECONDS * 1e3
134
+ );
123
135
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
124
136
  return { answer, correlationId: result.correlationId };
125
137
  };
126
- var notifyApprovalNeeded = async (apiKey, input) => {
138
+ var notifyApprovalNeeded = async (apiKey, input, lookup) => {
127
139
  try {
128
140
  await sendNotification(apiKey, {
129
141
  title: "Agent needs approval",
130
- body: describeToolCall(input.tool_name ?? "", input.tool_input ?? {}, "hook"),
142
+ body: describeFor(input, lookup),
131
143
  agentName: agentNameFor(input),
132
144
  sessionId: input.session_id,
133
145
  machineId: getMachineId()
@@ -140,7 +152,7 @@ var decidePermissionRequest = async (input) => {
140
152
  const apiKey = getApiKey();
141
153
  const modeState = await fetchModeState(apiKey, input.session_id);
142
154
  if (modeState.kill) return codexDeny(KILL_REASON);
143
- const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
155
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
144
156
  const policy = await getPolicy(apiKey, modeState.policyVersion);
145
157
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
146
158
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexAllow();
@@ -149,12 +161,13 @@ var decidePermissionRequest = async (input) => {
149
161
  }
150
162
  if (toolPolicy.mode === "terminal_only") return codexPass();
151
163
  if (toolPolicy.mode === "notify_only") {
152
- if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input);
164
+ if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
153
165
  return codexPass();
154
166
  }
155
167
  const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : toolPolicy.timeoutSeconds;
156
- const { answer, correlationId } = await pushQuestion(apiKey, input, waitSeconds);
168
+ const { answer, correlationId } = await pushQuestion(apiKey, input, lookup, waitSeconds, toolPolicy.mode);
157
169
  if (answer.answered) {
170
+ if (isDeferAnswer(answer.value)) return codexPass();
158
171
  if (answer.value === "yes") {
159
172
  if (toolPolicy.mode === "push_only") markApproved(input.tool_use_id);
160
173
  return codexAllow();
@@ -175,7 +188,7 @@ var decidePreToolUse = async (input) => {
175
188
  const apiKey = getApiKey();
176
189
  const modeState = await fetchModeState(apiKey, input.session_id);
177
190
  if (modeState.kill) return codexDeny(KILL_REASON);
178
- const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {});
191
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
179
192
  const policy = await getPolicy(apiKey, modeState.policyVersion);
180
193
  const toolPolicy = resolvePolicy(policy, lookup.tool, modeState.mode, lookup.input);
181
194
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") return codexPass();
@@ -186,19 +199,20 @@ var decidePreToolUse = async (input) => {
186
199
  if (consumeApproval(input.tool_use_id)) return codexPass();
187
200
  let pushed;
188
201
  try {
189
- pushed = await pushQuestion(apiKey, input, toolPolicy.timeoutSeconds);
202
+ pushed = await pushQuestion(apiKey, input, lookup, toolPolicy.timeoutSeconds, "push_only");
190
203
  } catch {
191
204
  return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
192
205
  }
193
206
  const { answer, correlationId } = pushed;
194
207
  if (answer.answered) {
208
+ if (isDeferAnswer(answer.value)) return codexPass();
195
209
  return answer.value === "yes" ? codexPass() : codexDeny(denyReasonFrom(answer.value));
196
210
  }
197
211
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
198
212
  return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
199
213
  }
200
214
  if (toolPolicy.mode === "notify_only") {
201
- if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input);
215
+ if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
202
216
  return codexPass();
203
217
  }
204
218
  return codexPass();
@@ -244,15 +258,17 @@ var main = async () => {
244
258
  case "PreToolUse":
245
259
  emit(toCodexWire("PreToolUse", await decidePreToolUse(input)));
246
260
  break;
247
- case "PostToolUse":
261
+ case "PostToolUse": {
262
+ const lookup = toPolicyLookup(input.tool_name ?? "", input.tool_input ?? {}, input.cwd);
248
263
  await handlePostToolUse({
249
- tool_name: input.tool_name ?? "",
250
- tool_input: input.tool_input ?? {},
264
+ tool_name: lookup.tool,
265
+ tool_input: lookup.input,
251
266
  tool_result: asToolResult(input.tool_response),
252
267
  cwd: input.cwd,
253
268
  session_id: input.session_id
254
269
  }, CODEX_AGENT);
255
270
  break;
271
+ }
256
272
  case "UserPromptSubmit":
257
273
  await handleUserPrompt({
258
274
  prompt: input.prompt,
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- reportEvent
4
- } from "../chunk-QY4L6XHN.js";
5
2
  import {
6
3
  askUser,
7
4
  getMachineId,
5
+ reportEvent,
8
6
  waitForAnswer
9
- } from "../chunk-ACE77TKQ.js";
10
- import "../chunk-USUCPCUC.js";
7
+ } from "../chunk-M4GDBGXF.js";
8
+ import "../chunk-WPVL6VIT.js";
11
9
  import "../chunk-DWED7BS3.js";
12
10
  import {
13
11
  getApiKey
@@ -1,27 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- denyReasonFrom
4
- } from "../chunk-N7VXDBQU.js";
5
- import {
6
- handlePostToolUse,
7
- handleStop,
8
- handleUserPrompt,
9
- reportEvent
10
- } from "../chunk-QY4L6XHN.js";
3
+ denyReasonFrom,
4
+ isDeferAnswer
5
+ } from "../chunk-KQYIHZ5E.js";
11
6
  import {
12
7
  DEFAULT_SESSION,
13
8
  askUser,
9
+ deriveActionBody,
10
+ deriveBlocker,
14
11
  deriveToolTarget,
15
12
  describeToolCall,
16
13
  fetchModeState,
17
14
  getMachineId,
18
15
  getPolicy,
16
+ handlePostToolUse,
17
+ handleStop,
18
+ handleUserPrompt,
19
+ readLastPrompt,
20
+ readLastUserPrompt,
21
+ reportEvent,
19
22
  resolvePolicy,
20
23
  savePendingQuestion,
21
24
  sendNotification,
22
25
  waitForAnswer
23
- } from "../chunk-ACE77TKQ.js";
24
- import "../chunk-USUCPCUC.js";
26
+ } from "../chunk-M4GDBGXF.js";
27
+ import {
28
+ DECISION_LINE_MAX
29
+ } from "../chunk-WPVL6VIT.js";
25
30
  import "../chunk-DWED7BS3.js";
26
31
  import {
27
32
  getApiKey
@@ -66,6 +71,7 @@ var geminiTimeoutDecision = (timeoutAction, denyReason = "No response within tim
66
71
  // bin/pushary-gemini-hook.ts
67
72
  var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
68
73
  var MAX_WAIT_SECONDS = 170;
74
+ var START_MS = Date.now();
69
75
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
70
76
  var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
71
77
  while (Date.now() < deadlineMs) {
@@ -85,7 +91,7 @@ var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3
85
91
  return { answered: false };
86
92
  };
87
93
  var agentNameFor = (input) => `${GEMINI_AGENT.label} - ${basename(input.cwd ?? process.cwd())}`;
88
- var pushQuestion = async (apiKey, input, lookup, waitSeconds, pollInterval = 2e3) => {
94
+ var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode, pollInterval = 2e3) => {
89
95
  const projectName = basename(input.cwd ?? process.cwd());
90
96
  const description = describeToolCall(lookup.tool, lookup.input, "hook");
91
97
  const result = await askUser(apiKey, {
@@ -96,9 +102,16 @@ var pushQuestion = async (apiKey, input, lookup, waitSeconds, pollInterval = 2e3
96
102
  sessionId: input.session_id,
97
103
  machineId: getMachineId(),
98
104
  toolName: lookup.tool,
99
- toolTarget: deriveToolTarget(lookup.tool, lookup.input)
105
+ toolTarget: deriveToolTarget(lookup.tool, lookup.input),
106
+ intent: readLastPrompt(input.session_id || DEFAULT_SESSION) ?? (input.transcript_path ? readLastUserPrompt(input.transcript_path) : void 0),
107
+ action: description.slice(0, DECISION_LINE_MAX),
108
+ blocker: deriveBlocker(mode),
109
+ actionBody: deriveActionBody(input.tool_name ?? "", input.tool_input ?? {})
100
110
  });
101
- const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
111
+ const deadline = Math.min(
112
+ Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3,
113
+ START_MS + MAX_WAIT_SECONDS * 1e3
114
+ );
102
115
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, pollInterval);
103
116
  return { answer, correlationId: result.correlationId };
104
117
  };
@@ -117,12 +130,13 @@ var notifyApprovalNeeded = async (apiKey, input, lookup) => {
117
130
  var handlePushOnly = async (apiKey, input, lookup, timeoutSeconds, timeoutAction) => {
118
131
  let pushed;
119
132
  try {
120
- pushed = await pushQuestion(apiKey, input, lookup, timeoutSeconds);
133
+ pushed = await pushQuestion(apiKey, input, lookup, timeoutSeconds, "push_only");
121
134
  } catch {
122
135
  return geminiTimeoutDecision(timeoutAction, "Push notification failed, denying per policy");
123
136
  }
124
137
  const { answer, correlationId } = pushed;
125
138
  if (answer.answered) {
139
+ if (isDeferAnswer(answer.value)) return geminiPass();
126
140
  return answer.value === "yes" ? geminiAllow() : geminiDeny(denyReasonFrom(answer.value));
127
141
  }
128
142
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
@@ -131,12 +145,13 @@ var handlePushOnly = async (apiKey, input, lookup, timeoutSeconds, timeoutAction
131
145
  var handlePushFirst = async (apiKey, input, lookup, pushFirstSeconds) => {
132
146
  let pushed;
133
147
  try {
134
- pushed = await pushQuestion(apiKey, input, lookup, pushFirstSeconds, 1500);
148
+ pushed = await pushQuestion(apiKey, input, lookup, pushFirstSeconds, "push_first", 1500);
135
149
  } catch {
136
150
  return geminiPass();
137
151
  }
138
152
  const { answer, correlationId } = pushed;
139
153
  if (answer.answered) {
154
+ if (isDeferAnswer(answer.value)) return geminiPass();
140
155
  return answer.value === "yes" ? geminiAllow() : geminiDeny(denyReasonFrom(answer.value));
141
156
  }
142
157
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-FXYTRZHC.js";
5
- import "../chunk-N7VXDBQU.js";
6
- import "../chunk-ACE77TKQ.js";
7
- import "../chunk-USUCPCUC.js";
4
+ } from "../chunk-FGZZRCOG.js";
5
+ import "../chunk-KQYIHZ5E.js";
6
+ import "../chunk-M4GDBGXF.js";
7
+ import "../chunk-WPVL6VIT.js";
8
8
  import "../chunk-DWED7BS3.js";
9
9
  import "../chunk-NKXSILEW.js";
10
10
 
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-QY4L6XHN.js";
5
- import "../chunk-ACE77TKQ.js";
6
- import "../chunk-USUCPCUC.js";
4
+ } from "../chunk-M4GDBGXF.js";
5
+ import "../chunk-WPVL6VIT.js";
7
6
  import "../chunk-DWED7BS3.js";
8
7
  import "../chunk-NKXSILEW.js";
9
8
 
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-QY4L6XHN.js";
5
- import "../chunk-ACE77TKQ.js";
6
- import "../chunk-USUCPCUC.js";
4
+ } from "../chunk-M4GDBGXF.js";
5
+ import "../chunk-WPVL6VIT.js";
7
6
  import "../chunk-DWED7BS3.js";
8
7
  import "../chunk-NKXSILEW.js";
9
8
 
@@ -17,7 +17,7 @@ import {
17
17
  } from "../chunk-BHAMEPOP.js";
18
18
  import {
19
19
  isValidApiKey
20
- } from "../chunk-USUCPCUC.js";
20
+ } from "../chunk-WPVL6VIT.js";
21
21
 
22
22
  // bin/pushary-setup.ts
23
23
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, cpSync, rmSync, chmodSync } from "fs";
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-QY4L6XHN.js";
5
- import "../chunk-ACE77TKQ.js";
6
- import "../chunk-USUCPCUC.js";
4
+ } from "../chunk-M4GDBGXF.js";
5
+ import "../chunk-WPVL6VIT.js";
7
6
  import "../chunk-DWED7BS3.js";
8
7
  import "../chunk-NKXSILEW.js";
9
8