pushary 1.8.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +1943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/data/SKILL.md +591 -0
  5. package/data/cowork/SKILL.md +77 -0
  6. package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
  7. package/data/cursor-plugin/CHANGELOG.md +70 -0
  8. package/data/cursor-plugin/CONTRIBUTING.md +40 -0
  9. package/data/cursor-plugin/LICENSE +21 -0
  10. package/data/cursor-plugin/README.md +113 -0
  11. package/data/cursor-plugin/SECURITY.md +33 -0
  12. package/data/cursor-plugin/assets/logo.png +0 -0
  13. package/data/cursor-plugin/commands/notify-when-done.md +14 -0
  14. package/data/cursor-plugin/commands/pushary-test.md +13 -0
  15. package/data/cursor-plugin/hooks/hooks.json +75 -0
  16. package/data/cursor-plugin/mcp.json +11 -0
  17. package/data/cursor-plugin/rules/pushary.mdc +40 -0
  18. package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
  19. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
  20. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  21. package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
  22. package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
  23. package/data/vscode-plugin/.mcp.json +11 -0
  24. package/data/vscode-plugin/CHANGELOG.md +49 -0
  25. package/data/vscode-plugin/CONTRIBUTING.md +40 -0
  26. package/data/vscode-plugin/LICENSE +21 -0
  27. package/data/vscode-plugin/README.md +128 -0
  28. package/data/vscode-plugin/SECURITY.md +56 -0
  29. package/data/vscode-plugin/assets/logo.png +0 -0
  30. package/data/vscode-plugin/commands/notify-when-done.md +14 -0
  31. package/data/vscode-plugin/commands/pushary-test.md +13 -0
  32. package/data/vscode-plugin/hooks/hooks.json +60 -0
  33. package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
  34. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  35. package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
  36. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  37. package/dist/bin/pushary-bell-hook.js +46 -0
  38. package/dist/bin/pushary-bell.d.ts +1 -0
  39. package/dist/bin/pushary-bell.js +146 -0
  40. package/dist/bin/pushary-claude.d.ts +1 -0
  41. package/dist/bin/pushary-claude.js +1916 -0
  42. package/dist/bin/pushary-clean.d.ts +1 -0
  43. package/dist/bin/pushary-clean.js +1300 -0
  44. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  45. package/dist/bin/pushary-codex-bridge.js +599 -0
  46. package/dist/bin/pushary-codex-hook.d.ts +1 -0
  47. package/dist/bin/pushary-codex-hook.js +886 -0
  48. package/dist/bin/pushary-codex.d.ts +1 -0
  49. package/dist/bin/pushary-codex.js +125 -0
  50. package/dist/bin/pushary-connect.d.ts +1 -0
  51. package/dist/bin/pushary-connect.js +100 -0
  52. package/dist/bin/pushary-cowork.d.ts +1 -0
  53. package/dist/bin/pushary-cowork.js +36 -0
  54. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  55. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  56. package/dist/bin/pushary-daemon.d.ts +1 -0
  57. package/dist/bin/pushary-daemon.js +878 -0
  58. package/dist/bin/pushary-disconnect.d.ts +1 -0
  59. package/dist/bin/pushary-disconnect.js +264 -0
  60. package/dist/bin/pushary-doctor.d.ts +1 -0
  61. package/dist/bin/pushary-doctor.js +1433 -0
  62. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  63. package/dist/bin/pushary-elicitation-hook.js +149 -0
  64. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  65. package/dist/bin/pushary-gemini-bridge.js +339 -0
  66. package/dist/bin/pushary-gemini-hook.d.ts +1 -0
  67. package/dist/bin/pushary-gemini-hook.js +381 -0
  68. package/dist/bin/pushary-hook.d.ts +1 -0
  69. package/dist/bin/pushary-hook.js +95 -0
  70. package/dist/bin/pushary-login.d.ts +1 -0
  71. package/dist/bin/pushary-login.js +198 -0
  72. package/dist/bin/pushary-logout.d.ts +1 -0
  73. package/dist/bin/pushary-logout.js +147 -0
  74. package/dist/bin/pushary-mcp.d.ts +1 -0
  75. package/dist/bin/pushary-mcp.js +114 -0
  76. package/dist/bin/pushary-mode.d.ts +1 -0
  77. package/dist/bin/pushary-mode.js +130 -0
  78. package/dist/bin/pushary-notification-hook.d.ts +1 -0
  79. package/dist/bin/pushary-notification-hook.js +56 -0
  80. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  81. package/dist/bin/pushary-opencode-hook.js +338 -0
  82. package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
  83. package/dist/bin/pushary-permission-denied-hook.js +60 -0
  84. package/dist/bin/pushary-permission-hook.d.ts +1 -0
  85. package/dist/bin/pushary-permission-hook.js +65 -0
  86. package/dist/bin/pushary-post-hook.d.ts +1 -0
  87. package/dist/bin/pushary-post-hook.js +70 -0
  88. package/dist/bin/pushary-prompt-hook.d.ts +1 -0
  89. package/dist/bin/pushary-prompt-hook.js +61 -0
  90. package/dist/bin/pushary-session-end-hook.d.ts +1 -0
  91. package/dist/bin/pushary-session-end-hook.js +59 -0
  92. package/dist/bin/pushary-session-start-hook.d.ts +1 -0
  93. package/dist/bin/pushary-session-start-hook.js +72 -0
  94. package/dist/bin/pushary-setup.d.ts +1 -0
  95. package/dist/bin/pushary-setup.js +2660 -0
  96. package/dist/bin/pushary-stats.d.ts +1 -0
  97. package/dist/bin/pushary-stats.js +44 -0
  98. package/dist/bin/pushary-status.d.ts +1 -0
  99. package/dist/bin/pushary-status.js +262 -0
  100. package/dist/bin/pushary-stop-hook.d.ts +1 -0
  101. package/dist/bin/pushary-stop-hook.js +63 -0
  102. package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
  103. package/dist/bin/pushary-stopfailure-hook.js +56 -0
  104. package/dist/bin/pushary-suggestions.d.ts +1 -0
  105. package/dist/bin/pushary-suggestions.js +100 -0
  106. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  107. package/dist/bin/pushary-transcript-register.js +40 -0
  108. package/dist/bin/pushary-transcripts.d.ts +1 -0
  109. package/dist/bin/pushary-transcripts.js +77 -0
  110. package/dist/bin/pushary-upgrade.d.ts +1 -0
  111. package/dist/bin/pushary-upgrade.js +599 -0
  112. package/dist/bin/pushary-wait.d.ts +1 -0
  113. package/dist/bin/pushary-wait.js +122 -0
  114. package/dist/bin/pushary.d.ts +1 -0
  115. package/dist/bin/pushary.js +74 -0
  116. package/dist/chunk-2ABTSGFT.js +173 -0
  117. package/dist/chunk-2OY3ZZ5N.js +124 -0
  118. package/dist/chunk-3EGEA4KH.js +44 -0
  119. package/dist/chunk-3EVPNIBE.js +185 -0
  120. package/dist/chunk-5OP5MQI7.js +45 -0
  121. package/dist/chunk-6J2JC2RT.js +149 -0
  122. package/dist/chunk-7QLSKOSU.js +19 -0
  123. package/dist/chunk-7Z7Q4GGS.js +88 -0
  124. package/dist/chunk-A2A2YTMG.js +835 -0
  125. package/dist/chunk-AHV4LNB5.js +377 -0
  126. package/dist/chunk-AXPCESYO.js +197 -0
  127. package/dist/chunk-C2WCPBF7.js +86 -0
  128. package/dist/chunk-CJVVKOSR.js +73 -0
  129. package/dist/chunk-CULUZTWQ.js +2117 -0
  130. package/dist/chunk-CVQYGMSR.js +142 -0
  131. package/dist/chunk-CVWG2N3Y.js +16 -0
  132. package/dist/chunk-D2FX3EPW.js +15 -0
  133. package/dist/chunk-DB3ONZB4.js +44 -0
  134. package/dist/chunk-DE4Y6TCC.js +160 -0
  135. package/dist/chunk-E4FJTUH4.js +249 -0
  136. package/dist/chunk-EPCXUP3P.js +41 -0
  137. package/dist/chunk-EX3GAPA4.js +44 -0
  138. package/dist/chunk-EYK3GSRH.js +143 -0
  139. package/dist/chunk-GMXKITVA.js +70 -0
  140. package/dist/chunk-GPEGOGRJ.js +49 -0
  141. package/dist/chunk-GU3EJCVV.js +43 -0
  142. package/dist/chunk-HQ6G3B5R.js +59 -0
  143. package/dist/chunk-HZVVNGEW.js +22 -0
  144. package/dist/chunk-IPVKH2ST.js +231 -0
  145. package/dist/chunk-J5TIV3J6.js +238 -0
  146. package/dist/chunk-JU6W3XAU.js +64 -0
  147. package/dist/chunk-JYG4B33G.js +227 -0
  148. package/dist/chunk-JZTHGAPA.js +503 -0
  149. package/dist/chunk-KGDWDSNL.js +102 -0
  150. package/dist/chunk-KVQBWI5T.js +99 -0
  151. package/dist/chunk-KZERVKTD.js +16 -0
  152. package/dist/chunk-LXQ5FBBD.js +47 -0
  153. package/dist/chunk-M5ICYGHT.js +138 -0
  154. package/dist/chunk-M5JDZ45W.js +943 -0
  155. package/dist/chunk-MDEPU45E.js +1226 -0
  156. package/dist/chunk-MPWXGONV.js +157 -0
  157. package/dist/chunk-N6IOSZOE.js +185 -0
  158. package/dist/chunk-NGDD6TXY.js +192 -0
  159. package/dist/chunk-NQTHFF4W.js +2830 -0
  160. package/dist/chunk-OKXS7WDZ.js +68 -0
  161. package/dist/chunk-OLQRRFBX.js +21 -0
  162. package/dist/chunk-P7WQEVWA.js +307 -0
  163. package/dist/chunk-POFBE6ZY.js +12 -0
  164. package/dist/chunk-PQ4K74WL.js +30 -0
  165. package/dist/chunk-PUZZELE6.js +37 -0
  166. package/dist/chunk-Q7E4I6UA.js +767 -0
  167. package/dist/chunk-QCGNGEGE.js +15 -0
  168. package/dist/chunk-R7437YI3.js +151 -0
  169. package/dist/chunk-RHR6NARM.js +305 -0
  170. package/dist/chunk-SAXBC4AZ.js +60 -0
  171. package/dist/chunk-SGZJ5LYH.js +76 -0
  172. package/dist/chunk-SIFVFP6T.js +70 -0
  173. package/dist/chunk-SLFABHW3.js +150 -0
  174. package/dist/chunk-T7E75EKA.js +67 -0
  175. package/dist/chunk-TY4JT7W3.js +38 -0
  176. package/dist/chunk-U4TMAANR.js +111 -0
  177. package/dist/chunk-UI36QSBI.js +177 -0
  178. package/dist/chunk-VQKRG3AC.js +339 -0
  179. package/dist/chunk-WE2J62NC.js +1131 -0
  180. package/dist/chunk-XHKBHWLX.js +14 -0
  181. package/dist/chunk-ZSZSSJCV.js +275 -0
  182. package/dist/chunk-ZVSKMPBZ.js +135 -0
  183. package/dist/reapply-CW3MA66V.js +34 -0
  184. package/dist/src/index.d.ts +204 -0
  185. package/dist/src/index.js +64 -0
  186. package/package.json +108 -0
@@ -0,0 +1,767 @@
1
+ import {
2
+ activeHookSource,
3
+ activeIdentity
4
+ } from "./chunk-J5TIV3J6.js";
5
+ import {
6
+ defersToNativeMode,
7
+ reportMachineDenial
8
+ } from "./chunk-CJVVKOSR.js";
9
+ import {
10
+ isGatingMoment,
11
+ recordKeylessMoment
12
+ } from "./chunk-JU6W3XAU.js";
13
+ import {
14
+ announceSubscriptionRequired,
15
+ denyReasonFrom,
16
+ isDeferAnswer,
17
+ oncePerSession
18
+ } from "./chunk-5OP5MQI7.js";
19
+ import {
20
+ UNNAMED_ACTION,
21
+ approvalAwaitingRetry,
22
+ blockedActionFingerprint,
23
+ claimThrottle,
24
+ deriveAction,
25
+ deriveActionBody,
26
+ deriveBlocker,
27
+ deriveToolTarget,
28
+ describeToolCall,
29
+ fetchModeState,
30
+ forgetBlockedActionHint,
31
+ getPolicy,
32
+ hasBlockedActionHint,
33
+ markApprovalReminded,
34
+ permissionDeniedAction,
35
+ readLastPrompt,
36
+ readLastUserPrompt,
37
+ recordBlockedActionHint,
38
+ repoKeyFor,
39
+ reportEvent,
40
+ scopePathFor,
41
+ settleQuestionsForToolUse
42
+ } from "./chunk-CULUZTWQ.js";
43
+ import {
44
+ DEFAULT_SESSION
45
+ } from "./chunk-N6IOSZOE.js";
46
+ import {
47
+ DECISION_STOP_REASON,
48
+ UNVERIFIABLE_APPROVAL_REASON,
49
+ askUser,
50
+ cancelQuestion,
51
+ claimBlockedAction,
52
+ isDecisionStopped,
53
+ isLocalQuestionHandoff,
54
+ pollForAnswer,
55
+ requestGate,
56
+ sendNotification,
57
+ waitForQuestion
58
+ } from "./chunk-NGDD6TXY.js";
59
+ import {
60
+ SubscriptionRequiredError
61
+ } from "./chunk-M5ICYGHT.js";
62
+ import {
63
+ getMachineId
64
+ } from "./chunk-LXQ5FBBD.js";
65
+ import {
66
+ getApiKey,
67
+ getBaseUrl
68
+ } from "./chunk-GPEGOGRJ.js";
69
+ import {
70
+ AGENT_QUESTION_STRATEGY,
71
+ GATE_BLOCK_REASON_MAX_LENGTH,
72
+ GATE_WIRE,
73
+ agentQuestionStrategy,
74
+ agentRulesAllowEveryInput,
75
+ buildDecisionEpisodeFeatures,
76
+ claudeAllowlistOwnsCall,
77
+ claudeConsultsItsAllowlist,
78
+ claudePermissionDeniedOutput,
79
+ claudePermissionRequestOutput,
80
+ claudePermissionRulesFromSettings,
81
+ claudePreToolUseOutput,
82
+ claudePromptsBeforeAnEdit,
83
+ editAsksAtTheAgentsPrompt,
84
+ effectiveWaitSeconds,
85
+ hookHoldWindowMs,
86
+ hookWaitClamped,
87
+ hookWaitDeadline,
88
+ hookWritePath,
89
+ isBlockedActionRequest,
90
+ isPermissionRequestOwnedByPreToolUse,
91
+ isPusharyOwnTool,
92
+ parseAgentQuestionAnswers,
93
+ parseAgentQuestions,
94
+ resolveGate,
95
+ stoppedOutput
96
+ } from "./chunk-NQTHFF4W.js";
97
+
98
+ // src/claude-permission-rules.ts
99
+ import { readFileSync, statSync } from "fs";
100
+ import { homedir } from "os";
101
+ import { join } from "path";
102
+ var MANAGED_CLAUDE_SETTINGS_BY_PLATFORM = {
103
+ darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
104
+ linux: "/etc/claude-code/managed-settings.json",
105
+ win32: "C:\\ProgramData\\ClaudeCode\\managed-settings.json"
106
+ };
107
+ var MAX_CLAUDE_SETTINGS_BYTES = 1048576;
108
+ var claudeSettingsLocationFor = (cwd, environment = process.env, home = homedir(), platform = process.platform) => ({
109
+ configDir: environment.CLAUDE_CONFIG_DIR || join(home, ".claude"),
110
+ projectDir: environment.CLAUDE_PROJECT_DIR || cwd || void 0,
111
+ managedSettingsPath: MANAGED_CLAUDE_SETTINGS_BY_PLATFORM[platform]
112
+ });
113
+ var claudeSettingsSources = (location) => [
114
+ ...location.managedSettingsPath ? [{ path: location.managedSettingsPath, trust: "grants" }] : [],
115
+ { path: join(location.configDir, "settings.json"), trust: "grants" },
116
+ ...location.projectDir ? [
117
+ { path: join(location.projectDir, ".claude", "settings.json"), trust: "restricts_only" },
118
+ { path: join(location.projectDir, ".claude", "settings.local.json"), trust: "grants" }
119
+ ] : []
120
+ ];
121
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
122
+ var restrictionsOnly = (document) => {
123
+ if (!isRecord(document) || !isRecord(document.permissions)) return null;
124
+ const { ask: ask2, deny: deny2 } = document.permissions;
125
+ return { permissions: { ask: ask2, deny: deny2 } };
126
+ };
127
+ var readSettingsDocument = (path) => {
128
+ try {
129
+ const stats = statSync(path);
130
+ if (!stats.isFile() || stats.size > MAX_CLAUDE_SETTINGS_BYTES) return null;
131
+ return JSON.parse(readFileSync(path, "utf8"));
132
+ } catch {
133
+ return null;
134
+ }
135
+ };
136
+ var trustedDocument = ({ path, trust }) => {
137
+ const document = readSettingsDocument(path);
138
+ return trust === "grants" ? document : restrictionsOnly(document);
139
+ };
140
+ var readClaudePermissionRules = (location) => claudePermissionRulesFromSettings(claudeSettingsSources(location).map(trustedDocument));
141
+
142
+ // src/decision-episode.ts
143
+ var EPISODE_PATH = "/api/agent/decision-episode";
144
+ var EMIT_TIMEOUT_MS = 3e3;
145
+ var buildEpisodeBody = (input) => ({
146
+ sessionId: input.sessionId,
147
+ machineId: input.machineId,
148
+ agentType: input.agentType,
149
+ features: buildDecisionEpisodeFeatures(input)
150
+ });
151
+ var reportDecisionEpisode = async (apiKey, input) => {
152
+ try {
153
+ const baseUrl = getBaseUrl();
154
+ await fetch(`${baseUrl}${EPISODE_PATH}`, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
157
+ body: JSON.stringify(buildEpisodeBody(input)),
158
+ signal: AbortSignal.timeout(EMIT_TIMEOUT_MS)
159
+ });
160
+ } catch {
161
+ }
162
+ };
163
+
164
+ // src/hook.ts
165
+ import { basename } from "path";
166
+ var START_MS = Date.now();
167
+ var ACTIVE_AGENT = activeIdentity(process.argv);
168
+ var ACTIVE_SOURCE = activeHookSource(process.argv);
169
+ var agentNameIn = (projectName) => `${ACTIVE_AGENT.label} - ${projectName}`;
170
+ var allow = (reason) => claudePreToolUseOutput("allow", reason);
171
+ var deny = (reason) => claudePreToolUseOutput("deny", reason);
172
+ var allowWithInput = (updatedInput) => claudePreToolUseOutput("allow", void 0, updatedInput);
173
+ var ask = (reason) => claudePreToolUseOutput("ask", reason);
174
+ var halted = (reason) => stoppedOutput(deny(reason), reason);
175
+ var outcomeOfFailedAsk = (error, sessionId, failure) => {
176
+ if (!(error instanceof SubscriptionRequiredError)) return failure;
177
+ announceSubscriptionRequired(error.upgradeUrl, sessionId);
178
+ return void 0;
179
+ };
180
+ var storedWaitDeadline = (result, requestedDeadline) => {
181
+ const stored = result.waitEndsAt ? Date.parse(result.waitEndsAt) : Number.NaN;
182
+ return Number.isFinite(stored) ? Math.min(stored, requestedDeadline) : requestedDeadline;
183
+ };
184
+ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, timeoutAction, sessionId, machineId, toolName, toolTarget, decision, signals) => {
185
+ const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
186
+ const now = Date.now();
187
+ const requestedDeadline = hookWaitDeadline(START_MS, effectiveWait, "claude", now);
188
+ const clampCut = hookWaitClamped(START_MS, effectiveWait, "claude", now);
189
+ let result;
190
+ try {
191
+ result = await askUser(apiKey, {
192
+ question: `Allow ${description || UNNAMED_ACTION}?`,
193
+ type: "confirm",
194
+ context: `Agent wants to run this in ${projectName}`,
195
+ agentName: agentNameIn(projectName),
196
+ sessionId,
197
+ machineId,
198
+ toolName,
199
+ toolTarget,
200
+ waitEndsAt: timeoutAction !== "approve" && timeoutAction !== "deny" || clampCut ? new Date(requestedDeadline).toISOString() : void 0,
201
+ ...decision
202
+ });
203
+ } catch (error) {
204
+ return outcomeOfFailedAsk(error, sessionId, halted(UNVERIFIABLE_APPROVAL_REASON));
205
+ }
206
+ if (isDecisionStopped(result)) return halted(DECISION_STOP_REASON);
207
+ const deadline = storedWaitDeadline(result, requestedDeadline);
208
+ if (!isLocalQuestionHandoff(result) && !result.answered) announceWait(effectiveWait, result.answerUrl, sessionId);
209
+ const answer = await waitForQuestion(apiKey, result, deadline);
210
+ if (isDecisionStopped(answer)) return halted(DECISION_STOP_REASON);
211
+ if (!answer.answered && isLocalQuestionHandoff(result)) return ask("Approve in the current client.");
212
+ if (answer.answered) {
213
+ if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
214
+ signals.humanDecision = true;
215
+ return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value, answer.note));
216
+ }
217
+ if (timeoutAction === "wait" || clampCut || deadline < requestedDeadline) {
218
+ return ask("The phone question was withdrawn. Approve in the current client.");
219
+ }
220
+ switch (timeoutAction) {
221
+ case "approve":
222
+ return allow();
223
+ case "deny":
224
+ return deny("No response within timeout");
225
+ default:
226
+ return ask("No push response, asking in terminal");
227
+ }
228
+ };
229
+ var handleTerminalOnly = () => {
230
+ return ask();
231
+ };
232
+ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds, sessionId, machineId, toolName, toolTarget, decision, signals) => {
233
+ const requestedDeadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
234
+ let result;
235
+ try {
236
+ result = await askUser(apiKey, {
237
+ question: `Allow ${description || UNNAMED_ACTION}?`,
238
+ type: "confirm",
239
+ context: `Agent wants to run this in ${projectName}`,
240
+ agentName: agentNameIn(projectName),
241
+ sessionId,
242
+ machineId,
243
+ toolName,
244
+ toolTarget,
245
+ waitEndsAt: new Date(requestedDeadline).toISOString(),
246
+ ...decision
247
+ });
248
+ } catch (error) {
249
+ return outcomeOfFailedAsk(error, sessionId, halted(UNVERIFIABLE_APPROVAL_REASON));
250
+ }
251
+ if (isDecisionStopped(result)) return halted(DECISION_STOP_REASON);
252
+ if (!isLocalQuestionHandoff(result) && !result.answered) announceWait(pushFirstSeconds, result.answerUrl, sessionId);
253
+ const deadline = storedWaitDeadline(result, requestedDeadline);
254
+ const answer = await waitForQuestion(apiKey, result, deadline, 1500);
255
+ if (isDecisionStopped(answer)) return halted(DECISION_STOP_REASON);
256
+ if (answer.answered) {
257
+ if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
258
+ signals.humanDecision = true;
259
+ return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value, answer.note));
260
+ }
261
+ return ask("The phone question was withdrawn. Approve in the current client.");
262
+ };
263
+ var handleNotifyOnly = async (apiKey, description, projectName, sessionId, machineId) => {
264
+ try {
265
+ await sendNotification(apiKey, {
266
+ title: "Agent needs approval",
267
+ body: description,
268
+ agentName: agentNameIn(projectName),
269
+ sessionId,
270
+ machineId
271
+ });
272
+ } catch {
273
+ }
274
+ return ask();
275
+ };
276
+ var emitDecisionEpisode = (apiKey, input, toolPolicy, machineId, toolTarget, decision, result) => {
277
+ const verdict = result?.hookSpecificOutput?.permissionDecision;
278
+ const outcome = verdict === "allow" ? "approve" : verdict === "deny" ? "deny" : null;
279
+ if (!outcome) return;
280
+ const ti = input.tool_input ?? {};
281
+ const commandOrPath = typeof ti.command === "string" ? ti.command : typeof ti.file_path === "string" ? ti.file_path : typeof ti.path === "string" ? ti.path : void 0;
282
+ void reportDecisionEpisode(apiKey, {
283
+ toolName: input.tool_name,
284
+ commandOrPath,
285
+ intent: decision.intent,
286
+ toolTarget,
287
+ agentType: ACTIVE_AGENT.type,
288
+ matchedPattern: toolPolicy.tool,
289
+ policyMode: toolPolicy.mode,
290
+ timeoutSeconds: toolPolicy.timeoutSeconds,
291
+ timeoutAction: toolPolicy.timeoutAction,
292
+ // Reaching a handler means a human gate was requested (auto-allow/deny
293
+ // returned earlier in the caller before dispatch).
294
+ requestedGate: "ask",
295
+ decision: outcome,
296
+ // Truthfully human: the caller only emits when signals.humanDecision is set
297
+ // (a real phone answer). Bronze label until reason + execution-outcome land.
298
+ origin: "human",
299
+ latencyMs: Date.now() - START_MS,
300
+ sessionId: input.session_id,
301
+ machineId
302
+ });
303
+ };
304
+ var dispatchModeHandler = async (apiKey, input, toolPolicy, machineId, trainingConsent = false, scopeReason, scopePath) => {
305
+ const description = describeToolCall(input.tool_name, input.tool_input, "hook");
306
+ const projectName = basename(input.cwd ?? process.cwd());
307
+ const sessionId = input.session_id;
308
+ const toolTarget = deriveToolTarget(input.tool_name, input.tool_input);
309
+ const intent = readLastPrompt(sessionId ?? DEFAULT_SESSION) ?? (input.transcript_path ? readLastUserPrompt(input.transcript_path) : void 0);
310
+ const decision = {
311
+ toolPath: hookWritePath(input.tool_name, input.tool_input),
312
+ intent,
313
+ action: deriveAction(input.tool_name, input.tool_input),
314
+ blocker: scopeReason ?? deriveBlocker(toolPolicy.mode),
315
+ actionBody: deriveActionBody(input.tool_name, input.tool_input),
316
+ repoKey: repoKeyFor(input.cwd),
317
+ // Only when the breach is what caused the question, so an ordinary approval
318
+ // can never silently widen a scope.
319
+ scopePath: scopeReason ? scopePath : void 0,
320
+ // The gate asks at most one question per tool call, so the runtime's id needs
321
+ // no suffix here. Absent on runtimes that do not send one, and the server then
322
+ // creates a fresh decision exactly as before.
323
+ toolUseId: input.tool_use_id,
324
+ sessionToolAware: true
325
+ };
326
+ const signals = {};
327
+ const runHandler = () => {
328
+ switch (toolPolicy.mode) {
329
+ case "push_only":
330
+ return handlePushOnly(apiKey, description, projectName, toolPolicy.timeoutSeconds, toolPolicy.timeoutAction, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
331
+ case "terminal_only":
332
+ return Promise.resolve(handleTerminalOnly());
333
+ case "push_first":
334
+ return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
335
+ case "notify_only":
336
+ return handleNotifyOnly(apiKey, description, projectName, sessionId, machineId);
337
+ default:
338
+ return handlePushFirst(apiKey, description, projectName, toolPolicy.pushFirstSeconds, sessionId, machineId, input.tool_name, toolTarget, decision, signals);
339
+ }
340
+ };
341
+ const result = await runHandler();
342
+ if (trainingConsent && signals.humanDecision) {
343
+ emitDecisionEpisode(apiKey, input, toolPolicy, machineId, toolTarget, decision, result);
344
+ }
345
+ return result;
346
+ };
347
+ var announceWait = (waitSeconds, answerUrl, sessionId) => {
348
+ try {
349
+ const handoff = waitSeconds > 0 ? `Waiting ${waitSeconds}s, then this terminal takes over.` : "Waiting for your answer.";
350
+ process.stderr.write(`[pushary] Sent to your phone. ${handoff}
351
+ `);
352
+ if (answerUrl && oncePerSession("answerurl", sessionId)) {
353
+ process.stderr.write(` Answer here instead: ${answerUrl}
354
+ `);
355
+ }
356
+ } catch {
357
+ }
358
+ };
359
+ var KEYLESS_NOTICE = "[pushary] Not connected (no API key), so your agent runs untouched. Approval moments are counted locally; see them with: npx @pushary/agent-hooks@latest stats. Connect: https://pushary.com/sign-up?utm_source=cli&utm_medium=keyless-hook";
360
+ var keylessNoticeOnce = (sessionId) => {
361
+ try {
362
+ if (!oncePerSession("keyless", sessionId)) return;
363
+ process.stderr.write(`${KEYLESS_NOTICE}
364
+ `);
365
+ } catch {
366
+ }
367
+ };
368
+ var shouldDeferToNativeMode = defersToNativeMode;
369
+ var withLabels = (question) => ({
370
+ ...question,
371
+ labels: question.options.map((option) => option.label)
372
+ });
373
+ var withoutLabels = ({ labels: _, ...question }) => question;
374
+ var parseQuestions = (toolInput) => parseAgentQuestions(toolInput)?.map(withLabels);
375
+ var questionContext = (question, index, total, project) => {
376
+ const subject = question.header ? `${question.header}: your agent is asking in ${project}` : `Your agent is asking in ${project}`;
377
+ return total > 1 ? `${subject} (${index + 1} of ${total})` : subject;
378
+ };
379
+ var askAsOneCard = async (apiKey, input, parsed, projectName, deadline) => {
380
+ const only = parsed[0];
381
+ let result;
382
+ try {
383
+ result = await askUser(apiKey, {
384
+ question: only.question,
385
+ type: "select",
386
+ options: [...only.labels],
387
+ questions: parsed.map(withoutLabels),
388
+ context: questionContext(only, 0, 1, projectName),
389
+ agentName: agentNameIn(projectName),
390
+ sessionId: input.session_id,
391
+ machineId: getMachineId(),
392
+ toolName: "AskUserQuestion",
393
+ toolUseId: input.tool_use_id,
394
+ waitEndsAt: new Date(deadline).toISOString()
395
+ });
396
+ } catch (error) {
397
+ return outcomeOfFailedAsk(error, input.session_id, null);
398
+ }
399
+ if (isDecisionStopped(result)) return null;
400
+ const answer = await waitForQuestion(apiKey, result, storedWaitDeadline(result, deadline));
401
+ if (isDecisionStopped(answer)) return null;
402
+ const values = answer.answered && answer.value ? parseAgentQuestionAnswers(parsed, answer.value) : null;
403
+ if (!values || isDeferAnswer(answer.value)) {
404
+ return void 0;
405
+ }
406
+ return values;
407
+ };
408
+ var askQuestionByQuestion = async (apiKey, input, parsed, projectName, deadline) => {
409
+ const answers = {};
410
+ for (const [index, question] of parsed.entries()) {
411
+ let result;
412
+ try {
413
+ result = await askUser(apiKey, {
414
+ question: question.question,
415
+ type: "select",
416
+ options: [...question.labels],
417
+ questions: [withoutLabels(question)],
418
+ context: questionContext(question, index, parsed.length, projectName),
419
+ agentName: agentNameIn(projectName),
420
+ sessionId: input.session_id,
421
+ machineId: getMachineId(),
422
+ toolName: "AskUserQuestion",
423
+ // One AskUserQuestion call raises several questions, and their payload
424
+ // fingerprints are identical (same tool, no target, no body). Without the
425
+ // index they would all collapse onto the first decision.
426
+ toolUseId: input.tool_use_id ? `${input.tool_use_id}#q${index}` : void 0,
427
+ waitEndsAt: new Date(deadline).toISOString()
428
+ });
429
+ } catch (error) {
430
+ return outcomeOfFailedAsk(error, input.session_id, null);
431
+ }
432
+ if (isDecisionStopped(result)) return null;
433
+ const answer = await waitForQuestion(apiKey, result, storedWaitDeadline(result, deadline));
434
+ if (isDecisionStopped(answer)) return null;
435
+ const legacyValue = answer.answered ? answer.value?.trim() : void 0;
436
+ const values = legacyValue ? parseAgentQuestionAnswers([question], legacyValue) ?? parseAgentQuestionAnswers([question], JSON.stringify({
437
+ [question.question]: question.multiSelect ? JSON.stringify([legacyValue]) : legacyValue
438
+ })) : null;
439
+ const value = values?.[question.question];
440
+ if (!value || isDeferAnswer(answer.value)) {
441
+ return void 0;
442
+ }
443
+ answers[question.question] = value;
444
+ }
445
+ return answers;
446
+ };
447
+ var handleAskUserQuestion = async (apiKey, input, modeState) => {
448
+ if (modeState.askHandoff) return void 0;
449
+ const parsed = parseQuestions(input.tool_input ?? {});
450
+ if (!parsed) return void 0;
451
+ const projectName = basename(input.cwd ?? process.cwd());
452
+ const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
453
+ const answers = parsed.length === 1 ? await askAsOneCard(apiKey, input, parsed, projectName, deadline) : await askQuestionByQuestion(apiKey, input, parsed, projectName, deadline);
454
+ if (answers === null) return halted(DECISION_STOP_REASON);
455
+ if (!answers) return void 0;
456
+ return allowWithInput({ ...input.tool_input ?? {}, answers });
457
+ };
458
+ var PLAN_APPROVE = "Approve, start coding";
459
+ var PLAN_KEEP_PLANNING = "Keep planning";
460
+ var handleExitPlanMode = async (apiKey, input) => {
461
+ const toolInput = input.tool_input ?? {};
462
+ const projectName = basename(input.cwd ?? process.cwd());
463
+ const deadline = hookWaitDeadline(START_MS, effectiveWaitSeconds("wait", 0, "claude"), "claude", Date.now());
464
+ let result;
465
+ try {
466
+ result = await askUser(apiKey, {
467
+ question: `Approve the plan and start coding in ${projectName}?`,
468
+ type: "select",
469
+ options: [PLAN_APPROVE, PLAN_KEEP_PLANNING],
470
+ context: `Your agent finished planning in ${projectName} and is waiting to start.`,
471
+ agentName: agentNameIn(projectName),
472
+ sessionId: input.session_id,
473
+ machineId: getMachineId(),
474
+ toolName: "ExitPlanMode",
475
+ action: deriveAction("ExitPlanMode", toolInput),
476
+ // The plan markdown itself, through the same redaction and cap as any diff,
477
+ // so approving from a lock screen is a real decision and not a blind yes.
478
+ actionBody: deriveActionBody("ExitPlanMode", toolInput),
479
+ intent: readLastPrompt(input.session_id ?? DEFAULT_SESSION) ?? (input.transcript_path ? readLastUserPrompt(input.transcript_path) : void 0),
480
+ blocker: "Waiting for you to approve the plan",
481
+ waitEndsAt: new Date(deadline).toISOString()
482
+ });
483
+ } catch (error) {
484
+ return outcomeOfFailedAsk(error, input.session_id, halted(DECISION_STOP_REASON));
485
+ }
486
+ if (isDecisionStopped(result)) return halted(DECISION_STOP_REASON);
487
+ const answer = await waitForQuestion(apiKey, result, storedWaitDeadline(result, deadline));
488
+ if (isDecisionStopped(answer)) return halted(DECISION_STOP_REASON);
489
+ const value = answer.answered ? (answer.value ?? "").trim() : "";
490
+ if (!value || isDeferAnswer(answer.value)) {
491
+ return void 0;
492
+ }
493
+ if (value === PLAN_APPROVE) return allow();
494
+ return deny("Plan declined from the phone. Ask what to change before coding.");
495
+ };
496
+ var handleKeyless = (input) => {
497
+ try {
498
+ if (isGatingMoment(input.tool_name, input.tool_input ?? {})) {
499
+ recordKeylessMoment({
500
+ ts: Date.now(),
501
+ tool: input.tool_name,
502
+ project: basename(input.cwd ?? process.cwd()),
503
+ sessionId: input.session_id
504
+ });
505
+ }
506
+ keylessNoticeOnce(input.session_id);
507
+ } catch {
508
+ }
509
+ return void 0;
510
+ };
511
+ var claudePermissionRulesFor = (input) => ACTIVE_SOURCE === "claude" ? readClaudePermissionRules(claudeSettingsLocationFor(input.cwd)) : null;
512
+ var claudeRulesMayCover = (input, rules) => rules !== null && claudeConsultsItsAllowlist(input.permission_mode) && agentRulesAllowEveryInput(rules, input.tool_name, [input.tool_input ?? {}], input.cwd);
513
+ var claudeGateInput = (modeState, config, input, canonical) => {
514
+ const toolName = canonical?.toolName ?? input.tool_name;
515
+ const toolInputs = canonical?.toolInputs ?? [input.tool_input ?? {}];
516
+ return {
517
+ modeState,
518
+ config,
519
+ toolName,
520
+ toolInputs,
521
+ scopePaths: modeState.scope ? toolInputs.flatMap((args) => {
522
+ const path = scopePathFor(toolName, args, input.cwd);
523
+ return path ? [path] : [];
524
+ }) : [],
525
+ cwd: input.cwd,
526
+ repoKey: repoKeyFor(input.cwd),
527
+ sessionId: input.session_id
528
+ };
529
+ };
530
+ var unreadableReason = "Pushary could not read every target of this call. Use a supported command or a single-file edit.";
531
+ var batchScopeReason = "This patch exceeds the agreed scope. Split it into single-file edits so each additional path can be approved.";
532
+ var BLOCKED_ACTION_GATE_TIMEOUT_MS = 15e3;
533
+ var BLOCKED_ACTION_CLAIM_TIMEOUT_MS = 4e3;
534
+ var BLOCKED_ACTION_ALLOW_REASON = "Approved from Pushary after auto mode blocked it";
535
+ var BLOCKED_ACTION_CHANGED_RETRY_REASON = "This is not the call the person allowed from Pushary, so their approval does not cover it. Send the call auto mode blocked again, exactly as it was with nothing changed, to use their approval.";
536
+ var blockReasonOf = (reason) => typeof reason === "string" ? reason.trim().slice(0, GATE_BLOCK_REASON_MAX_LENGTH).trim() || void 0 : void 0;
537
+ var blockedActionFingerprintFor = (input) => blockedActionFingerprint({
538
+ sessionId: input.session_id,
539
+ toolName: input.tool_name,
540
+ toolInput: input.tool_input,
541
+ cwd: input.cwd
542
+ });
543
+ var blockedActionGateRequest = (input, event) => ({
544
+ v: GATE_WIRE,
545
+ source: "claude",
546
+ event,
547
+ toolName: input.tool_name,
548
+ toolInputs: [input.tool_input ?? {}],
549
+ sessionId: input.session_id,
550
+ toolUseId: input.tool_use_id,
551
+ machineId: getMachineId(),
552
+ cwd: input.cwd,
553
+ repoKey: repoKeyFor(input.cwd),
554
+ permissionMode: input.permission_mode,
555
+ ...event === "PermissionDenied" ? { createDecision: true, localSurfaceAvailable: false, blockReason: blockReasonOf(input.reason) } : {}
556
+ });
557
+ var claimedBlockedActionRetry = async (apiKey, input) => {
558
+ const fingerprint = blockedActionFingerprintFor(input);
559
+ if (!fingerprint || !hasBlockedActionHint(fingerprint)) return false;
560
+ try {
561
+ const granted = await claimBlockedAction(
562
+ apiKey,
563
+ blockedActionGateRequest(input, "PreToolUse"),
564
+ BLOCKED_ACTION_CLAIM_TIMEOUT_MS
565
+ );
566
+ if (granted !== null) forgetBlockedActionHint(fingerprint);
567
+ return granted === true;
568
+ } catch {
569
+ return false;
570
+ }
571
+ };
572
+ var remindedOfApprovedRetry = (input) => {
573
+ const approval = approvalAwaitingRetry(input.session_id);
574
+ if (!approval || approval.reminded || approval.toolName !== input.tool_name) return false;
575
+ if (approval.fingerprint === blockedActionFingerprintFor(input)) return false;
576
+ markApprovalReminded(approval);
577
+ return true;
578
+ };
579
+ var handlePreToolUse = async (input, canonical) => {
580
+ if (isPusharyOwnTool(input.tool_name)) return void 0;
581
+ let apiKey;
582
+ try {
583
+ apiKey = getApiKey();
584
+ } catch {
585
+ return handleKeyless(input);
586
+ }
587
+ try {
588
+ const modeState = await fetchModeState(apiKey, input.session_id);
589
+ const policy = await getPolicy(apiKey, modeState.policyVersion, ACTIVE_AGENT.type, true, input.session_id);
590
+ const gateInput = claudeGateInput(modeState, policy, input, canonical);
591
+ const verdict = resolveGate(gateInput);
592
+ if (verdict.kind === "kill") {
593
+ await reportMachineDenial(input, verdict, ACTIVE_AGENT);
594
+ return halted(verdict.reason);
595
+ }
596
+ if (verdict.kind === "deny") {
597
+ await reportMachineDenial(input, verdict, ACTIVE_AGENT);
598
+ return deny(verdict.reason);
599
+ }
600
+ if (canonical === null) return deny(unreadableReason);
601
+ if (verdict.kind === "gate" && verdict.scopeReason && (canonical?.toolInputs.length ?? 0) > 1) return deny(batchScopeReason);
602
+ if (agentQuestionStrategy(ACTIVE_SOURCE, "PreToolUse", input.tool_name) === AGENT_QUESTION_STRATEGY.native) return void 0;
603
+ if (input.tool_name === "AskUserQuestion") {
604
+ return handleAskUserQuestion(apiKey, input, modeState);
605
+ }
606
+ if (input.tool_name === "ExitPlanMode") {
607
+ return handleExitPlanMode(apiKey, input);
608
+ }
609
+ if (await claimedBlockedActionRetry(apiKey, input)) return allow(BLOCKED_ACTION_ALLOW_REASON);
610
+ if (remindedOfApprovedRetry(input)) return deny(BLOCKED_ACTION_CHANGED_RETRY_REASON);
611
+ if (!(verdict.kind === "gate" && verdict.scopeReason) && shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
612
+ return void 0;
613
+ }
614
+ if (claudeAllowlistOwnsCall(gateInput, verdict, input.permission_mode, claudePermissionRulesFor(input))) return void 0;
615
+ if (claudePromptsBeforeAnEdit(input.permission_mode) && editAsksAtTheAgentsPrompt(gateInput, verdict)) return handleTerminalOnly();
616
+ switch (verdict.kind) {
617
+ case "allow":
618
+ return allow();
619
+ // No `deny` case: it is handled above the defer, and the compiler enforces
620
+ // that it stays there. Restoring one here would type-error as unreachable.
621
+ //
622
+ // State we could not establish, with an auto-approve on the table. No
623
+ // opinion, so Claude Code's own flow runs as if the hook were absent.
624
+ case "defer":
625
+ return void 0;
626
+ default:
627
+ return dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath);
628
+ }
629
+ } catch {
630
+ return void 0;
631
+ }
632
+ };
633
+ var permReqAllow = (updatedInput) => claudePermissionRequestOutput("allow", void 0, updatedInput);
634
+ var permReqDeny = (message) => claudePermissionRequestOutput("deny", message);
635
+ var permReqHalted = (reason) => stoppedOutput(permReqDeny(reason), reason);
636
+ var toPermissionRequestOutput = (hookOutput) => {
637
+ if (!hookOutput) return void 0;
638
+ const d = hookOutput.hookSpecificOutput;
639
+ if (d.permissionDecision === "allow") return permReqAllow(d.updatedInput);
640
+ if (d.permissionDecision === "deny") return permReqDeny(d.permissionDecisionReason);
641
+ return void 0;
642
+ };
643
+ var handlePermissionRequest = async (input, canonical) => {
644
+ const claudeRules = claudePermissionRulesFor(input);
645
+ if (canonical === void 0 && !claudeRulesMayCover(input, claudeRules) && isPermissionRequestOwnedByPreToolUse(input.tool_name, input.permission_mode, shouldDeferToNativeMode)) return void 0;
646
+ if (isPusharyOwnTool(input.tool_name)) return void 0;
647
+ let apiKey;
648
+ try {
649
+ apiKey = getApiKey();
650
+ } catch {
651
+ return void 0;
652
+ }
653
+ try {
654
+ const modeState = await fetchModeState(apiKey, input.session_id);
655
+ const policy = await getPolicy(apiKey, modeState.policyVersion, ACTIVE_AGENT.type, true, input.session_id);
656
+ const gateInput = claudeGateInput(modeState, policy, input, canonical);
657
+ const verdict = resolveGate(gateInput);
658
+ if (verdict.kind === "kill") {
659
+ await reportMachineDenial(input, verdict, ACTIVE_AGENT);
660
+ return permReqHalted(verdict.reason);
661
+ }
662
+ if (verdict.kind === "deny") {
663
+ await reportMachineDenial(input, verdict, ACTIVE_AGENT);
664
+ return permReqDeny(verdict.reason);
665
+ }
666
+ if (canonical === null) return permReqDeny(unreadableReason);
667
+ if (agentQuestionStrategy(ACTIVE_SOURCE, "PermissionRequest", input.tool_name) === AGENT_QUESTION_STRATEGY.native) return void 0;
668
+ if (verdict.kind === "gate" && verdict.scopeReason && (canonical?.toolInputs.length ?? 0) > 1) return permReqDeny(batchScopeReason);
669
+ if (!(verdict.kind === "gate" && verdict.scopeReason) && !claudeAllowlistOwnsCall(gateInput, verdict, input.permission_mode, claudeRules) && isPermissionRequestOwnedByPreToolUse(input.tool_name, input.permission_mode, shouldDeferToNativeMode)) return void 0;
670
+ switch (verdict.kind) {
671
+ case "allow":
672
+ return permReqAllow();
673
+ // No opinion, so Claude Code's own dialog shows.
674
+ case "defer":
675
+ return void 0;
676
+ default:
677
+ return toPermissionRequestOutput(
678
+ await dispatchModeHandler(apiKey, input, verdict.policy, getMachineId(), modeState.trainingConsent, verdict.scopeReason, verdict.scopePath)
679
+ );
680
+ }
681
+ } catch {
682
+ return void 0;
683
+ }
684
+ };
685
+ var reportPermissionDenied = (input) => reportEvent({
686
+ event: "permission_denied",
687
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
688
+ toolUseId: input.tool_use_id,
689
+ agentType: ACTIVE_AGENT.type,
690
+ agentName: agentNameIn(basename(input.cwd ?? process.cwd())),
691
+ action: permissionDeniedAction(input.tool_name),
692
+ sessionId: input.session_id
693
+ // Side report, not a command carrier. Without timeoutMs this takes the phone's
694
+ // queued instruction and acknowledges it delivered, and this hook can print
695
+ // only {retry}. 4s rather than the Notification path's 2.5s because
696
+ // PermissionDenied holds the agent for the full hook budget, not 10s.
697
+ }, { timeoutMs: 4e3 }).catch(() => void 0);
698
+ var isAffirmativeAnswer = (answer) => answer.answered && !isDecisionStopped(answer) && answer.value === "yes";
699
+ var PERMISSION_DENIED_NOTICE_WINDOW_MS = 5 * 60 * 1e3;
700
+ var blockedActionHoldEndsAt = (holdBudgetSeconds) => START_MS + hookHoldWindowMs("claude", holdBudgetSeconds);
701
+ var notifyPermissionDenied = async (apiKey, input) => {
702
+ const sessionKey = input.session_id || DEFAULT_SESSION;
703
+ if (claimThrottle(`permdenied:${sessionKey}:${input.tool_name}`, PERMISSION_DENIED_NOTICE_WINDOW_MS) !== "passed") return;
704
+ const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
705
+ if (mode?.kill || mode?.mode === "terminal_only") return;
706
+ const projectName = basename(input.cwd ?? process.cwd());
707
+ await sendNotification(apiKey, {
708
+ title: `Auto mode blocked ${input.tool_name}`,
709
+ body: `Your agent was denied ${input.tool_name} in ${projectName}.`,
710
+ agentName: agentNameIn(projectName),
711
+ sessionId: input.session_id
712
+ }, 4e3).catch(() => void 0);
713
+ };
714
+ var isRetryOutput = (output) => {
715
+ if (!output) return false;
716
+ try {
717
+ const parsed = JSON.parse(output);
718
+ return parsed?.hookSpecificOutput?.hookEventName === "PermissionDenied" && parsed.hookSpecificOutput.retry === true;
719
+ } catch {
720
+ return false;
721
+ }
722
+ };
723
+ var approvedCallOf = (input) => input.session_id ? { sessionId: input.session_id, toolName: input.tool_name } : void 0;
724
+ var retryOnce = (fingerprint, input) => recordBlockedActionHint(fingerprint, approvedCallOf(input)) ? claudePermissionDeniedOutput("allow") : void 0;
725
+ var askAboutBlockedAction = async (apiKey, input, fingerprint, holdEndsAt) => {
726
+ const reply = await requestGate(apiKey, blockedActionGateRequest(input, "PermissionDenied"), BLOCKED_ACTION_GATE_TIMEOUT_MS);
727
+ if (!reply) return void 0;
728
+ if (reply.kind === "no_opinion") return isRetryOutput(reply.output) ? retryOnce(fingerprint, input) : void 0;
729
+ if (reply.kind !== "ask" || !reply.correlationId || reply.deadlineSeconds === void 0) return void 0;
730
+ if (reply.noDevices) {
731
+ await cancelQuestion(apiKey, reply.correlationId, AbortSignal.timeout(4e3)).catch(() => void 0);
732
+ return void 0;
733
+ }
734
+ const deadline = Math.min(hookWaitDeadline(START_MS, reply.deadlineSeconds, "claude", Date.now()), holdEndsAt);
735
+ const answer = await pollForAnswer(apiKey, reply.correlationId, deadline);
736
+ return isAffirmativeAnswer(answer) ? retryOnce(fingerprint, input) : void 0;
737
+ };
738
+ var handlePermissionDenied = async (input, holdBudgetSeconds) => {
739
+ await reportPermissionDenied(input);
740
+ if (isPusharyOwnTool(input.tool_name)) return void 0;
741
+ let apiKey;
742
+ try {
743
+ apiKey = getApiKey();
744
+ } catch {
745
+ return void 0;
746
+ }
747
+ try {
748
+ await settleQuestionsForToolUse(input.session_id || DEFAULT_SESSION, input.tool_use_id);
749
+ if (!isBlockedActionRequest({ source: "claude", event: "PermissionDenied", toolName: input.tool_name })) return void 0;
750
+ const fingerprint = blockedActionFingerprintFor(input);
751
+ if (!fingerprint) return void 0;
752
+ const holdEndsAt = blockedActionHoldEndsAt(holdBudgetSeconds);
753
+ if (holdEndsAt - Date.now() <= BLOCKED_ACTION_GATE_TIMEOUT_MS) {
754
+ await notifyPermissionDenied(apiKey, input);
755
+ return void 0;
756
+ }
757
+ return await askAboutBlockedAction(apiKey, input, fingerprint, holdEndsAt);
758
+ } catch {
759
+ return void 0;
760
+ }
761
+ };
762
+
763
+ export {
764
+ handlePreToolUse,
765
+ handlePermissionRequest,
766
+ handlePermissionDenied
767
+ };