@pushary/agent-hooks 0.95.8 → 0.98.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 (82) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/bin/pushary-bell-hook.js +19 -21
  3. package/dist/bin/pushary-bell.js +6 -8
  4. package/dist/bin/pushary-claude.js +21 -23
  5. package/dist/bin/pushary-clean.js +19 -22
  6. package/dist/bin/pushary-codex-bridge.js +13 -14
  7. package/dist/bin/pushary-codex-hook.js +452 -104
  8. package/dist/bin/pushary-codex.js +8 -8
  9. package/dist/bin/pushary-connect.js +6 -6
  10. package/dist/bin/pushary-cowork.js +3 -3
  11. package/dist/bin/pushary-daemon.js +25 -27
  12. package/dist/bin/pushary-disconnect.js +6 -8
  13. package/dist/bin/pushary-doctor.js +28 -23
  14. package/dist/bin/pushary-elicitation-hook.js +19 -21
  15. package/dist/bin/pushary-gemini-bridge.js +11 -12
  16. package/dist/bin/pushary-gemini-hook.js +22 -24
  17. package/dist/bin/pushary-hook.js +24 -26
  18. package/dist/bin/pushary-login.js +7 -8
  19. package/dist/bin/pushary-logout.js +4 -4
  20. package/dist/bin/pushary-mode.js +3 -3
  21. package/dist/bin/pushary-notification-hook.js +21 -23
  22. package/dist/bin/pushary-opencode-hook.js +22 -24
  23. package/dist/bin/pushary-permission-denied-hook.js +28 -27
  24. package/dist/bin/pushary-permission-hook.js +24 -26
  25. package/dist/bin/pushary-post-hook.js +21 -23
  26. package/dist/bin/pushary-prompt-hook.js +21 -23
  27. package/dist/bin/pushary-session-end-hook.js +21 -23
  28. package/dist/bin/pushary-session-start-hook.js +24 -26
  29. package/dist/bin/pushary-setup.js +45 -40
  30. package/dist/bin/pushary-stats.js +2 -2
  31. package/dist/bin/pushary-status.js +10 -11
  32. package/dist/bin/pushary-stop-hook.js +21 -23
  33. package/dist/bin/pushary-stopfailure-hook.js +21 -23
  34. package/dist/bin/pushary-transcript-register.js +17 -19
  35. package/dist/bin/pushary-transcripts.js +5 -5
  36. package/dist/bin/pushary-upgrade.js +17 -20
  37. package/dist/bin/pushary-wait.js +3 -3
  38. package/dist/{chunk-GOPIMNVC.js → chunk-2GLOHBYB.js} +1 -1
  39. package/dist/{chunk-BQL7UDIB.js → chunk-3RVCQDC4.js} +1 -1
  40. package/dist/chunk-53QCTSWY.js +202 -0
  41. package/dist/{chunk-3YFJGKJQ.js → chunk-5B5QV3KC.js} +8 -8
  42. package/dist/{chunk-FRXZMCVA.js → chunk-5KLAETSU.js} +1 -1
  43. package/dist/{chunk-SDNTRS76.js → chunk-6C6MS57P.js} +1 -1
  44. package/dist/{chunk-A22VQ5ZL.js → chunk-6CYB77VD.js} +111 -55
  45. package/dist/{chunk-QD73WATQ.js → chunk-6LRGZJFB.js} +21 -21
  46. package/dist/{chunk-Z4JVEL7R.js → chunk-A76GNRIK.js} +191 -2
  47. package/dist/{chunk-RE25RJ5A.js → chunk-B35OCFXH.js} +4 -4
  48. package/dist/{chunk-UKMLSQXA.js → chunk-CP34FI4B.js} +2 -2
  49. package/dist/{chunk-I7LKI4WO.js → chunk-DOD3A6EU.js} +6 -8
  50. package/dist/{chunk-N6FAXQDD.js → chunk-FAJ4R7IB.js} +1 -1
  51. package/dist/{chunk-U5RIXR7I.js → chunk-HB2KB7QL.js} +1 -1
  52. package/dist/{chunk-QV266EGS.js → chunk-HFQ7WSHZ.js} +11 -14
  53. package/dist/{chunk-QJPYJVL3.js → chunk-IRLTW6KF.js} +1 -1
  54. package/dist/{chunk-Y6SCMXR6.js → chunk-J44RPHUZ.js} +2 -2
  55. package/dist/{chunk-R6LBGKWQ.js → chunk-JQU6PMH4.js} +17 -15
  56. package/dist/{chunk-4DUEIUUO.js → chunk-K6CH5NVW.js} +1 -1
  57. package/dist/{chunk-XZWUOCJZ.js → chunk-LBNAGGLV.js} +1 -1
  58. package/dist/{chunk-XS33QBJA.js → chunk-LGXUGYUH.js} +50 -1
  59. package/dist/{chunk-XO3JR3OP.js → chunk-LX4NNYII.js} +2 -2
  60. package/dist/{chunk-XEVOPOKT.js → chunk-M4TVEMLH.js} +1 -1
  61. package/dist/{chunk-TY6IJY6I.js → chunk-NIXVMMJM.js} +1 -1
  62. package/dist/{chunk-VERXHIQF.js → chunk-NQGYPTJQ.js} +1 -1
  63. package/dist/{chunk-EMGGUO2Q.js → chunk-OBYC6SN4.js} +1 -1
  64. package/dist/{chunk-XUTSZYYJ.js → chunk-OPR2BPD4.js} +1 -4
  65. package/dist/{chunk-MC5WUOJJ.js → chunk-OUAE3GKO.js} +75 -1
  66. package/dist/{chunk-ATKGKAP6.js → chunk-QXIF6R4P.js} +16 -13
  67. package/dist/chunk-SI7GYBQP.js +15 -0
  68. package/dist/{chunk-VMQU7YEJ.js → chunk-SJLGOVC6.js} +1 -1
  69. package/dist/{chunk-54W7ELDQ.js → chunk-TZHDKRMG.js} +2 -2
  70. package/dist/{chunk-F4TMY6XI.js → chunk-V47GITZV.js} +3 -3
  71. package/dist/{chunk-L7NOG547.js → chunk-VZD2GEKB.js} +1 -1
  72. package/dist/{chunk-R3OZ5ISU.js → chunk-W2MG2P46.js} +4 -4
  73. package/dist/{chunk-E47WV7BJ.js → chunk-WU6LSXJ6.js} +2 -2
  74. package/dist/{chunk-NOVA7KZ2.js → chunk-XL3RKJYQ.js} +308 -35
  75. package/dist/{chunk-5GKNAQTG.js → chunk-XOOWC62T.js} +1 -1
  76. package/dist/{chunk-SKPXQRKN.js → chunk-XPPXOKIG.js} +1 -1
  77. package/dist/{chunk-HUGIKB5O.js → chunk-YJDRHQLG.js} +6 -6
  78. package/dist/src/index.js +11 -11
  79. package/package.json +1 -1
  80. package/dist/chunk-3AJIK5AO.js +0 -57
  81. package/dist/chunk-L4ZCFMWW.js +0 -177
  82. package/dist/chunk-VDI645ZX.js +0 -18
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reapplyAllAgents
4
- } from "../chunk-R6LBGKWQ.js";
5
- import "../chunk-4DUEIUUO.js";
4
+ } from "../chunk-JQU6PMH4.js";
5
+ import "../chunk-K6CH5NVW.js";
6
6
  import {
7
7
  waitForMachineReadiness
8
- } from "../chunk-RE25RJ5A.js";
9
- import "../chunk-ATKGKAP6.js";
8
+ } from "../chunk-B35OCFXH.js";
9
+ import "../chunk-QXIF6R4P.js";
10
10
  import {
11
11
  detectProviderLiveCapabilities,
12
12
  hasSpawnProvider,
13
13
  resolveControlPolicy
14
- } from "../chunk-54W7ELDQ.js";
15
- import "../chunk-QD73WATQ.js";
14
+ } from "../chunk-TZHDKRMG.js";
15
+ import "../chunk-6LRGZJFB.js";
16
16
  import {
17
17
  daemonHasRemoteProof,
18
18
  daemonServiceManagerAvailable,
@@ -23,26 +23,23 @@ import {
23
23
  replaceDaemonService,
24
24
  stopDaemonService,
25
25
  waitForDaemonRemoteProof
26
- } from "../chunk-I7LKI4WO.js";
27
- import "../chunk-QV266EGS.js";
28
- import "../chunk-3AJIK5AO.js";
26
+ } from "../chunk-DOD3A6EU.js";
27
+ import "../chunk-HFQ7WSHZ.js";
29
28
  import {
30
29
  needsShell
31
30
  } from "../chunk-XHKBHWLX.js";
32
- import {
33
- execNpm,
34
- npmErrorMessage,
35
- resolveGlobalBinary
36
- } from "../chunk-L4ZCFMWW.js";
37
- import "../chunk-XUTSZYYJ.js";
38
- import "../chunk-E47WV7BJ.js";
31
+ import "../chunk-OPR2BPD4.js";
32
+ import "../chunk-WU6LSXJ6.js";
39
33
  import {
40
34
  claudeJson,
41
35
  codexConfigToml,
42
36
  cursorPluginDir,
37
+ execNpm,
43
38
  geminiSettings,
39
+ npmErrorMessage,
40
+ resolveGlobalBinary,
44
41
  vscodePluginDir
45
- } from "../chunk-NOVA7KZ2.js";
42
+ } from "../chunk-XL3RKJYQ.js";
46
43
  import {
47
44
  exitOnHelpFlag
48
45
  } from "../chunk-GOQHN2UH.js";
@@ -52,13 +49,13 @@ import {
52
49
  } from "../chunk-DINDL2CF.js";
53
50
  import {
54
51
  readControlMode
55
- } from "../chunk-VMQU7YEJ.js";
52
+ } from "../chunk-SJLGOVC6.js";
56
53
  import "../chunk-KZERVKTD.js";
57
- import "../chunk-L7NOG547.js";
54
+ import "../chunk-VZD2GEKB.js";
58
55
  import {
59
56
  getApiKey
60
57
  } from "../chunk-2UMNXADU.js";
61
- import "../chunk-MC5WUOJJ.js";
58
+ import "../chunk-OUAE3GKO.js";
62
59
 
63
60
  // bin/pushary-upgrade.ts
64
61
  import { existsSync } from "fs";
@@ -4,8 +4,8 @@ import {
4
4
  } from "../chunk-CVWG2N3Y.js";
5
5
  import {
6
6
  exitOnFailedResponse
7
- } from "../chunk-SDNTRS76.js";
8
- import "../chunk-BQL7UDIB.js";
7
+ } from "../chunk-6C6MS57P.js";
8
+ import "../chunk-3RVCQDC4.js";
9
9
  import {
10
10
  createIo
11
11
  } from "../chunk-5RUIFDTP.js";
@@ -28,7 +28,7 @@ import {
28
28
  getApiKey,
29
29
  getBaseUrl
30
30
  } from "../chunk-2UMNXADU.js";
31
- import "../chunk-MC5WUOJJ.js";
31
+ import "../chunk-OUAE3GKO.js";
32
32
 
33
33
  // bin/pushary-wait.ts
34
34
  exitOnHelpFlag("wait");
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  guardBinary
3
- } from "./chunk-3AJIK5AO.js";
3
+ } from "./chunk-XL3RKJYQ.js";
4
4
 
5
5
  // src/bell/install.ts
6
6
  import { join } from "path";
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  isValidApiKey,
15
15
  normalizeKeyScope
16
- } from "./chunk-MC5WUOJJ.js";
16
+ } from "./chunk-OUAE3GKO.js";
17
17
 
18
18
  // src/session.ts
19
19
  var samePusharyAccount = (left, right) => left.kind === "ok" && right.kind === "ok" && left.identity.workspaceId !== "" && left.identity.workspaceId === right.identity.workspaceId && left.identity.siteId === right.identity.siteId;
@@ -0,0 +1,202 @@
1
+ import {
2
+ canonicalGateInputs
3
+ } from "./chunk-A76GNRIK.js";
4
+ import {
5
+ canonicalJson
6
+ } from "./chunk-XL3RKJYQ.js";
7
+ import {
8
+ writeFileAtomic
9
+ } from "./chunk-VZD2GEKB.js";
10
+ import {
11
+ BLOCKED_ACTION_GRANT_TTL_SECONDS,
12
+ blockedActionIdentity,
13
+ shouldDeferToNativeMode
14
+ } from "./chunk-OUAE3GKO.js";
15
+
16
+ // src/native-defer.ts
17
+ var nativeDeferOptions = () => {
18
+ const respect = process.env.PUSHARY_RESPECT_PERMISSION_MODE;
19
+ const bypass = process.env.PUSHARY_DEFER_BYPASS;
20
+ return {
21
+ respectPermissionMode: !(respect === "0" || respect === "false"),
22
+ deferBypass: bypass === "1" || bypass === "true"
23
+ };
24
+ };
25
+ var defersToNativeMode = (permissionMode, toolName) => shouldDeferToNativeMode(permissionMode, toolName, nativeDeferOptions());
26
+
27
+ // src/codex-call-decision.ts
28
+ import { createHash } from "crypto";
29
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
30
+ import { tmpdir, userInfo } from "os";
31
+ import { join } from "path";
32
+ var CODEX_DECISION_TTL_MS = 60 * 1e3;
33
+ var userScope = () => {
34
+ try {
35
+ const info = userInfo();
36
+ const id = typeof info.uid === "number" && info.uid >= 0 ? String(info.uid) : info.username;
37
+ return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128) || "default";
38
+ } catch {
39
+ return "default";
40
+ }
41
+ };
42
+ var decisionDir = () => join(tmpdir(), `pushary-codex-decisions-${userScope()}`);
43
+ var decisionFile = (key) => join(decisionDir(), key);
44
+ var PRESENTATION_ONLY_INPUT_KEYS = ["description"];
45
+ var identifyingToolInput = (toolInput) => {
46
+ if (!toolInput) return {};
47
+ const identifying = {};
48
+ for (const [key, value] of Object.entries(toolInput)) {
49
+ if (PRESENTATION_ONLY_INPUT_KEYS.includes(key)) continue;
50
+ identifying[key] = value;
51
+ }
52
+ return identifying;
53
+ };
54
+ var codexCallKey = (identity) => {
55
+ if (!identity.sessionId || !identity.turnId || !identity.toolName) return null;
56
+ const material = canonicalJson([
57
+ identity.sessionId,
58
+ identity.turnId,
59
+ identity.toolName,
60
+ identifyingToolInput(identity.toolInput)
61
+ ]);
62
+ return createHash("sha256").update(material, "utf-8").digest("hex").slice(0, 32);
63
+ };
64
+ var parseDecision = (raw) => {
65
+ try {
66
+ const parsed = JSON.parse(raw);
67
+ if (parsed.kind === "approved") {
68
+ return typeof parsed.toolUseId === "string" && parsed.toolUseId ? { kind: "approved", toolUseId: parsed.toolUseId } : null;
69
+ }
70
+ if (parsed.kind === "handled") return { kind: "handled" };
71
+ if (parsed.kind === "denied") {
72
+ return { kind: "denied", reason: typeof parsed.reason === "string" ? parsed.reason : "" };
73
+ }
74
+ return null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+ var sweepExpired = (dir, nowMs) => {
80
+ try {
81
+ for (const name of readdirSync(dir)) {
82
+ const path = join(dir, name);
83
+ try {
84
+ if (nowMs - statSync(path).mtimeMs >= CODEX_DECISION_TTL_MS) rmSync(path, { force: true });
85
+ } catch {
86
+ continue;
87
+ }
88
+ }
89
+ } catch {
90
+ return;
91
+ }
92
+ };
93
+ var recordCodexCallDecision = (key, decision) => {
94
+ if (!key) return;
95
+ try {
96
+ const dir = decisionDir();
97
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
98
+ else sweepExpired(dir, Date.now());
99
+ writeFileSync(decisionFile(key), JSON.stringify(decision), { encoding: "utf-8", mode: 384 });
100
+ } catch {
101
+ return;
102
+ }
103
+ };
104
+ var consumeCodexCallDecision = (key) => {
105
+ if (!key) return null;
106
+ const path = decisionFile(key);
107
+ try {
108
+ const fresh = Date.now() - statSync(path).mtimeMs < CODEX_DECISION_TTL_MS;
109
+ const raw = readFileSync(path, "utf-8");
110
+ rmSync(path, { force: true });
111
+ return fresh ? parseDecision(raw) : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ };
116
+
117
+ // src/blocked-action-hint.ts
118
+ import { createHash as createHash2 } from "crypto";
119
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
120
+ import { tmpdir as tmpdir2 } from "os";
121
+ import { join as join2 } from "path";
122
+ var BLOCKED_ACTION_HINT_TTL_MS = BLOCKED_ACTION_GRANT_TTL_SECONDS * 1e3;
123
+ var FINGERPRINT = /^[0-9a-f]{64}$/;
124
+ var isFingerprint = (value) => typeof value === "string" && FINGERPRINT.test(value);
125
+ var blockedActionSubject = (source, sessionId, toolName, toolInput, cwd) => {
126
+ if (source !== "claude") return { source, sessionId, toolName, toolInput };
127
+ return cwd ? { source, sessionId, cwd, toolName, toolInput } : null;
128
+ };
129
+ var blockedActionFingerprint = ({ source = "claude", sessionId, toolName, toolInput, cwd }) => {
130
+ if (!sessionId || !toolInput) return null;
131
+ const canonical = canonicalGateInputs(source, toolName, [toolInput], cwd);
132
+ if (!canonical || canonical.toolInputs.length !== 1) return null;
133
+ const [canonicalInput] = canonical.toolInputs;
134
+ if (!canonicalInput) return null;
135
+ const subject = blockedActionSubject(source, sessionId, canonical.toolName, canonicalInput, cwd);
136
+ return subject ? createHash2("sha256").update(blockedActionIdentity(subject), "utf8").digest("hex") : null;
137
+ };
138
+ var hintDir = () => join2(tmpdir2(), `pushary-blocked-action-hints-${userScope()}`);
139
+ var hintFile = (fingerprint) => join2(hintDir(), fingerprint);
140
+ var isExpired = (mtimeMs, nowMs) => nowMs - mtimeMs >= BLOCKED_ACTION_HINT_TTL_MS;
141
+ var sweepExpired2 = (dir, nowMs) => {
142
+ try {
143
+ for (const name of readdirSync2(dir)) {
144
+ const path = join2(dir, name);
145
+ try {
146
+ if (isExpired(statSync2(path).mtimeMs, nowMs)) rmSync2(path, { force: true });
147
+ } catch {
148
+ continue;
149
+ }
150
+ }
151
+ } catch {
152
+ return;
153
+ }
154
+ };
155
+ var recordBlockedActionHint = (fingerprint) => {
156
+ if (!isFingerprint(fingerprint)) return false;
157
+ try {
158
+ const dir = hintDir();
159
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
160
+ else sweepExpired2(dir, Date.now());
161
+ writeFileAtomic(hintFile(fingerprint), JSON.stringify({ fingerprint }), 384);
162
+ return true;
163
+ } catch {
164
+ return false;
165
+ }
166
+ };
167
+ var hasBlockedActionHint = (fingerprint) => {
168
+ if (!isFingerprint(fingerprint)) return false;
169
+ const path = hintFile(fingerprint);
170
+ try {
171
+ const stat = statSync2(path);
172
+ if (!stat.isFile()) return false;
173
+ if (isExpired(stat.mtimeMs, Date.now())) {
174
+ rmSync2(path, { force: true });
175
+ return false;
176
+ }
177
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
178
+ return parsed?.fingerprint === fingerprint;
179
+ } catch {
180
+ return false;
181
+ }
182
+ };
183
+ var forgetBlockedActionHint = (fingerprint) => {
184
+ if (!isFingerprint(fingerprint)) return;
185
+ try {
186
+ rmSync2(hintFile(fingerprint), { force: true });
187
+ } catch {
188
+ return;
189
+ }
190
+ };
191
+
192
+ export {
193
+ defersToNativeMode,
194
+ userScope,
195
+ codexCallKey,
196
+ recordCodexCallDecision,
197
+ consumeCodexCallDecision,
198
+ blockedActionFingerprint,
199
+ recordBlockedActionHint,
200
+ hasBlockedActionHint,
201
+ forgetBlockedActionHint
202
+ };
@@ -3,19 +3,19 @@ import {
3
3
  loadPin,
4
4
  savePin,
5
5
  verifyRecipients
6
- } from "./chunk-EMGGUO2Q.js";
6
+ } from "./chunk-OBYC6SN4.js";
7
7
  import {
8
8
  hookOwnerOf
9
- } from "./chunk-R3OZ5ISU.js";
9
+ } from "./chunk-W2MG2P46.js";
10
10
  import {
11
11
  ensureDaemonRunning
12
- } from "./chunk-I7LKI4WO.js";
12
+ } from "./chunk-DOD3A6EU.js";
13
13
  import {
14
14
  buildTranscriptRecords,
15
15
  decodePublicKey,
16
16
  generateSessionKey,
17
17
  wrapSessionKey
18
- } from "./chunk-SKPXQRKN.js";
18
+ } from "./chunk-XPPXOKIG.js";
19
19
  import {
20
20
  fetchTranscriptRecipients,
21
21
  uploadTranscriptRecords
@@ -23,17 +23,17 @@ import {
23
23
  import {
24
24
  transcriptEnvironmentPreference,
25
25
  transcriptsEnabled
26
- } from "./chunk-VERXHIQF.js";
26
+ } from "./chunk-NQGYPTJQ.js";
27
27
  import {
28
28
  codexHome,
29
29
  pusharyDir
30
- } from "./chunk-NOVA7KZ2.js";
30
+ } from "./chunk-XL3RKJYQ.js";
31
31
  import {
32
32
  readTranscriptPreference
33
- } from "./chunk-VMQU7YEJ.js";
33
+ } from "./chunk-SJLGOVC6.js";
34
34
  import {
35
35
  writeJsonAtomic
36
- } from "./chunk-L7NOG547.js";
36
+ } from "./chunk-VZD2GEKB.js";
37
37
  import {
38
38
  getApiKey,
39
39
  getBaseUrl
@@ -3,7 +3,7 @@ import {
3
3
  decodePublicKey,
4
4
  generateSessionKey,
5
5
  wrapSessionKey
6
- } from "./chunk-SKPXQRKN.js";
6
+ } from "./chunk-XPPXOKIG.js";
7
7
  import {
8
8
  fetchTranscriptRecipients,
9
9
  uploadTranscriptRecords
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  describeKeyCheck,
3
3
  keyCheckFromResponse
4
- } from "./chunk-BQL7UDIB.js";
4
+ } from "./chunk-3RVCQDC4.js";
5
5
  import {
6
6
  createIo
7
7
  } from "./chunk-5RUIFDTP.js";
@@ -1,10 +1,14 @@
1
1
  import {
2
- defersToNativeMode
3
- } from "./chunk-VDI645ZX.js";
2
+ blockedActionFingerprint,
3
+ defersToNativeMode,
4
+ forgetBlockedActionHint,
5
+ hasBlockedActionHint,
6
+ recordBlockedActionHint
7
+ } from "./chunk-53QCTSWY.js";
4
8
  import {
5
9
  isGatingMoment,
6
10
  recordKeylessMoment
7
- } from "./chunk-5GKNAQTG.js";
11
+ } from "./chunk-XOOWC62T.js";
8
12
  import {
9
13
  denyReasonFrom,
10
14
  isDeferAnswer
@@ -20,7 +24,7 @@ import {
20
24
  reportEvent,
21
25
  settleQuestionsForToolUse,
22
26
  throttlePass
23
- } from "./chunk-HUGIKB5O.js";
27
+ } from "./chunk-YJDRHQLG.js";
24
28
  import {
25
29
  UNNAMED_ACTION,
26
30
  deriveAction,
@@ -30,37 +34,46 @@ import {
30
34
  describeToolCall,
31
35
  permissionDeniedAction,
32
36
  scopePathFor
33
- } from "./chunk-Z4JVEL7R.js";
37
+ } from "./chunk-A76GNRIK.js";
34
38
  import {
35
39
  DECISION_STOP_REASON,
36
40
  askUser,
41
+ cancelQuestion,
42
+ claimBlockedAction,
37
43
  isDecisionStopped,
38
44
  isLocalQuestionHandoff,
45
+ pollForAnswer,
46
+ requestGate,
39
47
  sendNotification,
40
48
  waitForQuestion
41
- } from "./chunk-XS33QBJA.js";
49
+ } from "./chunk-LGXUGYUH.js";
42
50
  import {
43
51
  getMachineId
44
- } from "./chunk-E47WV7BJ.js";
52
+ } from "./chunk-WU6LSXJ6.js";
45
53
  import {
46
54
  getApiKey,
47
55
  getBaseUrl
48
56
  } from "./chunk-2UMNXADU.js";
49
57
  import {
58
+ GATE_BLOCK_REASON_MAX_LENGTH,
59
+ GATE_WIRE,
50
60
  buildDecisionEpisodeFeatures,
61
+ claudePermissionDeniedOutput,
51
62
  claudePermissionRequestOutput,
52
63
  claudePreToolUseOutput,
53
64
  effectiveWaitSeconds,
65
+ hookHoldWindowMs,
54
66
  hookWaitClamped,
55
67
  hookWaitDeadline,
56
68
  hookWritePath,
69
+ isBlockedActionRequest,
57
70
  isPermissionRequestOwnedByPreToolUse,
58
71
  isPusharyOwnTool,
59
72
  parseAgentQuestionAnswers,
60
73
  parseAgentQuestions,
61
74
  resolveGate,
62
75
  stoppedOutput
63
- } from "./chunk-MC5WUOJJ.js";
76
+ } from "./chunk-OUAE3GKO.js";
64
77
 
65
78
  // src/decision-episode.ts
66
79
  var EPISODE_PATH = "/api/agent/decision-episode";
@@ -89,7 +102,7 @@ import { basename, join } from "path";
89
102
  import { tmpdir } from "os";
90
103
  import { existsSync, writeFileSync } from "fs";
91
104
  var START_MS = Date.now();
92
- var allow = () => claudePreToolUseOutput("allow");
105
+ var allow = (reason) => claudePreToolUseOutput("allow", reason);
93
106
  var deny = (reason) => claudePreToolUseOutput("deny", reason);
94
107
  var allowWithInput = (updatedInput) => claudePreToolUseOutput("allow", void 0, updatedInput);
95
108
  var ask = (reason) => claudePreToolUseOutput("ask", reason);
@@ -449,6 +462,45 @@ var claudeGateInput = (modeState, config, input) => {
449
462
  sessionId: input.session_id
450
463
  };
451
464
  };
465
+ var BLOCKED_ACTION_GATE_TIMEOUT_MS = 15e3;
466
+ var BLOCKED_ACTION_CLAIM_TIMEOUT_MS = 4e3;
467
+ var BLOCKED_ACTION_ALLOW_REASON = "Approved from Pushary after auto mode blocked it";
468
+ var blockReasonOf = (reason) => typeof reason === "string" ? reason.trim().slice(0, GATE_BLOCK_REASON_MAX_LENGTH).trim() || void 0 : void 0;
469
+ var blockedActionFingerprintFor = (input) => blockedActionFingerprint({
470
+ sessionId: input.session_id,
471
+ toolName: input.tool_name,
472
+ toolInput: input.tool_input,
473
+ cwd: input.cwd
474
+ });
475
+ var blockedActionGateRequest = (input, event) => ({
476
+ v: GATE_WIRE,
477
+ source: "claude",
478
+ event,
479
+ toolName: input.tool_name,
480
+ toolInputs: [input.tool_input ?? {}],
481
+ sessionId: input.session_id,
482
+ toolUseId: input.tool_use_id,
483
+ machineId: getMachineId(),
484
+ cwd: input.cwd,
485
+ repoKey: repoKeyFor(input.cwd),
486
+ permissionMode: input.permission_mode,
487
+ ...event === "PermissionDenied" ? { createDecision: true, localSurfaceAvailable: false, blockReason: blockReasonOf(input.reason) } : {}
488
+ });
489
+ var claimedBlockedActionRetry = async (apiKey, input) => {
490
+ const fingerprint = blockedActionFingerprintFor(input);
491
+ if (!fingerprint || !hasBlockedActionHint(fingerprint)) return false;
492
+ try {
493
+ const granted = await claimBlockedAction(
494
+ apiKey,
495
+ blockedActionGateRequest(input, "PreToolUse"),
496
+ BLOCKED_ACTION_CLAIM_TIMEOUT_MS
497
+ );
498
+ if (granted !== null) forgetBlockedActionHint(fingerprint);
499
+ return granted === true;
500
+ } catch {
501
+ return false;
502
+ }
503
+ };
452
504
  var handlePreToolUse = async (input) => {
453
505
  if (isPusharyOwnTool(input.tool_name)) return void 0;
454
506
  let apiKey;
@@ -469,6 +521,7 @@ var handlePreToolUse = async (input) => {
469
521
  if (input.tool_name === "ExitPlanMode") {
470
522
  return handleExitPlanMode(apiKey, input);
471
523
  }
524
+ if (await claimedBlockedActionRetry(apiKey, input)) return allow(BLOCKED_ACTION_ALLOW_REASON);
472
525
  if (shouldDeferToNativeMode(input.permission_mode, input.tool_name)) {
473
526
  return void 0;
474
527
  }
@@ -531,12 +584,6 @@ var handlePermissionRequest = async (input) => {
531
584
  return void 0;
532
585
  }
533
586
  };
534
- var PERMISSION_DENIED_WINDOW_MS = 5 * 60 * 1e3;
535
- var PERMISSION_DENIED_WAIT_SECONDS = 25;
536
- var automodeRetryEnabled = () => {
537
- const flag = process.env.PUSHARY_AUTOMODE_RETRY;
538
- return flag === "1" || flag === "true";
539
- };
540
587
  var reportPermissionDenied = (input) => reportEvent({
541
588
  event: "permission_denied",
542
589
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -548,9 +595,48 @@ var reportPermissionDenied = (input) => reportEvent({
548
595
  // Side report, not a command carrier. Without timeoutMs this takes the phone's
549
596
  // queued instruction and acknowledges it delivered, and this hook can print
550
597
  // only {retry}. 4s rather than the Notification path's 2.5s because
551
- // PermissionDenied is given a 60s budget, not 10s.
598
+ // PermissionDenied holds the agent for the full hook budget, not 10s.
552
599
  }, { timeoutMs: 4e3 }).catch(() => void 0);
553
- var handlePermissionDenied = async (input) => {
600
+ var isAffirmativeAnswer = (answer) => answer.answered && !isDecisionStopped(answer) && answer.value === "yes";
601
+ var PERMISSION_DENIED_NOTICE_WINDOW_MS = 5 * 60 * 1e3;
602
+ var blockedActionHoldEndsAt = (holdBudgetSeconds) => START_MS + hookHoldWindowMs("claude", holdBudgetSeconds);
603
+ var notifyPermissionDenied = async (apiKey, input) => {
604
+ const sessionKey = input.session_id || DEFAULT_SESSION;
605
+ if (!throttlePass(`permdenied:${sessionKey}:${input.tool_name}`, PERMISSION_DENIED_NOTICE_WINDOW_MS)) return;
606
+ const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
607
+ if (mode?.kill || mode?.mode === "terminal_only") return;
608
+ const projectName = basename(input.cwd ?? process.cwd());
609
+ await sendNotification(apiKey, {
610
+ title: `Auto mode blocked ${input.tool_name}`,
611
+ body: `Your agent was denied ${input.tool_name} in ${projectName}.`,
612
+ agentName: `Claude Code - ${projectName}`,
613
+ sessionId: input.session_id
614
+ }, 4e3).catch(() => void 0);
615
+ };
616
+ var isRetryOutput = (output) => {
617
+ if (!output) return false;
618
+ try {
619
+ const parsed = JSON.parse(output);
620
+ return parsed?.hookSpecificOutput?.hookEventName === "PermissionDenied" && parsed.hookSpecificOutput.retry === true;
621
+ } catch {
622
+ return false;
623
+ }
624
+ };
625
+ var retryOnce = (fingerprint) => recordBlockedActionHint(fingerprint) ? claudePermissionDeniedOutput("allow") : void 0;
626
+ var askAboutBlockedAction = async (apiKey, input, fingerprint, holdEndsAt) => {
627
+ const reply = await requestGate(apiKey, blockedActionGateRequest(input, "PermissionDenied"), BLOCKED_ACTION_GATE_TIMEOUT_MS);
628
+ if (!reply) return void 0;
629
+ if (reply.kind === "no_opinion") return isRetryOutput(reply.output) ? retryOnce(fingerprint) : void 0;
630
+ if (reply.kind !== "ask" || !reply.correlationId || reply.deadlineSeconds === void 0) return void 0;
631
+ if (reply.noDevices) {
632
+ await cancelQuestion(apiKey, reply.correlationId, AbortSignal.timeout(4e3)).catch(() => void 0);
633
+ return void 0;
634
+ }
635
+ const deadline = Math.min(hookWaitDeadline(START_MS, reply.deadlineSeconds, "claude", Date.now()), holdEndsAt);
636
+ const answer = await pollForAnswer(apiKey, reply.correlationId, deadline);
637
+ return isAffirmativeAnswer(answer) ? retryOnce(fingerprint) : void 0;
638
+ };
639
+ var handlePermissionDenied = async (input, holdBudgetSeconds) => {
554
640
  await reportPermissionDenied(input);
555
641
  if (isPusharyOwnTool(input.tool_name)) return void 0;
556
642
  let apiKey;
@@ -560,46 +646,16 @@ var handlePermissionDenied = async (input) => {
560
646
  return void 0;
561
647
  }
562
648
  try {
563
- const projectName = basename(input.cwd ?? process.cwd());
564
- const sessionKey = input.session_id || DEFAULT_SESSION;
565
- await settleQuestionsForToolUse(sessionKey, input.tool_use_id);
566
- if (!throttlePass(`permdenied:${sessionKey}:${input.tool_name}`, PERMISSION_DENIED_WINDOW_MS)) return void 0;
567
- const mode = await fetchModeState(apiKey, input.session_id).catch(() => null);
568
- if (mode?.kill || mode?.mode === "terminal_only") return void 0;
569
- if (!automodeRetryEnabled()) {
570
- await sendNotification(apiKey, {
571
- title: `Auto-mode blocked ${input.tool_name}`,
572
- body: `Your agent was denied ${input.tool_name} in ${projectName}.`,
573
- agentName: `Claude Code - ${projectName}`,
574
- sessionId: input.session_id
575
- }, 4e3).catch(() => {
576
- });
649
+ await settleQuestionsForToolUse(input.session_id || DEFAULT_SESSION, input.tool_use_id);
650
+ if (!isBlockedActionRequest({ source: "claude", event: "PermissionDenied", toolName: input.tool_name })) return void 0;
651
+ const fingerprint = blockedActionFingerprintFor(input);
652
+ if (!fingerprint) return void 0;
653
+ const holdEndsAt = blockedActionHoldEndsAt(holdBudgetSeconds);
654
+ if (holdEndsAt - Date.now() <= BLOCKED_ACTION_GATE_TIMEOUT_MS) {
655
+ await notifyPermissionDenied(apiKey, input);
577
656
  return void 0;
578
657
  }
579
- let result;
580
- try {
581
- result = await askUser(apiKey, {
582
- question: `Auto-mode blocked ${input.tool_name} in ${projectName}. Let the agent retry it?`,
583
- type: "confirm",
584
- context: "Your agent hit a permission wall in auto mode",
585
- agentName: `Claude Code - ${projectName}`,
586
- sessionId: input.session_id,
587
- machineId: getMachineId(),
588
- toolName: input.tool_name,
589
- // Suffixed because the PreToolUse gate may already have asked about this
590
- // same tool call. Without it, the retry prompt would collide with that
591
- // decision and hand back its answer instead of asking a new question.
592
- toolUseId: input.tool_use_id ? `${input.tool_use_id}#retry` : void 0
593
- });
594
- } catch {
595
- return void 0;
596
- }
597
- const deadline = hookWaitDeadline(START_MS, PERMISSION_DENIED_WAIT_SECONDS, "claude", Date.now());
598
- const answer = await waitForQuestion(apiKey, result, deadline);
599
- if (answer.answered && answer.value === "yes") {
600
- return { hookSpecificOutput: { hookEventName: "PermissionDenied", retry: true } };
601
- }
602
- return void 0;
658
+ return await askAboutBlockedAction(apiKey, input, fingerprint, holdEndsAt);
603
659
  } catch {
604
660
  return void 0;
605
661
  }