@pushary/agent-hooks 0.26.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.
@@ -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-SLX4AONG.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-A7DQCA2N.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-A7DQCA2N.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,
@@ -9,12 +12,15 @@ import {
9
12
  missingGeminiHookEvents,
10
13
  readCodexMcpAuth,
11
14
  untrustedCodexHookEvents
12
- } from "../chunk-SLX4AONG.js";
15
+ } from "../chunk-WXWHRXDK.js";
13
16
  import {
14
17
  callMcpTool,
15
18
  sendMcpRequest
16
19
  } from "../chunk-DWED7BS3.js";
17
- import "../chunk-NKXSILEW.js";
20
+ import "../chunk-FDR5WYT7.js";
21
+ import {
22
+ getBaseUrl
23
+ } from "../chunk-NKXSILEW.js";
18
24
 
19
25
  // bin/pushary-doctor.ts
20
26
  import { existsSync, readFileSync } from "fs";
@@ -407,6 +413,30 @@ var main = async () => {
407
413
  }
408
414
  }
409
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
+ }
410
440
  console.log();
411
441
  const failed = results.filter((r) => !r.passed);
412
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-A7DQCA2N.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-SA4BDS7T.js";
4
+ } from "../chunk-VHD3GMZ3.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
- import "../chunk-A7DQCA2N.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-A7DQCA2N.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-A7DQCA2N.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,7 +3,7 @@ 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,
@@ -16,12 +16,14 @@ import {
16
16
  renderAgentInstructions,
17
17
  renderProjectAgentInstructions,
18
18
  writeInstructionBlock
19
- } from "../chunk-SLX4AONG.js";
19
+ } from "../chunk-WXWHRXDK.js";
20
20
  import {
21
- isValidApiKey,
22
21
  reportEvent
23
- } from "../chunk-A7DQCA2N.js";
22
+ } from "../chunk-TUILTFLS.js";
24
23
  import "../chunk-DWED7BS3.js";
24
+ import {
25
+ isValidApiKey
26
+ } from "../chunk-FDR5WYT7.js";
25
27
  import "../chunk-NKXSILEW.js";
26
28
 
27
29
  // bin/pushary-setup.ts
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-A7DQCA2N.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
  }