@pushary/agent-hooks 0.40.0 → 0.43.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
  });
@@ -95,15 +104,24 @@ var runLocalPassthrough = (binary, args2) => {
95
104
  import { basename } from "path";
96
105
 
97
106
  // src/wrapper/sdkLoader.ts
107
+ import { spawn as spawn2 } from "child_process";
108
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
109
+ import { homedir } from "os";
110
+ import { join as join2 } from "path";
111
+ import { pathToFileURL } from "url";
98
112
  var userMessage = (text) => ({
99
113
  type: "user",
100
114
  message: { role: "user", content: text },
101
115
  parent_tool_use_id: null
102
116
  });
103
- var loadClaudeSdk = async () => {
104
- const specifier = "@anthropic-ai/claude-agent-sdk";
117
+ var SDK_VERSION = "0.3.207";
118
+ var SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
119
+ var SDK_SPEC = `${SDK_PACKAGE}@${SDK_VERSION}`;
120
+ var sdkCacheDir = () => process.env.PUSHARY_REMOTE_SDK_DIR?.trim() || join2(homedir(), ".pushary", "remote-sdk");
121
+ var cachedSdkEntry = () => join2(sdkCacheDir(), "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs");
122
+ var importSdk = async (specifierOrUrl) => {
105
123
  try {
106
- const mod = await import(specifier);
124
+ const mod = await import(specifierOrUrl);
107
125
  if (typeof mod.query === "function") return mod;
108
126
  if (mod.default && typeof mod.default.query === "function") return mod.default;
109
127
  return null;
@@ -111,6 +129,44 @@ var loadClaudeSdk = async () => {
111
129
  return null;
112
130
  }
113
131
  };
132
+ var loadClaudeSdk = async () => {
133
+ const fromResolution = await importSdk(SDK_PACKAGE);
134
+ if (fromResolution) return fromResolution;
135
+ const entry = cachedSdkEntry();
136
+ if (existsSync(entry)) return importSdk(pathToFileURL(entry).href);
137
+ return null;
138
+ };
139
+ var ensureClaudeSdk = async (log) => {
140
+ const present = await loadClaudeSdk();
141
+ if (present) return present;
142
+ const dir = sdkCacheDir();
143
+ try {
144
+ mkdirSync(dir, { recursive: true });
145
+ const pkgJson = join2(dir, "package.json");
146
+ if (!existsSync(pkgJson)) {
147
+ writeFileSync(pkgJson, `${JSON.stringify({ name: "pushary-remote-sdk", private: true })}
148
+ `);
149
+ }
150
+ } catch {
151
+ return null;
152
+ }
153
+ log?.("[pushary] setting up remote mode (one-time, fetching the Claude Agent SDK)...");
154
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
155
+ const installed = await new Promise((resolve) => {
156
+ const child = spawn2(
157
+ npmCmd,
158
+ ["install", "--no-optional", "--no-audit", "--no-fund", "--loglevel=error", SDK_SPEC],
159
+ { cwd: dir, stdio: "ignore", shell: process.platform === "win32" }
160
+ );
161
+ child.on("error", () => resolve(false));
162
+ child.on("close", (code) => resolve(code === 0));
163
+ });
164
+ if (!installed) {
165
+ log?.(`[pushary] could not auto-install the SDK. Install it manually: npm i -g ${SDK_SPEC}`);
166
+ return null;
167
+ }
168
+ return loadClaudeSdk();
169
+ };
114
170
 
115
171
  // src/wrapper/messageQueue.ts
116
172
  var BoundedInputQueue = class {
@@ -119,12 +175,14 @@ var BoundedInputQueue = class {
119
175
  closed = false;
120
176
  cap;
121
177
  onDrop;
178
+ coalesce;
122
179
  pushedCount = 0;
123
180
  deliveredCount = 0;
124
181
  droppedCount = 0;
125
182
  constructor(options = {}) {
126
183
  this.cap = Math.max(1, options.cap ?? 32);
127
184
  this.onDrop = options.onDrop;
185
+ this.coalesce = options.coalesce;
128
186
  }
129
187
  push(item) {
130
188
  if (this.closed) return;
@@ -179,6 +237,11 @@ var BoundedInputQueue = class {
179
237
  return {
180
238
  next: () => {
181
239
  if (this.buffer.length > 0) {
240
+ if (this.coalesce && this.buffer.length > 1) {
241
+ const items = this.buffer.splice(0, this.buffer.length);
242
+ this.deliveredCount += items.length;
243
+ return Promise.resolve({ value: this.coalesce(items), done: false });
244
+ }
182
245
  const value = this.buffer.shift();
183
246
  this.deliveredCount++;
184
247
  return Promise.resolve({ value, done: false });
@@ -383,13 +446,28 @@ var startCommandPoller = (opts) => {
383
446
  if (stopped) return;
384
447
  timer = setTimeout(tick, jitter(delayMs));
385
448
  };
449
+ const fetchMode = async () => {
450
+ if (!opts.fetchModeState || !opts.onModeState) return;
451
+ try {
452
+ const state = await opts.fetchModeState(opts.apiKey, opts.getSessionId?.());
453
+ try {
454
+ opts.onModeState(state);
455
+ } catch {
456
+ }
457
+ } catch {
458
+ }
459
+ };
386
460
  const tick = async () => {
387
461
  if (stopped) return;
388
462
  const controller = new AbortController();
389
463
  inflight = controller;
390
464
  const timeout = setTimeout(() => controller.abort(), requestTimeout);
391
465
  try {
392
- const command = await drain(opts.apiKey, opts.getSessionId?.(), controller.signal);
466
+ const wantCommands = opts.shouldDrainCommands ? opts.shouldDrainCommands() : true;
467
+ const [command] = await Promise.all([
468
+ wantCommands ? drain(opts.apiKey, opts.getSessionId?.(), controller.signal) : Promise.resolve(null),
469
+ fetchMode()
470
+ ]);
393
471
  errorStreak = 0;
394
472
  if (command) {
395
473
  idleTicks = 0;
@@ -420,6 +498,286 @@ var startCommandPoller = (opts) => {
420
498
  };
421
499
  };
422
500
 
501
+ // src/wrapper/wsProtocol.ts
502
+ var PROTOCOL_VERSION = 1;
503
+ var TRANSCRIPT_TEXT_MAX = 4e3;
504
+ var encodeFrame = (frame) => JSON.stringify(frame);
505
+ var helloFrame = (fields) => ({ v: PROTOCOL_VERSION, t: "hello", ...fields });
506
+ var transcriptFrame = (seq, kind, text, meta) => ({
507
+ v: PROTOCOL_VERSION,
508
+ t: "transcript",
509
+ seq,
510
+ kind,
511
+ ...text !== void 0 ? { text: text.slice(0, TRANSCRIPT_TEXT_MAX) } : {},
512
+ ...meta ? { meta } : {}
513
+ });
514
+ var parse = (raw) => {
515
+ try {
516
+ const value = JSON.parse(raw);
517
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
518
+ const obj = value;
519
+ if (obj.v !== PROTOCOL_VERSION) return null;
520
+ return obj;
521
+ } catch {
522
+ return null;
523
+ }
524
+ };
525
+ var isString = (v) => typeof v === "string";
526
+ var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
527
+ var decodeServerFrame = (raw) => {
528
+ const f = parse(raw);
529
+ if (!f) return null;
530
+ switch (f.t) {
531
+ case "welcome":
532
+ return isFiniteNumber(f.resumeFromSeq) ? { v: PROTOCOL_VERSION, t: "welcome", resumeFromSeq: f.resumeFromSeq } : null;
533
+ case "ping":
534
+ return { v: PROTOCOL_VERSION, t: "ping" };
535
+ case "pong":
536
+ return { v: PROTOCOL_VERSION, t: "pong" };
537
+ case "command":
538
+ return isString(f.id) && isString(f.text) ? { v: PROTOCOL_VERSION, t: "command", id: f.id, text: f.text } : null;
539
+ case "resume":
540
+ return isFiniteNumber(f.afterSeq) ? { v: PROTOCOL_VERSION, t: "resume", afterSeq: f.afterSeq } : null;
541
+ case "error":
542
+ if (isString(f.code) && isString(f.message)) {
543
+ return { v: PROTOCOL_VERSION, t: "error", code: f.code, message: f.message, fatal: f.fatal === true };
544
+ }
545
+ return null;
546
+ default:
547
+ return null;
548
+ }
549
+ };
550
+
551
+ // src/wrapper/relayClient.ts
552
+ var DEFAULT_HEARTBEAT_MS = 2e4;
553
+ var DEFAULT_PONG_TIMEOUT_MS = 1e4;
554
+ var DEFAULT_MAX_OUTBOUND = 128;
555
+ var DEFAULT_MAX_SEEN_COMMANDS = 256;
556
+ var defaultBackoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4);
557
+ var defaultSocketFactory = (url) => {
558
+ const WS = globalThis.WebSocket;
559
+ if (!WS) throw new Error("no WebSocket in this runtime");
560
+ const socket = new WS(url);
561
+ return {
562
+ send: (data) => socket.send(data),
563
+ close: () => socket.close(),
564
+ onOpen: (cb) => socket.addEventListener("open", () => cb()),
565
+ onMessage: (cb) => socket.addEventListener("message", (event) => cb(String(event.data))),
566
+ onClose: (cb) => socket.addEventListener("close", () => cb()),
567
+ onError: (cb) => socket.addEventListener("error", (event) => cb(event))
568
+ };
569
+ };
570
+ var createRelayClient = (options) => {
571
+ const factory = options.socketFactory ?? defaultSocketFactory;
572
+ const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
573
+ const pongTimeoutMs = options.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
574
+ const backoff = options.backoffMs ?? defaultBackoff;
575
+ const maxOutbound = Math.max(1, options.maxOutbound ?? DEFAULT_MAX_OUTBOUND);
576
+ const maxSeenCommands = Math.max(1, options.maxSeenCommands ?? DEFAULT_MAX_SEEN_COMMANDS);
577
+ let socket;
578
+ let isConnected = false;
579
+ let stopped = false;
580
+ let seq = 0;
581
+ let attempt = 0;
582
+ let reconnectTimer;
583
+ let heartbeatTimer;
584
+ let pongTimer;
585
+ const outbound = [];
586
+ const seenCommands = [];
587
+ const seenSet = /* @__PURE__ */ new Set();
588
+ const log = (message) => options.log?.(message);
589
+ const clearTimers = () => {
590
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
591
+ if (pongTimer) clearTimeout(pongTimer);
592
+ if (reconnectTimer) clearTimeout(reconnectTimer);
593
+ heartbeatTimer = void 0;
594
+ pongTimer = void 0;
595
+ reconnectTimer = void 0;
596
+ };
597
+ const rememberCommand = (id) => {
598
+ if (seenSet.has(id)) return false;
599
+ seenSet.add(id);
600
+ seenCommands.push(id);
601
+ while (seenCommands.length > maxSeenCommands) {
602
+ const evicted = seenCommands.shift();
603
+ if (evicted) seenSet.delete(evicted);
604
+ }
605
+ return true;
606
+ };
607
+ const sendFrame = (frame) => {
608
+ if (!socket || !isConnected) return;
609
+ try {
610
+ socket.send(encodeFrame(frame));
611
+ } catch {
612
+ }
613
+ };
614
+ const enqueueTranscript = (frame) => {
615
+ if (isConnected) {
616
+ sendFrame(frame);
617
+ return;
618
+ }
619
+ outbound.push(frame);
620
+ while (outbound.length > maxOutbound) outbound.shift();
621
+ };
622
+ const flushOutbound = () => {
623
+ while (outbound.length > 0 && isConnected) {
624
+ const frame = outbound.shift();
625
+ if (frame) sendFrame(frame);
626
+ }
627
+ };
628
+ const armPong = () => {
629
+ if (pongTimer) clearTimeout(pongTimer);
630
+ pongTimer = setTimeout(() => {
631
+ log("[pushary] relay heartbeat timed out; reconnecting");
632
+ dropAndReconnect();
633
+ }, pongTimeoutMs);
634
+ };
635
+ const startHeartbeat = () => {
636
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
637
+ heartbeatTimer = setInterval(() => {
638
+ sendFrame({ v: PROTOCOL_VERSION, t: "ping" });
639
+ armPong();
640
+ }, heartbeatMs);
641
+ };
642
+ const onServerFrame = (raw) => {
643
+ const frame = decodeServerFrame(raw);
644
+ if (!frame) return;
645
+ switch (frame.t) {
646
+ case "welcome":
647
+ attempt = 0;
648
+ flushOutbound();
649
+ log("[pushary] relay connected");
650
+ break;
651
+ case "command":
652
+ sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id });
653
+ if (rememberCommand(frame.id)) {
654
+ try {
655
+ options.onCommand(frame.text);
656
+ } catch {
657
+ }
658
+ }
659
+ break;
660
+ case "ping":
661
+ sendFrame({ v: PROTOCOL_VERSION, t: "pong" });
662
+ break;
663
+ case "pong":
664
+ if (pongTimer) clearTimeout(pongTimer);
665
+ pongTimer = void 0;
666
+ break;
667
+ case "resume":
668
+ log(`[pushary] relay signalled a gap; catch up after seq ${frame.afterSeq}`);
669
+ break;
670
+ case "error":
671
+ if (frame.fatal) {
672
+ log(`[pushary] relay fatal: ${frame.message}`);
673
+ fatal(frame.message);
674
+ } else {
675
+ log(`[pushary] relay error: ${frame.message}`);
676
+ }
677
+ break;
678
+ }
679
+ };
680
+ const dropSocket = () => {
681
+ isConnected = false;
682
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
683
+ if (pongTimer) clearTimeout(pongTimer);
684
+ heartbeatTimer = void 0;
685
+ pongTimer = void 0;
686
+ if (socket) {
687
+ try {
688
+ socket.close();
689
+ } catch {
690
+ }
691
+ }
692
+ socket = void 0;
693
+ };
694
+ const scheduleReconnect = () => {
695
+ if (stopped) return;
696
+ if (reconnectTimer) return;
697
+ const delay = backoff(attempt);
698
+ attempt++;
699
+ const jittered = Math.max(1, Math.round(delay * (0.85 + Math.random() * 0.3)));
700
+ reconnectTimer = setTimeout(() => {
701
+ reconnectTimer = void 0;
702
+ connect();
703
+ }, jittered);
704
+ };
705
+ const dropAndReconnect = () => {
706
+ dropSocket();
707
+ scheduleReconnect();
708
+ };
709
+ const fatal = (message) => {
710
+ stopped = true;
711
+ clearTimers();
712
+ dropSocket();
713
+ options.onFatal?.(message);
714
+ };
715
+ const connect = () => {
716
+ if (stopped) return;
717
+ let created;
718
+ try {
719
+ created = factory(options.url);
720
+ } catch (error) {
721
+ fatal(error instanceof Error ? error.message : String(error));
722
+ return;
723
+ }
724
+ socket = created;
725
+ created.onOpen(() => {
726
+ isConnected = true;
727
+ sendFrame(
728
+ helloFrame({
729
+ apiKey: options.apiKey,
730
+ machineId: options.machineId,
731
+ agentType: options.agentType,
732
+ sessionId: options.getSessionId(),
733
+ lastSeq: seq
734
+ })
735
+ );
736
+ startHeartbeat();
737
+ });
738
+ created.onMessage((data) => onServerFrame(data));
739
+ created.onClose(() => {
740
+ if (stopped) return;
741
+ dropAndReconnect();
742
+ });
743
+ created.onError(() => {
744
+ });
745
+ };
746
+ return {
747
+ start() {
748
+ if (stopped) return;
749
+ connect();
750
+ },
751
+ connected() {
752
+ return isConnected;
753
+ },
754
+ sendTranscript(kind, text, meta) {
755
+ if (stopped) return;
756
+ seq++;
757
+ enqueueTranscript(transcriptFrame(seq, kind, text, meta));
758
+ },
759
+ reannounce() {
760
+ if (stopped || !isConnected) return;
761
+ sendFrame(
762
+ helloFrame({
763
+ apiKey: options.apiKey,
764
+ machineId: options.machineId,
765
+ agentType: options.agentType,
766
+ sessionId: options.getSessionId(),
767
+ lastSeq: seq
768
+ })
769
+ );
770
+ },
771
+ stop() {
772
+ if (stopped) return;
773
+ stopped = true;
774
+ sendFrame({ v: PROTOCOL_VERSION, t: "bye" });
775
+ clearTimers();
776
+ dropSocket();
777
+ }
778
+ };
779
+ };
780
+
423
781
  // src/wrapper/remoteLoop.ts
424
782
  var errMsg = (err) => err instanceof Error ? err.message : String(err);
425
783
  var DEFAULT_MAX_RESTARTS = 5;
@@ -441,7 +799,8 @@ var runRemoteLoop = async (deps) => {
441
799
  permissionMode: deps.permissionMode,
442
800
  canUseTool: deps.canUseTool,
443
801
  env: deps.env,
444
- cwd: deps.cwd
802
+ cwd: deps.cwd,
803
+ pathToClaudeCodeExecutable: deps.pathToClaudeCodeExecutable
445
804
  }
446
805
  });
447
806
  } catch (err) {
@@ -457,6 +816,7 @@ var runRemoteLoop = async (deps) => {
457
816
  else deps.signal.addEventListener("abort", onAbort, { once: true });
458
817
  try {
459
818
  for await (const message of query) {
819
+ deps.onMessage?.(message);
460
820
  if (message.type === "system" && message.subtype === "init" && typeof message.session_id === "string") {
461
821
  resumeId = message.session_id;
462
822
  deps.onSessionId?.(resumeId);
@@ -490,6 +850,10 @@ var resolveClaudeArgs = (argv) => {
490
850
  const rest = argv.slice(2);
491
851
  return rest[0] === "claude" ? rest.slice(1) : rest;
492
852
  };
853
+ var extractRemoteFlag = (args2) => ({
854
+ remote: args2.includes("--remote"),
855
+ rest: args2.filter((arg) => arg !== "--remote")
856
+ });
493
857
  var flagValue = (args2, index) => {
494
858
  const inline = args2[index];
495
859
  const eq = inline?.indexOf("=") ?? -1;
@@ -516,7 +880,37 @@ var parseRemoteArgs = (args2) => {
516
880
 
517
881
  // src/wrapper/remoteMode.ts
518
882
  var INPUT_QUEUE_CAP = 32;
519
- var runRemoteMode = async (_binary, args2) => {
883
+ var extractAssistantText = (message) => {
884
+ const content = message.message?.content;
885
+ if (typeof content === "string") return content || void 0;
886
+ if (Array.isArray(content)) {
887
+ const text = content.filter((block) => !!block && typeof block === "object").filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
888
+ return text || void 0;
889
+ }
890
+ return void 0;
891
+ };
892
+ var echoLocal = (message) => {
893
+ if (message.type === "assistant") {
894
+ const text = extractAssistantText(message);
895
+ if (text) process.stdout.write(`${text}
896
+ `);
897
+ } else if (message.type === "result") {
898
+ process.stderr.write("[pushary] turn complete \u2014 standing by for phone commands (Ctrl-C to exit)\n");
899
+ }
900
+ };
901
+ var streamTranscript = (relay, message) => {
902
+ if (message.type === "assistant") {
903
+ const text = extractAssistantText(message);
904
+ if (text) relay.sendTranscript("assistant", text);
905
+ } else if (message.type === "result") {
906
+ relay.sendTranscript(
907
+ "turn_end",
908
+ void 0,
909
+ typeof message.total_cost_usd === "number" ? { cost: message.total_cost_usd } : void 0
910
+ );
911
+ }
912
+ };
913
+ var runRemoteMode = async (binary, args2) => {
520
914
  let apiKey;
521
915
  try {
522
916
  apiKey = getApiKey();
@@ -526,21 +920,24 @@ var runRemoteMode = async (_binary, args2) => {
526
920
  );
527
921
  return { implemented: false };
528
922
  }
529
- const sdk = await loadClaudeSdk();
923
+ const sdk = await ensureClaudeSdk((message) => process.stderr.write(`${message}
924
+ `));
530
925
  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
- );
926
+ process.stderr.write("[pushary] remote mode is unavailable; running the normal passthrough.\n");
534
927
  return { implemented: false };
535
928
  }
536
929
  const projectName = basename(process.cwd());
537
930
  const machineId = getMachineId();
538
931
  const controller = new AbortController();
539
932
  let sessionId;
933
+ let killed = false;
934
+ let totalCostUsd = 0;
540
935
  const input = new BoundedInputQueue({
541
936
  cap: INPUT_QUEUE_CAP,
542
937
  onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; dropped ${total} stale instruction(s)
543
- `)
938
+ `),
939
+ // Several instructions that piled up between turns are sent as one turn.
940
+ coalesce: (items) => userMessage(items.map((m) => m.message.content).join("\n\n"))
544
941
  });
545
942
  const approver = createRemoteApprover({
546
943
  apiKey,
@@ -548,10 +945,41 @@ var runRemoteMode = async (_binary, args2) => {
548
945
  projectName,
549
946
  getSessionId: () => sessionId
550
947
  });
948
+ const initialMode = await fetchModeState(apiKey).catch(() => null);
949
+ let relay;
950
+ if (initialMode?.relayUrl) {
951
+ relay = createRelayClient({
952
+ url: initialMode.relayUrl,
953
+ apiKey,
954
+ machineId,
955
+ agentType: "claude_code",
956
+ getSessionId: () => sessionId,
957
+ onCommand: (command) => input.push(userMessage(command)),
958
+ onFatal: (message) => process.stderr.write(`[pushary] relay unavailable (${message}); using polling
959
+ `),
960
+ log: (message) => process.stderr.write(`${message}
961
+ `)
962
+ });
963
+ relay.start();
964
+ }
551
965
  const poller = startCommandPoller({
552
966
  apiKey,
553
967
  getSessionId: () => sessionId,
554
968
  onCommand: (command) => input.push(userMessage(command)),
969
+ // While the relay socket owns command delivery, the poller must not also drain
970
+ // the single-use queue (that would deliver the command twice); it still reads
971
+ // mode/kill. When the socket is down or absent, the poller is the source.
972
+ shouldDrainCommands: () => !(relay?.connected() ?? false),
973
+ // Watch the kill switch: a phone "stop" interrupts the running turn (via the
974
+ // controller abort below), instead of only denying the next tool call.
975
+ fetchModeState,
976
+ onModeState: (state) => {
977
+ if (state.kill && !killed) {
978
+ killed = true;
979
+ process.stderr.write("[pushary] halted from Pushary \u2014 stopping the agent\n");
980
+ controller.abort();
981
+ }
982
+ },
555
983
  log: (message) => process.stderr.write(`${message}
556
984
  `)
557
985
  });
@@ -572,8 +1000,24 @@ var runRemoteMode = async (_binary, args2) => {
572
1000
  // recursion guard so a wrapper-spawned claude cannot re-enter the wrapper.
573
1001
  env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
574
1002
  cwd: process.cwd(),
1003
+ // Run the user's OWN claude, not the SDK's bundled copy, so whatever login
1004
+ // already works for `claude` works here with no extra setup. On Windows a
1005
+ // `.cmd`/`.bat` shim can't be spawned directly by the SDK, so there we let
1006
+ // the SDK use its bundled binary (Windows reads creds from a plain file, so
1007
+ // there is no keychain-ACL mismatch to work around).
1008
+ pathToClaudeCodeExecutable: needsShell(binary) ? void 0 : binary,
575
1009
  onSessionId: (id) => {
576
1010
  sessionId = id;
1011
+ relay?.reannounce();
1012
+ },
1013
+ onCost: (usd) => {
1014
+ totalCostUsd = usd;
1015
+ },
1016
+ // Always echo the turn to the local terminal; also stream a bounded live
1017
+ // transcript to the phone when the relay is up (it presence-gates it).
1018
+ onMessage: (message) => {
1019
+ echoLocal(message);
1020
+ if (relay) streamTranscript(relay, message);
577
1021
  },
578
1022
  log: (message) => process.stderr.write(`${message}
579
1023
  `)
@@ -581,9 +1025,14 @@ var runRemoteMode = async (_binary, args2) => {
581
1025
  return { implemented: true, exitCode: result.exitCode };
582
1026
  } finally {
583
1027
  for (const signal of signals) process.off(signal, onSignal);
1028
+ relay?.stop();
584
1029
  poller.stop();
585
1030
  approver.teardown();
586
1031
  input.close();
1032
+ if (totalCostUsd > 0) {
1033
+ process.stderr.write(`[pushary] remote session cost: $${totalCostUsd.toFixed(4)}
1034
+ `);
1035
+ }
587
1036
  }
588
1037
  };
589
1038
 
@@ -597,14 +1046,15 @@ var runClaudeWrapper = async (args2) => {
597
1046
  return 127;
598
1047
  }
599
1048
  const nested = process.env[WRAPPER_ACTIVE_ENV] === "1";
600
- if (!nested && process.env.PUSHARY_WRAPPER_REMOTE === "1") {
1049
+ const { remote: wantRemote, rest } = extractRemoteFlag(args2);
1050
+ if (!nested && wantRemote) {
601
1051
  try {
602
- const remote = await runRemoteMode(binary, args2);
1052
+ const remote = await runRemoteMode(binary, rest);
603
1053
  if (remote.implemented) return remote.exitCode ?? 0;
604
1054
  } catch {
605
1055
  }
606
1056
  }
607
- return runLocalPassthrough(binary, args2);
1057
+ return runLocalPassthrough(binary, rest);
608
1058
  };
609
1059
 
610
1060
  // 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 (uses your existing Claude login; sets up on first run)
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.43.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",