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,2117 @@
1
+ import {
2
+ DEFAULT_SESSION,
3
+ isDefaultSession,
4
+ listPendingQuestions,
5
+ listPendingQuestionsForToolUse,
6
+ markPendingCommandDelivered,
7
+ readPendingCommand,
8
+ removePendingCommand,
9
+ removePendingQuestion,
10
+ removePendingSession,
11
+ savePendingCommand
12
+ } from "./chunk-N6IOSZOE.js";
13
+ import {
14
+ IDLE_PROMPT_NOTIFICATION,
15
+ NOTIFICATION_PUSH_TYPES,
16
+ POSTTOOLUSE_MIRROR_ONLY_TOOLS
17
+ } from "./chunk-EPCXUP3P.js";
18
+ import {
19
+ cancelQuestion,
20
+ isPolicyConfig,
21
+ sendNotification,
22
+ waitForAnswer
23
+ } from "./chunk-NGDD6TXY.js";
24
+ import {
25
+ clientErrorKindOf,
26
+ reportClientError
27
+ } from "./chunk-CVQYGMSR.js";
28
+ import {
29
+ withRetry
30
+ } from "./chunk-M5ICYGHT.js";
31
+ import {
32
+ getMachineId
33
+ } from "./chunk-LXQ5FBBD.js";
34
+ import {
35
+ reportedInstallSource
36
+ } from "./chunk-DB3ONZB4.js";
37
+ import {
38
+ canonicalJson
39
+ } from "./chunk-A2A2YTMG.js";
40
+ import {
41
+ writeFileAtomic
42
+ } from "./chunk-T7E75EKA.js";
43
+ import {
44
+ getApiKey,
45
+ getBaseUrl
46
+ } from "./chunk-GPEGOGRJ.js";
47
+ import {
48
+ ACTION_BODY_MAX,
49
+ AGENT_LABELS_BY_TYPE,
50
+ BLOCKED_ACTION_GRANT_TTL_SECONDS,
51
+ DECISION_LINE_MAX,
52
+ FILE_TARGET_TOOLS,
53
+ GATE_MAX_TOOL_INPUTS,
54
+ HOOK_COMPATIBLE_PROFILES,
55
+ HOOK_SOURCES,
56
+ PENDING_COMMAND_PREFIX,
57
+ REPO_AWARE_AGENTS,
58
+ blockedActionIdentity,
59
+ canonicalToolName,
60
+ gateOutputFor,
61
+ hookCompatibleProfileBySource,
62
+ isAutoApprove,
63
+ localRepoKey,
64
+ modeStateFromResponse,
65
+ normalizeRepoRemote,
66
+ pendingCommandEvents,
67
+ redactSecrets,
68
+ redactSecretsDeep,
69
+ resolveAutoResolveOrigin,
70
+ resolvePolicy,
71
+ toToolLookup,
72
+ truncateLine
73
+ } from "./chunk-NQTHFF4W.js";
74
+
75
+ // src/policy.ts
76
+ import { createHash } from "crypto";
77
+ import { existsSync, readFileSync, writeFileSync } from "fs";
78
+ import { join } from "path";
79
+ import { tmpdir } from "os";
80
+ var CACHE_TTL_MS = 60 * 1e3;
81
+ var STALE_CACHE_MAX_MS = 12 * 60 * 60 * 1e3;
82
+ var policyRequestFor = (agentType) => ({
83
+ agent: agentType,
84
+ repoAware: REPO_AWARE_AGENTS.includes(agentType)
85
+ });
86
+ var policyCacheFile = (apiKey, agent, repoAware = false) => {
87
+ const parts = [apiKey, agent, repoAware ? "repoAware" : void 0, "sessionToolAware"].filter(Boolean);
88
+ const hash = createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 12);
89
+ return join(tmpdir(), `pushary-policy-${hash}.json`);
90
+ };
91
+ var fetchPolicy = async (apiKey, agent, repoAware = false, sessionId) => {
92
+ return withRetry(async () => {
93
+ const baseUrl = getBaseUrl();
94
+ const params = new URLSearchParams();
95
+ if (agent) params.set("agent", agent);
96
+ if (repoAware) params.set("repoAware", "1");
97
+ params.set("scopeAware", "1");
98
+ params.set("sessionToolAware", "1");
99
+ if (sessionId) params.set("sessionId", sessionId);
100
+ const query = params.toString();
101
+ const url = query ? `${baseUrl}/api/mcp/policy?${query}` : `${baseUrl}/api/mcp/policy`;
102
+ const response = await fetch(url, {
103
+ headers: { "Authorization": `Bearer ${apiKey}` },
104
+ signal: AbortSignal.timeout(1e4)
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(`Failed to fetch policy: ${response.status}`);
108
+ }
109
+ const raw = await response.json();
110
+ if (!isPolicyConfig(raw)) throw new Error("Invalid policy response");
111
+ return raw;
112
+ }, { maxAttempts: 2 });
113
+ };
114
+ var getPolicy = async (apiKey, expectedVersion, agent, repoAware = false, sessionId) => {
115
+ const path = policyCacheFile(apiKey, agent, repoAware);
116
+ let staleCache = null;
117
+ if (existsSync(path)) {
118
+ try {
119
+ const stat = readFileSync(path, "utf-8");
120
+ const cached = JSON.parse(stat);
121
+ if (!isPolicyConfig(cached)) throw new Error("Corrupted cache");
122
+ const versionStale = expectedVersion != null && (cached._policyVersion ?? null) !== expectedVersion;
123
+ const sessionStale = (cached._sessionId ?? null) !== (sessionId ?? null);
124
+ const age = cached._cachedAt ? Date.now() - cached._cachedAt : 0;
125
+ const ttlFresh = !cached._cachedAt || age < CACHE_TTL_MS;
126
+ if (ttlFresh && !versionStale && !sessionStale) {
127
+ return cached;
128
+ }
129
+ if (!sessionStale && age < STALE_CACHE_MAX_MS) staleCache = cached;
130
+ } catch {
131
+ }
132
+ }
133
+ try {
134
+ const policy = await fetchPolicy(apiKey, agent, repoAware, sessionId);
135
+ try {
136
+ writeFileSync(path, JSON.stringify({ ...policy, _cachedAt: Date.now(), _policyVersion: expectedVersion ?? null, _sessionId: sessionId ?? null }), { encoding: "utf-8", mode: 384 });
137
+ } catch {
138
+ }
139
+ return policy;
140
+ } catch {
141
+ if (staleCache) return staleCache;
142
+ throw new Error("Failed to fetch policy and no cached policy available");
143
+ }
144
+ };
145
+ var MODE_GRACE_MS = 60 * 1e3;
146
+ var modeStateCacheFile = (apiKey) => {
147
+ const hash = createHash("sha256").update(`mode:${apiKey}`).digest("hex").slice(0, 12);
148
+ return join(tmpdir(), `pushary-mode-${hash}.json`);
149
+ };
150
+ var readLastGoodMode = (apiKey) => {
151
+ try {
152
+ const path = modeStateCacheFile(apiKey);
153
+ if (!existsSync(path)) return null;
154
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
155
+ if (!parsed || typeof parsed.at !== "number" || !parsed.state) return null;
156
+ return parsed;
157
+ } catch {
158
+ return null;
159
+ }
160
+ };
161
+ var writeLastGoodMode = (apiKey, state, sessionId) => {
162
+ try {
163
+ const cached = { state, at: Date.now(), ...sessionId ? { sessionId } : {} };
164
+ writeFileSync(modeStateCacheFile(apiKey), JSON.stringify(cached), { encoding: "utf-8", mode: 384 });
165
+ } catch {
166
+ }
167
+ };
168
+ var unknownMode = () => ({
169
+ mode: null,
170
+ kill: false,
171
+ policyVersion: null,
172
+ relayUrl: null,
173
+ trainingConsent: false,
174
+ scope: null,
175
+ degraded: true
176
+ });
177
+ var degradedFallback = (apiKey, authoritative = false) => {
178
+ if (authoritative) return unknownMode();
179
+ const last = readLastGoodMode(apiKey);
180
+ if (last && Date.now() - last.at < MODE_GRACE_MS) {
181
+ return { ...last.state, degraded: false };
182
+ }
183
+ return unknownMode();
184
+ };
185
+ var MODE_REUSE_MS = CACHE_TTL_MS;
186
+ var readReusableMode = (apiKey, sessionId, maxAgeMs) => {
187
+ const last = readLastGoodMode(apiKey);
188
+ if (!last || (last.sessionId ?? null) !== (sessionId ?? null)) return null;
189
+ if (Date.now() - last.at >= maxAgeMs) return null;
190
+ return { ...last.state, degraded: false };
191
+ };
192
+ var fetchModeState = async (apiKey, sessionId, reuseWithinMs = 0, signal) => {
193
+ if (reuseWithinMs > 0) {
194
+ const reusable = readReusableMode(apiKey, sessionId, reuseWithinMs);
195
+ if (reusable) return reusable;
196
+ }
197
+ try {
198
+ const baseUrl = getBaseUrl();
199
+ const url = sessionId ? `${baseUrl}/api/mcp/mode?session=${encodeURIComponent(sessionId)}` : `${baseUrl}/api/mcp/mode`;
200
+ const response = await fetch(url, {
201
+ headers: { "Authorization": `Bearer ${apiKey}` },
202
+ signal: AbortSignal.any([AbortSignal.timeout(3e3), ...signal ? [signal] : []])
203
+ });
204
+ if (!response.ok) {
205
+ void reportClientError({ kind: "http_status", code: `mode_http_${response.status}` });
206
+ return degradedFallback(apiKey, response.status === 401 || response.status === 403);
207
+ }
208
+ const state = modeStateFromResponse(await response.json());
209
+ writeLastGoodMode(apiKey, state, sessionId);
210
+ return state;
211
+ } catch (error) {
212
+ void reportClientError({ kind: clientErrorKindOf(error), code: "mode_fetch_failed" });
213
+ return degradedFallback(apiKey);
214
+ }
215
+ };
216
+ var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
217
+
218
+ // ../hook-mapping/src/describe.ts
219
+ import { isAbsolute, relative, resolve } from "path";
220
+ var hookPrefixes = {
221
+ Bash: (input) => `bash: ${input.command ?? "(no command)"}`,
222
+ PowerShell: (input) => `powershell: ${input.command ?? "(no command)"}`,
223
+ Monitor: (input) => `monitor: ${input.command ?? "(no command)"}`,
224
+ Write: (input) => `write file: ${input.file_path ?? "(unknown path)"}`,
225
+ Edit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
226
+ MultiEdit: (input) => `edit file: ${input.file_path ?? "(unknown path)"}`,
227
+ Read: (input) => `read file: ${input.file_path ?? "(unknown path)"}`,
228
+ // Without an entry here the fallback stringifies the whole tool input, and for
229
+ // ExitPlanMode that input IS the plan: the action line became a truncated dump of
230
+ // the plan markdown. The plan belongs in the body, not the one-line summary.
231
+ ExitPlanMode: () => "finished planning and wants to start coding",
232
+ // Mirrored on PostToolUse since the matcher was widened. Without entries the
233
+ // fallback prints `Task: done` and `WebSearch: done`, which name the tool and
234
+ // drop the only part a person reads: which subagent, and what it looked for.
235
+ Task: (input) => `subagent: ${describeTaskCall(input)}`,
236
+ WebSearch: (input) => `web search: ${firstString(input.query) ?? "(no query)"}`
237
+ };
238
+ var describeTaskCall = (input) => firstString(input.description) ?? firstString(input.subagent_type) ?? "(unnamed)";
239
+ var describeTodoProgress = (todos) => {
240
+ if (!Array.isArray(todos)) return "updated the plan";
241
+ const items = todos.filter((t) => !!t && typeof t === "object");
242
+ const total = items.length;
243
+ if (total === 0) return "updated the plan";
244
+ const done = items.filter((t) => t.status === "completed").length;
245
+ const current = items.find((t) => t.status === "in_progress");
246
+ const label = current ? [current.activeForm, current.content].find((v) => typeof v === "string" && v.trim().length > 0) : void 0;
247
+ if (done >= total) return `finished all ${total} tasks`;
248
+ if (label) return `${label.trim()} (${done}/${total} done)`;
249
+ return `${done}/${total} tasks done`;
250
+ };
251
+ var eventPrefixes = {
252
+ Bash: (input) => `ran: ${String(input.command ?? "")}`,
253
+ PowerShell: (input) => `ran: ${String(input.command ?? "")}`,
254
+ Monitor: (input) => `ran: ${String(input.command ?? "")}`,
255
+ Write: (input) => `wrote: ${input.file_path ?? "unknown"}`,
256
+ Edit: (input) => `edited: ${input.file_path ?? "unknown"}`,
257
+ MultiEdit: (input) => `edited: ${input.file_path ?? "unknown"}`,
258
+ Read: (input) => `read: ${input.file_path ?? "unknown"}`,
259
+ TodoWrite: (input) => describeTodoProgress(input.todos),
260
+ Task: (input) => `ran subagent: ${describeTaskCall(input)}`,
261
+ WebSearch: (input) => `searched: ${firstString(input.query) ?? "the web"}`
262
+ };
263
+ var MCP_TOOL_PREFIX = "mcp__";
264
+ var parseMcpToolName = (toolName) => {
265
+ if (!toolName.startsWith(MCP_TOOL_PREFIX)) return void 0;
266
+ const rest = toolName.slice(MCP_TOOL_PREFIX.length);
267
+ const split = rest.indexOf("__");
268
+ if (split <= 0 || split + 2 >= rest.length) return void 0;
269
+ return { server: rest.slice(0, split), tool: rest.slice(split + 2) };
270
+ };
271
+ var NETWORK_TARGET_KEYS = ["url", "urls", "href", "host", "hosts", "hostname", "domain", "domains"];
272
+ var ABSOLUTE_HTTP_URL = /^https?:\/\//i;
273
+ var HOSTNAME = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+(?::\d{1,5})?$/i;
274
+ var hostOfUrl = (value) => {
275
+ try {
276
+ return new URL(value).host || void 0;
277
+ } catch {
278
+ return void 0;
279
+ }
280
+ };
281
+ var hostOf = (value, bareHostnames) => {
282
+ if (typeof value !== "string") return void 0;
283
+ const trimmed = value.trim();
284
+ if (ABSOLUTE_HTTP_URL.test(trimmed)) return hostOfUrl(trimmed);
285
+ return bareHostnames && HOSTNAME.test(trimmed) ? trimmed : void 0;
286
+ };
287
+ var firstHost = (value, bareHostnames) => Array.isArray(value) ? value.map((entry) => hostOf(entry, bareHostnames)).find((host) => host !== void 0) : hostOf(value, bareHostnames);
288
+ var deriveNetworkTarget = (toolInput) => {
289
+ for (const key of NETWORK_TARGET_KEYS) {
290
+ const host = firstHost(toolInput[key], true);
291
+ if (host) return host;
292
+ }
293
+ return Object.values(toolInput).map((value) => firstHost(value, false)).find((host) => host !== void 0);
294
+ };
295
+ var describeUnbuiltCall = (toolName, toolInput, format) => {
296
+ const mcp = parseMcpToolName(toolName);
297
+ const host = deriveNetworkTarget(toolInput);
298
+ if (mcp) {
299
+ if (format === "event") return `called ${mcp.tool} on ${mcp.server}`;
300
+ return host ? `call ${mcp.tool} on ${mcp.server} to reach ${host}` : `call ${mcp.tool} on ${mcp.server}`;
301
+ }
302
+ if (host) return format === "event" ? `accessed ${host}` : `network access to ${host}`;
303
+ return void 0;
304
+ };
305
+ var EVENT_ACTION_MAX = 120;
306
+ var HOOK_FALLBACK_MAX = 200;
307
+ var UNNAMED_ACTION = "this request";
308
+ var describeToolCall = (toolName, toolInput, format = "hook") => {
309
+ if (!toolName) return "";
310
+ const prefixes = format === "hook" ? hookPrefixes : eventPrefixes;
311
+ const builder = prefixes[toolName];
312
+ const unbuilt = builder ? void 0 : describeUnbuiltCall(toolName, toolInput ?? {}, format);
313
+ const detail = JSON.stringify(toolInput ?? {});
314
+ const raw = builder ? builder(toolInput) : unbuilt ?? (format === "hook" ? detail === "{}" || detail === void 0 ? toolName : `${toolName}: ${detail}` : `${toolName}: done`);
315
+ const redacted = redactSecrets(raw);
316
+ if (format === "event") return redacted.slice(0, EVENT_ACTION_MAX);
317
+ return builder || unbuilt ? redacted : redacted.slice(0, HOOK_FALLBACK_MAX);
318
+ };
319
+ var TOOL_TARGET_MAX_LENGTH = 80;
320
+ var deriveCommandHead = (command) => {
321
+ if (typeof command !== "string") return void 0;
322
+ const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
323
+ return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
324
+ };
325
+ var deriveToolTarget = (toolName, toolInput) => {
326
+ if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor") {
327
+ return deriveCommandHead(toolInput.command);
328
+ }
329
+ if (FILE_TARGET_TOOLS.has(toolName)) {
330
+ const filePath = toolInput.file_path;
331
+ if (typeof filePath !== "string") return void 0;
332
+ const separator = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
333
+ const base = filePath.slice(separator + 1);
334
+ const dot = base.lastIndexOf(".");
335
+ if (dot <= 0) return void 0;
336
+ return base.slice(dot).slice(0, TOOL_TARGET_MAX_LENGTH);
337
+ }
338
+ const mcp = parseMcpToolName(toolName);
339
+ if (mcp) return mcp.server.slice(0, TOOL_TARGET_MAX_LENGTH);
340
+ return deriveNetworkTarget(toolInput)?.slice(0, TOOL_TARGET_MAX_LENGTH);
341
+ };
342
+ var SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun", "npx", "deno", "rake", "make", "just", "task", "turbo"]);
343
+ var RUN_SUBCOMMANDS = /* @__PURE__ */ new Set(["run", "run-script", "exec", "task"]);
344
+ var deriveReceiptCommandHead = (command) => {
345
+ if (typeof command !== "string") return void 0;
346
+ const tokens = command.trim().split(/\s+/);
347
+ const [first, second] = tokens;
348
+ const keep = first && second && SCRIPT_RUNNERS.has(first.toLowerCase()) && RUN_SUBCOMMANDS.has(second.toLowerCase()) ? 3 : 2;
349
+ const head = tokens.slice(0, keep).join(" ");
350
+ return head ? redactSecrets(head).slice(0, TOOL_TARGET_MAX_LENGTH) : void 0;
351
+ };
352
+ var RECEIPT_TARGET_MAX_LENGTH = 256;
353
+ var parseApplyPatchOps = (command) => {
354
+ if (typeof command !== "string") return [];
355
+ const ops = [];
356
+ for (const line of command.split("\n")) {
357
+ const file = line.match(/^\*\*\*\s+(Add|Update|Delete) File:\s+(.+?)\s*$/);
358
+ if (file) {
359
+ ops.push({ op: file[1].toLowerCase(), path: file[2] });
360
+ continue;
361
+ }
362
+ const move = line.match(/^\*\*\*\s+Move to:\s+(.+?)\s*$/);
363
+ if (move) ops.push({ op: "move", path: move[1] });
364
+ }
365
+ return ops;
366
+ };
367
+ var parseApplyPatchFiles = (command) => [...new Set(parseApplyPatchOps(command).map((entry) => entry.path))];
368
+ var OPENCODE_TOOL_NAMES = {
369
+ bash: "Bash",
370
+ // A shell command reaching outside the worktree asks a second time under its
371
+ // own key. It is still the same command, and still Bash.
372
+ external_directory: "Bash",
373
+ edit: "Edit",
374
+ patch: "Edit",
375
+ apply_patch: "Edit",
376
+ write: "Write",
377
+ read: "Read",
378
+ glob: "Glob",
379
+ grep: "Grep",
380
+ webfetch: "WebFetch",
381
+ websearch: "WebSearch",
382
+ task: "Task"
383
+ };
384
+ var PATCH_ARG_KEYS = ["command", "patch", "input"];
385
+ var patchedFilesIn = (input) => [...new Set(PATCH_ARG_KEYS.flatMap((key) => parseApplyPatchFiles(input[key])))];
386
+ var lookupsForInput = (profile, toolName, input, absolute) => {
387
+ const lookup = toToolLookup(profile, toolName, input, absolute);
388
+ if (!lookup.unreadable) return [lookup];
389
+ const files = patchedFilesIn(input);
390
+ if (files.length === 0) return [lookup];
391
+ return files.slice(0, GATE_MAX_TOOL_INPUTS + 1).map((file) => toToolLookup(profile, toolName, { ...input, file_path: absolute(file) }, absolute));
392
+ };
393
+ var compatibleGateInputs = (profile, toolName, toolInputs, cwd) => {
394
+ const absolute = (path) => cwd && !isAbsolute(path) ? resolve(cwd, path) : path;
395
+ const lookups = toolInputs.flatMap((input) => lookupsForInput(profile, toolName, input, absolute));
396
+ if (lookups.length > GATE_MAX_TOOL_INPUTS) return null;
397
+ if (lookups.some((lookup) => lookup.unreadable)) return null;
398
+ return {
399
+ toolName: lookups[0]?.tool ?? canonicalToolName(profile, toolName),
400
+ toolInputs: lookups.map((lookup) => lookup.input)
401
+ };
402
+ };
403
+ var canonicalGateInputs = (source, toolName, toolInputs, cwd) => {
404
+ if (source === "codex" && toolName === "apply_patch" || source === "vscode" && ["apply_patch", "applyPatch", "copilot_applyPatch"].includes(toolName)) {
405
+ const paths = [...new Set(toolInputs.flatMap((input) => parseApplyPatchFiles(input.command ?? input.input ?? input.patch)))];
406
+ if (paths.length > GATE_MAX_TOOL_INPUTS) return null;
407
+ return {
408
+ toolName: "Edit",
409
+ toolInputs: paths.length === 0 ? toolInputs.map((input) => ({ file_path: input.command })) : paths.map((path) => ({ file_path: cwd && !isAbsolute(path) ? resolve(cwd, path) : path }))
410
+ };
411
+ }
412
+ if (source === "opencode") {
413
+ const canonical2 = OPENCODE_TOOL_NAMES[toolName] ?? toolName;
414
+ return {
415
+ toolName: canonical2,
416
+ toolInputs: toolInputs.map((input) => {
417
+ const path = input.file_path;
418
+ return typeof path === "string" && path.length > 0 && cwd && !isAbsolute(path) ? { ...input, file_path: resolve(cwd, path) } : input;
419
+ })
420
+ };
421
+ }
422
+ if (source === "vscode" || source === "cursor") {
423
+ if (source === "vscode" && toolName.startsWith("mcp_pushary_")) return { toolName: `mcp__pushary__${toolName.slice("mcp_pushary_".length)}`, toolInputs };
424
+ const alias = toolName.replace(/^copilot_/, "").toLowerCase().replace(/[^a-z0-9]/g, "");
425
+ const shells = [
426
+ "runterminalcommand",
427
+ "runinterminal",
428
+ "runcommand",
429
+ "terminalcommand",
430
+ "executecommand",
431
+ "runinterminalcommand",
432
+ "bash",
433
+ "shell",
434
+ "terminal",
435
+ "sendtoterminal",
436
+ "powershell",
437
+ "localshell"
438
+ ];
439
+ const canonical2 = shells.includes(alias) ? "Bash" : ["createfile", "write", "createdirectory", "createnewjupyternotebook"].includes(alias) ? "Write" : ["replacestringinfile", "multireplacestringinfile", "replacestring", "multireplacestring", "inserteditintofile", "insertedit", "edit", "editfile", "editfiles", "editnotebookfile", "editnotebook"].includes(alias) ? "Edit" : [
440
+ "readfile",
441
+ "viewimage",
442
+ "listdir",
443
+ "listdirectory",
444
+ "filesearch",
445
+ "findfiles",
446
+ "grepsearch",
447
+ "findtextinfiles",
448
+ "semanticsearch",
449
+ "searchcodebase",
450
+ "searchworkspacesymbols",
451
+ "geterrors",
452
+ "getchangedfiles",
453
+ "readprojectstructure",
454
+ "getnotebooksummary",
455
+ "readnotebookcelloutput",
456
+ "testsearch",
457
+ "findtestfiles",
458
+ "testfailure",
459
+ "getvscodeapi",
460
+ "getterminaloutput",
461
+ "gettaskoutput",
462
+ "terminalselection",
463
+ "terminallastcommand"
464
+ ].includes(alias) ? "Read" : toolName;
465
+ const inputs = toolInputs.flatMap((input) => ["multireplacestringinfile", "multireplacestring"].includes(alias) && Array.isArray(input.replacements) ? input.replacements.filter((item) => !!item && typeof item === "object" && !Array.isArray(item)) : alias === "editfiles" && Array.isArray(input.files) ? input.files.map((file) => typeof file === "string" ? { file_path: file } : file).filter((file) => !!file && typeof file === "object" && !Array.isArray(file)) : [input]);
466
+ if (inputs.length > GATE_MAX_TOOL_INPUTS) return null;
467
+ return { toolName: canonical2, toolInputs: inputs.map((input) => {
468
+ if (canonical2 === "Bash") return { ...input, command: [input.command, input.commandLine, input.command_line, input.cmd, input.script, input.input, input.text].find((value) => typeof value === "string") };
469
+ const path = input.file_path ?? input.filePath ?? input.path;
470
+ return typeof path === "string" ? { ...input, file_path: cwd && !isAbsolute(path) ? resolve(cwd, path) : path } : input;
471
+ }) };
472
+ }
473
+ const profile = hookCompatibleProfileBySource(source);
474
+ if (profile) return compatibleGateInputs(profile, toolName, toolInputs, cwd);
475
+ if (source !== "gemini") return { toolName, toolInputs };
476
+ if (toolName === "run_shell_command") {
477
+ return { toolName: "Bash", toolInputs: toolInputs.map((input) => ({ command: input.command })) };
478
+ }
479
+ const canonical = { write_file: "Write", replace: "Edit", read_file: "Read" }[toolName];
480
+ if (!canonical) return { toolName, toolInputs };
481
+ return {
482
+ toolName: canonical,
483
+ toolInputs: toolInputs.map((input) => {
484
+ const path = [input.file_path, input.absolute_path, input.path].find((value) => typeof value === "string" && value.length > 0);
485
+ return path ? { file_path: cwd && !isAbsolute(path) ? resolve(cwd, path) : path } : {};
486
+ })
487
+ };
488
+ };
489
+ var EXIT_CODE_FAILURE = /^Error: Exit code \d+/;
490
+ var isToolResultError = (toolResult) => {
491
+ try {
492
+ if (typeof toolResult === "string") return EXIT_CODE_FAILURE.test(toolResult);
493
+ if (!toolResult || typeof toolResult !== "object") return false;
494
+ return "error" in toolResult || "is_error" in toolResult;
495
+ } catch {
496
+ return false;
497
+ }
498
+ };
499
+ var toolResultErrorText = (toolResult) => {
500
+ try {
501
+ const raw = typeof toolResult === "string" ? toolResult : toolResult && typeof toolResult === "object" ? String(
502
+ toolResult.error ?? toolResult.stderr ?? ""
503
+ ) : "";
504
+ return redactSecrets(raw);
505
+ } catch {
506
+ return "";
507
+ }
508
+ };
509
+ var relativizeReceiptPath = (filePath, cwd) => {
510
+ if (!cwd || !isAbsolute(filePath)) return filePath;
511
+ return relative(cwd, filePath);
512
+ };
513
+ var deriveReceiptMeta = (toolName, toolInput, toolResult, cwd) => {
514
+ try {
515
+ const ok = !isToolResultError(toolResult);
516
+ if (toolName === "apply_patch") {
517
+ const files = parseApplyPatchFiles(toolInput.command);
518
+ const target = files.length === 1 ? files[0] : toolInput.command;
519
+ if (typeof target !== "string" || !target) return void 0;
520
+ return { kind: "edit", target: redactSecrets(target).slice(0, RECEIPT_TARGET_MAX_LENGTH), ok };
521
+ }
522
+ const canonicalTool = {
523
+ run_shell_command: "Bash",
524
+ write_file: "Write",
525
+ replace: "Edit"
526
+ }[toolName] ?? toolName;
527
+ const canonicalInput = canonicalTool === toolName ? toolInput : {
528
+ ...toolInput,
529
+ file_path: toolInput.file_path ?? toolInput.absolute_path ?? toolInput.path
530
+ };
531
+ if (FILE_TARGET_TOOLS.has(canonicalTool)) {
532
+ const filePath = canonicalInput.file_path;
533
+ if (typeof filePath !== "string" || !filePath) return void 0;
534
+ return {
535
+ kind: canonicalTool === "Write" ? "write" : "edit",
536
+ // A path is an unlikely place for a credential but not an impossible one
537
+ // (a token in a temp directory name), and this field is persisted.
538
+ target: redactSecrets(relativizeReceiptPath(filePath, cwd)).slice(0, RECEIPT_TARGET_MAX_LENGTH),
539
+ ok
540
+ };
541
+ }
542
+ if (canonicalTool === "Bash" || canonicalTool === "PowerShell" || canonicalTool === "Monitor") {
543
+ const head = deriveReceiptCommandHead(canonicalInput.command);
544
+ if (!head) return void 0;
545
+ return {
546
+ kind: head === "git commit" ? "commit" : "bash",
547
+ target: head,
548
+ ok
549
+ };
550
+ }
551
+ return void 0;
552
+ } catch {
553
+ return void 0;
554
+ }
555
+ };
556
+ var scopePathFor = (toolName, toolInput, cwd) => {
557
+ const meta = deriveReceiptMeta(toolName, toolInput, void 0, cwd);
558
+ if (!meta) return void 0;
559
+ return meta.kind === "edit" || meta.kind === "write" ? meta.target : void 0;
560
+ };
561
+ var firstString = (...values) => values.find((v) => typeof v === "string" && v.length > 0);
562
+ var ACTION_BODY_TRUNCATION_MARKER = "\n\u2026 [truncated]";
563
+ var capActionBody = (text) => truncateLine(text, ACTION_BODY_MAX, ACTION_BODY_TRUNCATION_MARKER);
564
+ var rawActionBody = (toolName, toolInput) => {
565
+ if (toolName === "Edit" || toolName === "replace") {
566
+ const before = firstString(toolInput.old_string);
567
+ const after = firstString(toolInput.new_string);
568
+ if (before === void 0 && after === void 0) return firstString(toolInput.diff);
569
+ return `- ${before ?? ""}
570
+ + ${after ?? ""}`;
571
+ }
572
+ if (toolName === "Write" || toolName === "write_file") {
573
+ return firstString(toolInput.content) ?? firstString(toolInput.diff);
574
+ }
575
+ if (toolName === "ExitPlanMode") return firstString(toolInput.plan);
576
+ if (toolName === "Bash" || toolName === "PowerShell" || toolName === "Monitor" || toolName === "run_shell_command" || toolName === "shell" || toolName === "apply_patch") {
577
+ return firstString(toolInput.command);
578
+ }
579
+ return firstString(toolInput.url, toolInput.href);
580
+ };
581
+ var deriveActionBody = (toolName, toolInput) => {
582
+ try {
583
+ const raw = rawActionBody(toolName, toolInput);
584
+ if (!raw) return void 0;
585
+ return capActionBody(redactSecretsDeep(raw));
586
+ } catch {
587
+ return void 0;
588
+ }
589
+ };
590
+ var deriveAction = (toolName, toolInput) => truncateLine(describeToolCall(toolName, toolInput, "hook"), DECISION_LINE_MAX);
591
+ var BLOCKER_BY_MODE = {
592
+ push_only: "Approval required before this runs",
593
+ push_first: "Paused for your approval before continuing",
594
+ escalate: "Escalated to you after the approval timeout"
595
+ };
596
+ var deriveBlocker = (mode, reason) => {
597
+ const text = reason?.replace(/\s+/g, " ").trim() || BLOCKER_BY_MODE[mode] || "Waiting for your approval";
598
+ return truncateLine(text, DECISION_LINE_MAX);
599
+ };
600
+
601
+ // ../hook-mapping/src/map.ts
602
+ import { basename } from "path";
603
+ var AGENT_IDENTITIES = {
604
+ claude: { type: "claude_code", label: AGENT_LABELS_BY_TYPE.claude_code },
605
+ codex: { type: "codex", label: AGENT_LABELS_BY_TYPE.codex },
606
+ gemini: { type: "gemini_cli", label: AGENT_LABELS_BY_TYPE.gemini_cli },
607
+ cursor: { type: "cursor", label: AGENT_LABELS_BY_TYPE.cursor },
608
+ vscode: { type: "vscode", label: AGENT_LABELS_BY_TYPE.vscode },
609
+ opencode: { type: "opencode", label: AGENT_LABELS_BY_TYPE.opencode },
610
+ qwen: { type: "qwen_code", label: AGENT_LABELS_BY_TYPE.qwen_code },
611
+ codebuddy: { type: "codebuddy", label: AGENT_LABELS_BY_TYPE.codebuddy },
612
+ qoder: { type: "qoder", label: AGENT_LABELS_BY_TYPE.qoder },
613
+ droid: { type: "droid", label: AGENT_LABELS_BY_TYPE.droid },
614
+ devin: { type: "devin", label: AGENT_LABELS_BY_TYPE.devin }
615
+ };
616
+ var TEAMMATE_IDLE_ACTION = "A teammate went idle";
617
+ var CLAUDE_MEANINGS = {
618
+ PostToolUse: { kind: "tool", event: "tool_complete" },
619
+ UserPromptSubmit: { kind: "prompt", event: "user_prompt" },
620
+ Stop: { kind: "stop", event: "session_end" },
621
+ SubagentStop: { kind: "subagent_stop", event: "session_end" },
622
+ PreCompact: { kind: "compaction", event: "compaction" },
623
+ PostCompact: { kind: "compaction", event: "compaction" },
624
+ SessionStart: { kind: "session_start", event: "session_start" },
625
+ SessionEnd: { kind: "session_closed", event: "session_closed" },
626
+ SubagentStart: { kind: "session_start", event: "session_start", action: "Subagent started" },
627
+ Notification: { kind: "notification", event: "notification" },
628
+ TeammateIdle: { kind: "teammate_idle", event: "teammate_idle", action: TEAMMATE_IDLE_ACTION },
629
+ StopFailure: { kind: "stop_failure", event: "turn_error" },
630
+ PermissionDenied: { kind: "permission_denied", event: "permission_denied" }
631
+ };
632
+ var compatibleMeanings = (agentType) => {
633
+ const events = HOOK_COMPATIBLE_PROFILES.find((profile) => profile.agentType === agentType)?.events ?? [];
634
+ return Object.fromEntries(events.flatMap((event) => {
635
+ const meaning = CLAUDE_MEANINGS[event];
636
+ return meaning ? [[event, meaning]] : [];
637
+ }));
638
+ };
639
+ var MEANING_BY_HOOK = {
640
+ claude: CLAUDE_MEANINGS,
641
+ qwen: compatibleMeanings("qwen_code"),
642
+ codebuddy: compatibleMeanings("codebuddy"),
643
+ qoder: compatibleMeanings("qoder"),
644
+ droid: compatibleMeanings("droid"),
645
+ devin: compatibleMeanings("devin"),
646
+ codex: {
647
+ PostToolUse: { kind: "tool", event: "tool_complete" },
648
+ UserPromptSubmit: { kind: "prompt", event: "user_prompt" },
649
+ Stop: { kind: "stop", event: "session_end" },
650
+ SessionStart: { kind: "session_start", event: "session_begin", action: "Session started" },
651
+ SessionEnd: { kind: "session_closed", event: "session_closed" },
652
+ // Codex names all four in its own HookEventName enum, and the CLI registers
653
+ // them. A row missing here is the same defect as the dropped Codex
654
+ // SessionEnd: the CLI reports the event, the Mac app forwards the identical
655
+ // envelope, and only the app's copy vanishes.
656
+ SubagentStart: { kind: "session_start", event: "session_begin", action: "Subagent started" },
657
+ SubagentStop: { kind: "subagent_stop", event: "session_end" },
658
+ PreCompact: { kind: "compaction", event: "compaction" },
659
+ PostCompact: { kind: "compaction", event: "compaction" }
660
+ },
661
+ gemini: {
662
+ AfterTool: { kind: "tool", event: "tool_complete" },
663
+ BeforeAgent: { kind: "prompt", event: "user_prompt" },
664
+ AfterAgent: { kind: "stop", event: "session_end" },
665
+ SessionStart: { kind: "session_start", event: "session_begin", action: "Session started" },
666
+ SessionEnd: { kind: "session_closed", event: "session_closed" }
667
+ },
668
+ cursor: {
669
+ afterShellExecution: { kind: "tool", event: "tool_complete" },
670
+ afterFileEdit: { kind: "tool", event: "tool_complete" },
671
+ postToolUse: { kind: "tool", event: "tool_complete" },
672
+ stop: { kind: "stop", event: "session_end" },
673
+ sessionStart: { kind: "session_start", event: "session_begin" },
674
+ sessionEnd: { kind: "session_closed", event: "session_closed" },
675
+ beforeSubmitPrompt: { kind: "prompt", event: "user_prompt" },
676
+ preCompact: { kind: "compaction", event: "compaction" },
677
+ subagentStart: { kind: "session_start", event: "session_begin", action: "Subagent started" },
678
+ subagentStop: { kind: "subagent_stop", event: "notification" }
679
+ },
680
+ vscode: {
681
+ PostToolUse: { kind: "tool", event: "tool_complete" },
682
+ Stop: { kind: "stop", event: "session_end" },
683
+ SessionStart: { kind: "session_start", event: "session_begin" },
684
+ UserPromptSubmit: { kind: "prompt", event: "user_prompt" },
685
+ PreCompact: { kind: "compaction", event: "compaction" },
686
+ SubagentStart: { kind: "session_start", event: "session_begin", action: "Subagent started" },
687
+ SubagentStop: { kind: "subagent_stop", event: "session_end" }
688
+ },
689
+ // OpenCode's own names, because the plugin sends the hook it was called from
690
+ // rather than translating. `session.idle` is the turn boundary — its Stop, not
691
+ // its SessionEnd — and there is no session-start entry on purpose: OpenCode
692
+ // publishes `session.created` for every subagent fork too.
693
+ opencode: {
694
+ "tool.execute.after": { kind: "tool", event: "tool_complete" },
695
+ "chat.message": { kind: "prompt", event: "user_prompt" },
696
+ "session.idle": { kind: "stop", event: "session_end" }
697
+ }
698
+ };
699
+ var TASK_TITLE_MAX_LENGTH = 120;
700
+ var STOP_SUMMARY_MAX_LENGTH = 200;
701
+ var SESSION_END_ACTIONS = {
702
+ clear: "Session cleared",
703
+ logout: "Logged out",
704
+ resume: "Session suspended",
705
+ prompt_input_exit: "Session ended",
706
+ bypass_permissions_disabled: "Session ended",
707
+ other: "Session ended"
708
+ };
709
+ var countArray = (value) => Array.isArray(value) ? value.length : 0;
710
+ var summarizeFinalMessage = (message) => {
711
+ if (typeof message !== "string") return "Session ended";
712
+ const collapsed = message.replace(/\s+/g, " ").trim();
713
+ if (!collapsed) return "Session ended";
714
+ return collapsed.slice(0, STOP_SUMMARY_MAX_LENGTH);
715
+ };
716
+ var summarizeStop = (input) => {
717
+ const running = countArray(input.background_tasks);
718
+ if (running > 0) return `Paused with ${running} background task${running === 1 ? "" : "s"} still running`;
719
+ const crons = countArray(input.session_crons);
720
+ if (crons > 0) return `Idle until a scheduled task wakes it (${crons} pending)`;
721
+ return summarizeFinalMessage(input.last_assistant_message ?? input.prompt_response);
722
+ };
723
+ var subagentStopAction = (agentType) => {
724
+ const name = typeof agentType === "string" ? agentType.replace(/\s+/g, " ").trim() : "";
725
+ return name ? `${name} subagent finished` : "Subagent finished";
726
+ };
727
+ var subagentStopSummary = (input) => {
728
+ const message = input.last_assistant_message ?? input.prompt_response;
729
+ if (typeof message === "string" && message.trim()) return summarizeStop(input);
730
+ return subagentStopAction(input.agent_type);
731
+ };
732
+ var COMPACTION_TRIGGERS = {
733
+ manual: "on request",
734
+ auto: "automatically"
735
+ };
736
+ var compactionAction = (hookEvent, trigger) => {
737
+ const how = typeof trigger === "string" ? COMPACTION_TRIGGERS[trigger] : void 0;
738
+ const base = hookEvent === "PostCompact" ? "Compacted context" : "Compacting context";
739
+ return how ? `${base} ${how}` : base;
740
+ };
741
+ var sessionStartAction = (source) => source === "resume" ? "Session resumed" : "Session started";
742
+ var sessionEndAction = (reason) => typeof reason === "string" && SESSION_END_ACTIONS[reason] || "Session ended";
743
+ var taskTitleFrom = (prompt) => typeof prompt === "string" ? prompt.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
744
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
745
+ var NOTIFICATION_ACTION_MAX_LENGTH = 200;
746
+ var STOP_FAILURE_REASON_MAX_LENGTH = 300;
747
+ var notificationTypeOf = (payload) => asString(payload.notification_type) ?? asString(payload.type);
748
+ var notificationAction = (payload) => {
749
+ const text = asString(payload.message) ?? asString(payload.title);
750
+ const collapsed = redactSecrets(text ?? "").replace(/\s+/g, " ").trim();
751
+ return collapsed ? collapsed.slice(0, NOTIFICATION_ACTION_MAX_LENGTH) : "Notification";
752
+ };
753
+ var STOP_FAILURE_REASONS = {
754
+ rate_limit: "Rate limited by the API",
755
+ overloaded: "The API is overloaded right now",
756
+ authentication_failed: "Authentication failed",
757
+ billing_error: "A billing error stopped the turn"
758
+ };
759
+ var STOP_FAILURE_FALLBACK = "The turn ended on an API error (rate limit, billing, or auth).";
760
+ var describeStopFailure = (error) => {
761
+ const fromString = (value) => {
762
+ const trimmed = value.trim();
763
+ if (!trimmed) return void 0;
764
+ return STOP_FAILURE_REASONS[trimmed] ?? redactSecrets(trimmed).slice(0, STOP_FAILURE_REASON_MAX_LENGTH);
765
+ };
766
+ if (typeof error === "string") {
767
+ const described = fromString(error);
768
+ if (described) return described;
769
+ } else if (error && typeof error === "object") {
770
+ for (const key of ["type", "message", "error", "code"]) {
771
+ const value = error[key];
772
+ if (typeof value === "string") {
773
+ const described = fromString(value);
774
+ if (described) return described;
775
+ }
776
+ }
777
+ }
778
+ return STOP_FAILURE_FALLBACK;
779
+ };
780
+ var permissionDeniedAction = (toolName) => {
781
+ const tool = asString(toolName);
782
+ return tool ? `Auto-mode blocked ${tool}` : "Auto-mode blocked a tool call";
783
+ };
784
+ var sessionIdentityOf = (payload) => {
785
+ const sessionId = asString(payload.session_id ?? payload.sessionId ?? payload.conversation_id);
786
+ const agentId = asString(payload.agent_id);
787
+ if (!agentId) return { sessionId };
788
+ return { sessionId: agentId, parentSessionId: sessionId };
789
+ };
790
+
791
+ // ../hook-mapping/src/codex-rollout.ts
792
+ var CODEX_TURN_CONTEXT_TYPE = "turn_context";
793
+ var CODEX_AUTO_REVIEWER = "auto_review";
794
+ var CODEX_GUARDIAN_THREAD_SOURCE = "guardian_review";
795
+ var CODEX_PLANNED_ACTION_MARKER = "Planned action JSON:\n";
796
+ var CODEX_APPROVAL_REQUEST_END = ">>> APPROVAL REQUEST END";
797
+ var CODEX_SHELL_TOOL = "exec_command";
798
+ var CODEX_SHELL_COMMAND_FLAGS = ["-lc", "-c"];
799
+ var asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
800
+ var parseRecord = (text) => {
801
+ try {
802
+ return asRecord(JSON.parse(text));
803
+ } catch {
804
+ return null;
805
+ }
806
+ };
807
+ var nonEmptyString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
808
+ var isTimestamp = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
809
+ var readCodexTurnContext = (line) => {
810
+ const entry = parseRecord(line);
811
+ if (entry?.type !== CODEX_TURN_CONTEXT_TYPE) return null;
812
+ const payload = asRecord(entry.payload);
813
+ const turnId = nonEmptyString(payload?.turn_id);
814
+ if (!payload || !turnId) return null;
815
+ return {
816
+ turnId,
817
+ approvalsReviewer: payload.approvals_reviewer === CODEX_AUTO_REVIEWER ? "auto_review" : "user",
818
+ ...isTimestamp(entry.timestamp) ? { startedAt: entry.timestamp } : {}
819
+ };
820
+ };
821
+ var isCodexGuardianRolloutHead = (line, sessionId) => {
822
+ if (!sessionId) return false;
823
+ const entry = parseRecord(line);
824
+ const payload = asRecord(entry?.payload);
825
+ return entry?.type === "session_meta" && payload?.parent_thread_id === sessionId && payload.thread_source === CODEX_GUARDIAN_THREAD_SOURCE;
826
+ };
827
+ var readCodexPlannedAction = (message) => {
828
+ const marker = message.lastIndexOf(CODEX_PLANNED_ACTION_MARKER);
829
+ if (marker < 0) return null;
830
+ const body = message.slice(marker + CODEX_PLANNED_ACTION_MARKER.length);
831
+ const end = body.lastIndexOf(CODEX_APPROVAL_REQUEST_END);
832
+ if (end < 0 || body.slice(end + CODEX_APPROVAL_REQUEST_END.length).trim().length > 0) return null;
833
+ return parseRecord(body.slice(0, end).trim());
834
+ };
835
+ var readCodexReviewVerdict = (message) => {
836
+ const verdict = parseRecord(message.trim());
837
+ if (verdict?.outcome !== "allow" && verdict?.outcome !== "deny") return null;
838
+ const rationale = typeof verdict.rationale === "string" ? verdict.rationale.trim() : "";
839
+ return { outcome: verdict.outcome, ...rationale ? { rationale } : {} };
840
+ };
841
+ var scanCodexGuardianReviews = (lines) => {
842
+ const reviews = [];
843
+ let pending = null;
844
+ for (const line of lines) {
845
+ const entry = parseRecord(line.text);
846
+ const payload = asRecord(entry?.payload);
847
+ if (entry?.type !== "event_msg" || typeof payload?.message !== "string") continue;
848
+ if (payload.type === "user_message") {
849
+ const plannedAction = readCodexPlannedAction(payload.message);
850
+ pending = plannedAction ? { offset: line.offset, plannedAction, ...isTimestamp(entry.timestamp) ? { requestedAt: entry.timestamp } : {} } : null;
851
+ continue;
852
+ }
853
+ if (payload.type !== "agent_message" || !pending) continue;
854
+ const verdict = readCodexReviewVerdict(payload.message);
855
+ if (!verdict) continue;
856
+ reviews.push({ ...pending, verdict });
857
+ pending = null;
858
+ }
859
+ return { reviews, ...pending ? { pendingOffset: pending.offset } : {} };
860
+ };
861
+ var codexPlannedShellCommand = (plannedAction) => {
862
+ if (plannedAction?.tool !== CODEX_SHELL_TOOL) return null;
863
+ const argv = plannedAction.command;
864
+ if (!Array.isArray(argv) || argv.length !== 3) return null;
865
+ const [shell, flag, command] = argv;
866
+ if (typeof shell !== "string" || shell.trim().length === 0) return null;
867
+ if (typeof flag !== "string" || !CODEX_SHELL_COMMAND_FLAGS.includes(flag)) return null;
868
+ if (typeof command !== "string" || command.trim().length === 0) return null;
869
+ const cwd = nonEmptyString(plannedAction.cwd);
870
+ return { command, ...cwd ? { cwd } : {} };
871
+ };
872
+ var codexReviewerDenialsSince = (reviews, since) => {
873
+ const sinceMs = Date.parse(since);
874
+ if (!Number.isFinite(sinceMs)) return [];
875
+ return reviews.flatMap((review) => {
876
+ if (review.verdict.outcome !== "deny" || !review.requestedAt) return [];
877
+ if (Date.parse(review.requestedAt) < sinceMs) return [];
878
+ const shell = codexPlannedShellCommand(review.plannedAction);
879
+ if (!shell) return [];
880
+ return [{
881
+ ...shell,
882
+ offset: review.offset,
883
+ requestedAt: review.requestedAt,
884
+ plannedAction: review.plannedAction,
885
+ ...review.verdict.rationale ? { rationale: review.verdict.rationale } : {}
886
+ }];
887
+ });
888
+ };
889
+
890
+ // src/repo.ts
891
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
892
+ import { basename as basename2, dirname, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
893
+ var MAX_PARENT_WALK = 64;
894
+ var GIT_FILE_PREFIX = "gitdir:";
895
+ var findGitDir = (startDir) => {
896
+ let current = resolve2(startDir);
897
+ for (let depth = 0; depth < MAX_PARENT_WALK; depth += 1) {
898
+ const candidate = join2(current, ".git");
899
+ try {
900
+ if (existsSync2(candidate)) {
901
+ if (statSync(candidate).isDirectory()) return candidate;
902
+ const pointer = readFileSync2(candidate, "utf-8").trim();
903
+ if (pointer.startsWith(GIT_FILE_PREFIX)) {
904
+ const target = pointer.slice(GIT_FILE_PREFIX.length).trim();
905
+ return isAbsolute2(target) ? target : resolve2(current, target);
906
+ }
907
+ }
908
+ } catch {
909
+ }
910
+ const parent = dirname(current);
911
+ if (parent === current) return void 0;
912
+ current = parent;
913
+ }
914
+ return void 0;
915
+ };
916
+ var configPathFor = (gitDir) => {
917
+ const index = gitDir.replace(/\\/g, "/").indexOf("/worktrees/");
918
+ return index === -1 ? join2(gitDir, "config") : join2(gitDir.slice(0, index), "config");
919
+ };
920
+ var ORIGIN_SECTION = /^\[remote "origin"\]/;
921
+ var SECTION_HEADER = /^\[/;
922
+ var URL_LINE = /^url\s*=\s*(.+)$/;
923
+ var readOriginRemote = (gitDir) => {
924
+ try {
925
+ const configPath = configPathFor(gitDir);
926
+ if (!existsSync2(configPath)) return void 0;
927
+ let inOrigin = false;
928
+ for (const rawLine of readFileSync2(configPath, "utf-8").split("\n")) {
929
+ const line = rawLine.trim();
930
+ if (SECTION_HEADER.test(line)) {
931
+ inOrigin = ORIGIN_SECTION.test(line);
932
+ continue;
933
+ }
934
+ if (!inOrigin) continue;
935
+ const url = URL_LINE.exec(line);
936
+ if (url) return url[1]?.trim();
937
+ }
938
+ } catch {
939
+ }
940
+ return void 0;
941
+ };
942
+ var deriveRepoKey = (cwd) => {
943
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
944
+ try {
945
+ const gitDir = findGitDir(dir);
946
+ if (gitDir) {
947
+ const remote = readOriginRemote(gitDir);
948
+ const normalized = remote ? normalizeRepoRemote(remote) : void 0;
949
+ if (normalized) return normalized;
950
+ const root = gitDir.endsWith(".git") ? dirname(gitDir) : dir;
951
+ return localRepoKey(basename2(root));
952
+ }
953
+ return localRepoKey(basename2(resolve2(dir)));
954
+ } catch {
955
+ return void 0;
956
+ }
957
+ };
958
+ var repoKeyCache = /* @__PURE__ */ new Map();
959
+ var REPO_KEY_ENV = "PUSHARY_REPO_KEY";
960
+ var repoKeyFor = (cwd) => {
961
+ const override = process.env[REPO_KEY_ENV];
962
+ if (typeof override === "string") {
963
+ const trimmed = override.trim();
964
+ if (trimmed.toLowerCase() === "off") return void 0;
965
+ if (trimmed.length > 0) return trimmed.toLowerCase();
966
+ }
967
+ const dir = cwd && cwd.length > 0 ? cwd : process.cwd();
968
+ if (repoKeyCache.has(dir)) return repoKeyCache.get(dir);
969
+ const derived = deriveRepoKey(dir);
970
+ repoKeyCache.set(dir, derived);
971
+ return derived;
972
+ };
973
+
974
+ // src/usage.ts
975
+ import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, readSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
976
+ import { createHash as createHash2 } from "crypto";
977
+ import { join as join3 } from "path";
978
+ import { tmpdir as tmpdir2 } from "os";
979
+ var DEFAULT_PRICES = [
980
+ { match: "opus-4-1", in: 15, out: 75 },
981
+ { match: "opus-4-0", in: 15, out: 75 },
982
+ { match: "3-opus", in: 15, out: 75 },
983
+ { match: "opus", in: 5, out: 25 },
984
+ { match: "haiku", in: 1, out: 5 },
985
+ { match: "sonnet", in: 3, out: 15 }
986
+ ];
987
+ var FALLBACK_PRICE = { match: "", in: 3, out: 15 };
988
+ var CACHE_WRITE_MULTIPLIER = 1.25;
989
+ var CACHE_READ_MULTIPLIER = 0.1;
990
+ var RECENT_ID_LIMIT = 200;
991
+ var READ_CHUNK_BYTES = 1024 * 1024;
992
+ var isModelPrice = (value) => {
993
+ if (!value || typeof value !== "object") return false;
994
+ const candidate = value;
995
+ return typeof candidate.match === "string" && typeof candidate.in === "number" && typeof candidate.out === "number";
996
+ };
997
+ var priceTable = () => {
998
+ const raw = process.env.PUSHARY_MODEL_PRICING?.trim();
999
+ if (!raw) return DEFAULT_PRICES;
1000
+ try {
1001
+ const parsed = JSON.parse(raw);
1002
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isModelPrice)) return parsed;
1003
+ } catch {
1004
+ }
1005
+ return DEFAULT_PRICES;
1006
+ };
1007
+ var estimateCostUsd = (usage, model) => {
1008
+ const price = priceTable().find((p) => model.includes(p.match)) ?? FALLBACK_PRICE;
1009
+ const perTokenIn = price.in / 1e6;
1010
+ const perTokenOut = price.out / 1e6;
1011
+ return usage.inputTokens * perTokenIn + usage.outputTokens * perTokenOut + usage.cacheCreationTokens * perTokenIn * CACHE_WRITE_MULTIPLIER + usage.cacheReadTokens * perTokenIn * CACHE_READ_MULTIPLIER;
1012
+ };
1013
+ var stateDir = () => process.env.PUSHARY_USAGE_DIR?.trim() || join3(tmpdir2(), "pushary-usage");
1014
+ var stateFile = (sessionId) => join3(stateDir(), sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"));
1015
+ var emptyState = () => ({ offset: 0, tokensIn: 0, tokensOut: 0, costUsd: 0, recentIds: [] });
1016
+ var readState = (path) => {
1017
+ try {
1018
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
1019
+ if (typeof parsed.offset === "number" && parsed.offset >= 0 && typeof parsed.tokensIn === "number" && typeof parsed.tokensOut === "number" && typeof parsed.costUsd === "number" && Array.isArray(parsed.recentIds)) {
1020
+ return {
1021
+ offset: parsed.offset,
1022
+ tokensIn: parsed.tokensIn,
1023
+ tokensOut: parsed.tokensOut,
1024
+ costUsd: parsed.costUsd,
1025
+ recentIds: parsed.recentIds.filter((id) => typeof id === "string")
1026
+ };
1027
+ }
1028
+ } catch {
1029
+ }
1030
+ return emptyState();
1031
+ };
1032
+ var readRange = (path, start, end) => {
1033
+ const fd = openSync(path, "r");
1034
+ try {
1035
+ const chunks = [];
1036
+ let position = start;
1037
+ while (position < end) {
1038
+ const length = Math.min(READ_CHUNK_BYTES, end - position);
1039
+ const buffer = Buffer.alloc(length);
1040
+ const bytesRead = readSync(fd, buffer, 0, length, position);
1041
+ if (bytesRead <= 0) break;
1042
+ chunks.push(buffer.subarray(0, bytesRead));
1043
+ position += bytesRead;
1044
+ }
1045
+ return Buffer.concat(chunks);
1046
+ } finally {
1047
+ closeSync(fd);
1048
+ }
1049
+ };
1050
+ var toCount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
1051
+ var applyLine = (state, line) => {
1052
+ let parsed;
1053
+ try {
1054
+ parsed = JSON.parse(line);
1055
+ } catch {
1056
+ return 0;
1057
+ }
1058
+ if (parsed.type !== "assistant") return 0;
1059
+ const usage = parsed.message?.usage;
1060
+ if (!usage || typeof usage !== "object") return 0;
1061
+ const model = typeof parsed.message?.model === "string" ? parsed.message.model : "";
1062
+ if (model.includes("<synthetic>")) return 0;
1063
+ const id = typeof parsed.message?.id === "string" ? parsed.message.id : null;
1064
+ if (id) {
1065
+ if (state.recentIds.includes(id)) return 0;
1066
+ state.recentIds.push(id);
1067
+ if (state.recentIds.length > RECENT_ID_LIMIT) state.recentIds.splice(0, state.recentIds.length - RECENT_ID_LIMIT);
1068
+ }
1069
+ const messageUsage = {
1070
+ inputTokens: toCount(usage.input_tokens),
1071
+ outputTokens: toCount(usage.output_tokens),
1072
+ cacheCreationTokens: toCount(usage.cache_creation_input_tokens),
1073
+ cacheReadTokens: toCount(usage.cache_read_input_tokens)
1074
+ };
1075
+ const cost = estimateCostUsd(messageUsage, model);
1076
+ state.tokensIn += messageUsage.inputTokens + messageUsage.cacheCreationTokens + messageUsage.cacheReadTokens;
1077
+ state.tokensOut += messageUsage.outputTokens;
1078
+ state.costUsd += cost;
1079
+ return cost;
1080
+ };
1081
+ var USER_PROMPT_TAIL_BYTES = 256 * 1024;
1082
+ var USER_PROMPT_MAX = 2048;
1083
+ var extractUserText = (content) => {
1084
+ if (typeof content === "string") return content.replace(/\s+/g, " ").trim() || void 0;
1085
+ if (Array.isArray(content)) {
1086
+ const text = content.filter((block) => Boolean(block) && typeof block === "object").filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(" ").replace(/\s+/g, " ").trim();
1087
+ return text || void 0;
1088
+ }
1089
+ return void 0;
1090
+ };
1091
+ var readLastUserPrompt = (transcriptPath) => {
1092
+ try {
1093
+ const size = statSync2(transcriptPath).size;
1094
+ const start = Math.max(0, size - USER_PROMPT_TAIL_BYTES);
1095
+ const lines = readRange(transcriptPath, start, size).toString("utf-8").split("\n");
1096
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
1097
+ const line = lines[i].trim();
1098
+ if (!line) continue;
1099
+ let parsed;
1100
+ try {
1101
+ parsed = JSON.parse(line);
1102
+ } catch {
1103
+ continue;
1104
+ }
1105
+ if (parsed.type !== "user") continue;
1106
+ const text = extractUserText(parsed.message?.content);
1107
+ if (text) return text.slice(0, USER_PROMPT_MAX);
1108
+ }
1109
+ return void 0;
1110
+ } catch {
1111
+ return void 0;
1112
+ }
1113
+ };
1114
+ var readNewUsage = (transcriptPath, sessionId) => {
1115
+ try {
1116
+ const file = statSync2(transcriptPath);
1117
+ const size = file.size;
1118
+ const path = stateFile(sessionId);
1119
+ let state = readState(path);
1120
+ if (size < state.offset) state = { ...emptyState(), recentIds: state.recentIds };
1121
+ const startOffset = state.offset;
1122
+ let deltaUsd = 0;
1123
+ if (size > state.offset) {
1124
+ const buffer = readRange(transcriptPath, state.offset, size);
1125
+ const lastNewline = buffer.lastIndexOf(10);
1126
+ if (lastNewline >= 0) {
1127
+ const complete = buffer.subarray(0, lastNewline + 1);
1128
+ for (const line of complete.toString("utf-8").split("\n")) {
1129
+ if (line.trim()) deltaUsd += applyLine(state, line);
1130
+ }
1131
+ state.offset += lastNewline + 1;
1132
+ }
1133
+ }
1134
+ mkdirSync(stateDir(), { recursive: true });
1135
+ writeFileSync2(path, JSON.stringify(state), "utf-8");
1136
+ if (state.tokensIn === 0 && state.tokensOut === 0) return null;
1137
+ return {
1138
+ // Identical cursor-range reports share an identity across transport retries.
1139
+ // Native message identity distinguishes an in-place rotation of the range.
1140
+ // This does not serialize readers of different, potentially overlapping ranges.
1141
+ eventId: createHash2("sha256").update(JSON.stringify([
1142
+ sessionId,
1143
+ file.dev,
1144
+ file.ino,
1145
+ startOffset,
1146
+ state.offset,
1147
+ state.recentIds.at(-1)
1148
+ ])).digest("hex").slice(0, 32),
1149
+ reportedAt: Date.now(),
1150
+ tokensIn: state.tokensIn,
1151
+ tokensOut: state.tokensOut,
1152
+ costUsd: state.costUsd,
1153
+ deltaUsd
1154
+ };
1155
+ } catch {
1156
+ return null;
1157
+ }
1158
+ };
1159
+
1160
+ // src/codex-adapter.ts
1161
+ import { resolve as resolve3 } from "path";
1162
+ var CODEX_AGENT = { type: "codex", label: "Codex" };
1163
+ var codexAllow = () => ({ kind: "allow" });
1164
+ var codexDeny = (reason) => ({ kind: "deny", reason });
1165
+ var codexPass = () => ({ kind: "pass" });
1166
+ var toCodexWire = (event, decision) => {
1167
+ if (decision.kind === "pass") return null;
1168
+ return gateOutputFor(
1169
+ "codex",
1170
+ event,
1171
+ decision.kind,
1172
+ decision.kind === "deny" ? decision.reason : void 0
1173
+ ) ?? null;
1174
+ };
1175
+ var describeApplyPatch = (command) => {
1176
+ const ops = parseApplyPatchOps(command);
1177
+ if (ops.length === 0) return null;
1178
+ const files = [...new Set(ops.map((entry) => entry.path))];
1179
+ if (files.length === 1) {
1180
+ const verb = ops[0].op === "add" ? "create" : ops[0].op === "delete" ? "delete" : ops[0].op === "move" ? "move" : "edit";
1181
+ return `${verb} file: ${files[0]}`;
1182
+ }
1183
+ return `apply patch to ${files.length} files`;
1184
+ };
1185
+ var toPolicyLookup = (toolName, toolInput, cwd) => {
1186
+ if (toolName !== "apply_patch") return { tool: toolName, input: toolInput };
1187
+ const command = toolInput.command;
1188
+ const files = parseApplyPatchFiles(command);
1189
+ if (files.length === 1) {
1190
+ return { tool: "Edit", input: { file_path: cwd ? resolve3(cwd, files[0]) : files[0] } };
1191
+ }
1192
+ return { tool: "Edit", input: typeof command === "string" ? { file_path: command } : {} };
1193
+ };
1194
+ var toPolicyLookups = (toolName, toolInput, cwd) => {
1195
+ if (toolName !== "apply_patch") {
1196
+ const single = toPolicyLookup(toolName, toolInput, cwd);
1197
+ return { tool: single.tool, inputs: [single.input] };
1198
+ }
1199
+ const files = parseApplyPatchFiles(toolInput.command);
1200
+ if (files.length === 0) {
1201
+ const single = toPolicyLookup(toolName, toolInput, cwd);
1202
+ return { tool: single.tool, inputs: [single.input] };
1203
+ }
1204
+ return {
1205
+ tool: "Edit",
1206
+ inputs: files.map((file) => ({ file_path: cwd ? resolve3(cwd, file) : file }))
1207
+ };
1208
+ };
1209
+ var permissionTimeoutDecision = (timeoutAction) => {
1210
+ if (timeoutAction === "approve") return codexAllow();
1211
+ if (timeoutAction === "deny") return codexDeny("No response within timeout");
1212
+ return codexPass();
1213
+ };
1214
+ var preToolUseTimeoutDecision = (timeoutAction, denyReason = "No response within timeout") => timeoutAction === "deny" ? codexDeny(denyReason) : codexPass();
1215
+
1216
+ // src/codex-call-decision.ts
1217
+ import { createHash as createHash3 } from "crypto";
1218
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync, rmSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
1219
+ import { tmpdir as tmpdir3, userInfo } from "os";
1220
+ import { join as join4 } from "path";
1221
+ var CODEX_DECISION_TTL_MS = 60 * 1e3;
1222
+ var userScope = () => {
1223
+ try {
1224
+ const info = userInfo();
1225
+ const id = typeof info.uid === "number" && info.uid >= 0 ? String(info.uid) : info.username;
1226
+ return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || "default";
1227
+ } catch {
1228
+ return "default";
1229
+ }
1230
+ };
1231
+ var decisionDir = () => join4(tmpdir3(), `pushary-codex-decisions-${userScope()}`);
1232
+ var decisionFile = (key) => join4(decisionDir(), key);
1233
+ var PRESENTATION_ONLY_INPUT_KEYS = ["description"];
1234
+ var identifyingToolInput = (toolInput) => {
1235
+ if (!toolInput) return {};
1236
+ const identifying = {};
1237
+ for (const [key, value] of Object.entries(toolInput)) {
1238
+ if (PRESENTATION_ONLY_INPUT_KEYS.includes(key)) continue;
1239
+ identifying[key] = value;
1240
+ }
1241
+ return identifying;
1242
+ };
1243
+ var codexCallKey = (identity) => {
1244
+ if (!identity.sessionId || !identity.turnId || !identity.toolName) return null;
1245
+ const material = canonicalJson([
1246
+ identity.sessionId,
1247
+ identity.turnId,
1248
+ identity.toolName,
1249
+ identifyingToolInput(identity.toolInput)
1250
+ ]);
1251
+ return createHash3("sha256").update(material, "utf-8").digest("hex").slice(0, 32);
1252
+ };
1253
+ var parseDecision = (raw) => {
1254
+ try {
1255
+ const parsed = JSON.parse(raw);
1256
+ if (parsed.kind === "approved") {
1257
+ return typeof parsed.toolUseId === "string" && parsed.toolUseId ? { kind: "approved", toolUseId: parsed.toolUseId } : null;
1258
+ }
1259
+ if (parsed.kind === "handled") return { kind: "handled" };
1260
+ if (parsed.kind === "denied") {
1261
+ return { kind: "denied", reason: typeof parsed.reason === "string" ? parsed.reason : "" };
1262
+ }
1263
+ return null;
1264
+ } catch {
1265
+ return null;
1266
+ }
1267
+ };
1268
+ var sweepExpired = (dir, nowMs) => {
1269
+ try {
1270
+ for (const name of readdirSync(dir)) {
1271
+ const path = join4(dir, name);
1272
+ try {
1273
+ if (nowMs - statSync3(path).mtimeMs >= CODEX_DECISION_TTL_MS) rmSync(path, { force: true });
1274
+ } catch {
1275
+ continue;
1276
+ }
1277
+ }
1278
+ } catch {
1279
+ return;
1280
+ }
1281
+ };
1282
+ var recordCodexCallDecision = (key, decision) => {
1283
+ if (!key) return;
1284
+ try {
1285
+ const dir = decisionDir();
1286
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
1287
+ else sweepExpired(dir, Date.now());
1288
+ writeFileSync3(decisionFile(key), JSON.stringify(decision), { encoding: "utf-8", mode: 384 });
1289
+ } catch {
1290
+ return;
1291
+ }
1292
+ };
1293
+ var consumeCodexCallDecision = (key) => {
1294
+ if (!key) return null;
1295
+ const path = decisionFile(key);
1296
+ try {
1297
+ const fresh = Date.now() - statSync3(path).mtimeMs < CODEX_DECISION_TTL_MS;
1298
+ const raw = readFileSync4(path, "utf-8");
1299
+ rmSync(path, { force: true });
1300
+ return fresh ? parseDecision(raw) : null;
1301
+ } catch {
1302
+ return null;
1303
+ }
1304
+ };
1305
+
1306
+ // src/driver.ts
1307
+ var DRIVER_ENV = "PUSHARY_DRIVER";
1308
+ var PHONE_DRIVER = "phone";
1309
+ var driverFromEnv = (env) => env[DRIVER_ENV] === PHONE_DRIVER ? PHONE_DRIVER : void 0;
1310
+ var phoneDrivenEnv = (base) => ({
1311
+ ...base,
1312
+ [DRIVER_ENV]: PHONE_DRIVER
1313
+ });
1314
+
1315
+ // src/throttle.ts
1316
+ import { join as join5 } from "path";
1317
+ import { tmpdir as tmpdir4 } from "os";
1318
+ import { createHash as createHash4 } from "crypto";
1319
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync4, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
1320
+ var THROTTLE_DIR = join5(tmpdir4(), "pushary-throttle");
1321
+ var markerPath = (key) => join5(THROTTLE_DIR, createHash4("sha256").update(key).digest("hex").slice(0, 16));
1322
+ var claimThrottle = (key, windowMs) => {
1323
+ try {
1324
+ const path = markerPath(key);
1325
+ if (existsSync4(path) && Date.now() - statSync4(path).mtimeMs < windowMs) return "throttled";
1326
+ mkdirSync3(THROTTLE_DIR, { recursive: true });
1327
+ writeFileSync4(path, "", "utf-8");
1328
+ return "passed";
1329
+ } catch {
1330
+ return "unavailable";
1331
+ }
1332
+ };
1333
+ var releaseThrottle = (key) => {
1334
+ rmSync2(markerPath(key), { force: true });
1335
+ };
1336
+
1337
+ // src/blocked-action-hint.ts
1338
+ import { createHash as createHash5 } from "crypto";
1339
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, rmSync as rmSync3, statSync as statSync5, utimesSync } from "fs";
1340
+ import { tmpdir as tmpdir5 } from "os";
1341
+ import { join as join6 } from "path";
1342
+ var BLOCKED_ACTION_HINT_TTL_MS = BLOCKED_ACTION_GRANT_TTL_SECONDS * 1e3;
1343
+ var FINGERPRINT = /^[0-9a-f]{64}$/;
1344
+ var isFingerprint = (value) => typeof value === "string" && FINGERPRINT.test(value);
1345
+ var blockedActionSubject = (source, sessionId, toolName, toolInput, cwd) => {
1346
+ if (source !== "claude") return { source, sessionId, toolName, toolInput };
1347
+ return cwd ? { source, sessionId, cwd, toolName, toolInput } : null;
1348
+ };
1349
+ var blockedActionFingerprint = ({ source = "claude", sessionId, toolName, toolInput, cwd }) => {
1350
+ if (!sessionId || !toolInput) return null;
1351
+ const canonical = canonicalGateInputs(source, toolName, [toolInput], cwd);
1352
+ if (!canonical || canonical.toolInputs.length !== 1) return null;
1353
+ const [canonicalInput] = canonical.toolInputs;
1354
+ if (!canonicalInput) return null;
1355
+ const subject = blockedActionSubject(source, sessionId, canonical.toolName, canonicalInput, cwd);
1356
+ return subject ? createHash5("sha256").update(blockedActionIdentity(subject), "utf8").digest("hex") : null;
1357
+ };
1358
+ var hintDir = () => join6(tmpdir5(), `pushary-blocked-action-hints-${userScope()}`);
1359
+ var hintFile = (fingerprint) => join6(hintDir(), fingerprint);
1360
+ var isExpired = (mtimeMs, nowMs) => nowMs - mtimeMs >= BLOCKED_ACTION_HINT_TTL_MS;
1361
+ var sweepExpired2 = (dir, nowMs) => {
1362
+ try {
1363
+ for (const name of readdirSync2(dir)) {
1364
+ const path = join6(dir, name);
1365
+ try {
1366
+ if (isExpired(statSync5(path).mtimeMs, nowMs)) rmSync3(path, { force: true });
1367
+ } catch {
1368
+ continue;
1369
+ }
1370
+ }
1371
+ } catch {
1372
+ return;
1373
+ }
1374
+ };
1375
+ var recordBlockedActionHint = (fingerprint, approved) => {
1376
+ if (!isFingerprint(fingerprint)) return false;
1377
+ try {
1378
+ const dir = hintDir();
1379
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
1380
+ else sweepExpired2(dir, Date.now());
1381
+ writeFileAtomic(hintFile(fingerprint), JSON.stringify({ fingerprint, ...approved }), 384);
1382
+ return true;
1383
+ } catch {
1384
+ return false;
1385
+ }
1386
+ };
1387
+ var readLiveHint = (fingerprint) => {
1388
+ const path = hintFile(fingerprint);
1389
+ try {
1390
+ const stat = statSync5(path);
1391
+ if (!stat.isFile()) return null;
1392
+ if (isExpired(stat.mtimeMs, Date.now())) {
1393
+ rmSync3(path, { force: true });
1394
+ return null;
1395
+ }
1396
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
1397
+ return parsed?.fingerprint === fingerprint ? parsed : null;
1398
+ } catch {
1399
+ return null;
1400
+ }
1401
+ };
1402
+ var hasBlockedActionHint = (fingerprint) => isFingerprint(fingerprint) && readLiveHint(fingerprint) !== null;
1403
+ var approvalIn = (fingerprint, sessionId) => {
1404
+ const hint = readLiveHint(fingerprint);
1405
+ if (hint?.sessionId !== sessionId || typeof hint.toolName !== "string") return null;
1406
+ return { fingerprint, sessionId, toolName: hint.toolName, reminded: hint.reminded === true };
1407
+ };
1408
+ var approvalAwaitingRetry = (sessionId) => {
1409
+ if (!sessionId) return null;
1410
+ try {
1411
+ for (const name of readdirSync2(hintDir())) {
1412
+ const approval = isFingerprint(name) ? approvalIn(name, sessionId) : null;
1413
+ if (approval) return approval;
1414
+ }
1415
+ return null;
1416
+ } catch {
1417
+ return null;
1418
+ }
1419
+ };
1420
+ var markApprovalReminded = (approval) => {
1421
+ const path = hintFile(approval.fingerprint);
1422
+ try {
1423
+ const { atime, mtime } = statSync5(path);
1424
+ const { fingerprint, sessionId, toolName } = approval;
1425
+ writeFileAtomic(path, JSON.stringify({ fingerprint, sessionId, toolName, reminded: true }), 384);
1426
+ utimesSync(path, atime, mtime);
1427
+ } catch {
1428
+ return;
1429
+ }
1430
+ };
1431
+ var forgetBlockedActionHint = (fingerprint) => {
1432
+ if (!isFingerprint(fingerprint)) return;
1433
+ try {
1434
+ rmSync3(hintFile(fingerprint), { force: true });
1435
+ } catch {
1436
+ return;
1437
+ }
1438
+ };
1439
+
1440
+ // src/background-work.ts
1441
+ import { createHash as createHash6 } from "crypto";
1442
+ import { mkdirSync as mkdirSync5, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
1443
+ import { tmpdir as tmpdir6 } from "os";
1444
+ import { join as join7 } from "path";
1445
+ var BACKGROUND_WORK_TTL_MS = 60 * 60 * 1e3;
1446
+ var backgroundWorkDir = () => join7(tmpdir6(), "pushary-background-work");
1447
+ var markerFor = (sessionKey) => join7(backgroundWorkDir(), createHash6("sha256").update(sessionKey).digest("hex"));
1448
+ var hasRunningBackgroundTasks = (backgroundTasks) => Array.isArray(backgroundTasks) && backgroundTasks.length > 0;
1449
+ var noteBackgroundWork = (sessionKey, running) => {
1450
+ try {
1451
+ if (!running) {
1452
+ rmSync4(markerFor(sessionKey), { force: true });
1453
+ return;
1454
+ }
1455
+ mkdirSync5(backgroundWorkDir(), { recursive: true, mode: 448 });
1456
+ writeFileSync5(markerFor(sessionKey), "", { mode: 384 });
1457
+ } catch {
1458
+ return;
1459
+ }
1460
+ };
1461
+ var isWaitingOnBackgroundWork = (sessionKey, nowMs = Date.now()) => {
1462
+ try {
1463
+ return nowMs - statSync6(markerFor(sessionKey)).mtimeMs < BACKGROUND_WORK_TTL_MS;
1464
+ } catch {
1465
+ return false;
1466
+ }
1467
+ };
1468
+
1469
+ // src/events.ts
1470
+ import { basename as basename3, join as join8 } from "path";
1471
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, statSync as statSync7, writeFileSync as writeFileSync6 } from "fs";
1472
+ import { tmpdir as tmpdir7 } from "os";
1473
+ var INTENT_DIR = join8(tmpdir7(), "pushary-intent");
1474
+ var INTENT_MAX_BYTES = 2048;
1475
+ var INTENT_GRACE_MS = 10 * 60 * 1e3;
1476
+ var sanitizeSession = (sessionId) => sessionId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || DEFAULT_SESSION;
1477
+ var intentFile = (sessionId) => join8(INTENT_DIR, sanitizeSession(sessionId));
1478
+ var saveLastPrompt = (sessionId, prompt) => {
1479
+ try {
1480
+ const trimmed = prompt.replace(/\s+/g, " ").trim().slice(0, INTENT_MAX_BYTES);
1481
+ if (!trimmed) return;
1482
+ mkdirSync6(INTENT_DIR, { recursive: true });
1483
+ writeFileSync6(intentFile(sessionId), trimmed, "utf-8");
1484
+ } catch {
1485
+ }
1486
+ };
1487
+ var readLastPrompt = (sessionId) => {
1488
+ try {
1489
+ const path = intentFile(sessionId);
1490
+ if (isDefaultSession(sessionId) && statSync7(path).mtimeMs < Date.now() - INTENT_GRACE_MS) return void 0;
1491
+ const value = readFileSync6(path, "utf-8").trim();
1492
+ return value.length > 0 ? value : void 0;
1493
+ } catch {
1494
+ return void 0;
1495
+ }
1496
+ };
1497
+ var pendingQuestionsToSettle = (sessionId, toolUseId, insideSubagent = false) => {
1498
+ const settledElsewhere = toolUseId ? listPendingQuestionsForToolUse(sessionId, toolUseId) : [];
1499
+ if (insideSubagent) return settledElsewhere;
1500
+ return [.../* @__PURE__ */ new Set([...settledElsewhere, ...listPendingQuestions(sessionId)])];
1501
+ };
1502
+ var reconcileQuestions = async (sessionId, files, signal) => {
1503
+ const late = [];
1504
+ try {
1505
+ if (files.length === 0) return late;
1506
+ const apiKey = getApiKey();
1507
+ if (signal) signal = AbortSignal.any([signal, AbortSignal.timeout(2e3)]);
1508
+ for (const correlationId of files) {
1509
+ if (signal?.aborted) break;
1510
+ try {
1511
+ const answer = await waitForAnswer(apiKey, correlationId, 1e3, signal);
1512
+ if (answer.answered) {
1513
+ late.push({ correlationId, value: answer.value });
1514
+ continue;
1515
+ }
1516
+ await cancelQuestion(apiKey, correlationId, signal);
1517
+ } catch {
1518
+ continue;
1519
+ }
1520
+ removePendingQuestion(sessionId, correlationId);
1521
+ }
1522
+ } catch {
1523
+ }
1524
+ return late;
1525
+ };
1526
+ var reconcilePendingQuestions = (sessionId, toolUseId, signal) => reconcileQuestions(sessionId, pendingQuestionsToSettle(sessionId, toolUseId), signal);
1527
+ var describeLateAnswer = (value) => {
1528
+ const v = (value ?? "").trim();
1529
+ if (v === "yes") return "approved it";
1530
+ if (v === "no") return "rejected it";
1531
+ if (v === "defer") return "chose to handle it on this machine";
1532
+ return v ? `answered "${v.slice(0, 200)}"` : "responded";
1533
+ };
1534
+ var buildLateAnswerContext = (late) => {
1535
+ if (late.length === 0) return void 0;
1536
+ const parts = late.map((l) => `you ${describeLateAnswer(l.value)} on your phone`);
1537
+ return `Pushary: a permission prompt you were sent was answered late \u2014 ${parts.join("; ")} \u2014 after the terminal already took over. Treat this as the user's latest intent; do not assume the earlier terminal decision matched it.`;
1538
+ };
1539
+ var notifyLateAnswers = async (apiKey, late, agentName, sessionId, signal) => {
1540
+ if (late.length === 0) return;
1541
+ const summary = late.map((l) => describeLateAnswer(l.value)).join(", ");
1542
+ await sendNotification(apiKey, {
1543
+ title: "Answer arrived late",
1544
+ body: `You ${summary} after the agent had already moved on.`,
1545
+ agentName,
1546
+ sessionId
1547
+ }, 2500, signal).catch(() => {
1548
+ });
1549
+ };
1550
+ var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
1551
+ var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
1552
+ var readFreshCachedPolicy = (apiKey, agentType, sessionId) => {
1553
+ const request = policyRequestFor(agentType);
1554
+ const path = policyCacheFile(apiKey, request.agent, request.repoAware);
1555
+ if (!existsSync6(path)) return null;
1556
+ const cached = JSON.parse(readFileSync6(path, "utf-8"));
1557
+ if (!isPolicyConfig(cached)) return null;
1558
+ if ((cached._sessionId ?? null) !== (sessionId ?? null)) return null;
1559
+ if (!cached._cachedAt || Date.now() - cached._cachedAt >= POLICY_CACHE_TTL_MS) return null;
1560
+ return cached;
1561
+ };
1562
+ var autoDecisionEnabled = () => {
1563
+ const flag = process.env.PUSHARY_AUTO_DECISION_BEACON;
1564
+ return flag === "1" || flag === "true";
1565
+ };
1566
+ var deriveDecisionSource = (toolName, toolInput, liveMode, agentType, cwd, sessionId) => {
1567
+ try {
1568
+ if (liveMode.kill) return "terminal";
1569
+ if (!autoDecisionEnabled()) return void 0;
1570
+ const policy = readFreshCachedPolicy(getApiKey(), agentType, sessionId);
1571
+ if (!policy) return void 0;
1572
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd), sessionId);
1573
+ if (isAutoApprove(resolved)) return "policy_auto";
1574
+ if (resolved.mode === "push_only" || resolved.mode === "push_first") return "human";
1575
+ return "terminal";
1576
+ } catch {
1577
+ return void 0;
1578
+ }
1579
+ };
1580
+ var deriveAutoDecisionBeacon = (toolName, toolInput, liveMode, agentType, cwd, sessionId) => {
1581
+ try {
1582
+ if (liveMode.kill) return void 0;
1583
+ if (!autoDecisionEnabled()) return void 0;
1584
+ const policy = readFreshCachedPolicy(getApiKey(), agentType, sessionId);
1585
+ if (!policy) return void 0;
1586
+ const resolved = resolvePolicy(policy, toolName, liveMode.mode, toolInput, cwd, repoKeyFor(cwd), sessionId);
1587
+ if (resolved.timeoutSeconds !== 0 || resolved.timeoutAction !== "approve") return void 0;
1588
+ return {
1589
+ decisionOrigin: resolveAutoResolveOrigin(policy, toolName, toolInput, cwd, repoKeyFor(cwd), sessionId),
1590
+ toolName,
1591
+ toolTarget: deriveToolTarget(toolName, toolInput)
1592
+ };
1593
+ } catch {
1594
+ return void 0;
1595
+ }
1596
+ };
1597
+ var AUTO_DECISION_WINDOW_MS = 15 * 60 * 1e3;
1598
+ var HOOK_ACTIVE_WINDOW_MS = 30 * 60 * 1e3;
1599
+ var deriveUsage = (transcriptPath, sessionId) => {
1600
+ if (!transcriptPath || process.env.PUSHARY_COST_TRACKING === "off") return void 0;
1601
+ try {
1602
+ return readNewUsage(transcriptPath, sessionId || DEFAULT_SESSION) ?? void 0;
1603
+ } catch {
1604
+ return void 0;
1605
+ }
1606
+ };
1607
+ var mergeAdditionalContext = (existing, reported) => {
1608
+ const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
1609
+ if (typeof pendingCommand !== "string" || pendingCommand.trim().length === 0) return existing;
1610
+ const injected = `${PENDING_COMMAND_PREFIX} ${pendingCommand.trim()}`;
1611
+ return existing ? `${existing}
1612
+
1613
+ ${injected}` : injected;
1614
+ };
1615
+ var consumesActivityContext = (agentType) => {
1616
+ const source = HOOK_SOURCES.find((candidate) => AGENT_IDENTITIES[candidate].type === agentType);
1617
+ if (!source) return false;
1618
+ return pendingCommandEvents(source).some((event) => event !== "Stop");
1619
+ };
1620
+ var detectInstallMode = () => (process.argv[1] ?? "").includes("_npx") ? "npx" : "cli";
1621
+ var acknowledgePendingCommand = async (apiKey, baseUrl, commandId, sessionId, machineId, timeoutMs, state = "delivered", signal) => {
1622
+ try {
1623
+ signal?.throwIfAborted();
1624
+ if (timeoutMs <= 0) return false;
1625
+ const response = await fetch(`${baseUrl}/api/agent/command/ack`, {
1626
+ method: "POST",
1627
+ headers: {
1628
+ "Content-Type": "application/json",
1629
+ "Authorization": `Bearer ${apiKey}`
1630
+ },
1631
+ body: JSON.stringify({
1632
+ commandId,
1633
+ sessionId: sessionId ?? null,
1634
+ machineId,
1635
+ state,
1636
+ ...state === "failed" ? { failureCode: "consumer_rejected" } : {}
1637
+ }),
1638
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...signal ? [signal] : []])
1639
+ });
1640
+ return response.ok;
1641
+ } catch {
1642
+ return false;
1643
+ }
1644
+ };
1645
+ var ACK_RESERVE_MS = 1500;
1646
+ var reportEvent = async (event, options = {}) => {
1647
+ const apiKey = options.apiKey ?? getApiKey();
1648
+ const baseUrl = getBaseUrl();
1649
+ const machineId = event.machineId ?? getMachineId();
1650
+ const sessionKey = event.sessionId || DEFAULT_SESSION;
1651
+ const receiptNamespace = `${apiKey}:${machineId}:${event.agentType}`;
1652
+ const clientCanDrain = consumesActivityContext(event.agentType) && options.timeoutMs === void 0 && options.skipDrain !== true;
1653
+ const deadlineMs = options.budgetMs === void 0 ? void 0 : Date.now() + options.budgetMs;
1654
+ const ackReserveMs = deadlineMs === void 0 ? 0 : Math.min(ACK_RESERVE_MS, Math.floor((options.budgetMs ?? 0) * 0.4));
1655
+ const postTimeoutMs = (fallback) => deadlineMs === void 0 ? fallback : Math.max(0, Math.min(fallback, deadlineMs - ackReserveMs - Date.now()));
1656
+ const ackTimeoutMs = (fallback) => deadlineMs === void 0 ? fallback : Math.max(0, Math.min(fallback, deadlineMs - Date.now()));
1657
+ return withRetry(async () => {
1658
+ options.signal?.throwIfAborted();
1659
+ const timeoutMs = postTimeoutMs(options.timeoutMs ?? 1e4);
1660
+ if (timeoutMs <= 0) return null;
1661
+ const storedCommand = clientCanDrain ? readPendingCommand(sessionKey, receiptNamespace) : null;
1662
+ const res = await fetch(`${baseUrl}/api/agent/event`, {
1663
+ method: "POST",
1664
+ headers: {
1665
+ "Content-Type": "application/json",
1666
+ "Authorization": `Bearer ${apiKey}`
1667
+ },
1668
+ body: JSON.stringify({
1669
+ ...event,
1670
+ machineId,
1671
+ // Lets the phone's Stop end this process instead of only denying its next
1672
+ // tool call. Ours alone: never another process, never a parent.
1673
+ pid: event.pid,
1674
+ repoKey: event.repoKey ?? repoKeyFor(),
1675
+ installMode: detectInstallMode(),
1676
+ // How the binary was INVOKED this time and which front door produced the
1677
+ // install are different questions, and only the second is attribution.
1678
+ // A machine installed from the plugin marketplace that later runs
1679
+ // `npx ... doctor` reports npx for that invocation and claude_plugin as
1680
+ // its origin, permanently.
1681
+ installSource: reportedInstallSource(),
1682
+ // Link-back for a phone-spawned session: forward the spawn id the daemon
1683
+ // set in the environment so the server can map spawnId -> sessionId.
1684
+ ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {},
1685
+ ...driverFromEnv(process.env) ? { driver: driverFromEnv(process.env) } : {},
1686
+ canDrainCommand: clientCanDrain && !storedCommand,
1687
+ ackMode: clientCanDrain && !storedCommand,
1688
+ skipCommandDrain: options.skipDrain === true || storedCommand != null
1689
+ }),
1690
+ signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...options.signal ? [options.signal] : []])
1691
+ });
1692
+ if ((options.requireOk || event.usage !== void 0) && !res.ok) throw new Error(`/api/agent/event rejected (${res.status})`);
1693
+ try {
1694
+ const result = await res.json();
1695
+ const withoutPending = () => ({ ...result, pendingCommand: void 0, pendingCommandId: void 0 });
1696
+ if (storedCommand) {
1697
+ const failed = storedCommand.failed === true;
1698
+ const acknowledged = await acknowledgePendingCommand(
1699
+ apiKey,
1700
+ baseUrl,
1701
+ storedCommand.id,
1702
+ event.sessionId,
1703
+ machineId,
1704
+ ackTimeoutMs(options.timeoutMs ?? 1e4),
1705
+ failed ? "failed" : "delivered",
1706
+ options.signal
1707
+ );
1708
+ if (!acknowledged) return withoutPending();
1709
+ if (storedCommand.delivered || failed) {
1710
+ removePendingCommand(sessionKey, storedCommand.id, receiptNamespace);
1711
+ return withoutPending();
1712
+ }
1713
+ if (!markPendingCommandDelivered(sessionKey, storedCommand.id, receiptNamespace)) return withoutPending();
1714
+ return { ...withoutPending(), pendingCommand: storedCommand.text, pendingCommandId: storedCommand.id };
1715
+ }
1716
+ if (typeof result.pendingCommandId === "string" && typeof result.pendingCommand === "string") {
1717
+ const saved = savePendingCommand(sessionKey, {
1718
+ id: result.pendingCommandId,
1719
+ text: result.pendingCommand,
1720
+ expiresAtMs: typeof result.pendingCommandExpiresAtMs === "number" ? result.pendingCommandExpiresAtMs : Date.now() + 36e5,
1721
+ delivered: false
1722
+ }, receiptNamespace);
1723
+ if (!saved) return withoutPending();
1724
+ const acknowledged = await acknowledgePendingCommand(
1725
+ apiKey,
1726
+ baseUrl,
1727
+ result.pendingCommandId,
1728
+ event.sessionId,
1729
+ machineId,
1730
+ ackTimeoutMs(options.timeoutMs ?? 1e4),
1731
+ "delivered",
1732
+ options.signal
1733
+ );
1734
+ if (!acknowledged || !markPendingCommandDelivered(sessionKey, result.pendingCommandId, receiptNamespace)) {
1735
+ return withoutPending();
1736
+ }
1737
+ }
1738
+ return result;
1739
+ } catch {
1740
+ return null;
1741
+ }
1742
+ }, { maxAttempts: options.maxAttempts ?? 2, baseDelayMs: 300 });
1743
+ };
1744
+ var NON_GATED_MIRROR_TOOLS = new Set(POSTTOOLUSE_MIRROR_ONLY_TOOLS);
1745
+ var HOOK_BUDGET_MS = 9e3;
1746
+ var DRAINING_REPORT_BUDGET_MS = 8e3;
1747
+ var settleQuestionsForToolUse = async (sessionId, toolUseId) => {
1748
+ if (!toolUseId) return;
1749
+ const late = await reconcileQuestions(sessionId, listPendingQuestionsForToolUse(sessionId, toolUseId));
1750
+ for (const answer of late) removePendingQuestion(sessionId, answer.correlationId);
1751
+ };
1752
+ var ranInsideSubagent = (input) => sessionIdentityOf(input).parentSessionId !== void 0;
1753
+ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
1754
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
1755
+ const deadline = Date.now() + HOOK_BUDGET_MS;
1756
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
1757
+ try {
1758
+ const projectName = basename3(input.cwd ?? process.cwd());
1759
+ const action = describeToolCall(input.tool_name, input.tool_input, "event");
1760
+ const lookup = toPolicyLookup(input.tool_name, input.tool_input);
1761
+ const toolResult = input.tool_response ?? input.tool_result;
1762
+ const isError = isToolResultError(toolResult);
1763
+ const receiptsEnabled = process.env.PUSHARY_RECEIPTS !== "off";
1764
+ const sessionKey = input.session_id || DEFAULT_SESSION;
1765
+ const insideSubagent = ranInsideSubagent(input);
1766
+ const questionsToSettle = pendingQuestionsToSettle(sessionKey, input.tool_use_id, insideSubagent);
1767
+ const [liveMode, late] = await Promise.all([
1768
+ fetchModeState(getApiKey(), input.session_id, MODE_REUSE_MS, signal),
1769
+ reconcileQuestions(sessionKey, questionsToSettle, signal)
1770
+ ]);
1771
+ let additionalContext;
1772
+ if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
1773
+ additionalContext = buildLateAnswerContext(late);
1774
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
1775
+ }
1776
+ const isMirrorOnly = NON_GATED_MIRROR_TOOLS.has(input.tool_name);
1777
+ const decisionSource = isMirrorOnly ? void 0 : deriveDecisionSource(lookup.tool, lookup.input, liveMode, agent.type, input.cwd, input.session_id);
1778
+ const beacon = !isMirrorOnly && decisionSource === "policy_auto" && claimThrottle(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) === "passed" ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode, agent.type, input.cwd, input.session_id) : void 0;
1779
+ const toolReport = reportEvent({
1780
+ event: isError ? "tool_error" : "tool_complete",
1781
+ occurredAt,
1782
+ toolUseId: input.tool_use_id,
1783
+ agentType: agent.type,
1784
+ agentName: `${agent.label} - ${projectName}`,
1785
+ action,
1786
+ sessionId: input.session_id,
1787
+ error: isError ? toolResultErrorText(toolResult).slice(0, 500) : void 0,
1788
+ decisionSource,
1789
+ ...beacon ? { decisionOrigin: beacon.decisionOrigin, toolName: beacon.toolName, toolTarget: beacon.toolTarget } : {},
1790
+ meta: receiptsEnabled ? deriveReceiptMeta(lookup.tool, lookup.input, toolResult, input.cwd ?? process.cwd()) : void 0,
1791
+ usage: deriveUsage(input.transcript_path, input.session_id)
1792
+ }, {
1793
+ maxAttempts: 1,
1794
+ budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())),
1795
+ signal,
1796
+ skipDrain: insideSubagent
1797
+ });
1798
+ const extraReports = [];
1799
+ if (claimThrottle(`hookactive:${sessionKey}`, HOOK_ACTIVE_WINDOW_MS) === "passed") {
1800
+ extraReports.push(
1801
+ reportEvent(
1802
+ {
1803
+ event: "agent_hook_active",
1804
+ agentType: agent.type,
1805
+ agentName: `${agent.label} - ${projectName}`,
1806
+ sessionId: input.session_id
1807
+ },
1808
+ { maxAttempts: 1, timeoutMs: 800, signal }
1809
+ )
1810
+ );
1811
+ }
1812
+ const [reported] = await Promise.allSettled([toolReport, ...extraReports]);
1813
+ if (consumesActivityContext(agent.type)) {
1814
+ additionalContext = mergeAdditionalContext(additionalContext, reported);
1815
+ }
1816
+ return additionalContext;
1817
+ } catch {
1818
+ return void 0;
1819
+ }
1820
+ };
1821
+ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
1822
+ const deadline = Date.now() + HOOK_BUDGET_MS;
1823
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
1824
+ try {
1825
+ const projectName = basename3(input.cwd ?? process.cwd());
1826
+ const sessionKey = input.session_id || DEFAULT_SESSION;
1827
+ const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
1828
+ const taskTitle = titlesEnabled ? taskTitleFrom(input.prompt) : void 0;
1829
+ if (input.prompt) saveLastPrompt(sessionKey, input.prompt);
1830
+ const late = await reconcilePendingQuestions(sessionKey, void 0, signal);
1831
+ let additionalContext;
1832
+ if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
1833
+ additionalContext = buildLateAnswerContext(late);
1834
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
1835
+ }
1836
+ const [reported] = await Promise.allSettled([
1837
+ reportEvent({
1838
+ event: "user_prompt",
1839
+ agentType: agent.type,
1840
+ agentName: `${agent.label} - ${projectName}`,
1841
+ sessionId: input.session_id,
1842
+ taskTitle
1843
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal })
1844
+ ]);
1845
+ if (consumesActivityContext(agent.type)) {
1846
+ additionalContext = mergeAdditionalContext(additionalContext, reported);
1847
+ }
1848
+ return additionalContext;
1849
+ } catch {
1850
+ return void 0;
1851
+ }
1852
+ };
1853
+ var UNUSED_APPROVAL_INSTRUCTION = "The person allowed, from Pushary, the call auto mode blocked. Send that call again now, exactly as it was with nothing changed, to use their approval. It covers only that exact call.";
1854
+ var phoneInstructionOf = (pendingCommand) => typeof pendingCommand === "string" && pendingCommand.trim().length > 0 ? `The user sent a new instruction from their phone via Pushary: ${pendingCommand.trim()}` : null;
1855
+ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
1856
+ const deadline = Date.now() + HOOK_BUDGET_MS;
1857
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
1858
+ try {
1859
+ const projectName = basename3(input.cwd ?? process.cwd());
1860
+ const sessionKey = input.session_id || DEFAULT_SESSION;
1861
+ noteBackgroundWork(sessionKey, hasRunningBackgroundTasks(input.background_tasks));
1862
+ const late = await reconcilePendingQuestions(sessionKey, void 0, signal);
1863
+ const tasks = [
1864
+ reportEvent({
1865
+ event: "session_end",
1866
+ agentType: agent.type,
1867
+ agentName: `${agent.label} - ${projectName}`,
1868
+ action: summarizeStop(input),
1869
+ ...sessionIdentityOf(input),
1870
+ usage: deriveUsage(input.transcript_path, input.session_id)
1871
+ }, { maxAttempts: 1, budgetMs: Math.min(DRAINING_REPORT_BUDGET_MS, Math.max(0, deadline - Date.now())), signal })
1872
+ ];
1873
+ if (late.length > 0) {
1874
+ tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id, signal));
1875
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
1876
+ }
1877
+ if (!isDefaultSession(sessionKey) && listPendingQuestions(sessionKey).length === 0) removePendingSession(sessionKey);
1878
+ const [reported] = await Promise.allSettled(tasks);
1879
+ const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
1880
+ const unusedApproval = !input.stop_hook_active && approvalAwaitingRetry(input.session_id) !== null;
1881
+ const instructions = [unusedApproval ? UNUSED_APPROVAL_INSTRUCTION : null, phoneInstructionOf(pendingCommand)].filter((instruction) => instruction !== null);
1882
+ return instructions.length > 0 ? { decision: "block", reason: instructions.join("\n\n") } : void 0;
1883
+ } catch {
1884
+ return void 0;
1885
+ }
1886
+ };
1887
+ var isSubagentStop = (input) => input.hook_event_name === "SubagentStop" || typeof input.agent_id === "string" && input.agent_id.length > 0;
1888
+ var SUBAGENT_STOP_REPORT_TIMEOUT_MS = 4e3;
1889
+ var handleSubagentStop = async (input, agent = CLAUDE_CODE_AGENT) => {
1890
+ try {
1891
+ const projectName = basename3(input.cwd ?? process.cwd());
1892
+ await reportEvent({
1893
+ event: "session_end",
1894
+ agentType: agent.type,
1895
+ agentName: `${agent.label} - ${projectName}`,
1896
+ action: subagentStopSummary(input),
1897
+ ...sessionIdentityOf(input)
1898
+ // A subagent's end is a `session_end` like any other as far as the route is
1899
+ // concerned, and this caller discards the response, so without this the
1900
+ // parent's queued instruction is popped and thrown away.
1901
+ }, { maxAttempts: 1, timeoutMs: SUBAGENT_STOP_REPORT_TIMEOUT_MS, skipDrain: true });
1902
+ } catch {
1903
+ }
1904
+ };
1905
+ var isCompactionEvent = (hookEventName) => hookEventName === "PreCompact" || hookEventName === "PostCompact";
1906
+ var COMPACTION_REPORT_TIMEOUT_MS = 4e3;
1907
+ var handleCompaction = async (input, agent = CLAUDE_CODE_AGENT) => {
1908
+ try {
1909
+ const projectName = basename3(input.cwd ?? process.cwd());
1910
+ await reportEvent({
1911
+ event: "compaction",
1912
+ agentType: agent.type,
1913
+ agentName: `${agent.label} - ${projectName}`,
1914
+ action: compactionAction(input.hook_event_name, input.trigger),
1915
+ ...sessionIdentityOf(input)
1916
+ }, { maxAttempts: 1, timeoutMs: COMPACTION_REPORT_TIMEOUT_MS });
1917
+ } catch {
1918
+ }
1919
+ };
1920
+ var handleSessionStart = async (input, agent = CLAUDE_CODE_AGENT) => {
1921
+ try {
1922
+ const projectName = basename3(input.cwd ?? process.cwd());
1923
+ const action = input.agent_id ? "Subagent started" : sessionStartAction(input.source);
1924
+ await reportEvent({
1925
+ event: "session_start",
1926
+ agentType: agent.type,
1927
+ agentName: `${agent.label} - ${projectName}`,
1928
+ action,
1929
+ ...sessionIdentityOf(input)
1930
+ }, { maxAttempts: 2, timeoutMs: 4e3 });
1931
+ } catch {
1932
+ }
1933
+ };
1934
+ var handleSessionEnd = async (input, agent = CLAUDE_CODE_AGENT, options = {}) => {
1935
+ try {
1936
+ const projectName = basename3(input.cwd ?? process.cwd());
1937
+ const action = sessionEndAction(input.reason);
1938
+ const sessionKey = input.session_id || DEFAULT_SESSION;
1939
+ if (options.reconcile ?? true) {
1940
+ const late = await reconcilePendingQuestions(sessionKey);
1941
+ if (late.length > 0) {
1942
+ await notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id).catch(() => {
1943
+ });
1944
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
1945
+ }
1946
+ }
1947
+ if (!isDefaultSession(sessionKey)) removePendingSession(sessionKey);
1948
+ if (options.reportClosed === false) return;
1949
+ await reportEvent({
1950
+ event: "session_closed",
1951
+ agentType: agent.type,
1952
+ agentName: `${agent.label} - ${projectName}`,
1953
+ action,
1954
+ sessionId: input.session_id
1955
+ }, { maxAttempts: options.reportAttempts ?? 2, timeoutMs: options.reportTimeoutMs ?? 3e3 });
1956
+ } catch {
1957
+ }
1958
+ };
1959
+ var NOTIFICATION_THROTTLE_MS = 60 * 1e3;
1960
+ var pushesNotification = (notifType, sessionKey) => NOTIFICATION_PUSH_TYPES.has(notifType) && !(notifType === IDLE_PROMPT_NOTIFICATION && isWaitingOnBackgroundWork(sessionKey));
1961
+ var handleNotification = async (input, agent = CLAUDE_CODE_AGENT) => {
1962
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
1963
+ try {
1964
+ const projectName = basename3(input.cwd ?? process.cwd());
1965
+ const notifType = notificationTypeOf(input) ?? "";
1966
+ const sessionKey = input.session_id || DEFAULT_SESSION;
1967
+ const teammateIdle = input.hook_event_name === "TeammateIdle";
1968
+ const action = teammateIdle ? TEAMMATE_IDLE_ACTION : notificationAction(input);
1969
+ const failed = !teammateIdle && notifType === "error";
1970
+ await reportEvent({
1971
+ event: teammateIdle ? "teammate_idle" : failed ? "error" : "notification",
1972
+ agentType: agent.type,
1973
+ agentName: `${agent.label} - ${projectName}`,
1974
+ action,
1975
+ sessionId: input.session_id,
1976
+ error: failed ? action : void 0
1977
+ // `timeoutMs` is not decoration. It is this file's way of saying "side
1978
+ // report", and it sets `clientCanDrain` false. Without it this call
1979
+ // advertises itself as able to consume a phone-queued instruction, and the
1980
+ // stored-command replay inside `reportEvent` hands one back, acknowledges it
1981
+ // as delivered and marks the local receipt delivered, to a caller that
1982
+ // discards the return value. The notification hook prints nothing, so the
1983
+ // instruction is destroyed rather than delayed. Hoisting this report out of
1984
+ // the else-branch put every notification type on that path, not just the
1985
+ // quiet ones.
1986
+ //
1987
+ // 2.5s rather than the 4s a side report usually gets: on the push branch
1988
+ // this is followed by a ~3s `fetchModeState` and a 4s `sendNotification`,
1989
+ // and all three have to fit inside Claude Code's 10s Notification budget.
1990
+ }, { maxAttempts: 1, timeoutMs: 2500, signal }).catch(() => {
1991
+ });
1992
+ if (!teammateIdle && pushesNotification(notifType, sessionKey)) {
1993
+ if (claimThrottle(`notify:${sessionKey}:${notifType}`, NOTIFICATION_THROTTLE_MS) !== "passed") return;
1994
+ let apiKey;
1995
+ try {
1996
+ apiKey = getApiKey();
1997
+ } catch {
1998
+ return;
1999
+ }
2000
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS, signal).catch(() => null);
2001
+ if (mode?.kill || mode?.mode === "terminal_only") return;
2002
+ await sendNotification(apiKey, {
2003
+ title: `${agent.label} is waiting`,
2004
+ body: input.message ? action.slice(0, 200) : "Your agent is waiting for you.",
2005
+ agentName: `${agent.label} - ${projectName}`,
2006
+ sessionId: input.session_id,
2007
+ // Makes this an `update` server-side, so resolveSiteMode gates it on
2008
+ // the wildcard policy too. `info` because nothing finished.
2009
+ context: { type: "info" }
2010
+ }, 4e3, signal).catch(() => {
2011
+ });
2012
+ }
2013
+ } catch {
2014
+ }
2015
+ };
2016
+ var handleStopFailure = async (input, agent = CLAUDE_CODE_AGENT) => {
2017
+ const signal = AbortSignal.timeout(HOOK_BUDGET_MS);
2018
+ try {
2019
+ let apiKey;
2020
+ try {
2021
+ apiKey = getApiKey();
2022
+ } catch {
2023
+ return;
2024
+ }
2025
+ const projectName = basename3(input.cwd ?? process.cwd());
2026
+ const reason = describeStopFailure(input.error);
2027
+ await reportEvent({
2028
+ event: "turn_error",
2029
+ agentType: agent.type,
2030
+ agentName: `${agent.label} - ${projectName}`,
2031
+ action: reason,
2032
+ error: reason,
2033
+ sessionId: input.session_id
2034
+ // Same reason as the Notification report above, and the same budget shape:
2035
+ // this is followed by a ~3s fetchModeState and a 4s sendNotification inside
2036
+ // Claude Code's 10s StopFailure budget. The stopfailure hook prints nothing,
2037
+ // so without this the phone's queued instruction is taken and destroyed.
2038
+ }, { maxAttempts: 1, timeoutMs: 2500, signal }).catch(() => {
2039
+ });
2040
+ const mode = await fetchModeState(apiKey, input.session_id, MODE_REUSE_MS, signal).catch(() => null);
2041
+ if (mode?.kill || mode?.mode === "terminal_only") return;
2042
+ await sendNotification(apiKey, {
2043
+ title: `${agent.label} stopped on an error`,
2044
+ body: reason,
2045
+ agentName: `${agent.label} - ${projectName}`,
2046
+ sessionId: input.session_id,
2047
+ context: { type: "error", errorMessage: reason }
2048
+ }, 4e3, signal).catch(() => {
2049
+ });
2050
+ } catch {
2051
+ }
2052
+ };
2053
+
2054
+ export {
2055
+ policyRequestFor,
2056
+ getPolicy,
2057
+ fetchModeState,
2058
+ fetchModeOverride,
2059
+ phoneDrivenEnv,
2060
+ HOOK_FALLBACK_MAX,
2061
+ UNNAMED_ACTION,
2062
+ describeToolCall,
2063
+ deriveToolTarget,
2064
+ parseApplyPatchFiles,
2065
+ OPENCODE_TOOL_NAMES,
2066
+ canonicalGateInputs,
2067
+ scopePathFor,
2068
+ deriveActionBody,
2069
+ deriveAction,
2070
+ deriveBlocker,
2071
+ permissionDeniedAction,
2072
+ sessionIdentityOf,
2073
+ readCodexTurnContext,
2074
+ isCodexGuardianRolloutHead,
2075
+ scanCodexGuardianReviews,
2076
+ codexReviewerDenialsSince,
2077
+ repoKeyFor,
2078
+ claimThrottle,
2079
+ releaseThrottle,
2080
+ readLastUserPrompt,
2081
+ CODEX_AGENT,
2082
+ codexAllow,
2083
+ codexDeny,
2084
+ codexPass,
2085
+ toCodexWire,
2086
+ describeApplyPatch,
2087
+ toPolicyLookup,
2088
+ toPolicyLookups,
2089
+ permissionTimeoutDecision,
2090
+ preToolUseTimeoutDecision,
2091
+ userScope,
2092
+ codexCallKey,
2093
+ recordCodexCallDecision,
2094
+ consumeCodexCallDecision,
2095
+ blockedActionFingerprint,
2096
+ recordBlockedActionHint,
2097
+ hasBlockedActionHint,
2098
+ approvalAwaitingRetry,
2099
+ markApprovalReminded,
2100
+ forgetBlockedActionHint,
2101
+ readLastPrompt,
2102
+ CLAUDE_CODE_AGENT,
2103
+ AUTO_DECISION_WINDOW_MS,
2104
+ reportEvent,
2105
+ settleQuestionsForToolUse,
2106
+ handlePostToolUse,
2107
+ handleUserPrompt,
2108
+ handleStop,
2109
+ isSubagentStop,
2110
+ handleSubagentStop,
2111
+ isCompactionEvent,
2112
+ handleCompaction,
2113
+ handleSessionStart,
2114
+ handleSessionEnd,
2115
+ handleNotification,
2116
+ handleStopFailure
2117
+ };