@pushary/agent-hooks 0.31.0 → 0.32.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/data/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pushary
3
- version: 0.4.1
3
+ version: 0.4.2
4
4
  description: Push notifications and human-in-the-loop for AI coding agents. Send alerts when tasks finish, ask questions (yes/no, multiple choice, or free text) via push, and get answers from the user's lock screen. Use these tools proactively — do not wait for the user to ask for notifications. Works with Cursor, Claude Code, Windsurf, Hermes, and any MCP client.
5
5
  metadata:
6
6
  hermes:
@@ -248,7 +248,7 @@ Cancel a pending question so it can no longer be answered. Use when the question
248
248
 
249
249
  ## Permission Gating (REQUIRED)
250
250
 
251
- Before executing any of the following, you MUST call `ask_user` with type "confirm" and wait for approval. Do NOT proceed without an explicit "yes" from the user:
251
+ Before executing any of the following, make sure the user has approved it. Call `ask_user` with type "confirm" and wait for an explicit "yes" before you proceed:
252
252
 
253
253
  - File deletion (`rm`, `unlink`, any destructive file operation)
254
254
  - Database mutations (`DROP`, `DELETE`, `TRUNCATE`, migrations)
@@ -258,6 +258,10 @@ Before executing any of the following, you MUST call `ask_user` with type "confi
258
258
  - Network configuration changes (firewall, DNS, proxy)
259
259
  - Any command the user has flagged as dangerous
260
260
 
261
+ Honor the user's delivery mode: it decides WHERE approval is requested (phone, terminal, or awareness only), not WHETHER approval is required. In "Terminal" mode the user wants to approve at the keyboard, so do not force a phone push; a single terminal approval is enough.
262
+
263
+ If the Pushary hook is installed, it also gates these at the tool level and routes them by delivery mode. When it has already surfaced the approval for an action, one "yes" is enough, do not ask a second time for the same action.
264
+
261
265
  If `ask_user` returns `answered: false`, do NOT execute the command. Send a notification that the operation was skipped due to no response.
262
266
 
263
267
  This is not optional. Treat it as a hard constraint, not a suggestion.
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  removeClaudeMcpServers,
4
4
  removePusharySettings
5
- } from "../chunk-H3LQRYMW.js";
5
+ } from "../chunk-M6YUEVRV.js";
6
6
  import {
7
7
  removeCodexHooks,
8
8
  removeGeminiSettings,
9
9
  removeInstructionBlock
10
- } from "../chunk-WUNVKFFY.js";
10
+ } from "../chunk-SVVEC4QV.js";
11
11
  import {
12
12
  execNpm
13
13
  } from "../chunk-J7JWI3KU.js";
@@ -36,7 +36,7 @@ import {
36
36
  toCodexWire,
37
37
  toPolicyLookup,
38
38
  waitForAnswer
39
- } from "../chunk-IAOXM7X5.js";
39
+ } from "../chunk-USRIQPWI.js";
40
40
  import "../chunk-DWED7BS3.js";
41
41
  import {
42
42
  DECISION_LINE_MAX,
@@ -4,7 +4,7 @@ import {
4
4
  getMachineId,
5
5
  reportEvent,
6
6
  waitForAnswer
7
- } from "../chunk-IAOXM7X5.js";
7
+ } from "../chunk-USRIQPWI.js";
8
8
  import "../chunk-DWED7BS3.js";
9
9
  import "../chunk-Z5PL3K7C.js";
10
10
  import {
@@ -11,7 +11,7 @@ import {
11
11
  missingGeminiHookEvents,
12
12
  readCodexMcpAuth,
13
13
  untrustedCodexHookEvents
14
- } from "../chunk-WUNVKFFY.js";
14
+ } from "../chunk-SVVEC4QV.js";
15
15
  import {
16
16
  execNpm
17
17
  } from "../chunk-J7JWI3KU.js";
@@ -28,7 +28,7 @@ import {
28
28
  savePendingQuestion,
29
29
  sendNotification,
30
30
  waitForAnswer
31
- } from "../chunk-IAOXM7X5.js";
31
+ } from "../chunk-USRIQPWI.js";
32
32
  import "../chunk-DWED7BS3.js";
33
33
  import {
34
34
  DECISION_LINE_MAX,
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-CY5YP34L.js";
4
+ } from "../chunk-FD4NV6BO.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
6
  import "../chunk-R5AJNXZS.js";
7
- import "../chunk-IAOXM7X5.js";
7
+ import "../chunk-USRIQPWI.js";
8
8
  import "../chunk-DWED7BS3.js";
9
9
  import "../chunk-Z5PL3K7C.js";
10
10
  import "../chunk-NKXSILEW.js";
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ handleNotification
4
+ } from "../chunk-USRIQPWI.js";
5
+ import "../chunk-DWED7BS3.js";
6
+ import "../chunk-Z5PL3K7C.js";
7
+ import "../chunk-NKXSILEW.js";
8
+
9
+ // bin/pushary-notification-hook.ts
10
+ var main = async () => {
11
+ let rawInput = "";
12
+ for await (const chunk of process.stdin) {
13
+ rawInput += chunk;
14
+ }
15
+ if (!rawInput.trim()) {
16
+ process.exit(0);
17
+ }
18
+ try {
19
+ const input = JSON.parse(rawInput);
20
+ await handleNotification(input);
21
+ } catch {
22
+ }
23
+ };
24
+ main();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-IAOXM7X5.js";
4
+ } from "../chunk-USRIQPWI.js";
5
5
  import "../chunk-DWED7BS3.js";
6
6
  import "../chunk-Z5PL3K7C.js";
7
7
  import "../chunk-NKXSILEW.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-IAOXM7X5.js";
4
+ } from "../chunk-USRIQPWI.js";
5
5
  import "../chunk-DWED7BS3.js";
6
6
  import "../chunk-Z5PL3K7C.js";
7
7
  import "../chunk-NKXSILEW.js";
@@ -3,7 +3,7 @@ import {
3
3
  addClaudeMcpServer,
4
4
  addPusharyHooks,
5
5
  addPusharyToolPermissions
6
- } from "../chunk-H3LQRYMW.js";
6
+ } from "../chunk-M6YUEVRV.js";
7
7
  import {
8
8
  GEMINI_HOOK_BINARY,
9
9
  addCodexHookTrust,
@@ -14,7 +14,7 @@ import {
14
14
  renderAgentInstructions,
15
15
  renderProjectAgentInstructions,
16
16
  writeInstructionBlock
17
- } from "../chunk-WUNVKFFY.js";
17
+ } from "../chunk-SVVEC4QV.js";
18
18
  import {
19
19
  execNpm,
20
20
  npmErrorMessage,
@@ -24,7 +24,7 @@ import {
24
24
  } from "../chunk-J7JWI3KU.js";
25
25
  import {
26
26
  reportEvent
27
- } from "../chunk-IAOXM7X5.js";
27
+ } from "../chunk-USRIQPWI.js";
28
28
  import "../chunk-DWED7BS3.js";
29
29
  import {
30
30
  isValidApiKey
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-IAOXM7X5.js";
4
+ } from "../chunk-USRIQPWI.js";
5
5
  import "../chunk-DWED7BS3.js";
6
6
  import "../chunk-Z5PL3K7C.js";
7
7
  import "../chunk-NKXSILEW.js";
@@ -24,7 +24,7 @@ import {
24
24
  savePendingQuestion,
25
25
  sendNotification,
26
26
  waitForAnswer
27
- } from "./chunk-IAOXM7X5.js";
27
+ } from "./chunk-USRIQPWI.js";
28
28
  import {
29
29
  effectiveWaitSeconds,
30
30
  hookWaitClamped,
@@ -98,7 +98,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
98
98
  case "deny":
99
99
  return deny("Push notification failed, denying per policy");
100
100
  default:
101
- return ask("Push notification failed, asking in terminal");
101
+ return void 0;
102
102
  }
103
103
  }
104
104
  if (result.suppressed) {
@@ -113,7 +113,7 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
113
113
  case "deny":
114
114
  return deny("No device connected to approve on");
115
115
  default:
116
- return ask("No device connected \u2014 asking in terminal");
116
+ return void 0;
117
117
  }
118
118
  }
119
119
  const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
@@ -156,7 +156,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
156
156
  ...decision
157
157
  });
158
158
  } catch {
159
- return ask("Push notification failed, asking in terminal");
159
+ return void 0;
160
160
  }
161
161
  if (result.suppressed) {
162
162
  await cancelQuestion(apiKey, result.correlationId).catch(() => {
@@ -164,7 +164,7 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
164
164
  return ask("You are at the keyboard \u2014 approve here.");
165
165
  }
166
166
  if (result.noDevices) {
167
- return ask("No device connected \u2014 asking in terminal.");
167
+ return void 0;
168
168
  }
169
169
  const deadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
170
170
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
@@ -200,6 +200,12 @@ var keylessNoticeOnce = (sessionId) => {
200
200
  } catch {
201
201
  }
202
202
  };
203
+ var DEFER_PERMISSION_MODES = /* @__PURE__ */ new Set(["bypassPermissions", "plan", "auto"]);
204
+ var shouldDeferToNativeMode = (permissionMode) => {
205
+ const flag = process.env.PUSHARY_RESPECT_PERMISSION_MODE;
206
+ if (flag !== "1" && flag !== "true") return false;
207
+ return typeof permissionMode === "string" && DEFER_PERMISSION_MODES.has(permissionMode);
208
+ };
203
209
  var handleKeyless = (input) => {
204
210
  try {
205
211
  if (isGatingMoment(input.tool_name, input.tool_input ?? {})) {
@@ -216,6 +222,7 @@ var handleKeyless = (input) => {
216
222
  return void 0;
217
223
  };
218
224
  var handlePreToolUse = async (input) => {
225
+ if (input.tool_name.startsWith("mcp__pushary__")) return void 0;
219
226
  let apiKey;
220
227
  try {
221
228
  apiKey = getApiKey();
@@ -228,6 +235,9 @@ var handlePreToolUse = async (input) => {
228
235
  if (modeState.kill) {
229
236
  return deny("Stopped by user \u2014 this agent was halted from Pushary");
230
237
  }
238
+ if (shouldDeferToNativeMode(input.permission_mode)) {
239
+ return void 0;
240
+ }
231
241
  const toolPolicy = resolvePolicy(policy, input.tool_name, modeState.mode, input.tool_input);
232
242
  if (toolPolicy.timeoutSeconds === 0 && toolPolicy.timeoutAction === "approve") {
233
243
  return allow();
@@ -260,7 +270,7 @@ var handlePreToolUse = async (input) => {
260
270
  return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision);
261
271
  }
262
272
  } catch {
263
- return ask("Pushary unavailable, falling back to terminal approval");
273
+ return void 0;
264
274
  }
265
275
  };
266
276
 
@@ -20,7 +20,7 @@ var isPusharyHook = (entry) => {
20
20
  if (!Array.isArray(hooks)) return false;
21
21
  return hooks.some((hook) => {
22
22
  const command = String(asRecord(hook)?.command ?? "");
23
- return command.includes("pushary-hook") || command.includes("pushary-post-hook") || command.includes("pushary-stop-hook") || command.includes("pushary-prompt-hook");
23
+ return command.includes("pushary-hook") || command.includes("pushary-post-hook") || command.includes("pushary-stop-hook") || command.includes("pushary-prompt-hook") || command.includes("pushary-notification-hook");
24
24
  });
25
25
  };
26
26
  var addClaudeMcpServer = (config, apiKey) => {
@@ -67,7 +67,7 @@ var addPusharyHooks = (settings, binDir) => {
67
67
  const hooks = ensureRecord(settings, "hooks");
68
68
  const preToolUse = (Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : []).filter((entry) => !isPusharyHook(entry));
69
69
  preToolUse.push({
70
- matcher: "Bash|Write|Edit",
70
+ matcher: "Bash|Write|Edit|NotebookEdit",
71
71
  hooks: [{
72
72
  type: "command",
73
73
  command: resolve("pushary-hook"),
@@ -77,7 +77,7 @@ var addPusharyHooks = (settings, binDir) => {
77
77
  hooks.PreToolUse = preToolUse;
78
78
  const postToolUse = (Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : []).filter((entry) => !isPusharyHook(entry));
79
79
  postToolUse.push({
80
- matcher: "Bash|Write|Edit",
80
+ matcher: "Bash|Write|Edit|NotebookEdit",
81
81
  hooks: [{
82
82
  type: "command",
83
83
  command: resolve("pushary-post-hook"),
@@ -103,6 +103,16 @@ var addPusharyHooks = (settings, binDir) => {
103
103
  }]
104
104
  });
105
105
  hooks.UserPromptSubmit = userPromptSubmit;
106
+ const notification = (Array.isArray(hooks.Notification) ? hooks.Notification : []).filter((entry) => !isPusharyHook(entry));
107
+ notification.push({
108
+ matcher: "permission_prompt|idle_prompt|agent_needs_input|agent_completed",
109
+ hooks: [{
110
+ type: "command",
111
+ command: resolve("pushary-notification-hook"),
112
+ timeout: 10
113
+ }]
114
+ });
115
+ hooks.Notification = notification;
106
116
  };
107
117
  var removePusharySettings = (settings) => {
108
118
  let changed = removeClaudeMcpServers(settings);
@@ -121,7 +131,7 @@ var removePusharySettings = (settings) => {
121
131
  }
122
132
  const hooks = asRecord(settings.hooks);
123
133
  if (hooks) {
124
- for (const key of ["PreToolUse", "PostToolUse", "Stop", "UserPromptSubmit"]) {
134
+ for (const key of ["PreToolUse", "PostToolUse", "Stop", "UserPromptSubmit", "Notification"]) {
125
135
  const entries = hooks[key];
126
136
  if (!Array.isArray(entries)) continue;
127
137
  const filtered = entries.filter((entry) => !isPusharyHook(entry));
@@ -251,6 +251,7 @@ You have Pushary MCP tools that reach the user on their phone. Use them proactiv
251
251
  - When you are blocked, stuck, or hit an error you cannot resolve on your own, call \`send_notification\` so the user knows, and call \`ask_user\` if you need a decision to continue.
252
252
  - When a task that took several steps finishes, call \`send_notification\` so the user knows it is done.
253
253
  - Use \`cancel_question\` to retract a pending question once it is no longer needed.
254
+ - Respect the user's Pushary delivery mode: it decides where an approval is surfaced (phone, terminal, or awareness only), not whether it is required. In Terminal mode do not expect a phone answer; the approval waits in the terminal. If the Pushary hook already gated an action, a single approval is enough, do not ask again.
254
255
 
255
256
  Pass \`agentName\` as "${label} - <project folder name>" so the user knows which session is asking. If a question times out with no answer, retry once with a longer timeout before falling back to asking in the terminal.`;
256
257
  var renderAgentInstructions = (label) => instructionBody(label);
@@ -752,6 +752,15 @@ var deriveUsage = (transcriptPath, sessionId) => {
752
752
  return void 0;
753
753
  }
754
754
  };
755
+ var PENDING_COMMAND_PREFIX = "The user sent a new instruction from their phone via Pushary:";
756
+ var mergeAdditionalContext = (existing, reported) => {
757
+ const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
758
+ if (typeof pendingCommand !== "string" || pendingCommand.trim().length === 0) return existing;
759
+ const injected = `${PENDING_COMMAND_PREFIX} ${pendingCommand.trim()}`;
760
+ return existing ? `${existing}
761
+
762
+ ${injected}` : injected;
763
+ };
755
764
  var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
756
765
  var reportEvent = async (event, options = {}) => {
757
766
  const apiKey = getApiKey();
@@ -766,7 +775,14 @@ var reportEvent = async (event, options = {}) => {
766
775
  body: JSON.stringify({
767
776
  ...event,
768
777
  machineId: event.machineId ?? getMachineId(),
769
- installMode: detectInstallMode()
778
+ installMode: detectInstallMode(),
779
+ // Advertise that this client reads `pendingCommand` back off the
780
+ // response, so the server may drain the phone-queued instruction on
781
+ // activity events (tool_complete/tool_error/user_prompt), not only Stop.
782
+ // Only Claude Code consumes additionalContext from these hooks; Codex and
783
+ // Gemini keep their existing session_end-only drain, so they must NOT
784
+ // advertise it or the server would pop-and-drop their queued command.
785
+ canDrainCommand: event.agentType === CLAUDE_CODE_AGENT.type
770
786
  }),
771
787
  signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
772
788
  });
@@ -794,22 +810,21 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
794
810
  }
795
811
  const decisionSource = deriveDecisionSource(lookup.tool, lookup.input, liveMode);
796
812
  const beacon = decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode) : void 0;
797
- const reports = [
798
- reportEvent({
799
- event: isError ? "tool_error" : "tool_complete",
800
- agentType: agent.type,
801
- agentName: `${agent.label} - ${projectName}`,
802
- action,
803
- sessionId: input.session_id,
804
- error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0,
805
- decisionSource,
806
- ...beacon ? { decisionOrigin: beacon.decisionOrigin, toolName: beacon.toolName, toolTarget: beacon.toolTarget } : {},
807
- meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, input.tool_result, input.cwd ?? process.cwd()) : void 0,
808
- usage: deriveUsage(input.transcript_path, input.session_id)
809
- })
810
- ];
813
+ const toolReport = reportEvent({
814
+ event: isError ? "tool_error" : "tool_complete",
815
+ agentType: agent.type,
816
+ agentName: `${agent.label} - ${projectName}`,
817
+ action,
818
+ sessionId: input.session_id,
819
+ error: isError ? String(input.tool_result?.error ?? input.tool_result?.stderr ?? "").slice(0, 500) : void 0,
820
+ decisionSource,
821
+ ...beacon ? { decisionOrigin: beacon.decisionOrigin, toolName: beacon.toolName, toolTarget: beacon.toolTarget } : {},
822
+ meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, input.tool_result, input.cwd ?? process.cwd()) : void 0,
823
+ usage: deriveUsage(input.transcript_path, input.session_id)
824
+ }, { maxAttempts: 1 });
825
+ const extraReports = [];
811
826
  if (throttlePass(`hookactive:${sessionKey}`, HOOK_ACTIVE_WINDOW_MS)) {
812
- reports.push(
827
+ extraReports.push(
813
828
  reportEvent(
814
829
  {
815
830
  event: "agent_hook_active",
@@ -821,7 +836,10 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
821
836
  )
822
837
  );
823
838
  }
824
- await Promise.allSettled(reports);
839
+ const [reported] = await Promise.allSettled([toolReport, ...extraReports]);
840
+ if (agent.type === CLAUDE_CODE_AGENT.type) {
841
+ additionalContext = mergeAdditionalContext(additionalContext, reported);
842
+ }
825
843
  return additionalContext;
826
844
  } catch {
827
845
  return void 0;
@@ -841,13 +859,18 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
841
859
  additionalContext = buildLateAnswerContext(late);
842
860
  for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
843
861
  }
844
- await reportEvent({
845
- event: "user_prompt",
846
- agentType: agent.type,
847
- agentName: `${agent.label} - ${projectName}`,
848
- sessionId: input.session_id,
849
- taskTitle
850
- }, { maxAttempts: 1, timeoutMs: 800 });
862
+ const [reported] = await Promise.allSettled([
863
+ reportEvent({
864
+ event: "user_prompt",
865
+ agentType: agent.type,
866
+ agentName: `${agent.label} - ${projectName}`,
867
+ sessionId: input.session_id,
868
+ taskTitle
869
+ }, { maxAttempts: 1, timeoutMs: 800 })
870
+ ]);
871
+ if (agent.type === CLAUDE_CODE_AGENT.type) {
872
+ additionalContext = mergeAdditionalContext(additionalContext, reported);
873
+ }
851
874
  return additionalContext;
852
875
  } catch {
853
876
  return void 0;
@@ -859,6 +882,9 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
859
882
  const sessionKey = input.session_id || DEFAULT_SESSION;
860
883
  const late = await reconcilePendingQuestions(sessionKey);
861
884
  const tasks = [
885
+ // maxAttempts:1 for the same reason as the activity-event drains: session_end
886
+ // always pops the command server-side (single-use LPOP), so a retry after a
887
+ // slow-but-successful response would pop a second command and drop the first.
862
888
  reportEvent({
863
889
  event: "session_end",
864
890
  agentType: agent.type,
@@ -866,7 +892,7 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
866
892
  action: "Session ended",
867
893
  sessionId: input.session_id,
868
894
  usage: deriveUsage(input.transcript_path, input.session_id)
869
- })
895
+ }, { maxAttempts: 1 })
870
896
  ];
871
897
  if (late.length > 0) {
872
898
  tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id));
@@ -886,16 +912,40 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
886
912
  return void 0;
887
913
  }
888
914
  };
889
- var handleNotification = async (input) => {
915
+ var NOTIFICATION_PUSH_TYPES = /* @__PURE__ */ new Set(["idle_prompt", "agent_needs_input"]);
916
+ var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
917
+ var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
890
918
  try {
891
919
  const projectName = basename(input.cwd ?? process.cwd());
920
+ const notifType = input.notification_type ?? input.type ?? "";
921
+ const sessionKey = input.session_id || DEFAULT_SESSION;
922
+ if (NOTIFICATION_PUSH_TYPES.has(notifType)) {
923
+ if (!throttlePass(`notify:${sessionKey}:${notifType}`, NOTIFICATION_THROTTLE_MS)) return;
924
+ let apiKey;
925
+ try {
926
+ apiKey = getApiKey();
927
+ } catch {
928
+ return;
929
+ }
930
+ const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
931
+ if (mode?.kill || mode?.mode === "terminal_only") return;
932
+ await sendNotification(apiKey, {
933
+ title: `${agent.label} is waiting`,
934
+ body: (input.message ?? "Your agent is waiting for you.").slice(0, 200),
935
+ agentName: `${agent.label} - ${projectName}`,
936
+ sessionId: input.session_id
937
+ }, 4e3).catch(() => {
938
+ });
939
+ return;
940
+ }
892
941
  await reportEvent({
893
- event: input.type === "error" ? "error" : "notification",
894
- agentType: "claude_code",
895
- agentName: `Claude Code - ${projectName}`,
896
- action: input.title ?? input.message ?? "Notification",
942
+ event: notifType === "error" ? "error" : "notification",
943
+ agentType: agent.type,
944
+ agentName: `${agent.label} - ${projectName}`,
945
+ action: input.message ?? input.title ?? "Notification",
897
946
  sessionId: input.session_id,
898
- error: input.type === "error" ? input.message : void 0
947
+ error: notifType === "error" ? input.message ?? input.title : void 0
948
+ }).catch(() => {
899
949
  });
900
950
  } catch {
901
951
  }
@@ -7,6 +7,7 @@ interface ToolInput {
7
7
  session_id?: string;
8
8
  cwd?: string;
9
9
  transcript_path?: string;
10
+ permission_mode?: string;
10
11
  }
11
12
  interface HookOutput {
12
13
  hookSpecificOutput: {
@@ -95,9 +96,10 @@ declare const handleNotification: (input: {
95
96
  message?: string;
96
97
  title?: string;
97
98
  type?: string;
99
+ notification_type?: string;
98
100
  cwd?: string;
99
101
  session_id?: string;
100
- }) => Promise<void>;
102
+ }, agent?: AgentIdentity) => Promise<void>;
101
103
 
102
104
  interface AskUserParams {
103
105
  question: string;
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-CY5YP34L.js";
3
+ } from "../chunk-FD4NV6BO.js";
4
4
  import "../chunk-KQYIHZ5E.js";
5
5
  import "../chunk-R5AJNXZS.js";
6
6
  import {
@@ -15,7 +15,7 @@ import {
15
15
  reportEvent,
16
16
  resolvePolicy,
17
17
  waitForAnswer
18
- } from "../chunk-IAOXM7X5.js";
18
+ } from "../chunk-USRIQPWI.js";
19
19
  import "../chunk-DWED7BS3.js";
20
20
  import "../chunk-Z5PL3K7C.js";
21
21
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -47,6 +47,7 @@
47
47
  "pushary-post-hook": "./dist/bin/pushary-post-hook.js",
48
48
  "pushary-stop-hook": "./dist/bin/pushary-stop-hook.js",
49
49
  "pushary-prompt-hook": "./dist/bin/pushary-prompt-hook.js",
50
+ "pushary-notification-hook": "./dist/bin/pushary-notification-hook.js",
50
51
  "pushary-codex": "./dist/bin/pushary-codex.js",
51
52
  "pushary-codex-hook": "./dist/bin/pushary-codex-hook.js",
52
53
  "pushary-gemini-hook": "./dist/bin/pushary-gemini-hook.js",