@pushary/agent-hooks 0.40.0 → 0.42.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.
@@ -12,7 +12,7 @@ import {
12
12
  getPolicy,
13
13
  resolvePolicy,
14
14
  waitForAnswer
15
- } from "../chunk-AUEPQATK.js";
15
+ } from "../chunk-ETDXSKR5.js";
16
16
  import "../chunk-DWED7BS3.js";
17
17
  import "../chunk-Z5PL3K7C.js";
18
18
  import {
@@ -46,8 +46,17 @@ var findClaudeBinary = () => {
46
46
  return null;
47
47
  };
48
48
 
49
- // src/wrapper/localPassthrough.ts
49
+ // src/wrapper/spawnClaude.ts
50
50
  import { spawn } from "child_process";
51
+ var needsShell = (binary, platform = process.platform) => platform === "win32" && /\.(cmd|bat)$/i.test(binary);
52
+ var spawnClaude = (binary, args2, options) => {
53
+ if (needsShell(binary)) {
54
+ return spawn(`"${binary}"`, args2, { ...options, shell: true });
55
+ }
56
+ return spawn(binary, args2, options);
57
+ };
58
+
59
+ // src/wrapper/localPassthrough.ts
51
60
  var SIGNAL_NUMBERS = {
52
61
  SIGHUP: 1,
53
62
  SIGINT: 2,
@@ -60,7 +69,7 @@ var runLocalPassthrough = (binary, args2) => {
60
69
  return new Promise((resolve) => {
61
70
  let child;
62
71
  try {
63
- child = spawn(binary, args2, {
72
+ child = spawnClaude(binary, args2, {
64
73
  stdio: "inherit",
65
74
  env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" }
66
75
  });
@@ -119,12 +128,14 @@ var BoundedInputQueue = class {
119
128
  closed = false;
120
129
  cap;
121
130
  onDrop;
131
+ coalesce;
122
132
  pushedCount = 0;
123
133
  deliveredCount = 0;
124
134
  droppedCount = 0;
125
135
  constructor(options = {}) {
126
136
  this.cap = Math.max(1, options.cap ?? 32);
127
137
  this.onDrop = options.onDrop;
138
+ this.coalesce = options.coalesce;
128
139
  }
129
140
  push(item) {
130
141
  if (this.closed) return;
@@ -179,6 +190,11 @@ var BoundedInputQueue = class {
179
190
  return {
180
191
  next: () => {
181
192
  if (this.buffer.length > 0) {
193
+ if (this.coalesce && this.buffer.length > 1) {
194
+ const items = this.buffer.splice(0, this.buffer.length);
195
+ this.deliveredCount += items.length;
196
+ return Promise.resolve({ value: this.coalesce(items), done: false });
197
+ }
182
198
  const value = this.buffer.shift();
183
199
  this.deliveredCount++;
184
200
  return Promise.resolve({ value, done: false });
@@ -383,13 +399,28 @@ var startCommandPoller = (opts) => {
383
399
  if (stopped) return;
384
400
  timer = setTimeout(tick, jitter(delayMs));
385
401
  };
402
+ const fetchMode = async () => {
403
+ if (!opts.fetchModeState || !opts.onModeState) return;
404
+ try {
405
+ const state = await opts.fetchModeState(opts.apiKey, opts.getSessionId?.());
406
+ try {
407
+ opts.onModeState(state);
408
+ } catch {
409
+ }
410
+ } catch {
411
+ }
412
+ };
386
413
  const tick = async () => {
387
414
  if (stopped) return;
388
415
  const controller = new AbortController();
389
416
  inflight = controller;
390
417
  const timeout = setTimeout(() => controller.abort(), requestTimeout);
391
418
  try {
392
- const command = await drain(opts.apiKey, opts.getSessionId?.(), controller.signal);
419
+ const wantCommands = opts.shouldDrainCommands ? opts.shouldDrainCommands() : true;
420
+ const [command] = await Promise.all([
421
+ wantCommands ? drain(opts.apiKey, opts.getSessionId?.(), controller.signal) : Promise.resolve(null),
422
+ fetchMode()
423
+ ]);
393
424
  errorStreak = 0;
394
425
  if (command) {
395
426
  idleTicks = 0;
@@ -420,6 +451,286 @@ var startCommandPoller = (opts) => {
420
451
  };
421
452
  };
422
453
 
454
+ // src/wrapper/wsProtocol.ts
455
+ var PROTOCOL_VERSION = 1;
456
+ var TRANSCRIPT_TEXT_MAX = 4e3;
457
+ var encodeFrame = (frame) => JSON.stringify(frame);
458
+ var helloFrame = (fields) => ({ v: PROTOCOL_VERSION, t: "hello", ...fields });
459
+ var transcriptFrame = (seq, kind, text, meta) => ({
460
+ v: PROTOCOL_VERSION,
461
+ t: "transcript",
462
+ seq,
463
+ kind,
464
+ ...text !== void 0 ? { text: text.slice(0, TRANSCRIPT_TEXT_MAX) } : {},
465
+ ...meta ? { meta } : {}
466
+ });
467
+ var parse = (raw) => {
468
+ try {
469
+ const value = JSON.parse(raw);
470
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
471
+ const obj = value;
472
+ if (obj.v !== PROTOCOL_VERSION) return null;
473
+ return obj;
474
+ } catch {
475
+ return null;
476
+ }
477
+ };
478
+ var isString = (v) => typeof v === "string";
479
+ var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
480
+ var decodeServerFrame = (raw) => {
481
+ const f = parse(raw);
482
+ if (!f) return null;
483
+ switch (f.t) {
484
+ case "welcome":
485
+ return isFiniteNumber(f.resumeFromSeq) ? { v: PROTOCOL_VERSION, t: "welcome", resumeFromSeq: f.resumeFromSeq } : null;
486
+ case "ping":
487
+ return { v: PROTOCOL_VERSION, t: "ping" };
488
+ case "pong":
489
+ return { v: PROTOCOL_VERSION, t: "pong" };
490
+ case "command":
491
+ return isString(f.id) && isString(f.text) ? { v: PROTOCOL_VERSION, t: "command", id: f.id, text: f.text } : null;
492
+ case "resume":
493
+ return isFiniteNumber(f.afterSeq) ? { v: PROTOCOL_VERSION, t: "resume", afterSeq: f.afterSeq } : null;
494
+ case "error":
495
+ if (isString(f.code) && isString(f.message)) {
496
+ return { v: PROTOCOL_VERSION, t: "error", code: f.code, message: f.message, fatal: f.fatal === true };
497
+ }
498
+ return null;
499
+ default:
500
+ return null;
501
+ }
502
+ };
503
+
504
+ // src/wrapper/relayClient.ts
505
+ var DEFAULT_HEARTBEAT_MS = 2e4;
506
+ var DEFAULT_PONG_TIMEOUT_MS = 1e4;
507
+ var DEFAULT_MAX_OUTBOUND = 128;
508
+ var DEFAULT_MAX_SEEN_COMMANDS = 256;
509
+ var defaultBackoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4);
510
+ var defaultSocketFactory = (url) => {
511
+ const WS = globalThis.WebSocket;
512
+ if (!WS) throw new Error("no WebSocket in this runtime");
513
+ const socket = new WS(url);
514
+ return {
515
+ send: (data) => socket.send(data),
516
+ close: () => socket.close(),
517
+ onOpen: (cb) => socket.addEventListener("open", () => cb()),
518
+ onMessage: (cb) => socket.addEventListener("message", (event) => cb(String(event.data))),
519
+ onClose: (cb) => socket.addEventListener("close", () => cb()),
520
+ onError: (cb) => socket.addEventListener("error", (event) => cb(event))
521
+ };
522
+ };
523
+ var createRelayClient = (options) => {
524
+ const factory = options.socketFactory ?? defaultSocketFactory;
525
+ const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
526
+ const pongTimeoutMs = options.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
527
+ const backoff = options.backoffMs ?? defaultBackoff;
528
+ const maxOutbound = Math.max(1, options.maxOutbound ?? DEFAULT_MAX_OUTBOUND);
529
+ const maxSeenCommands = Math.max(1, options.maxSeenCommands ?? DEFAULT_MAX_SEEN_COMMANDS);
530
+ let socket;
531
+ let isConnected = false;
532
+ let stopped = false;
533
+ let seq = 0;
534
+ let attempt = 0;
535
+ let reconnectTimer;
536
+ let heartbeatTimer;
537
+ let pongTimer;
538
+ const outbound = [];
539
+ const seenCommands = [];
540
+ const seenSet = /* @__PURE__ */ new Set();
541
+ const log = (message) => options.log?.(message);
542
+ const clearTimers = () => {
543
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
544
+ if (pongTimer) clearTimeout(pongTimer);
545
+ if (reconnectTimer) clearTimeout(reconnectTimer);
546
+ heartbeatTimer = void 0;
547
+ pongTimer = void 0;
548
+ reconnectTimer = void 0;
549
+ };
550
+ const rememberCommand = (id) => {
551
+ if (seenSet.has(id)) return false;
552
+ seenSet.add(id);
553
+ seenCommands.push(id);
554
+ while (seenCommands.length > maxSeenCommands) {
555
+ const evicted = seenCommands.shift();
556
+ if (evicted) seenSet.delete(evicted);
557
+ }
558
+ return true;
559
+ };
560
+ const sendFrame = (frame) => {
561
+ if (!socket || !isConnected) return;
562
+ try {
563
+ socket.send(encodeFrame(frame));
564
+ } catch {
565
+ }
566
+ };
567
+ const enqueueTranscript = (frame) => {
568
+ if (isConnected) {
569
+ sendFrame(frame);
570
+ return;
571
+ }
572
+ outbound.push(frame);
573
+ while (outbound.length > maxOutbound) outbound.shift();
574
+ };
575
+ const flushOutbound = () => {
576
+ while (outbound.length > 0 && isConnected) {
577
+ const frame = outbound.shift();
578
+ if (frame) sendFrame(frame);
579
+ }
580
+ };
581
+ const armPong = () => {
582
+ if (pongTimer) clearTimeout(pongTimer);
583
+ pongTimer = setTimeout(() => {
584
+ log("[pushary] relay heartbeat timed out; reconnecting");
585
+ dropAndReconnect();
586
+ }, pongTimeoutMs);
587
+ };
588
+ const startHeartbeat = () => {
589
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
590
+ heartbeatTimer = setInterval(() => {
591
+ sendFrame({ v: PROTOCOL_VERSION, t: "ping" });
592
+ armPong();
593
+ }, heartbeatMs);
594
+ };
595
+ const onServerFrame = (raw) => {
596
+ const frame = decodeServerFrame(raw);
597
+ if (!frame) return;
598
+ switch (frame.t) {
599
+ case "welcome":
600
+ attempt = 0;
601
+ flushOutbound();
602
+ log("[pushary] relay connected");
603
+ break;
604
+ case "command":
605
+ sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id });
606
+ if (rememberCommand(frame.id)) {
607
+ try {
608
+ options.onCommand(frame.text);
609
+ } catch {
610
+ }
611
+ }
612
+ break;
613
+ case "ping":
614
+ sendFrame({ v: PROTOCOL_VERSION, t: "pong" });
615
+ break;
616
+ case "pong":
617
+ if (pongTimer) clearTimeout(pongTimer);
618
+ pongTimer = void 0;
619
+ break;
620
+ case "resume":
621
+ log(`[pushary] relay signalled a gap; catch up after seq ${frame.afterSeq}`);
622
+ break;
623
+ case "error":
624
+ if (frame.fatal) {
625
+ log(`[pushary] relay fatal: ${frame.message}`);
626
+ fatal(frame.message);
627
+ } else {
628
+ log(`[pushary] relay error: ${frame.message}`);
629
+ }
630
+ break;
631
+ }
632
+ };
633
+ const dropSocket = () => {
634
+ isConnected = false;
635
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
636
+ if (pongTimer) clearTimeout(pongTimer);
637
+ heartbeatTimer = void 0;
638
+ pongTimer = void 0;
639
+ if (socket) {
640
+ try {
641
+ socket.close();
642
+ } catch {
643
+ }
644
+ }
645
+ socket = void 0;
646
+ };
647
+ const scheduleReconnect = () => {
648
+ if (stopped) return;
649
+ if (reconnectTimer) return;
650
+ const delay = backoff(attempt);
651
+ attempt++;
652
+ const jittered = Math.max(1, Math.round(delay * (0.85 + Math.random() * 0.3)));
653
+ reconnectTimer = setTimeout(() => {
654
+ reconnectTimer = void 0;
655
+ connect();
656
+ }, jittered);
657
+ };
658
+ const dropAndReconnect = () => {
659
+ dropSocket();
660
+ scheduleReconnect();
661
+ };
662
+ const fatal = (message) => {
663
+ stopped = true;
664
+ clearTimers();
665
+ dropSocket();
666
+ options.onFatal?.(message);
667
+ };
668
+ const connect = () => {
669
+ if (stopped) return;
670
+ let created;
671
+ try {
672
+ created = factory(options.url);
673
+ } catch (error) {
674
+ fatal(error instanceof Error ? error.message : String(error));
675
+ return;
676
+ }
677
+ socket = created;
678
+ created.onOpen(() => {
679
+ isConnected = true;
680
+ sendFrame(
681
+ helloFrame({
682
+ apiKey: options.apiKey,
683
+ machineId: options.machineId,
684
+ agentType: options.agentType,
685
+ sessionId: options.getSessionId(),
686
+ lastSeq: seq
687
+ })
688
+ );
689
+ startHeartbeat();
690
+ });
691
+ created.onMessage((data) => onServerFrame(data));
692
+ created.onClose(() => {
693
+ if (stopped) return;
694
+ dropAndReconnect();
695
+ });
696
+ created.onError(() => {
697
+ });
698
+ };
699
+ return {
700
+ start() {
701
+ if (stopped) return;
702
+ connect();
703
+ },
704
+ connected() {
705
+ return isConnected;
706
+ },
707
+ sendTranscript(kind, text, meta) {
708
+ if (stopped) return;
709
+ seq++;
710
+ enqueueTranscript(transcriptFrame(seq, kind, text, meta));
711
+ },
712
+ reannounce() {
713
+ if (stopped || !isConnected) return;
714
+ sendFrame(
715
+ helloFrame({
716
+ apiKey: options.apiKey,
717
+ machineId: options.machineId,
718
+ agentType: options.agentType,
719
+ sessionId: options.getSessionId(),
720
+ lastSeq: seq
721
+ })
722
+ );
723
+ },
724
+ stop() {
725
+ if (stopped) return;
726
+ stopped = true;
727
+ sendFrame({ v: PROTOCOL_VERSION, t: "bye" });
728
+ clearTimers();
729
+ dropSocket();
730
+ }
731
+ };
732
+ };
733
+
423
734
  // src/wrapper/remoteLoop.ts
424
735
  var errMsg = (err) => err instanceof Error ? err.message : String(err);
425
736
  var DEFAULT_MAX_RESTARTS = 5;
@@ -457,6 +768,7 @@ var runRemoteLoop = async (deps) => {
457
768
  else deps.signal.addEventListener("abort", onAbort, { once: true });
458
769
  try {
459
770
  for await (const message of query) {
771
+ deps.onMessage?.(message);
460
772
  if (message.type === "system" && message.subtype === "init" && typeof message.session_id === "string") {
461
773
  resumeId = message.session_id;
462
774
  deps.onSessionId?.(resumeId);
@@ -490,6 +802,10 @@ var resolveClaudeArgs = (argv) => {
490
802
  const rest = argv.slice(2);
491
803
  return rest[0] === "claude" ? rest.slice(1) : rest;
492
804
  };
805
+ var extractRemoteFlag = (args2) => ({
806
+ remote: args2.includes("--remote"),
807
+ rest: args2.filter((arg) => arg !== "--remote")
808
+ });
493
809
  var flagValue = (args2, index) => {
494
810
  const inline = args2[index];
495
811
  const eq = inline?.indexOf("=") ?? -1;
@@ -516,6 +832,27 @@ var parseRemoteArgs = (args2) => {
516
832
 
517
833
  // src/wrapper/remoteMode.ts
518
834
  var INPUT_QUEUE_CAP = 32;
835
+ var extractAssistantText = (message) => {
836
+ const content = message.message?.content;
837
+ if (typeof content === "string") return content || void 0;
838
+ if (Array.isArray(content)) {
839
+ const text = content.filter((block) => !!block && typeof block === "object").filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
840
+ return text || void 0;
841
+ }
842
+ return void 0;
843
+ };
844
+ var streamTranscript = (relay, message) => {
845
+ if (message.type === "assistant") {
846
+ const text = extractAssistantText(message);
847
+ if (text) relay.sendTranscript("assistant", text);
848
+ } else if (message.type === "result") {
849
+ relay.sendTranscript(
850
+ "turn_end",
851
+ void 0,
852
+ typeof message.total_cost_usd === "number" ? { cost: message.total_cost_usd } : void 0
853
+ );
854
+ }
855
+ };
519
856
  var runRemoteMode = async (_binary, args2) => {
520
857
  let apiKey;
521
858
  try {
@@ -537,10 +874,14 @@ var runRemoteMode = async (_binary, args2) => {
537
874
  const machineId = getMachineId();
538
875
  const controller = new AbortController();
539
876
  let sessionId;
877
+ let killed = false;
878
+ let totalCostUsd = 0;
540
879
  const input = new BoundedInputQueue({
541
880
  cap: INPUT_QUEUE_CAP,
542
881
  onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; dropped ${total} stale instruction(s)
543
- `)
882
+ `),
883
+ // Several instructions that piled up between turns are sent as one turn.
884
+ coalesce: (items) => userMessage(items.map((m) => m.message.content).join("\n\n"))
544
885
  });
545
886
  const approver = createRemoteApprover({
546
887
  apiKey,
@@ -548,10 +889,41 @@ var runRemoteMode = async (_binary, args2) => {
548
889
  projectName,
549
890
  getSessionId: () => sessionId
550
891
  });
892
+ const initialMode = await fetchModeState(apiKey).catch(() => null);
893
+ let relay;
894
+ if (initialMode?.relayUrl) {
895
+ relay = createRelayClient({
896
+ url: initialMode.relayUrl,
897
+ apiKey,
898
+ machineId,
899
+ agentType: "claude_code",
900
+ getSessionId: () => sessionId,
901
+ onCommand: (command) => input.push(userMessage(command)),
902
+ onFatal: (message) => process.stderr.write(`[pushary] relay unavailable (${message}); using polling
903
+ `),
904
+ log: (message) => process.stderr.write(`${message}
905
+ `)
906
+ });
907
+ relay.start();
908
+ }
551
909
  const poller = startCommandPoller({
552
910
  apiKey,
553
911
  getSessionId: () => sessionId,
554
912
  onCommand: (command) => input.push(userMessage(command)),
913
+ // While the relay socket owns command delivery, the poller must not also drain
914
+ // the single-use queue (that would deliver the command twice); it still reads
915
+ // mode/kill. When the socket is down or absent, the poller is the source.
916
+ shouldDrainCommands: () => !(relay?.connected() ?? false),
917
+ // Watch the kill switch: a phone "stop" interrupts the running turn (via the
918
+ // controller abort below), instead of only denying the next tool call.
919
+ fetchModeState,
920
+ onModeState: (state) => {
921
+ if (state.kill && !killed) {
922
+ killed = true;
923
+ process.stderr.write("[pushary] halted from Pushary \u2014 stopping the agent\n");
924
+ controller.abort();
925
+ }
926
+ },
555
927
  log: (message) => process.stderr.write(`${message}
556
928
  `)
557
929
  });
@@ -574,16 +946,27 @@ var runRemoteMode = async (_binary, args2) => {
574
946
  cwd: process.cwd(),
575
947
  onSessionId: (id) => {
576
948
  sessionId = id;
949
+ relay?.reannounce();
577
950
  },
951
+ onCost: (usd) => {
952
+ totalCostUsd = usd;
953
+ },
954
+ // Stream a bounded live transcript to the phone (relay presence-gates it).
955
+ onMessage: relay ? (message) => streamTranscript(relay, message) : void 0,
578
956
  log: (message) => process.stderr.write(`${message}
579
957
  `)
580
958
  });
581
959
  return { implemented: true, exitCode: result.exitCode };
582
960
  } finally {
583
961
  for (const signal of signals) process.off(signal, onSignal);
962
+ relay?.stop();
584
963
  poller.stop();
585
964
  approver.teardown();
586
965
  input.close();
966
+ if (totalCostUsd > 0) {
967
+ process.stderr.write(`[pushary] remote session cost: $${totalCostUsd.toFixed(4)}
968
+ `);
969
+ }
587
970
  }
588
971
  };
589
972
 
@@ -597,14 +980,15 @@ var runClaudeWrapper = async (args2) => {
597
980
  return 127;
598
981
  }
599
982
  const nested = process.env[WRAPPER_ACTIVE_ENV] === "1";
600
- if (!nested && process.env.PUSHARY_WRAPPER_REMOTE === "1") {
983
+ const { remote: wantRemote, rest } = extractRemoteFlag(args2);
984
+ if (!nested && wantRemote) {
601
985
  try {
602
- const remote = await runRemoteMode(binary, args2);
986
+ const remote = await runRemoteMode(binary, rest);
603
987
  if (remote.implemented) return remote.exitCode ?? 0;
604
988
  } catch {
605
989
  }
606
990
  }
607
- return runLocalPassthrough(binary, args2);
991
+ return runLocalPassthrough(binary, rest);
608
992
  };
609
993
 
610
994
  // bin/pushary-claude.ts
@@ -20,7 +20,7 @@ import {
20
20
  savePendingQuestion,
21
21
  toCodexWire,
22
22
  toPolicyLookup
23
- } from "../chunk-V2WKECMG.js";
23
+ } from "../chunk-NRCQ5UUR.js";
24
24
  import {
25
25
  askUser,
26
26
  cancelQuestion,
@@ -34,7 +34,7 @@ import {
34
34
  resolvePolicy,
35
35
  sendNotification,
36
36
  waitForAnswer
37
- } from "../chunk-AUEPQATK.js";
37
+ } from "../chunk-ETDXSKR5.js";
38
38
  import {
39
39
  isGatingMoment,
40
40
  recordKeylessMoment
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  reportEvent
4
- } from "../chunk-V2WKECMG.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
5
  import {
6
6
  askUser,
7
7
  getMachineId,
8
8
  waitForAnswer
9
- } from "../chunk-AUEPQATK.js";
9
+ } from "../chunk-ETDXSKR5.js";
10
10
  import "../chunk-DWED7BS3.js";
11
11
  import "../chunk-Z5PL3K7C.js";
12
12
  import {
@@ -12,7 +12,7 @@ import {
12
12
  readLastUserPrompt,
13
13
  reportEvent,
14
14
  savePendingQuestion
15
- } from "../chunk-V2WKECMG.js";
15
+ } from "../chunk-NRCQ5UUR.js";
16
16
  import {
17
17
  askUser,
18
18
  cancelQuestion,
@@ -26,7 +26,7 @@ import {
26
26
  resolvePolicy,
27
27
  sendNotification,
28
28
  waitForAnswer
29
- } from "../chunk-AUEPQATK.js";
29
+ } from "../chunk-ETDXSKR5.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePreToolUse
4
- } from "../chunk-7JSFIVNA.js";
4
+ } from "../chunk-V2TSLWTV.js";
5
5
  import "../chunk-7EW3USQF.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-V2WKECMG.js";
8
- import "../chunk-AUEPQATK.js";
7
+ import "../chunk-NRCQ5UUR.js";
8
+ import "../chunk-ETDXSKR5.js";
9
9
  import "../chunk-R5AJNXZS.js";
10
10
  import "../chunk-DWED7BS3.js";
11
11
  import "../chunk-Z5PL3K7C.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleNotification
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionDenied
4
- } from "../chunk-7JSFIVNA.js";
4
+ } from "../chunk-V2TSLWTV.js";
5
5
  import "../chunk-7EW3USQF.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-V2WKECMG.js";
8
- import "../chunk-AUEPQATK.js";
7
+ import "../chunk-NRCQ5UUR.js";
8
+ import "../chunk-ETDXSKR5.js";
9
9
  import "../chunk-R5AJNXZS.js";
10
10
  import "../chunk-DWED7BS3.js";
11
11
  import "../chunk-Z5PL3K7C.js";
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePermissionRequest
4
- } from "../chunk-7JSFIVNA.js";
4
+ } from "../chunk-V2TSLWTV.js";
5
5
  import "../chunk-7EW3USQF.js";
6
6
  import "../chunk-KQYIHZ5E.js";
7
- import "../chunk-V2WKECMG.js";
8
- import "../chunk-AUEPQATK.js";
7
+ import "../chunk-NRCQ5UUR.js";
8
+ import "../chunk-ETDXSKR5.js";
9
9
  import "../chunk-R5AJNXZS.js";
10
10
  import "../chunk-DWED7BS3.js";
11
11
  import "../chunk-Z5PL3K7C.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handlePostToolUse
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleUserPrompt
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionEnd
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleSessionStart
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -24,8 +24,8 @@ import {
24
24
  } from "../chunk-7EW3USQF.js";
25
25
  import {
26
26
  reportEvent
27
- } from "../chunk-V2WKECMG.js";
28
- import "../chunk-AUEPQATK.js";
27
+ } from "../chunk-NRCQ5UUR.js";
28
+ import "../chunk-ETDXSKR5.js";
29
29
  import "../chunk-DWED7BS3.js";
30
30
  import {
31
31
  isValidApiKey
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStop
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleStopFailure
4
- } from "../chunk-V2WKECMG.js";
5
- import "../chunk-AUEPQATK.js";
4
+ } from "../chunk-NRCQ5UUR.js";
5
+ import "../chunk-ETDXSKR5.js";
6
6
  import "../chunk-DWED7BS3.js";
7
7
  import "../chunk-Z5PL3K7C.js";
8
8
  import "../chunk-NKXSILEW.js";
@@ -26,7 +26,8 @@ Pushary Agent Hooks
26
26
 
27
27
  Commands:
28
28
  setup Configure Claude Code, Codex, Gemini CLI, Hermes, or Cursor with Pushary
29
- claude Run Claude Code through Pushary (experimental wrapper; today a transparent passthrough)
29
+ claude Run Claude Code through Pushary (transparent passthrough; add --remote to
30
+ drive it from your phone: reach and re-prompt it even while idle)
30
31
  doctor Verify your Pushary installation is working
31
32
  clean Remove all Pushary configuration (--yes for non-interactive)
32
33
  mode Switch approval mode (push_only, push_first, terminal_only)
@@ -47,5 +48,6 @@ Usage:
47
48
  npx @pushary/agent-hooks@latest doctor
48
49
  npx @pushary/agent-hooks@latest mode push_only --for 30m
49
50
  npx @pushary/agent-hooks@latest wait 45
51
+ pushary claude --remote -p "start the refactor" # drive from your phone (needs @anthropic-ai/claude-agent-sdk)
50
52
  `);
51
53
  }
@@ -158,16 +158,17 @@ var fetchModeState = async (apiKey, sessionId) => {
158
158
  headers: { "Authorization": `Bearer ${apiKey}` },
159
159
  signal: AbortSignal.timeout(3e3)
160
160
  });
161
- if (!response.ok) return { mode: null, kill: false, policyVersion: null };
161
+ if (!response.ok) return { mode: null, kill: false, policyVersion: null, relayUrl: null };
162
162
  const data = await response.json();
163
163
  const mode = data.override?.mode;
164
164
  return {
165
165
  mode: isApprovalMode(mode) ? mode : null,
166
166
  kill: data.kill === true,
167
- policyVersion: toPolicyVersion(data.policyVersion)
167
+ policyVersion: toPolicyVersion(data.policyVersion),
168
+ relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
168
169
  };
169
170
  } catch {
170
- return { mode: null, kill: false, policyVersion: null };
171
+ return { mode: null, kill: false, policyVersion: null, relayUrl: null };
171
172
  }
172
173
  };
173
174
  var fetchModeOverride = async (apiKey) => (await fetchModeState(apiKey)).mode;
@@ -11,7 +11,7 @@ import {
11
11
  resolvePolicy,
12
12
  sendNotification,
13
13
  waitForAnswer
14
- } from "./chunk-AUEPQATK.js";
14
+ } from "./chunk-ETDXSKR5.js";
15
15
  import {
16
16
  withRetry
17
17
  } from "./chunk-DWED7BS3.js";
@@ -11,7 +11,7 @@ import {
11
11
  readLastUserPrompt,
12
12
  savePendingQuestion,
13
13
  throttlePass
14
- } from "./chunk-V2WKECMG.js";
14
+ } from "./chunk-NRCQ5UUR.js";
15
15
  import {
16
16
  askUser,
17
17
  cancelQuestion,
@@ -26,7 +26,7 @@ import {
26
26
  resolvePolicy,
27
27
  sendNotification,
28
28
  waitForAnswer
29
- } from "./chunk-AUEPQATK.js";
29
+ } from "./chunk-ETDXSKR5.js";
30
30
  import {
31
31
  isGatingMoment,
32
32
  recordKeylessMoment
@@ -33,6 +33,7 @@ interface ModeState {
33
33
  readonly mode: ApprovalMode | null;
34
34
  readonly kill: boolean;
35
35
  readonly policyVersion: string | null;
36
+ readonly relayUrl: string | null;
36
37
  }
37
38
  declare const fetchModeState: (apiKey: string, sessionId?: string) => Promise<ModeState>;
38
39
  declare const fetchModeOverride: (apiKey: string) => Promise<ApprovalMode | null>;
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-7JSFIVNA.js";
3
+ } from "../chunk-V2TSLWTV.js";
4
4
  import "../chunk-7EW3USQF.js";
5
5
  import "../chunk-KQYIHZ5E.js";
6
6
  import {
@@ -8,7 +8,7 @@ import {
8
8
  handlePostToolUse,
9
9
  handleStop,
10
10
  reportEvent
11
- } from "../chunk-V2WKECMG.js";
11
+ } from "../chunk-NRCQ5UUR.js";
12
12
  import {
13
13
  askUser,
14
14
  cancelQuestion,
@@ -17,7 +17,7 @@ import {
17
17
  getPolicy,
18
18
  resolvePolicy,
19
19
  waitForAnswer
20
- } from "../chunk-AUEPQATK.js";
20
+ } from "../chunk-ETDXSKR5.js";
21
21
  import "../chunk-R5AJNXZS.js";
22
22
  import "../chunk-DWED7BS3.js";
23
23
  import "../chunk-Z5PL3K7C.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushary/agent-hooks",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Permission hooks for AI coding agents: route tool approvals through Pushary push notifications",
5
5
  "keywords": [
6
6
  "pushary",
@@ -72,7 +72,7 @@
72
72
  "scripts": {
73
73
  "build": "node scripts/bundle-plugin.mjs && tsup",
74
74
  "dev": "tsup --watch",
75
- "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/reapply.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 && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts"
75
+ "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/reapply.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 && bun test src/ledger.test.ts && bun test src/wrapper/wrapper.test.ts && bun test src/wrapper/messageQueue.test.ts && bun test src/wrapper/approver.test.ts && bun test src/wrapper/remoteLoop.test.ts && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.test.ts"
76
76
  },
77
77
  "dependencies": {
78
78
  "@inquirer/prompts": "^8.4.2",