@pushary/agent-hooks 0.25.0 → 0.27.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 (102) hide show
  1. package/data/cursor-plugin/scripts/pushary-gate.mjs +21 -1
  2. package/dist/bin/pushary-clean.js +3 -2
  3. package/dist/bin/pushary-codex-hook.js +37 -12
  4. package/dist/bin/pushary-codex.js +2 -1
  5. package/dist/bin/pushary-doctor.js +52 -10
  6. package/dist/bin/pushary-gemini-hook.js +36 -11
  7. package/dist/bin/pushary-hook.js +3 -2
  8. package/dist/bin/pushary-mode.js +5 -3
  9. package/dist/bin/pushary-post-hook.js +8 -2
  10. package/dist/bin/pushary-prompt-hook.js +8 -2
  11. package/dist/bin/pushary-setup.js +11 -21
  12. package/dist/bin/pushary-stop-hook.js +2 -1
  13. package/dist/bin/pushary-wait.d.ts +1 -0
  14. package/dist/bin/pushary-wait.js +86 -0
  15. package/dist/bin/pushary.js +4 -0
  16. package/dist/{chunk-WPVL6VIT.js → chunk-FDR5WYT7.js} +50 -2
  17. package/dist/chunk-IIENZE2J.js +15 -0
  18. package/dist/{chunk-5MA3CPZB.js → chunk-MVNOP5IZ.js} +5 -1
  19. package/dist/{chunk-GYPPVD3P.js → chunk-TUILTFLS.js} +74 -184
  20. package/dist/{chunk-O2QL6U6G.js → chunk-VHD3GMZ3.js} +40 -3
  21. package/dist/{chunk-JPRRYC5X.js → chunk-WXWHRXDK.js} +32 -3
  22. package/dist/src/index.d.ts +7 -1
  23. package/dist/src/index.js +3 -2
  24. package/package.json +4 -3
  25. package/dist/chunk-22CV7V7A.js +0 -38
  26. package/dist/chunk-2BE2IPMO.js +0 -315
  27. package/dist/chunk-2CRLBMOX.js +0 -244
  28. package/dist/chunk-2HMNOZPY.js +0 -109
  29. package/dist/chunk-2I6DLXJN.js +0 -219
  30. package/dist/chunk-2Q6XPZ4Q.js +0 -395
  31. package/dist/chunk-3LC4YUMA.js +0 -194
  32. package/dist/chunk-3MIR7ODJ.js +0 -112
  33. package/dist/chunk-3QZESA66.js +0 -222
  34. package/dist/chunk-4Z4MB37G.js +0 -96
  35. package/dist/chunk-5GFUI5N6.js +0 -132
  36. package/dist/chunk-5JEDLXEC.js +0 -99
  37. package/dist/chunk-5LTQNC7R.js +0 -824
  38. package/dist/chunk-5QHQVXWN.js +0 -174
  39. package/dist/chunk-6A3WCJO2.js +0 -416
  40. package/dist/chunk-6QWFMVCD.js +0 -328
  41. package/dist/chunk-6SRXMZAN.js +0 -99
  42. package/dist/chunk-7PTU7TGE.js +0 -96
  43. package/dist/chunk-AB4KX4XT.js +0 -109
  44. package/dist/chunk-AC4UYAGX.js +0 -136
  45. package/dist/chunk-ACE77TKQ.js +0 -328
  46. package/dist/chunk-BHAMEPOP.js +0 -323
  47. package/dist/chunk-BYNYYZWW.js +0 -412
  48. package/dist/chunk-C5TFTNHG.js +0 -244
  49. package/dist/chunk-CH53PBQN.js +0 -265
  50. package/dist/chunk-DF3BM6BF.js +0 -195
  51. package/dist/chunk-DKZVKEOW.js +0 -175
  52. package/dist/chunk-EMPL27ZV.js +0 -96
  53. package/dist/chunk-EQJXMEIR.js +0 -175
  54. package/dist/chunk-FGZZRCOG.js +0 -194
  55. package/dist/chunk-FXYTRZHC.js +0 -177
  56. package/dist/chunk-GTJGZBTQ.js +0 -328
  57. package/dist/chunk-HJMFYDWY.js +0 -222
  58. package/dist/chunk-HRQEECB6.js +0 -315
  59. package/dist/chunk-HWEMAAOY.js +0 -370
  60. package/dist/chunk-IBWCHA5M.js +0 -10
  61. package/dist/chunk-KTM4ZQ3W.js +0 -96
  62. package/dist/chunk-KTP2EPVB.js +0 -27
  63. package/dist/chunk-KYARP7KP.js +0 -97
  64. package/dist/chunk-M2N5DYWN.js +0 -247
  65. package/dist/chunk-M4GDBGXF.js +0 -824
  66. package/dist/chunk-M5F2L4KF.js +0 -177
  67. package/dist/chunk-M6S3M6E3.js +0 -244
  68. package/dist/chunk-N7VXDBQU.js +0 -6
  69. package/dist/chunk-O6A5RHWY.js +0 -122
  70. package/dist/chunk-ODUXELPM.js +0 -219
  71. package/dist/chunk-OF5WIOYS.js +0 -129
  72. package/dist/chunk-OP4QAN23.js +0 -99
  73. package/dist/chunk-P4JH2Q7Z.js +0 -102
  74. package/dist/chunk-PMD5JSV3.js +0 -242
  75. package/dist/chunk-PSSHNBEL.js +0 -177
  76. package/dist/chunk-QEPRH7JB.js +0 -315
  77. package/dist/chunk-QRXWPZKN.js +0 -308
  78. package/dist/chunk-QY4L6XHN.js +0 -384
  79. package/dist/chunk-RNWPCELY.js +0 -176
  80. package/dist/chunk-RSHN2AQ7.js +0 -29
  81. package/dist/chunk-RZLVE57X.js +0 -38
  82. package/dist/chunk-SDCIKREA.js +0 -241
  83. package/dist/chunk-SDREQWNI.js +0 -175
  84. package/dist/chunk-SH26ZOHU.js +0 -159
  85. package/dist/chunk-TRLBBLSS.js +0 -176
  86. package/dist/chunk-U6OXZPYC.js +0 -384
  87. package/dist/chunk-UIBTFZUD.js +0 -314
  88. package/dist/chunk-USUCPCUC.js +0 -156
  89. package/dist/chunk-V4GLWPD7.js +0 -174
  90. package/dist/chunk-VC6U3QGF.js +0 -142
  91. package/dist/chunk-VIST7ACL.js +0 -88
  92. package/dist/chunk-VUNL35KE.js +0 -16
  93. package/dist/chunk-VWBNI4SC.js +0 -174
  94. package/dist/chunk-W5KRWUNE.js +0 -262
  95. package/dist/chunk-WCGKLHCL.js +0 -154
  96. package/dist/chunk-WNXGIEX7.js +0 -219
  97. package/dist/chunk-XPVSZIA3.js +0 -315
  98. package/dist/chunk-Y633ZIJF.js +0 -384
  99. package/dist/chunk-YCL4GUFR.js +0 -222
  100. package/dist/chunk-YTMKB44I.js +0 -220
  101. package/dist/chunk-ZWBS3T7Q.js +0 -244
  102. package/dist/pushary-mode-T7XOPI6Z.js +0 -83
@@ -1,6 +1,24 @@
1
- // ../contracts/src/index.ts
1
+ // ../../../../../packages/contracts/src/index.ts
2
2
  var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
3
3
  var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
4
+ var HOOK_BUDGETS = {
5
+ claude: { budgetSeconds: 120, guardSeconds: 10 },
6
+ codex: { budgetSeconds: 180, guardSeconds: 10 },
7
+ gemini: { budgetSeconds: 180, guardSeconds: 10 },
8
+ // The Cursor gate is a dependency-free .mjs that cannot import this module; it
9
+ // mirrors these as MAX_BLOCK_MS (45s) inside its 60s failClosed budget.
10
+ cursor: { budgetSeconds: 60, guardSeconds: 15 }
11
+ };
12
+ var hookMaxWaitSeconds = (agent) => Math.max(HOOK_BUDGETS[agent].budgetSeconds - HOOK_BUDGETS[agent].guardSeconds, 1);
13
+ var hookWaitDeadline = (startMs, policyWaitSeconds, agent, nowMs) => {
14
+ const maxWait = hookMaxWaitSeconds(agent);
15
+ return Math.min(
16
+ nowMs + Math.min(Math.max(policyWaitSeconds, 0), maxWait) * 1e3,
17
+ startMs + maxWait * 1e3
18
+ );
19
+ };
20
+ var hookWaitClamped = (startMs, policyWaitSeconds, agent, nowMs) => startMs + hookMaxWaitSeconds(agent) * 1e3 < nowMs + Math.max(policyWaitSeconds, 0) * 1e3;
21
+ var effectiveWaitSeconds = (timeoutAction, policySeconds, agent) => timeoutAction === "wait" ? hookMaxWaitSeconds(agent) : policySeconds;
4
22
  var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
5
23
  var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
6
24
  var matchToolPattern = (pattern, toolName, arg) => {
@@ -147,14 +165,44 @@ var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
147
165
  var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
148
166
  var ACTION_BODY_MAX = 4e3;
149
167
  var DECISION_LINE_MAX = 500;
168
+ var SECRET_REDACTION_RULES = [
169
+ { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
170
+ { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
171
+ { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: "[redacted]" },
172
+ { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
173
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[redacted]" },
174
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: "[redacted]" },
175
+ { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[redacted]" },
176
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[redacted]" },
177
+ { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: "[redacted]" },
178
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[redacted]" },
179
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: "[redacted]" },
180
+ { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
181
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
182
+ { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: "bearer [redacted]" },
183
+ { pattern: /\bauthorization:\s*\S+/gi, replacement: "authorization: [redacted]" },
184
+ {
185
+ pattern: /((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi,
186
+ replacement: "$1[redacted]"
187
+ }
188
+ ];
189
+ var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
190
+ var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
191
+ var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
150
192
 
151
193
  export {
152
194
  isApprovalMode,
195
+ HOOK_BUDGETS,
196
+ hookWaitDeadline,
197
+ hookWaitClamped,
198
+ effectiveWaitSeconds,
153
199
  matchRankWeight,
154
200
  matchToolPattern,
155
201
  extractPolicyArg,
156
202
  isSafeReadOnlyCommand,
157
203
  isValidApiKey,
158
204
  ACTION_BODY_MAX,
159
- DECISION_LINE_MAX
205
+ DECISION_LINE_MAX,
206
+ redactSecrets,
207
+ redactSecretsDeep
160
208
  };
@@ -0,0 +1,15 @@
1
+ // src/wait-ladder.ts
2
+ var describeWaitLadder = (p) => {
3
+ if (p.mode === "terminal_only") return "decide in the terminal, no push";
4
+ if (p.mode === "notify_only") return "send a heads-up, the terminal decides";
5
+ if (p.mode === "push_only") {
6
+ if (p.timeoutAction === "wait") return "wait for your phone, then ask in the terminal";
7
+ const tail = p.timeoutAction === "approve" ? "then auto-approve" : p.timeoutAction === "deny" ? "then auto-deny" : "then ask in the terminal";
8
+ return `wait for your phone up to ${p.timeoutSeconds}s, ${tail}`;
9
+ }
10
+ return `ping your phone ${p.pushFirstSeconds}s, then ask in the terminal`;
11
+ };
12
+
13
+ export {
14
+ describeWaitLadder
15
+ };
@@ -1,3 +1,7 @@
1
+ import {
2
+ HOOK_BUDGETS
3
+ } from "./chunk-FDR5WYT7.js";
4
+
1
5
  // src/claude-config.ts
2
6
  import { join } from "path";
3
7
  var PUSHARY_MCP_URL = "https://pushary.com/api/mcp/mcp";
@@ -64,7 +68,7 @@ var addPusharyHooks = (settings, binDir) => {
64
68
  hooks: [{
65
69
  type: "command",
66
70
  command: resolve("pushary-hook"),
67
- timeout: 120
71
+ timeout: HOOK_BUDGETS.claude.budgetSeconds
68
72
  }]
69
73
  });
70
74
  hooks.PreToolUse = preToolUse;
@@ -2,185 +2,22 @@ import {
2
2
  callMcpTool,
3
3
  withRetry
4
4
  } from "./chunk-DWED7BS3.js";
5
+ import {
6
+ ACTION_BODY_MAX,
7
+ DECISION_LINE_MAX,
8
+ extractPolicyArg,
9
+ isApprovalMode,
10
+ isSafeReadOnlyCommand,
11
+ matchRankWeight,
12
+ matchToolPattern,
13
+ redactSecrets,
14
+ redactSecretsDeep
15
+ } from "./chunk-FDR5WYT7.js";
5
16
  import {
6
17
  getApiKey,
7
18
  getBaseUrl
8
19
  } from "./chunk-NKXSILEW.js";
9
20
 
10
- // ../contracts/src/index.ts
11
- var APPROVAL_MODES = ["push_only", "terminal_only", "push_first", "notify_only"];
12
- var isApprovalMode = (value) => typeof value === "string" && APPROVAL_MODES.includes(value);
13
- var MATCH_RANKS = ["none", "tool", "prefix", "exact"];
14
- var matchRankWeight = (rank) => MATCH_RANKS.indexOf(rank);
15
- var matchToolPattern = (pattern, toolName, arg) => {
16
- const open = pattern.indexOf("(");
17
- if (open === -1 || !pattern.endsWith(")")) {
18
- return pattern === toolName ? "tool" : "none";
19
- }
20
- if (pattern.slice(0, open) !== toolName || arg === void 0) return "none";
21
- const inner = pattern.slice(open + 1, -1);
22
- if (inner.endsWith(":*")) {
23
- return arg.startsWith(inner.slice(0, -2)) ? "prefix" : "none";
24
- }
25
- return arg === inner ? "exact" : "none";
26
- };
27
- var POLICY_ARG_KEYS = {
28
- Bash: "command",
29
- Edit: "file_path",
30
- Write: "file_path"
31
- };
32
- var extractPolicyArg = (toolName, toolInput) => {
33
- const key = POLICY_ARG_KEYS[toolName];
34
- if (!key) return void 0;
35
- const value = toolInput[key];
36
- return typeof value === "string" ? value : void 0;
37
- };
38
- var SAFE_SHELL_COMMANDS = /* @__PURE__ */ new Set([
39
- "ls",
40
- "pwd",
41
- "cd",
42
- "cat",
43
- "head",
44
- "tail",
45
- "wc",
46
- "echo",
47
- "printf",
48
- "which",
49
- "type",
50
- "whoami",
51
- "id",
52
- "uname",
53
- "arch",
54
- "printenv",
55
- "locale",
56
- "tty",
57
- "dirname",
58
- "basename",
59
- "realpath",
60
- "readlink",
61
- "stat",
62
- "cut",
63
- "nl",
64
- "tr",
65
- "comm",
66
- "diff",
67
- "cmp",
68
- "grep",
69
- "egrep",
70
- "fgrep",
71
- "jq",
72
- "cksum",
73
- "md5sum",
74
- "sha1sum",
75
- "sha256sum",
76
- "du",
77
- "df",
78
- "ps",
79
- "true"
80
- ]);
81
- var SAFE_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set([
82
- "status",
83
- "log",
84
- "diff",
85
- "show",
86
- "rev-parse",
87
- "describe",
88
- "blame",
89
- "shortlog",
90
- "ls-files",
91
- "ls-tree",
92
- "cat-file",
93
- "whatchanged",
94
- "rev-list",
95
- "name-rev",
96
- "for-each-ref",
97
- "var",
98
- "count-objects"
99
- ]);
100
- var GIT_WRITE_FLAGS = (token) => token === "--output" || token.startsWith("--output=");
101
- var UNSAFE_SHELL_CHARS = /[;&|<>(){}`\\\n\r]/;
102
- var basenameOf = (token) => {
103
- const slash = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
104
- return slash === -1 ? token : token.slice(slash + 1);
105
- };
106
- var tokenizeShellCommand = (command) => {
107
- const tokens = [];
108
- let current = "";
109
- let quote = null;
110
- let started = false;
111
- for (const ch of command) {
112
- if (quote) {
113
- if (ch === quote) quote = null;
114
- else current += ch;
115
- started = true;
116
- continue;
117
- }
118
- if (ch === '"' || ch === "'") {
119
- quote = ch;
120
- started = true;
121
- continue;
122
- }
123
- if (ch === " " || ch === " ") {
124
- if (started) {
125
- tokens.push(current);
126
- current = "";
127
- started = false;
128
- }
129
- continue;
130
- }
131
- current += ch;
132
- started = true;
133
- }
134
- if (quote) return null;
135
- if (started) tokens.push(current);
136
- return tokens;
137
- };
138
- var isSafeReadOnlyCommand = (command) => {
139
- const trimmed = command.trim();
140
- if (!trimmed || trimmed.length > 2e3) return false;
141
- if (UNSAFE_SHELL_CHARS.test(trimmed)) return false;
142
- const tokens = tokenizeShellCommand(trimmed);
143
- if (!tokens || tokens.length === 0) return false;
144
- const first = tokens[0];
145
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first) || first.includes("$")) return false;
146
- const exe = basenameOf(first);
147
- if (exe === "git") {
148
- const sub = tokens[1];
149
- if (!sub || sub.startsWith("-")) return false;
150
- if (!SAFE_GIT_SUBCOMMANDS.has(sub)) return false;
151
- return !tokens.some(GIT_WRITE_FLAGS);
152
- }
153
- return SAFE_SHELL_COMMANDS.has(exe);
154
- };
155
- var API_KEY_PATTERN = /^pk_[a-f0-9]+\.[a-f0-9]+$/;
156
- var isValidApiKey = (value) => API_KEY_PATTERN.test(value);
157
- var ACTION_BODY_MAX = 4e3;
158
- var DECISION_LINE_MAX = 500;
159
- var SECRET_REDACTION_RULES = [
160
- { pattern: /-----BEGIN[A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z0-9 ]*PRIVATE KEY-----/g, replacement: "[redacted key]" },
161
- { pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/g, replacement: "[redacted]" },
162
- { pattern: /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: "[redacted]" },
163
- { pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
164
- { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g, replacement: "[redacted]" },
165
- { pattern: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, replacement: "[redacted]" },
166
- { pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, replacement: "[redacted]" },
167
- { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, replacement: "[redacted]" },
168
- { pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, replacement: "[redacted]" },
169
- { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[redacted]" },
170
- { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: "[redacted]" },
171
- { pattern: /\bxai-[A-Za-z0-9]{16,}\b/g, replacement: "[redacted]" },
172
- { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: "[redacted]" },
173
- { pattern: /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi, replacement: "bearer [redacted]" },
174
- { pattern: /\bauthorization:\s*\S+/gi, replacement: "authorization: [redacted]" },
175
- {
176
- pattern: /((?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi,
177
- replacement: "$1[redacted]"
178
- }
179
- ];
180
- var HIGH_ENTROPY_RULE = { pattern: /[A-Za-z0-9+/]{40,}={0,2}/g, replacement: "[redacted]" };
181
- var redactSecrets = (text) => SECRET_REDACTION_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.replacement), text);
182
- var redactSecretsDeep = (text) => redactSecrets(text).replace(HIGH_ENTROPY_RULE.pattern, HIGH_ENTROPY_RULE.replacement);
183
-
184
21
  // src/validate.ts
185
22
  var isPolicyConfig = (data) => {
186
23
  if (!data || typeof data !== "object") return false;
@@ -818,20 +655,52 @@ var readLastPrompt = (sessionId) => {
818
655
  return void 0;
819
656
  }
820
657
  };
821
- var cleanupPendingQuestions = async (sessionId) => {
658
+ var reconcilePendingQuestions = async (sessionId) => {
659
+ const late = [];
822
660
  try {
823
661
  const files = listPendingQuestions(sessionId);
662
+ if (files.length === 0) return late;
824
663
  const apiKey = getApiKey();
825
664
  for (const correlationId of files) {
826
665
  try {
827
- await cancelQuestion(apiKey, correlationId);
666
+ const answer = await waitForAnswer(apiKey, correlationId, 1e3);
667
+ if (answer.answered) {
668
+ late.push({ correlationId, value: answer.value });
669
+ continue;
670
+ }
671
+ await cancelQuestion(apiKey, correlationId).catch(() => {
672
+ });
828
673
  } catch {
674
+ continue;
829
675
  }
830
676
  removePendingQuestion(sessionId, correlationId);
831
677
  }
832
- if (!isDefaultSession(sessionId)) removePendingSession(sessionId);
833
678
  } catch {
834
679
  }
680
+ return late;
681
+ };
682
+ var describeLateAnswer = (value) => {
683
+ const v = (value ?? "").trim();
684
+ if (v === "yes") return "approved it";
685
+ if (v === "no") return "rejected it";
686
+ if (v === "defer") return "chose to handle it on this machine";
687
+ return v ? `answered "${v.slice(0, 200)}"` : "responded";
688
+ };
689
+ var buildLateAnswerContext = (late) => {
690
+ if (late.length === 0) return void 0;
691
+ const parts = late.map((l) => `you ${describeLateAnswer(l.value)} on your phone`);
692
+ 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.`;
693
+ };
694
+ var notifyLateAnswers = async (apiKey, late, agentName, sessionId) => {
695
+ if (late.length === 0) return;
696
+ const summary = late.map((l) => describeLateAnswer(l.value)).join(", ");
697
+ await sendNotification(apiKey, {
698
+ title: "Answer arrived late",
699
+ body: `You ${summary} after the agent had already moved on.`,
700
+ agentName,
701
+ sessionId
702
+ }, 2500).catch(() => {
703
+ });
835
704
  };
836
705
  var CLAUDE_CODE_AGENT = { type: "claude_code", label: "Claude Code" };
837
706
  var POLICY_CACHE_TTL_MS = 5 * 60 * 1e3;
@@ -917,10 +786,15 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
917
786
  const receiptsEnabled = process.env.PUSHARY_RECEIPTS !== "off";
918
787
  const sessionKey = input.session_id || DEFAULT_SESSION;
919
788
  const liveMode = await fetchModeState(getApiKey(), input.session_id);
789
+ const late = await reconcilePendingQuestions(sessionKey);
790
+ let additionalContext;
791
+ if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
792
+ additionalContext = buildLateAnswerContext(late);
793
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
794
+ }
920
795
  const decisionSource = deriveDecisionSource(lookup.tool, lookup.input, liveMode);
921
796
  const beacon = decisionSource === "policy_auto" && throttlePass(`autodecision:${sessionKey}:${lookup.tool}`, AUTO_DECISION_WINDOW_MS) ? deriveAutoDecisionBeacon(lookup.tool, lookup.input, liveMode) : void 0;
922
797
  const reports = [
923
- cleanupPendingQuestions(sessionKey),
924
798
  reportEvent({
925
799
  event: isError ? "tool_error" : "tool_complete",
926
800
  agentType: agent.type,
@@ -948,16 +822,25 @@ var handlePostToolUse = async (input, agent = CLAUDE_CODE_AGENT) => {
948
822
  );
949
823
  }
950
824
  await Promise.allSettled(reports);
825
+ return additionalContext;
951
826
  } catch {
827
+ return void 0;
952
828
  }
953
829
  };
954
830
  var TASK_TITLE_MAX_LENGTH = 120;
955
831
  var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
956
832
  try {
957
833
  const projectName = basename(input.cwd ?? process.cwd());
834
+ const sessionKey = input.session_id || DEFAULT_SESSION;
958
835
  const titlesEnabled = process.env.PUSHARY_TASK_TITLES !== "off";
959
836
  const taskTitle = titlesEnabled ? input.prompt?.replace(/\s+/g, " ").trim().slice(0, TASK_TITLE_MAX_LENGTH) || void 0 : void 0;
960
- if (input.prompt) saveLastPrompt(input.session_id || DEFAULT_SESSION, input.prompt);
837
+ if (input.prompt) saveLastPrompt(sessionKey, input.prompt);
838
+ const late = await reconcilePendingQuestions(sessionKey);
839
+ let additionalContext;
840
+ if (late.length > 0 && agent.type === CLAUDE_CODE_AGENT.type) {
841
+ additionalContext = buildLateAnswerContext(late);
842
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
843
+ }
961
844
  await reportEvent({
962
845
  event: "user_prompt",
963
846
  agentType: agent.type,
@@ -965,14 +848,17 @@ var handleUserPrompt = async (input, agent = CLAUDE_CODE_AGENT) => {
965
848
  sessionId: input.session_id,
966
849
  taskTitle
967
850
  }, { maxAttempts: 1, timeoutMs: 800 });
851
+ return additionalContext;
968
852
  } catch {
853
+ return void 0;
969
854
  }
970
855
  };
971
856
  var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
972
857
  try {
973
858
  const projectName = basename(input.cwd ?? process.cwd());
974
- const [, reported] = await Promise.allSettled([
975
- cleanupPendingQuestions(input.session_id || DEFAULT_SESSION),
859
+ const sessionKey = input.session_id || DEFAULT_SESSION;
860
+ const late = await reconcilePendingQuestions(sessionKey);
861
+ const tasks = [
976
862
  reportEvent({
977
863
  event: "session_end",
978
864
  agentType: agent.type,
@@ -981,7 +867,13 @@ var handleStop = async (input, agent = CLAUDE_CODE_AGENT) => {
981
867
  sessionId: input.session_id,
982
868
  usage: deriveUsage(input.transcript_path, input.session_id)
983
869
  })
984
- ]);
870
+ ];
871
+ if (late.length > 0) {
872
+ tasks.push(notifyLateAnswers(getApiKey(), late, `${agent.label} - ${projectName}`, input.session_id));
873
+ for (const l of late) removePendingQuestion(sessionKey, l.correlationId);
874
+ }
875
+ if (!isDefaultSession(sessionKey)) removePendingSession(sessionKey);
876
+ const [reported] = await Promise.allSettled(tasks);
985
877
  const pendingCommand = reported.status === "fulfilled" ? reported.value?.pendingCommand : void 0;
986
878
  if (typeof pendingCommand === "string" && pendingCommand.trim().length > 0) {
987
879
  return {
@@ -1010,8 +902,6 @@ var handleNotification = async (input) => {
1010
902
  };
1011
903
 
1012
904
  export {
1013
- isValidApiKey,
1014
- DECISION_LINE_MAX,
1015
905
  askUser,
1016
906
  waitForAnswer,
1017
907
  cancelQuestion,
@@ -5,6 +5,7 @@ import {
5
5
  import {
6
6
  DEFAULT_SESSION,
7
7
  askUser,
8
+ cancelQuestion,
8
9
  deriveAction,
9
10
  deriveActionBody,
10
11
  deriveBlocker,
@@ -19,7 +20,12 @@ import {
19
20
  savePendingQuestion,
20
21
  sendNotification,
21
22
  waitForAnswer
22
- } from "./chunk-GYPPVD3P.js";
23
+ } from "./chunk-TUILTFLS.js";
24
+ import {
25
+ effectiveWaitSeconds,
26
+ hookWaitClamped,
27
+ hookWaitDeadline
28
+ } from "./chunk-FDR5WYT7.js";
23
29
  import {
24
30
  getApiKey
25
31
  } from "./chunk-NKXSILEW.js";
@@ -27,6 +33,7 @@ import {
27
33
  // src/hook.ts
28
34
  import { basename } from "path";
29
35
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
36
+ var START_MS = Date.now();
30
37
  var allow = () => ({
31
38
  hookSpecificOutput: {
32
39
  hookEventName: "PreToolUse",
@@ -88,12 +95,34 @@ var handlePushOnly = async (apiKey, description, projectName, timeoutSeconds, ti
88
95
  return ask("Push notification failed, asking in terminal");
89
96
  }
90
97
  }
91
- const deadline = Date.now() + timeoutSeconds * 1e3;
98
+ if (result.suppressed) {
99
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
100
+ });
101
+ return ask("You are at the keyboard \u2014 approve here.");
102
+ }
103
+ if (result.noDevices) {
104
+ switch (timeoutAction) {
105
+ case "approve":
106
+ return allow();
107
+ case "deny":
108
+ return deny("No device connected to approve on");
109
+ default:
110
+ return ask("No device connected \u2014 asking in terminal");
111
+ }
112
+ }
113
+ const effectiveWait = effectiveWaitSeconds(timeoutAction, timeoutSeconds, "claude");
114
+ const now = Date.now();
115
+ const deadline = hookWaitDeadline(START_MS, effectiveWait, "claude", now);
116
+ const clampCut = hookWaitClamped(START_MS, effectiveWait, "claude", now);
92
117
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
93
118
  if (answer.answered) {
94
119
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
95
120
  return answer.value === "yes" ? allow() : deny(denyReasonFrom(answer.value));
96
121
  }
122
+ if (timeoutAction === "wait" || clampCut) {
123
+ savePendingQuestion(sessionId || DEFAULT_SESSION, result.correlationId);
124
+ return ask(timeoutAction === "wait" ? "Waited for your phone \u2014 asking here now." : "Approval window exceeds this hook budget \u2014 asking here.");
125
+ }
97
126
  switch (timeoutAction) {
98
127
  case "approve":
99
128
  return allow();
@@ -123,7 +152,15 @@ var handlePushFirst = async (apiKey, description, projectName, pushFirstSeconds,
123
152
  } catch {
124
153
  return ask("Push notification failed, asking in terminal");
125
154
  }
126
- const deadline = Date.now() + pushFirstSeconds * 1e3;
155
+ if (result.suppressed) {
156
+ await cancelQuestion(apiKey, result.correlationId).catch(() => {
157
+ });
158
+ return ask("You are at the keyboard \u2014 approve here.");
159
+ }
160
+ if (result.noDevices) {
161
+ return ask("No device connected \u2014 asking in terminal.");
162
+ }
163
+ const deadline = hookWaitDeadline(START_MS, pushFirstSeconds, "claude", Date.now());
127
164
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, 1500);
128
165
  if (answer.answered) {
129
166
  if (isDeferAnswer(answer.value)) return ask("Handling on your machine");
@@ -1,9 +1,13 @@
1
+ import {
2
+ HOOK_BUDGETS
3
+ } from "./chunk-FDR5WYT7.js";
4
+
1
5
  // src/codex-config.ts
2
6
  import { createHash } from "crypto";
3
7
  var CODEX_HOOK_BINARY = "pushary-codex-hook";
4
8
  var CODEX_HOOK_EVENTS = [
5
- { event: "PermissionRequest", matcher: "Bash|apply_patch", timeout: 180, statusMessage: "Waiting for your phone" },
6
- { event: "PreToolUse", matcher: "Bash|apply_patch", timeout: 180, statusMessage: "Checking Pushary policy" },
9
+ { event: "PermissionRequest", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Waiting for your phone" },
10
+ { event: "PreToolUse", matcher: "Bash|apply_patch", timeout: HOOK_BUDGETS.codex.budgetSeconds, statusMessage: "Checking Pushary policy" },
7
11
  { event: "PostToolUse", matcher: "Bash|apply_patch", timeout: 10 },
8
12
  { event: "UserPromptSubmit", timeout: 10 },
9
13
  { event: "Stop", timeout: 10 },
@@ -25,6 +29,29 @@ var ensureRecord = (target, key) => {
25
29
  target[key] = created;
26
30
  return created;
27
31
  };
32
+ var CODEX_MCP_URL = "https://pushary.com/api/mcp/mcp";
33
+ var addCodexMcpServer = (config, apiKey) => {
34
+ const servers = ensureRecord(config, "mcp_servers");
35
+ const pushary = ensureRecord(servers, "pushary");
36
+ pushary.url = CODEX_MCP_URL;
37
+ ensureRecord(pushary, "http_headers").Authorization = `Bearer ${apiKey}`;
38
+ pushary.default_tools_approval_mode = "approve";
39
+ delete pushary.bearer_token_env_var;
40
+ delete pushary.tools;
41
+ };
42
+ var readCodexMcpAuth = (config) => {
43
+ const pushary = asRecord(asRecord(config.mcp_servers)?.pushary);
44
+ if (!pushary) return void 0;
45
+ const authorization = asRecord(pushary.http_headers)?.Authorization;
46
+ if (typeof authorization === "string" && authorization.startsWith("Bearer ")) {
47
+ return { method: "embedded", key: authorization.slice("Bearer ".length).trim() };
48
+ }
49
+ if (typeof pushary.bearer_token_env_var === "string") {
50
+ return { method: "env", envVar: pushary.bearer_token_env_var };
51
+ }
52
+ if (typeof pushary.bearer_token === "string") return { method: "legacy-bearer" };
53
+ return void 0;
54
+ };
28
55
  var isPusharyCodexHook = (entry) => {
29
56
  const hooks = asRecord(entry)?.hooks;
30
57
  if (!Array.isArray(hooks)) return false;
@@ -118,7 +145,7 @@ var addCodexHookTrust = (config, hooksJsonPath, command) => {
118
145
  var GEMINI_HOOK_BINARY = "pushary-gemini-hook";
119
146
  var GEMINI_MCP_URL = "https://pushary.com/api/mcp/mcp";
120
147
  var GEMINI_HOOK_EVENTS = [
121
- { event: "BeforeTool", matcher: "run_shell_command|write_file|replace", timeoutMs: 18e4 },
148
+ { event: "BeforeTool", matcher: "run_shell_command|write_file|replace", timeoutMs: HOOK_BUDGETS.gemini.budgetSeconds * 1e3 },
122
149
  { event: "AfterTool", matcher: "run_shell_command|write_file|replace", timeoutMs: 1e4 },
123
150
  { event: "BeforeAgent", timeoutMs: 1e4 },
124
151
  { event: "SessionStart", timeoutMs: 1e4 },
@@ -306,6 +333,8 @@ var execNpm = (args, options = {}) => {
306
333
  };
307
334
 
308
335
  export {
336
+ addCodexMcpServer,
337
+ readCodexMcpAuth,
309
338
  addCodexHooks,
310
339
  removeCodexHooks,
311
340
  hasCodexHooks,
@@ -80,7 +80,7 @@ declare const handlePostToolUse: (input: {
80
80
  cwd?: string;
81
81
  session_id?: string;
82
82
  transcript_path?: string;
83
- }, agent?: AgentIdentity) => Promise<void>;
83
+ }, agent?: AgentIdentity) => Promise<string | undefined>;
84
84
  interface StopHookOutput {
85
85
  readonly decision: 'block';
86
86
  readonly reason: string;
@@ -117,6 +117,12 @@ interface AskUserParams {
117
117
  interface AskUserResponse {
118
118
  correlationId: string;
119
119
  status: string;
120
+ delivery?: {
121
+ web: number;
122
+ mobile: number;
123
+ };
124
+ suppressed?: boolean;
125
+ noDevices?: boolean;
120
126
  }
121
127
  interface WaitForAnswerResponse {
122
128
  answered: boolean;
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-O2QL6U6G.js";
3
+ } from "../chunk-VHD3GMZ3.js";
4
4
  import "../chunk-KQYIHZ5E.js";
5
5
  import {
6
6
  askUser,
@@ -14,8 +14,9 @@ import {
14
14
  reportEvent,
15
15
  resolvePolicy,
16
16
  waitForAnswer
17
- } from "../chunk-GYPPVD3P.js";
17
+ } from "../chunk-TUILTFLS.js";
18
18
  import "../chunk-DWED7BS3.js";
19
+ import "../chunk-FDR5WYT7.js";
19
20
  import {
20
21
  getApiKey,
21
22
  getBaseUrl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "author": "Pushary <business@pushary.com>",
6
6
  "homepage": "https://pushary.com",
@@ -29,7 +29,8 @@
29
29
  "pushary-setup": "./dist/bin/pushary-setup.js",
30
30
  "pushary-clean": "./dist/bin/pushary-clean.js",
31
31
  "pushary-doctor": "./dist/bin/pushary-doctor.js",
32
- "pushary-mode": "./dist/bin/pushary-mode.js"
32
+ "pushary-mode": "./dist/bin/pushary-mode.js",
33
+ "pushary-wait": "./dist/bin/pushary-wait.js"
33
34
  },
34
35
  "files": [
35
36
  "dist",
@@ -38,7 +39,7 @@
38
39
  "scripts": {
39
40
  "build": "node scripts/bundle-plugin.mjs && tsup",
40
41
  "dev": "tsup --watch",
41
- "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts"
42
+ "test": "bun test src/api.test.ts && bun test src/claude-config.test.ts && bun test src/config.test.ts && bun test src/mcp-http.test.ts && bun test src/retry.test.ts && bun test src/usage.test.ts && bun test src/validate.test.ts && bun test src/policy.test.ts && bun test src/npm.test.ts && bun test src/identity.test.ts && bun test src/pending.test.ts && bun test src/events.test.ts && bun test src/describe.test.ts && bun test src/suggestions.test.ts && bun test src/safe-commands.test.ts && bun test src/hook.test.ts && bun test src/codex-adapter.test.ts && bun test src/codex-config.test.ts && bun test src/gemini-adapter.test.ts && bun test src/gemini-config.test.ts && bun test src/instruction-file.test.ts && bun test src/pairing.test.ts && bun test src/wait-ladder.test.ts"
42
43
  },
43
44
  "dependencies": {
44
45
  "@inquirer/prompts": "^8.4.2",