@pushary/agent-hooks 0.38.0 → 0.40.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.
@@ -1,6 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- getApiKey
3
+ isDeferAnswer
4
+ } from "../chunk-KQYIHZ5E.js";
5
+ import {
6
+ askUser,
7
+ cancelQuestion,
8
+ deriveToolTarget,
9
+ describeToolCall,
10
+ fetchModeState,
11
+ getMachineId,
12
+ getPolicy,
13
+ resolvePolicy,
14
+ waitForAnswer
15
+ } from "../chunk-AUEPQATK.js";
16
+ import "../chunk-DWED7BS3.js";
17
+ import "../chunk-Z5PL3K7C.js";
18
+ import {
19
+ getApiKey,
20
+ getBaseUrl
4
21
  } from "../chunk-NKXSILEW.js";
5
22
 
6
23
  // src/wrapper/claudeBinary.ts
@@ -74,57 +91,500 @@ var runLocalPassthrough = (binary, args2) => {
74
91
  });
75
92
  };
76
93
 
94
+ // src/wrapper/remoteMode.ts
95
+ import { basename } from "path";
96
+
97
+ // src/wrapper/sdkLoader.ts
98
+ var userMessage = (text) => ({
99
+ type: "user",
100
+ message: { role: "user", content: text },
101
+ parent_tool_use_id: null
102
+ });
103
+ var loadClaudeSdk = async () => {
104
+ const specifier = "@anthropic-ai/claude-agent-sdk";
105
+ try {
106
+ const mod = await import(specifier);
107
+ if (typeof mod.query === "function") return mod;
108
+ if (mod.default && typeof mod.default.query === "function") return mod.default;
109
+ return null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ };
114
+
115
+ // src/wrapper/messageQueue.ts
116
+ var BoundedInputQueue = class {
117
+ buffer = [];
118
+ waiter;
119
+ closed = false;
120
+ cap;
121
+ onDrop;
122
+ pushedCount = 0;
123
+ deliveredCount = 0;
124
+ droppedCount = 0;
125
+ constructor(options = {}) {
126
+ this.cap = Math.max(1, options.cap ?? 32);
127
+ this.onDrop = options.onDrop;
128
+ }
129
+ push(item) {
130
+ if (this.closed) return;
131
+ this.pushedCount++;
132
+ if (this.waiter) {
133
+ const resolve = this.waiter;
134
+ this.waiter = void 0;
135
+ this.deliveredCount++;
136
+ resolve({ value: item, done: false });
137
+ return;
138
+ }
139
+ this.buffer.push(item);
140
+ while (this.buffer.length > this.cap) {
141
+ const dropped = this.buffer.shift();
142
+ this.droppedCount++;
143
+ this.onDrop?.(dropped, this.droppedCount);
144
+ }
145
+ }
146
+ // Mark done. Any parked consumer completes; further push() is ignored. The
147
+ // buffer is cleared so nothing is retained after shutdown.
148
+ close() {
149
+ if (this.closed) return;
150
+ this.closed = true;
151
+ this.buffer.length = 0;
152
+ if (this.waiter) {
153
+ const resolve = this.waiter;
154
+ this.waiter = void 0;
155
+ resolve({ value: void 0, done: true });
156
+ }
157
+ }
158
+ // End the CURRENT consumer without closing the queue, so a fresh query can
159
+ // attach after a restart and keep draining the same buffered items. Resolving
160
+ // the parked waiter as done lets the abandoned query's for-await unwind cleanly
161
+ // instead of hanging on a promise that would otherwise never settle.
162
+ detachConsumer() {
163
+ if (this.waiter) {
164
+ const resolve = this.waiter;
165
+ this.waiter = void 0;
166
+ resolve({ value: void 0, done: true });
167
+ }
168
+ }
169
+ get isClosed() {
170
+ return this.closed;
171
+ }
172
+ get bufferedCount() {
173
+ return this.buffer.length;
174
+ }
175
+ get stats() {
176
+ return { pushed: this.pushedCount, delivered: this.deliveredCount, dropped: this.droppedCount };
177
+ }
178
+ [Symbol.asyncIterator]() {
179
+ return {
180
+ next: () => {
181
+ if (this.buffer.length > 0) {
182
+ const value = this.buffer.shift();
183
+ this.deliveredCount++;
184
+ return Promise.resolve({ value, done: false });
185
+ }
186
+ if (this.closed) {
187
+ return Promise.resolve({ value: void 0, done: true });
188
+ }
189
+ if (this.waiter) {
190
+ return Promise.reject(
191
+ new Error("BoundedInputQueue: concurrent consumers are not supported")
192
+ );
193
+ }
194
+ return new Promise((resolve) => {
195
+ this.waiter = resolve;
196
+ });
197
+ },
198
+ return: () => {
199
+ this.close();
200
+ return Promise.resolve({ value: void 0, done: true });
201
+ }
202
+ };
203
+ }
204
+ };
205
+
206
+ // src/wrapper/approver.ts
207
+ var DEFAULT_MAX_CONCURRENT = 16;
208
+ var DEFAULT_APPROVAL_WINDOW_MS = 5 * 6e4;
209
+ var MAX_APPROVAL_WINDOW_MS = 30 * 6e4;
210
+ var allow = (input) => ({
211
+ behavior: "allow",
212
+ updatedInput: input
213
+ });
214
+ var deny = (message) => ({ behavior: "deny", message });
215
+ var createRemoteApprover = (deps) => {
216
+ const fetchModeState2 = deps.fetchModeState ?? fetchModeState;
217
+ const getPolicy2 = deps.getPolicy ?? getPolicy;
218
+ const resolvePolicy2 = deps.resolvePolicy ?? resolvePolicy;
219
+ const askUser2 = deps.askUser ?? askUser;
220
+ const waitForAnswer2 = deps.waitForAnswer ?? waitForAnswer;
221
+ const cancelQuestion2 = deps.cancelQuestion ?? cancelQuestion;
222
+ const now = deps.now ?? (() => Date.now());
223
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
224
+ const maxConcurrent = Math.max(1, deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT);
225
+ const pending = /* @__PURE__ */ new Set();
226
+ let tornDown = false;
227
+ const windowFor = (policy) => {
228
+ const base = policy.timeoutSeconds > 0 ? policy.timeoutSeconds * 1e3 : deps.approvalWindowMs ?? DEFAULT_APPROVAL_WINDOW_MS;
229
+ const ceiling = policy.timeoutAction === "wait" ? MAX_APPROVAL_WINDOW_MS : base;
230
+ return Math.min(Math.max(base, 15e3), Math.max(ceiling, 15e3));
231
+ };
232
+ const applyTimeout = (policy, input) => policy.timeoutAction === "approve" ? allow(input) : deny("No approval received in time");
233
+ const waitForPhone = async (entry, correlationId, windowMs) => {
234
+ const deadline = now() + windowMs;
235
+ while (now() < deadline && !entry.controller.signal.aborted) {
236
+ const remaining = Math.min(Math.max(deadline - now(), 1e3), 3e4);
237
+ const abortPromise = new Promise((resolve) => {
238
+ entry.controller.signal.addEventListener("abort", () => resolve({ answered: false, aborted: true }), {
239
+ once: true
240
+ });
241
+ });
242
+ let outcome;
243
+ try {
244
+ outcome = await Promise.race([
245
+ waitForAnswer2(deps.apiKey, correlationId, remaining),
246
+ abortPromise
247
+ ]);
248
+ } catch {
249
+ if (now() + 1e3 >= deadline) break;
250
+ await sleep(1e3);
251
+ continue;
252
+ }
253
+ if ("aborted" in outcome) return { answered: false };
254
+ if (outcome.answered) return outcome;
255
+ if (now() + 500 >= deadline) break;
256
+ await sleep(500);
257
+ }
258
+ return { answered: false };
259
+ };
260
+ const canUseTool = async (toolName, input, options) => {
261
+ if (toolName.startsWith("mcp__pushary__")) return allow(input);
262
+ if (tornDown) return deny("Remote session is shutting down");
263
+ if (pending.size >= maxConcurrent) {
264
+ deps.log?.(`[pushary] too many concurrent approvals (${pending.size}); denying ${toolName}`);
265
+ return deny("Too many pending approvals; try again");
266
+ }
267
+ const controller = new AbortController();
268
+ const entry = { controller };
269
+ pending.add(entry);
270
+ const outerAbort = () => controller.abort();
271
+ if (options?.signal) {
272
+ if (options.signal.aborted) controller.abort();
273
+ else options.signal.addEventListener("abort", outerAbort, { once: true });
274
+ }
275
+ try {
276
+ const sessionId = deps.getSessionId?.();
277
+ const modeState = await fetchModeState2(deps.apiKey, sessionId);
278
+ if (modeState.kill) return deny("Stopped by user \u2014 this agent was halted from Pushary");
279
+ const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion);
280
+ const policy = resolvePolicy2(policyConfig, toolName, modeState.mode, input);
281
+ if (policy.timeoutSeconds === 0 && policy.timeoutAction === "approve") return allow(input);
282
+ if (policy.timeoutSeconds === 0 && policy.timeoutAction === "deny") {
283
+ return deny(`Denied by policy for ${policy.tool}`);
284
+ }
285
+ const description = describeToolCall(toolName, input, "hook");
286
+ const toolTarget = deriveToolTarget(toolName, input);
287
+ let question;
288
+ try {
289
+ question = await askUser2(deps.apiKey, {
290
+ question: `Allow ${description}?`,
291
+ type: "confirm",
292
+ context: `Your agent wants to run this in ${deps.projectName} (you are away)`,
293
+ agentName: `Claude Code - ${deps.projectName}`,
294
+ sessionId,
295
+ machineId: deps.machineId,
296
+ toolName,
297
+ toolTarget
298
+ });
299
+ } catch {
300
+ return applyTimeout(policy, input);
301
+ }
302
+ entry.correlationId = question.correlationId;
303
+ if (question.noDevices) return applyTimeout(policy, input);
304
+ const answer = await waitForPhone(entry, question.correlationId, windowFor(policy));
305
+ if (answer.answered) {
306
+ if (isDeferAnswer(answer.value)) return deny("Deferred \u2014 handle it on the machine");
307
+ return answer.value === "yes" ? allow(input) : deny("Denied from your phone");
308
+ }
309
+ return applyTimeout(policy, input);
310
+ } catch {
311
+ return deny("Approval unavailable; denied by default");
312
+ } finally {
313
+ if (options?.signal) options.signal.removeEventListener("abort", outerAbort);
314
+ pending.delete(entry);
315
+ }
316
+ };
317
+ const teardown = () => {
318
+ tornDown = true;
319
+ for (const entry of pending) {
320
+ entry.controller.abort();
321
+ if (entry.correlationId) {
322
+ void cancelQuestion2(deps.apiKey, entry.correlationId).catch(() => {
323
+ });
324
+ }
325
+ }
326
+ pending.clear();
327
+ };
328
+ return {
329
+ canUseTool,
330
+ teardown,
331
+ pendingCount: () => pending.size
332
+ };
333
+ };
334
+
335
+ // src/wrapper/drainClient.ts
336
+ var drainPendingCommand = async (apiKey, sessionId, signal, baseUrl = getBaseUrl()) => {
337
+ try {
338
+ const response = await fetch(`${baseUrl}/api/agent/command/drain`, {
339
+ method: "POST",
340
+ headers: {
341
+ "Content-Type": "application/json",
342
+ Authorization: `Bearer ${apiKey}`
343
+ },
344
+ body: JSON.stringify(sessionId ? { sessionId } : {}),
345
+ signal
346
+ });
347
+ if (!response.ok) return null;
348
+ const data = await response.json();
349
+ return typeof data.command === "string" && data.command.length > 0 ? data.command : null;
350
+ } catch {
351
+ return null;
352
+ }
353
+ };
354
+
77
355
  // src/wrapper/commandPoller.ts
78
- var FAST_POLL_MS = 2e3;
79
356
  var SLOW_POLL_MS = 3e4;
80
- var isEnabled = () => {
81
- const flag = process.env.PUSHARY_WRAPPER_POLL;
82
- return flag === "1" || flag === "true";
83
- };
357
+ var FAST_POLL_MS = 2e3;
358
+ var FAST_DECAY_TICKS = 3;
359
+ var MAX_ERROR_BACKOFF_MS = 12e4;
360
+ var MAX_ERROR_STREAK = 6;
361
+ var REQUEST_TIMEOUT_MS = 1e4;
84
362
  var startCommandPoller = (opts) => {
363
+ const drain = opts.drain ?? drainPendingCommand;
364
+ const slow = opts.slowPollMs ?? SLOW_POLL_MS;
365
+ const fast = opts.fastPollMs ?? FAST_POLL_MS;
366
+ const requestTimeout = opts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
85
367
  let stopped = false;
86
368
  let timer;
87
- if (!isEnabled()) return { stop() {
88
- } };
89
- try {
90
- getApiKey();
91
- } catch {
92
- return { stop() {
93
- } };
94
- }
369
+ let inflight;
370
+ let idleTicks = 0;
371
+ let errorStreak = 0;
372
+ const jitter = (ms) => {
373
+ const r = opts.jitter ? opts.jitter() : Math.random();
374
+ return Math.max(1, Math.round(ms * (0.85 + r * 0.3)));
375
+ };
376
+ const nextDelay = () => {
377
+ if (errorStreak > 0) {
378
+ return Math.min(slow * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
379
+ }
380
+ return idleTicks >= FAST_DECAY_TICKS ? slow : fast;
381
+ };
95
382
  const schedule = (delayMs) => {
96
383
  if (stopped) return;
97
- timer = setTimeout(tick, delayMs);
384
+ timer = setTimeout(tick, jitter(delayMs));
98
385
  };
99
386
  const tick = async () => {
100
387
  if (stopped) return;
101
- let next = SLOW_POLL_MS;
388
+ const controller = new AbortController();
389
+ inflight = controller;
390
+ const timeout = setTimeout(() => controller.abort(), requestTimeout);
102
391
  try {
103
- const command = await drainPendingCommand(opts.sessionId);
392
+ const command = await drain(opts.apiKey, opts.getSessionId?.(), controller.signal);
393
+ errorStreak = 0;
104
394
  if (command) {
105
- next = FAST_POLL_MS;
106
- opts.onCommand(command);
395
+ idleTicks = 0;
396
+ try {
397
+ opts.onCommand(command);
398
+ } catch {
399
+ }
400
+ } else {
401
+ idleTicks++;
107
402
  }
108
403
  } catch {
404
+ errorStreak = Math.min(errorStreak + 1, MAX_ERROR_STREAK);
405
+ } finally {
406
+ clearTimeout(timeout);
407
+ inflight = void 0;
109
408
  }
110
- schedule(next);
409
+ schedule(nextDelay());
111
410
  };
112
- opts.log?.("[pushary] wrapper command poller enabled (experimental)");
113
- schedule(SLOW_POLL_MS);
411
+ schedule(fast);
412
+ opts.log?.("[pushary] wrapper command poller active (experimental)");
114
413
  return {
115
414
  stop() {
116
415
  stopped = true;
117
416
  if (timer) clearTimeout(timer);
417
+ inflight?.abort();
418
+ inflight = void 0;
118
419
  }
119
420
  };
120
421
  };
121
- var drainPendingCommand = async (_sessionId) => {
122
- return null;
422
+
423
+ // src/wrapper/remoteLoop.ts
424
+ var errMsg = (err) => err instanceof Error ? err.message : String(err);
425
+ var DEFAULT_MAX_RESTARTS = 5;
426
+ var runRemoteLoop = async (deps) => {
427
+ const maxRestarts = deps.maxRestarts ?? DEFAULT_MAX_RESTARTS;
428
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
429
+ const backoff = deps.backoffMs ?? ((attempt) => Math.min(1e3 * 2 ** attempt, 15e3));
430
+ let resumeId = deps.initialResume;
431
+ let totalCost = 0;
432
+ let restarts = 0;
433
+ while (!deps.signal.aborted) {
434
+ deps.input.detachConsumer();
435
+ let query;
436
+ try {
437
+ query = deps.sdk.query({
438
+ prompt: deps.input,
439
+ options: {
440
+ resume: resumeId,
441
+ permissionMode: deps.permissionMode,
442
+ canUseTool: deps.canUseTool,
443
+ env: deps.env,
444
+ cwd: deps.cwd
445
+ }
446
+ });
447
+ } catch (err) {
448
+ deps.log?.(`[pushary] remote mode: query failed to start: ${errMsg(err)}`);
449
+ return { exitCode: 1, totalCostUsd: totalCost };
450
+ }
451
+ const onAbort = () => {
452
+ deps.input.close();
453
+ void query.interrupt?.().catch(() => {
454
+ });
455
+ };
456
+ if (deps.signal.aborted) onAbort();
457
+ else deps.signal.addEventListener("abort", onAbort, { once: true });
458
+ try {
459
+ for await (const message of query) {
460
+ if (message.type === "system" && message.subtype === "init" && typeof message.session_id === "string") {
461
+ resumeId = message.session_id;
462
+ deps.onSessionId?.(resumeId);
463
+ }
464
+ if (message.type === "result" && typeof message.total_cost_usd === "number") {
465
+ totalCost = message.total_cost_usd;
466
+ deps.onCost?.(totalCost);
467
+ }
468
+ }
469
+ deps.signal.removeEventListener("abort", onAbort);
470
+ return { exitCode: deps.signal.aborted ? 130 : 0, totalCostUsd: totalCost };
471
+ } catch (err) {
472
+ deps.signal.removeEventListener("abort", onAbort);
473
+ if (deps.signal.aborted) return { exitCode: 130, totalCostUsd: totalCost };
474
+ restarts++;
475
+ if (restarts > maxRestarts) {
476
+ deps.log?.(`[pushary] remote mode: too many restarts, giving up: ${errMsg(err)}`);
477
+ return { exitCode: 1, totalCostUsd: totalCost };
478
+ }
479
+ deps.log?.(
480
+ `[pushary] remote mode: transient error, resuming session (attempt ${restarts}/${maxRestarts}): ${errMsg(err)}`
481
+ );
482
+ await sleep(backoff(restarts));
483
+ }
484
+ }
485
+ return { exitCode: 130, totalCostUsd: totalCost };
486
+ };
487
+
488
+ // src/wrapper/args.ts
489
+ var resolveClaudeArgs = (argv) => {
490
+ const rest = argv.slice(2);
491
+ return rest[0] === "claude" ? rest.slice(1) : rest;
492
+ };
493
+ var flagValue = (args2, index) => {
494
+ const inline = args2[index];
495
+ const eq = inline?.indexOf("=") ?? -1;
496
+ if (inline && eq > 0) return inline.slice(eq + 1);
497
+ const next = args2[index + 1];
498
+ return next && !next.startsWith("-") ? next : void 0;
499
+ };
500
+ var parseRemoteArgs = (args2) => {
501
+ let initialPrompt;
502
+ let resume;
503
+ let permissionMode;
504
+ for (let i = 0; i < args2.length; i++) {
505
+ const arg = args2[i];
506
+ if (arg === "-p" || arg === "--print" || arg?.startsWith("--print=")) {
507
+ initialPrompt = flagValue(args2, i);
508
+ } else if (arg === "-r" || arg === "--resume" || arg?.startsWith("--resume=")) {
509
+ resume = flagValue(args2, i);
510
+ } else if (arg === "--permission-mode" || arg?.startsWith("--permission-mode=")) {
511
+ permissionMode = flagValue(args2, i);
512
+ }
513
+ }
514
+ return { initialPrompt, resume, permissionMode };
123
515
  };
124
516
 
125
517
  // src/wrapper/remoteMode.ts
126
- var runRemoteMode = async (_binary, _args) => {
127
- return { implemented: false };
518
+ var INPUT_QUEUE_CAP = 32;
519
+ var runRemoteMode = async (_binary, args2) => {
520
+ let apiKey;
521
+ try {
522
+ apiKey = getApiKey();
523
+ } catch {
524
+ process.stderr.write(
525
+ "[pushary] remote mode needs an API key. Run `npx @pushary/agent-hooks setup`. Running the normal passthrough for now.\n"
526
+ );
527
+ return { implemented: false };
528
+ }
529
+ const sdk = await loadClaudeSdk();
530
+ if (!sdk) {
531
+ process.stderr.write(
532
+ "[pushary] remote mode needs the Claude Agent SDK, which is not installed. Enable it with:\n npm i -g @anthropic-ai/claude-agent-sdk@0.3.207\nRunning the normal passthrough for now.\n"
533
+ );
534
+ return { implemented: false };
535
+ }
536
+ const projectName = basename(process.cwd());
537
+ const machineId = getMachineId();
538
+ const controller = new AbortController();
539
+ let sessionId;
540
+ const input = new BoundedInputQueue({
541
+ cap: INPUT_QUEUE_CAP,
542
+ onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; dropped ${total} stale instruction(s)
543
+ `)
544
+ });
545
+ const approver = createRemoteApprover({
546
+ apiKey,
547
+ machineId,
548
+ projectName,
549
+ getSessionId: () => sessionId
550
+ });
551
+ const poller = startCommandPoller({
552
+ apiKey,
553
+ getSessionId: () => sessionId,
554
+ onCommand: (command) => input.push(userMessage(command)),
555
+ log: (message) => process.stderr.write(`${message}
556
+ `)
557
+ });
558
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
559
+ const onSignal = () => controller.abort();
560
+ for (const signal of signals) process.on(signal, onSignal);
561
+ const { initialPrompt, resume, permissionMode } = parseRemoteArgs(args2);
562
+ try {
563
+ if (initialPrompt) input.push(userMessage(initialPrompt));
564
+ const result = await runRemoteLoop({
565
+ sdk,
566
+ input,
567
+ canUseTool: approver.canUseTool,
568
+ signal: controller.signal,
569
+ permissionMode,
570
+ initialResume: resume,
571
+ // The SDK REPLACES the subprocess env, so spread process.env and set the
572
+ // recursion guard so a wrapper-spawned claude cannot re-enter the wrapper.
573
+ env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
574
+ cwd: process.cwd(),
575
+ onSessionId: (id) => {
576
+ sessionId = id;
577
+ },
578
+ log: (message) => process.stderr.write(`${message}
579
+ `)
580
+ });
581
+ return { implemented: true, exitCode: result.exitCode };
582
+ } finally {
583
+ for (const signal of signals) process.off(signal, onSignal);
584
+ poller.stop();
585
+ approver.teardown();
586
+ input.close();
587
+ }
128
588
  };
129
589
 
130
590
  // src/wrapper/runClaudeWrapper.ts
@@ -144,24 +604,7 @@ var runClaudeWrapper = async (args2) => {
144
604
  } catch {
145
605
  }
146
606
  }
147
- const poller = nested ? { stop() {
148
- } } : startCommandPoller({
149
- onCommand: () => {
150
- },
151
- log: (m) => process.stderr.write(`${m}
152
- `)
153
- });
154
- try {
155
- return await runLocalPassthrough(binary, args2);
156
- } finally {
157
- poller.stop();
158
- }
159
- };
160
-
161
- // src/wrapper/args.ts
162
- var resolveClaudeArgs = (argv) => {
163
- const rest = argv.slice(2);
164
- return rest[0] === "claude" ? rest.slice(1) : rest;
607
+ return runLocalPassthrough(binary, args2);
165
608
  };
166
609
 
167
610
  // bin/pushary-claude.ts
@@ -6,19 +6,10 @@ import {
6
6
  import {
7
7
  CODEX_AGENT,
8
8
  DEFAULT_SESSION,
9
- askUser,
10
- cancelQuestion,
11
9
  codexAllow,
12
10
  codexDeny,
13
11
  codexPass,
14
- deriveActionBody,
15
- deriveBlocker,
16
- deriveToolTarget,
17
12
  describeApplyPatch,
18
- describeToolCall,
19
- fetchModeState,
20
- getMachineId,
21
- getPolicy,
22
13
  handlePostToolUse,
23
14
  handleStop,
24
15
  handleUserPrompt,
@@ -26,13 +17,24 @@ import {
26
17
  preToolUseTimeoutDecision,
27
18
  readLastPrompt,
28
19
  reportEvent,
29
- resolvePolicy,
30
20
  savePendingQuestion,
31
- sendNotification,
32
21
  toCodexWire,
33
- toPolicyLookup,
22
+ toPolicyLookup
23
+ } from "../chunk-V2WKECMG.js";
24
+ import {
25
+ askUser,
26
+ cancelQuestion,
27
+ deriveActionBody,
28
+ deriveBlocker,
29
+ deriveToolTarget,
30
+ describeToolCall,
31
+ fetchModeState,
32
+ getMachineId,
33
+ getPolicy,
34
+ resolvePolicy,
35
+ sendNotification,
34
36
  waitForAnswer
35
- } from "../chunk-JHN3H6LX.js";
37
+ } from "../chunk-AUEPQATK.js";
36
38
  import {
37
39
  isGatingMoment,
38
40
  recordKeylessMoment
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ reportEvent
4
+ } from "../chunk-V2WKECMG.js";
2
5
  import {
3
6
  askUser,
4
7
  getMachineId,
5
- reportEvent,
6
8
  waitForAnswer
7
- } from "../chunk-JHN3H6LX.js";
9
+ } from "../chunk-AUEPQATK.js";
8
10
  import "../chunk-DWED7BS3.js";
9
11
  import "../chunk-Z5PL3K7C.js";
10
12
  import {
@@ -148,14 +148,22 @@ var main = async () => {
148
148
  const settings = readJson(CLAUDE_SETTINGS);
149
149
  if (settings) {
150
150
  const hooks = settings.hooks;
151
- const hasPreHook = JSON.stringify(hooks?.PreToolUse ?? []).includes("pushary-hook");
152
- const hasPostHook = JSON.stringify(hooks?.PostToolUse ?? []).includes("pushary-post-hook");
153
- const hasStopHook = JSON.stringify(hooks?.Stop ?? []).includes("pushary-stop-hook");
154
- const hasPromptHook = JSON.stringify(hooks?.UserPromptSubmit ?? []).includes("pushary-prompt-hook");
155
- check(hasPreHook, "Claude Code: PreToolUse hook");
156
- check(hasPostHook, "Claude Code: PostToolUse hook");
157
- check(hasStopHook, "Claude Code: Stop hook");
158
- check(hasPromptHook, "Claude Code: UserPromptSubmit hook", hasPromptHook ? void 0 : "missing, re-run setup to register it");
151
+ const CLAUDE_HOOK_CHECKS = [
152
+ { event: "PreToolUse", needle: "pushary-hook", label: "PreToolUse hook" },
153
+ { event: "PostToolUse", needle: "pushary-post-hook", label: "PostToolUse hook" },
154
+ { event: "UserPromptSubmit", needle: "pushary-prompt-hook", label: "UserPromptSubmit hook" },
155
+ { event: "Stop", needle: "pushary-stop-hook", label: "Stop hook" },
156
+ { event: "StopFailure", needle: "pushary-stopfailure-hook", label: "StopFailure hook" },
157
+ { event: "Notification", needle: "pushary-notification-hook", label: "Notification hook" },
158
+ { event: "SessionStart", needle: "pushary-session-start-hook", label: "SessionStart hook" },
159
+ { event: "SessionEnd", needle: "pushary-session-end-hook", label: "SessionEnd hook" },
160
+ { event: "PermissionRequest", needle: "pushary-permission-hook", label: "PermissionRequest hook" },
161
+ { event: "PermissionDenied", needle: "pushary-permission-denied-hook", label: "PermissionDenied hook" }
162
+ ];
163
+ for (const { event, needle, label } of CLAUDE_HOOK_CHECKS) {
164
+ const present = JSON.stringify(hooks?.[event] ?? []).includes(needle);
165
+ check(present, `Claude Code: ${label}`, present ? void 0 : "missing \u2014 run npx @pushary/agent-hooks@latest upgrade");
166
+ }
159
167
  const preHookCommand = extractHookCommand(hooks?.PreToolUse, "pushary-hook");
160
168
  if (preHookCommand) {
161
169
  const resolves = commandResolves(preHookCommand);