@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
@@ -295,7 +295,19 @@ const handlePushOnly = async (apiKey, command, project, ident, timeoutSeconds, t
295
295
  }
296
296
  if (!asked?.correlationId) return ask()
297
297
 
298
- const realMs = Math.max(timeoutSeconds, 1) * 1000
298
+ // Keyboard bypass: at the keyboard, so Cursor's own prompt handles it now.
299
+ if (asked.suppressed) {
300
+ await callTool(apiKey, 'cancel_question', { correlationId: asked.correlationId }).catch(() => {})
301
+ return ask('You are at the keyboard — approve here.')
302
+ }
303
+ // No reachable device: apply the configured fallback immediately.
304
+ if (asked.noDevices) {
305
+ return fromTimeoutAction(timeoutAction, 'No device connected to approve on; denied per your Pushary policy.')
306
+ }
307
+
308
+ // `wait` holds the full block window for the phone (fromTimeoutAction maps it
309
+ // to ask, so on timeout it hands to Cursor's prompt, never auto-denies).
310
+ const realMs = timeoutAction === 'wait' ? MAX_BLOCK_MS : Math.max(timeoutSeconds, 1) * 1000
299
311
  const cap = Math.min(realMs, MAX_BLOCK_MS)
300
312
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
301
313
  if (answer.answered) {
@@ -319,6 +331,14 @@ const handlePushFirst = async (apiKey, command, project, ident, pushFirstSeconds
319
331
  }
320
332
  if (!asked?.correlationId) return ask()
321
333
 
334
+ // Keyboard bypass / no reachable device: push_first already falls to Cursor's
335
+ // prompt, so do it now rather than racing a push nobody will see.
336
+ if (asked.suppressed) {
337
+ await callTool(apiKey, 'cancel_question', { correlationId: asked.correlationId }).catch(() => {})
338
+ return ask('You are at the keyboard — approve here.')
339
+ }
340
+ if (asked.noDevices) return ask('No device connected — approve here.')
341
+
322
342
  const cap = Math.min(Math.max(pushFirstSeconds, 1) * 1000, MAX_BLOCK_MS)
323
343
  const answer = await pollForAnswer(apiKey, asked.correlationId, Date.now() + cap)
324
344
  if (answer.answered) {
@@ -2,13 +2,14 @@
2
2
  import {
3
3
  removeClaudeMcpServers,
4
4
  removePusharySettings
5
- } from "../chunk-5MA3CPZB.js";
5
+ } from "../chunk-MVNOP5IZ.js";
6
6
  import {
7
7
  execNpm,
8
8
  removeCodexHooks,
9
9
  removeGeminiSettings,
10
10
  removeInstructionBlock
11
- } from "../chunk-JPRRYC5X.js";
11
+ } from "../chunk-WXWHRXDK.js";
12
+ import "../chunk-FDR5WYT7.js";
12
13
 
13
14
  // bin/pushary-clean.ts
14
15
  import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
@@ -5,9 +5,9 @@ import {
5
5
  } from "../chunk-KQYIHZ5E.js";
6
6
  import {
7
7
  CODEX_AGENT,
8
- DECISION_LINE_MAX,
9
8
  DEFAULT_SESSION,
10
9
  askUser,
10
+ cancelQuestion,
11
11
  codexAllow,
12
12
  codexDeny,
13
13
  codexPass,
@@ -32,8 +32,14 @@ import {
32
32
  toCodexWire,
33
33
  toPolicyLookup,
34
34
  waitForAnswer
35
- } from "../chunk-GYPPVD3P.js";
35
+ } from "../chunk-TUILTFLS.js";
36
36
  import "../chunk-DWED7BS3.js";
37
+ import {
38
+ DECISION_LINE_MAX,
39
+ effectiveWaitSeconds,
40
+ hookWaitClamped,
41
+ hookWaitDeadline
42
+ } from "../chunk-FDR5WYT7.js";
37
43
  import {
38
44
  getApiKey
39
45
  } from "../chunk-NKXSILEW.js";
@@ -43,7 +49,6 @@ import { basename, join } from "path";
43
49
  import { tmpdir, userInfo } from "os";
44
50
  import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "fs";
45
51
  var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
46
- var MAX_WAIT_SECONDS = 170;
47
52
  var START_MS = Date.now();
48
53
  var APPROVAL_TTL_MS = 10 * 60 * 1e3;
49
54
  var sanitizeId = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
@@ -126,12 +131,20 @@ var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode) => {
126
131
  blocker: deriveBlocker(mode),
127
132
  actionBody: deriveActionBody(input.tool_name ?? "", input.tool_input ?? {})
128
133
  });
129
- const deadline = Math.min(
130
- Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3,
131
- START_MS + MAX_WAIT_SECONDS * 1e3
132
- );
134
+ if (result.suppressed || result.noDevices) {
135
+ return {
136
+ answer: { answered: false },
137
+ correlationId: result.correlationId,
138
+ suppressed: !!result.suppressed,
139
+ noDevices: !!result.noDevices,
140
+ clampCut: false
141
+ };
142
+ }
143
+ const now = Date.now();
144
+ const deadline = hookWaitDeadline(START_MS, waitSeconds, "codex", now);
145
+ const clampCut = hookWaitClamped(START_MS, waitSeconds, "codex", now);
133
146
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline);
134
- return { answer, correlationId: result.correlationId };
147
+ return { answer, correlationId: result.correlationId, suppressed: false, noDevices: false, clampCut };
135
148
  };
136
149
  var notifyApprovalNeeded = async (apiKey, input, lookup) => {
137
150
  try {
@@ -162,8 +175,13 @@ var decidePermissionRequest = async (input) => {
162
175
  if (claimNotify(input.tool_use_id)) await notifyApprovalNeeded(apiKey, input, lookup);
163
176
  return codexPass();
164
177
  }
165
- const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : toolPolicy.timeoutSeconds;
166
- const { answer, correlationId } = await pushQuestion(apiKey, input, lookup, waitSeconds, toolPolicy.mode);
178
+ const waitSeconds = toolPolicy.mode === "push_first" ? toolPolicy.pushFirstSeconds : effectiveWaitSeconds(toolPolicy.timeoutAction, toolPolicy.timeoutSeconds, "codex");
179
+ const { answer, correlationId, suppressed, clampCut } = await pushQuestion(apiKey, input, lookup, waitSeconds, toolPolicy.mode);
180
+ if (suppressed) {
181
+ await cancelQuestion(apiKey, correlationId).catch(() => {
182
+ });
183
+ return codexPass();
184
+ }
167
185
  if (answer.answered) {
168
186
  if (isDeferAnswer(answer.value)) return codexPass();
169
187
  if (answer.value === "yes") {
@@ -174,6 +192,7 @@ var decidePermissionRequest = async (input) => {
174
192
  }
175
193
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
176
194
  if (toolPolicy.mode === "push_first") return codexPass();
195
+ if (clampCut) return codexPass();
177
196
  const decision = permissionTimeoutDecision(toolPolicy.timeoutAction);
178
197
  if (decision.kind === "allow") markApproved(input.tool_use_id);
179
198
  return decision;
@@ -197,16 +216,22 @@ var decidePreToolUse = async (input) => {
197
216
  if (consumeApproval(input.tool_use_id)) return codexPass();
198
217
  let pushed;
199
218
  try {
200
- pushed = await pushQuestion(apiKey, input, lookup, toolPolicy.timeoutSeconds, "push_only");
219
+ pushed = await pushQuestion(apiKey, input, lookup, effectiveWaitSeconds(toolPolicy.timeoutAction, toolPolicy.timeoutSeconds, "codex"), "push_only");
201
220
  } catch {
202
221
  return preToolUseTimeoutDecision(toolPolicy.timeoutAction, "Push notification failed, denying per policy");
203
222
  }
204
- const { answer, correlationId } = pushed;
223
+ const { answer, correlationId, suppressed, clampCut } = pushed;
224
+ if (suppressed) {
225
+ await cancelQuestion(apiKey, correlationId).catch(() => {
226
+ });
227
+ return codexPass();
228
+ }
205
229
  if (answer.answered) {
206
230
  if (isDeferAnswer(answer.value)) return codexPass();
207
231
  return answer.value === "yes" ? codexPass() : codexDeny(denyReasonFrom(answer.value));
208
232
  }
209
233
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
234
+ if (clampCut) return codexPass();
210
235
  return preToolUseTimeoutDecision(toolPolicy.timeoutAction);
211
236
  }
212
237
  if (toolPolicy.mode === "notify_only") {
@@ -4,8 +4,9 @@ import {
4
4
  getMachineId,
5
5
  reportEvent,
6
6
  waitForAnswer
7
- } from "../chunk-GYPPVD3P.js";
7
+ } from "../chunk-TUILTFLS.js";
8
8
  import "../chunk-DWED7BS3.js";
9
+ import "../chunk-FDR5WYT7.js";
9
10
  import {
10
11
  getApiKey
11
12
  } from "../chunk-NKXSILEW.js";
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ describeWaitLadder
4
+ } from "../chunk-IIENZE2J.js";
2
5
  import {
3
6
  GEMINI_HOOK_BINARY,
4
7
  execNpm,
@@ -7,13 +10,17 @@ import {
7
10
  hasInstructionBlock,
8
11
  missingCodexHookEvents,
9
12
  missingGeminiHookEvents,
13
+ readCodexMcpAuth,
10
14
  untrustedCodexHookEvents
11
- } from "../chunk-JPRRYC5X.js";
15
+ } from "../chunk-WXWHRXDK.js";
12
16
  import {
13
17
  callMcpTool,
14
18
  sendMcpRequest
15
19
  } from "../chunk-DWED7BS3.js";
16
- import "../chunk-NKXSILEW.js";
20
+ import "../chunk-FDR5WYT7.js";
21
+ import {
22
+ getBaseUrl
23
+ } from "../chunk-NKXSILEW.js";
17
24
 
18
25
  // bin/pushary-doctor.ts
19
26
  import { existsSync, readFileSync } from "fs";
@@ -171,22 +178,33 @@ var main = async () => {
171
178
  }
172
179
  const codexMcp = codexParsed.mcp_servers?.pushary;
173
180
  const codexTransport = !!codexMcp && (typeof codexMcp.url === "string" || typeof codexMcp.command === "string");
174
- const codexEnvVar = codexMcp && typeof codexMcp.bearer_token_env_var === "string" ? codexMcp.bearer_token_env_var : void 0;
175
- const codexHasCred = !!codexEnvVar || !!codexMcp && typeof codexMcp.bearer_token === "string";
176
- const codexMcpOk = !!codexMcp && codexTransport && codexHasCred;
177
- check(codexMcpOk, "Codex: MCP server configured", codexMcpOk ? void 0 : !codexMcp ? "no [mcp_servers.pushary] \u2014 re-run setup" : !codexTransport ? "entry has no url \u2014 `codex mcp add` likely failed, re-run setup" : "entry has no bearer_token/_env_var \u2014 re-run setup");
181
+ const codexAuth = readCodexMcpAuth(codexParsed);
182
+ const codexMcpOk = !!codexMcp && codexTransport && !!codexAuth;
183
+ check(codexMcpOk, "Codex: MCP server configured", codexMcpOk ? void 0 : !codexMcp ? "no [mcp_servers.pushary] \u2014 re-run setup" : !codexTransport ? "entry has no url \u2014 re-run setup" : "entry has no Authorization header or bearer token \u2014 re-run setup");
178
184
  if (codexMcp) {
179
185
  const hasAutoApprove = codexMcp.default_tools_approval_mode === "approve";
180
186
  check(hasAutoApprove, "Codex: tools auto-allowed", hasAutoApprove ? "default_tools_approval_mode = approve" : "missing \u2014 MCP calls will prompt for approval");
181
187
  if (codexMcp.tools && typeof codexMcp.tools === "object") {
182
188
  console.log(` ${warn} Codex: per-tool approval overrides detected ${dim("(redundant with default_tools_approval_mode)")}`);
183
189
  }
184
- if (codexEnvVar) {
185
- const envValue = process.env[codexEnvVar]?.trim();
186
- check(!!envValue, `Codex: MCP key available in shell ($${codexEnvVar})`, envValue ? "set" : `not set \u2014 Codex reads its key from your shell; the ~/.pushary/config.json fallback only covers hooks. Add the export to your profile or re-run setup`);
190
+ if (codexAuth?.method === "embedded") {
191
+ const keyMatches = !apiKey || codexAuth.key === apiKey;
192
+ check(keyMatches, "Codex: MCP key embedded (config.toml)", keyMatches ? "no shell export needed" : "differs from your active key \u2014 re-run setup");
193
+ } else if (codexAuth?.method === "env") {
194
+ const envValue = process.env[codexAuth.envVar]?.trim();
195
+ const exportedInProfile = SHELL_FILES.some((f) => {
196
+ try {
197
+ return readFileSync(f, "utf-8").includes(`export ${codexAuth.envVar}=`);
198
+ } catch {
199
+ return false;
200
+ }
201
+ });
202
+ check(!!envValue, `Codex: MCP key available in shell ($${codexAuth.envVar})`, envValue ? "set \u2014 re-run setup to embed the key and drop the shell dependency" : exportedInProfile ? "exported in your profile but not in this shell \u2014 restart the terminal, or re-run setup to embed the key in config.toml" : "not set \u2014 Codex reads its key from your shell; re-run setup to embed the key in config.toml");
187
203
  if (envValue && apiKey && envValue !== apiKey) {
188
- console.log(` ${warn} Codex: $${codexEnvVar} differs from your active key ${dim("(rotate or re-run setup so hooks and MCP match)")}`);
204
+ console.log(` ${warn} Codex: $${codexAuth.envVar} differs from your active key ${dim("(rotate or re-run setup so hooks and MCP match)")}`);
189
205
  }
206
+ } else if (codexAuth?.method === "legacy-bearer") {
207
+ console.log(` ${warn} Codex: legacy bearer_token entry ${dim("(re-run setup to embed the key as an Authorization header)")}`);
190
208
  }
191
209
  }
192
210
  const codexNotifyPath = codexConfig.match(/["']([^"']*pushary-codex[^"']*)["']/)?.[1] ?? null;
@@ -395,6 +413,30 @@ var main = async () => {
395
413
  }
396
414
  }
397
415
  }
416
+ if (apiKey) {
417
+ try {
418
+ const res = await fetch(`${getBaseUrl()}/api/mcp/policy`, {
419
+ headers: { Authorization: `Bearer ${apiKey}` }
420
+ });
421
+ if (res.ok) {
422
+ const policy = await res.json();
423
+ console.log();
424
+ console.log(` ${dim("Wait Ladder")} ${dim("(what happens when you don't answer)")}`);
425
+ const wildcard = policy.policies?.find((p) => p.tool === "*");
426
+ const defaultWindow = policy.defaultPushFirstSeconds ?? 20;
427
+ console.log(` ${dim("\xB7")} default: ${cyan(`${defaultWindow}s`)} ${dim("\u2014")} ${wildcard ? describeWaitLadder(wildcard) : `ping your phone ${defaultWindow}s, then ask in the terminal`}`);
428
+ if (policy.modeOverride) {
429
+ console.log(` ${warn} mode override active: ${cyan(policy.modeOverride)} ${dim("(pushary mode status)")}`);
430
+ }
431
+ const overrides = (policy.policies ?? []).filter((p) => p.tool !== "*").slice(0, 4);
432
+ for (const p of overrides) {
433
+ console.log(` ${dim("\xB7")} ${p.tool}: ${describeWaitLadder(p)}`);
434
+ }
435
+ console.log(` ${dim("Tune it:")} ${cyan("pushary wait <seconds>")} ${dim("\xB7 full ladder:")} ${cyan("pushary wait status")}`);
436
+ }
437
+ } catch {
438
+ }
439
+ }
398
440
  console.log();
399
441
  const failed = results.filter((r) => !r.passed);
400
442
  if (failed.length === 0) {
@@ -4,9 +4,9 @@ import {
4
4
  isDeferAnswer
5
5
  } from "../chunk-KQYIHZ5E.js";
6
6
  import {
7
- DECISION_LINE_MAX,
8
7
  DEFAULT_SESSION,
9
8
  askUser,
9
+ cancelQuestion,
10
10
  deriveActionBody,
11
11
  deriveBlocker,
12
12
  deriveToolTarget,
@@ -24,8 +24,14 @@ import {
24
24
  savePendingQuestion,
25
25
  sendNotification,
26
26
  waitForAnswer
27
- } from "../chunk-GYPPVD3P.js";
27
+ } from "../chunk-TUILTFLS.js";
28
28
  import "../chunk-DWED7BS3.js";
29
+ import {
30
+ DECISION_LINE_MAX,
31
+ effectiveWaitSeconds,
32
+ hookWaitClamped,
33
+ hookWaitDeadline
34
+ } from "../chunk-FDR5WYT7.js";
29
35
  import {
30
36
  getApiKey
31
37
  } from "../chunk-NKXSILEW.js";
@@ -68,7 +74,6 @@ var geminiTimeoutDecision = (timeoutAction, denyReason = "No response within tim
68
74
 
69
75
  // bin/pushary-gemini-hook.ts
70
76
  var KILL_REASON = "Stopped by user: this agent was halted from Pushary";
71
- var MAX_WAIT_SECONDS = 170;
72
77
  var START_MS = Date.now();
73
78
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
74
79
  var pollForAnswer = async (apiKey, correlationId, deadlineMs, pollInterval = 2e3) => {
@@ -106,12 +111,20 @@ var pushQuestion = async (apiKey, input, lookup, waitSeconds, mode, pollInterval
106
111
  blocker: deriveBlocker(mode),
107
112
  actionBody: deriveActionBody(input.tool_name ?? "", input.tool_input ?? {})
108
113
  });
109
- const deadline = Math.min(
110
- Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3,
111
- START_MS + MAX_WAIT_SECONDS * 1e3
112
- );
114
+ if (result.suppressed || result.noDevices) {
115
+ return {
116
+ answer: { answered: false },
117
+ correlationId: result.correlationId,
118
+ suppressed: !!result.suppressed,
119
+ noDevices: !!result.noDevices,
120
+ clampCut: false
121
+ };
122
+ }
123
+ const now = Date.now();
124
+ const deadline = hookWaitDeadline(START_MS, waitSeconds, "gemini", now);
125
+ const clampCut = hookWaitClamped(START_MS, waitSeconds, "gemini", now);
113
126
  const answer = await pollForAnswer(apiKey, result.correlationId, deadline, pollInterval);
114
- return { answer, correlationId: result.correlationId };
127
+ return { answer, correlationId: result.correlationId, suppressed: false, noDevices: false, clampCut };
115
128
  };
116
129
  var notifyApprovalNeeded = async (apiKey, input, lookup) => {
117
130
  try {
@@ -128,16 +141,22 @@ var notifyApprovalNeeded = async (apiKey, input, lookup) => {
128
141
  var handlePushOnly = async (apiKey, input, lookup, timeoutSeconds, timeoutAction) => {
129
142
  let pushed;
130
143
  try {
131
- pushed = await pushQuestion(apiKey, input, lookup, timeoutSeconds, "push_only");
144
+ pushed = await pushQuestion(apiKey, input, lookup, effectiveWaitSeconds(timeoutAction, timeoutSeconds, "gemini"), "push_only");
132
145
  } catch {
133
146
  return geminiTimeoutDecision(timeoutAction, "Push notification failed, denying per policy");
134
147
  }
135
- const { answer, correlationId } = pushed;
148
+ const { answer, correlationId, suppressed, clampCut } = pushed;
149
+ if (suppressed) {
150
+ await cancelQuestion(apiKey, correlationId).catch(() => {
151
+ });
152
+ return geminiPass();
153
+ }
136
154
  if (answer.answered) {
137
155
  if (isDeferAnswer(answer.value)) return geminiPass();
138
156
  return answer.value === "yes" ? geminiAllow() : geminiDeny(denyReasonFrom(answer.value));
139
157
  }
140
158
  savePendingQuestion(input.session_id || DEFAULT_SESSION, correlationId);
159
+ if (clampCut) return geminiPass();
141
160
  return geminiTimeoutDecision(timeoutAction);
142
161
  };
143
162
  var handlePushFirst = async (apiKey, input, lookup, pushFirstSeconds) => {
@@ -147,7 +166,13 @@ var handlePushFirst = async (apiKey, input, lookup, pushFirstSeconds) => {
147
166
  } catch {
148
167
  return geminiPass();
149
168
  }
150
- const { answer, correlationId } = pushed;
169
+ const { answer, correlationId, suppressed, noDevices } = pushed;
170
+ if (suppressed) {
171
+ await cancelQuestion(apiKey, correlationId).catch(() => {
172
+ });
173
+ return geminiPass();
174
+ }
175
+ if (noDevices) return geminiPass();
151
176
  if (answer.answered) {
152
177
  if (isDeferAnswer(answer.value)) return geminiPass();
153
178
  return answer.value === "yes" ? geminiAllow() : geminiDeny(denyReasonFrom(answer.value));
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-O2QL6U6G.js";
4
+ } from "../chunk-VHD3GMZ3.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
- import "../chunk-GYPPVD3P.js";
6
+ import "../chunk-TUILTFLS.js";
7
7
  import "../chunk-DWED7BS3.js";
8
+ import "../chunk-FDR5WYT7.js";
8
9
  import "../chunk-NKXSILEW.js";
9
10
 
10
11
  // bin/pushary-hook.ts
@@ -19,9 +19,11 @@ var parseDuration = (value) => {
19
19
  var main = async () => {
20
20
  const apiKey = getApiKey();
21
21
  const baseUrl = getBaseUrl();
22
- const mode = process.argv[2];
23
- const forFlag = process.argv[3];
24
- const forValue = process.argv[4];
22
+ const rest = process.argv.slice(2);
23
+ const args = rest[0] === "mode" ? rest.slice(1) : rest;
24
+ const mode = args[0];
25
+ const forFlag = args[1];
26
+ const forValue = args[2];
25
27
  const headers = {
26
28
  "Authorization": `Bearer ${apiKey}`,
27
29
  "Content-Type": "application/json"
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-GYPPVD3P.js";
4
+ } from "../chunk-TUILTFLS.js";
5
5
  import "../chunk-DWED7BS3.js";
6
+ import "../chunk-FDR5WYT7.js";
6
7
  import "../chunk-NKXSILEW.js";
7
8
 
8
9
  // bin/pushary-post-hook.ts
@@ -16,7 +17,12 @@ var main = async () => {
16
17
  }
17
18
  try {
18
19
  const input = JSON.parse(rawInput);
19
- await handlePostToolUse(input);
20
+ const additionalContext = await handlePostToolUse(input);
21
+ if (additionalContext) {
22
+ process.stdout.write(JSON.stringify({
23
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext }
24
+ }));
25
+ }
20
26
  } catch {
21
27
  }
22
28
  };
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-GYPPVD3P.js";
4
+ } from "../chunk-TUILTFLS.js";
5
5
  import "../chunk-DWED7BS3.js";
6
+ import "../chunk-FDR5WYT7.js";
6
7
  import "../chunk-NKXSILEW.js";
7
8
 
8
9
  // bin/pushary-prompt-hook.ts
@@ -16,7 +17,12 @@ var main = async () => {
16
17
  }
17
18
  try {
18
19
  const input = JSON.parse(rawInput);
19
- await handleUserPrompt(input);
20
+ const additionalContext = await handleUserPrompt(input);
21
+ if (additionalContext) {
22
+ process.stdout.write(JSON.stringify({
23
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext }
24
+ }));
25
+ }
20
26
  } catch {
21
27
  }
22
28
  };
@@ -3,11 +3,12 @@ import {
3
3
  addClaudeMcpServer,
4
4
  addPusharyHooks,
5
5
  addPusharyToolPermissions
6
- } from "../chunk-5MA3CPZB.js";
6
+ } from "../chunk-MVNOP5IZ.js";
7
7
  import {
8
8
  GEMINI_HOOK_BINARY,
9
9
  addCodexHookTrust,
10
10
  addCodexHooks,
11
+ addCodexMcpServer,
11
12
  addGeminiHooks,
12
13
  addGeminiMcpServer,
13
14
  execNpm,
@@ -15,12 +16,14 @@ import {
15
16
  renderAgentInstructions,
16
17
  renderProjectAgentInstructions,
17
18
  writeInstructionBlock
18
- } from "../chunk-JPRRYC5X.js";
19
+ } from "../chunk-WXWHRXDK.js";
19
20
  import {
20
- isValidApiKey,
21
21
  reportEvent
22
- } from "../chunk-GYPPVD3P.js";
22
+ } from "../chunk-TUILTFLS.js";
23
23
  import "../chunk-DWED7BS3.js";
24
+ import {
25
+ isValidApiKey
26
+ } from "../chunk-FDR5WYT7.js";
24
27
  import "../chunk-NKXSILEW.js";
25
28
 
26
29
  // bin/pushary-setup.ts
@@ -602,7 +605,7 @@ var addCodexNotifyEntry = (codexConfig) => {
602
605
  writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
603
606
  }
604
607
  };
605
- var setupCodex = async (_apiKey) => {
608
+ var setupCodex = async (apiKey) => {
606
609
  console.log(`
607
610
  ${bold2("Setting up Codex")}
608
611
  `);
@@ -613,28 +616,14 @@ var setupCodex = async (_apiKey) => {
613
616
  }
614
617
  await installGlobally();
615
618
  const codexConfig = join(homedir(), ".codex", "config.toml");
616
- await spinner("Adding Pushary MCP server to Codex", async () => {
617
- try {
618
- execSync(
619
- "codex mcp add pushary --url https://pushary.com/api/mcp/mcp --bearer-token-env-var PUSHARY_API_KEY",
620
- { stdio: "ignore", timeout: 15e3 }
621
- );
622
- } catch {
623
- }
624
- });
625
- await spinner("Auto-allowing all Pushary tools", async () => {
619
+ await spinner("Adding Pushary MCP server (key embedded, auto-allowed)", async () => {
626
620
  let raw = "";
627
621
  try {
628
622
  raw = readFileSync(codexConfig, "utf-8");
629
623
  } catch {
630
624
  }
631
625
  const config = raw ? parseTOML(raw) : {};
632
- const mcpServers = config.mcp_servers ?? {};
633
- const pushary = mcpServers.pushary ?? {};
634
- pushary.default_tools_approval_mode = "approve";
635
- delete pushary.tools;
636
- mcpServers.pushary = pushary;
637
- config.mcp_servers = mcpServers;
626
+ addCodexMcpServer(config, apiKey);
638
627
  writeFileSync(codexConfig, stringifyTOML(config), "utf-8");
639
628
  });
640
629
  const codexVersion = readCodexVersion();
@@ -681,6 +670,7 @@ var setupCodex = async (_apiKey) => {
681
670
  console.log();
682
671
  console.log(` ${dim2("What this configured:")}`);
683
672
  console.log(` ${dim2("\u2022")} MCP server: Codex can send notifications and ask questions`);
673
+ console.log(` ${dim2("\u2022")} API key embedded in config.toml: works without a shell reload or export`);
684
674
  console.log(` ${dim2("\u2022")} Proactive instructions: ~/.codex/AGENTS.md tells Codex to ask via push, not the terminal`);
685
675
  console.log(` ${dim2("\u2022")} Auto-allowed tools: no permission prompts for Pushary MCP calls`);
686
676
  if (hooksSupported) {
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-GYPPVD3P.js";
4
+ } from "../chunk-TUILTFLS.js";
5
5
  import "../chunk-DWED7BS3.js";
6
+ import "../chunk-FDR5WYT7.js";
6
7
  import "../chunk-NKXSILEW.js";
7
8
 
8
9
  // bin/pushary-stop-hook.ts
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ describeWaitLadder
4
+ } from "../chunk-IIENZE2J.js";
5
+ import {
6
+ getApiKey,
7
+ getBaseUrl
8
+ } from "../chunk-NKXSILEW.js";
9
+
10
+ // bin/pushary-wait.ts
11
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
12
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
13
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
14
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
15
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
16
+ var MAX_WINDOW = 300;
17
+ var printLadder = (data) => {
18
+ console.log();
19
+ console.log(` ${bold("Wait ladder")} ${dim("(what happens when you don't answer)")}`);
20
+ console.log();
21
+ console.log(` Default push window: ${cyan(`${data.defaultPushFirstSeconds}s`)} ${dim("(catch-all tools)")}`);
22
+ if (data.modeOverride) {
23
+ console.log(` ${yellow("!")} Active mode override: ${cyan(data.modeOverride)} ${dim("(pushary mode status)")}`);
24
+ }
25
+ console.log();
26
+ const rows = [...data.policies].sort((a, b) => a.tool === "*" ? -1 : b.tool === "*" ? 1 : a.tool.localeCompare(b.tool));
27
+ const width = Math.max(...rows.map((p) => p.tool.length), 4);
28
+ for (const p of rows) {
29
+ const label = p.tool === "*" ? "default (*)" : p.tool;
30
+ console.log(` ${label.padEnd(width + 2)} ${dim("\xB7")} ${describeWaitLadder(p)}`);
31
+ }
32
+ console.log();
33
+ console.log(` ${dim("Set the default window:")} ${cyan("pushary wait <seconds>")}`);
34
+ console.log(` ${dim("Reset it:")} ${cyan("pushary wait clear")}`);
35
+ console.log(` ${dim("Per-tool rules live in the dashboard under Agent \u2192 Policies.")}`);
36
+ };
37
+ var fetchPolicy = async (baseUrl, headers) => {
38
+ const res = await fetch(`${baseUrl}/api/mcp/policy`, { headers });
39
+ if (!res.ok) throw new Error(`policy fetch failed (${res.status})`);
40
+ return res.json();
41
+ };
42
+ var setWindow = async (baseUrl, headers, seconds) => {
43
+ const res = await fetch(`${baseUrl}/api/mcp/policy`, {
44
+ method: "PATCH",
45
+ headers,
46
+ body: JSON.stringify({ defaultPushFirstSeconds: seconds })
47
+ });
48
+ if (!res.ok) {
49
+ const err = await res.json().catch(() => ({}));
50
+ throw new Error(err.error ?? res.statusText);
51
+ }
52
+ const data = await res.json();
53
+ return data.defaultPushFirstSeconds;
54
+ };
55
+ var main = async () => {
56
+ const apiKey = getApiKey();
57
+ const baseUrl = getBaseUrl();
58
+ const headers = {
59
+ "Authorization": `Bearer ${apiKey}`,
60
+ "Content-Type": "application/json"
61
+ };
62
+ const rest = process.argv.slice(2);
63
+ const args = rest[0] === "wait" ? rest.slice(1) : rest;
64
+ const arg = args[0];
65
+ if (!arg || arg === "status") {
66
+ printLadder(await fetchPolicy(baseUrl, headers));
67
+ return;
68
+ }
69
+ if (arg === "clear" || arg === "reset") {
70
+ const value2 = await setWindow(baseUrl, headers, 20);
71
+ console.log(` ${green("\u2713")} Default push window reset to ${cyan(`${value2}s`)}`);
72
+ return;
73
+ }
74
+ const seconds = Number(arg);
75
+ if (!Number.isFinite(seconds) || seconds < 0 || seconds > MAX_WINDOW) {
76
+ console.log(` ${yellow("!")} Invalid window: ${arg} ${dim(`(use 0-${MAX_WINDOW} seconds, or "status" / "clear")`)}`);
77
+ console.log(` ${dim("Usage: pushary wait <seconds> | status | clear")}`);
78
+ return;
79
+ }
80
+ const value = await setWindow(baseUrl, headers, Math.trunc(seconds));
81
+ console.log(` ${green("\u2713")} Default push window set to ${cyan(`${value}s`)} ${dim("\u2014 ping your phone that long, then the terminal takes over")}`);
82
+ };
83
+ main().catch((err) => {
84
+ console.error(` ${yellow("!")} ${err instanceof Error ? err.message : err}`);
85
+ process.exit(1);
86
+ });
@@ -12,6 +12,8 @@ if (command === "setup") {
12
12
  await import("./pushary-doctor.js");
13
13
  } else if (command === "mode") {
14
14
  await import("./pushary-mode.js");
15
+ } else if (command === "wait") {
16
+ await import("./pushary-wait.js");
15
17
  } else {
16
18
  console.log(`
17
19
  Pushary Agent Hooks
@@ -21,6 +23,7 @@ Commands:
21
23
  doctor Verify your Pushary installation is working
22
24
  clean Remove all Pushary configuration
23
25
  mode Switch approval mode (push_only, push_first, terminal_only)
26
+ wait Show or set the "wait for your phone" ladder (pushary wait 45)
24
27
  hook Run as a PreToolUse hook (reads stdin, writes stdout)
25
28
 
26
29
  Setup options:
@@ -34,5 +37,6 @@ Usage:
34
37
  npx @pushary/agent-hooks@latest setup --key pk_xxx.xxx --skip-phone
35
38
  npx @pushary/agent-hooks@latest doctor
36
39
  npx @pushary/agent-hooks@latest mode push_only --for 30m
40
+ npx @pushary/agent-hooks@latest wait 45
37
41
  `);
38
42
  }