@pushary/agent-hooks 0.44.0 → 0.48.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.
- package/dist/bin/pushary-claude.js +449 -191
- package/dist/bin/pushary.js +5 -3
- package/package.json +2 -2
|
@@ -101,7 +101,7 @@ var runLocalPassthrough = (binary, args2) => {
|
|
|
101
101
|
});
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
// src/wrapper/
|
|
104
|
+
// src/wrapper/dualMode.ts
|
|
105
105
|
import { basename } from "path";
|
|
106
106
|
|
|
107
107
|
// src/wrapper/sdkLoader.ts
|
|
@@ -151,7 +151,7 @@ var ensureClaudeSdk = async (log) => {
|
|
|
151
151
|
} catch {
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
|
-
log?.("[pushary] setting up
|
|
154
|
+
log?.("[pushary] setting up phone control (one-time, fetching the Claude Agent SDK)...");
|
|
155
155
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
156
156
|
const installed = await new Promise((resolve) => {
|
|
157
157
|
const child = spawn2(
|
|
@@ -225,6 +225,12 @@ var BoundedInputQueue = class {
|
|
|
225
225
|
resolve({ value: void 0, done: true });
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
// Drop every buffered item without closing the queue. Used when phone control
|
|
229
|
+
// turns out to be unavailable (offline SDK setup) so a pending instruction does
|
|
230
|
+
// not make the local leg switch again into a mode that cannot run.
|
|
231
|
+
clear() {
|
|
232
|
+
this.buffer.length = 0;
|
|
233
|
+
}
|
|
228
234
|
get isClosed() {
|
|
229
235
|
return this.closed;
|
|
230
236
|
}
|
|
@@ -330,6 +336,7 @@ var createRemoteApprover = (deps) => {
|
|
|
330
336
|
}
|
|
331
337
|
const controller = new AbortController();
|
|
332
338
|
const entry = { controller };
|
|
339
|
+
let answered = false;
|
|
333
340
|
pending.add(entry);
|
|
334
341
|
const outerAbort = () => controller.abort();
|
|
335
342
|
if (options?.signal) {
|
|
@@ -366,6 +373,7 @@ var createRemoteApprover = (deps) => {
|
|
|
366
373
|
entry.correlationId = question.correlationId;
|
|
367
374
|
if (question.noDevices) return applyTimeout(policy, input);
|
|
368
375
|
const answer = await waitForPhone(entry, question.correlationId, windowFor(policy));
|
|
376
|
+
answered = answer.answered;
|
|
369
377
|
if (answer.answered) {
|
|
370
378
|
if (isDeferAnswer(answer.value)) return deny("Deferred \u2014 handle it on the machine");
|
|
371
379
|
return answer.value === "yes" ? allow(input) : deny("Denied from your phone");
|
|
@@ -375,6 +383,10 @@ var createRemoteApprover = (deps) => {
|
|
|
375
383
|
return deny("Approval unavailable; denied by default");
|
|
376
384
|
} finally {
|
|
377
385
|
if (options?.signal) options.signal.removeEventListener("abort", outerAbort);
|
|
386
|
+
if (entry.correlationId && !answered && !tornDown) {
|
|
387
|
+
void cancelQuestion2(deps.apiKey, entry.correlationId).catch(() => {
|
|
388
|
+
});
|
|
389
|
+
}
|
|
378
390
|
pending.delete(entry);
|
|
379
391
|
}
|
|
380
392
|
};
|
|
@@ -465,10 +477,23 @@ var startCommandPoller = (opts) => {
|
|
|
465
477
|
const timeout = setTimeout(() => controller.abort(), requestTimeout);
|
|
466
478
|
try {
|
|
467
479
|
const wantCommands = opts.shouldDrainCommands ? opts.shouldDrainCommands() : true;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
480
|
+
let command;
|
|
481
|
+
if (opts.poll) {
|
|
482
|
+
const result = await opts.poll(opts.apiKey, opts.getSessionId?.(), wantCommands, controller.signal);
|
|
483
|
+
command = result.command;
|
|
484
|
+
if (result.mode && opts.onModeState) {
|
|
485
|
+
try {
|
|
486
|
+
opts.onModeState(result.mode);
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
;
|
|
492
|
+
[command] = await Promise.all([
|
|
493
|
+
wantCommands ? drain(opts.apiKey, opts.getSessionId?.(), controller.signal) : Promise.resolve(null),
|
|
494
|
+
fetchMode()
|
|
495
|
+
]);
|
|
496
|
+
}
|
|
472
497
|
errorStreak = 0;
|
|
473
498
|
if (command) {
|
|
474
499
|
idleTicks = 0;
|
|
@@ -499,19 +524,39 @@ var startCommandPoller = (opts) => {
|
|
|
499
524
|
};
|
|
500
525
|
};
|
|
501
526
|
|
|
527
|
+
// src/wrapper/pollClient.ts
|
|
528
|
+
var APPROVAL_MODES = /* @__PURE__ */ new Set(["push_only", "terminal_only", "push_first", "notify_only"]);
|
|
529
|
+
var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBaseUrl()) => {
|
|
530
|
+
try {
|
|
531
|
+
const response = await fetch(`${baseUrl}/api/agent/poll`, {
|
|
532
|
+
method: "POST",
|
|
533
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
534
|
+
body: JSON.stringify({
|
|
535
|
+
...sessionId ? { sessionId } : {},
|
|
536
|
+
...wantCommand ? {} : { drain: false }
|
|
537
|
+
}),
|
|
538
|
+
signal
|
|
539
|
+
});
|
|
540
|
+
if (!response.ok) return { command: null, mode: null };
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
const command = typeof data.command === "string" && data.command.length > 0 ? data.command : null;
|
|
543
|
+
const m = data.override?.mode;
|
|
544
|
+
const mode = {
|
|
545
|
+
mode: typeof m === "string" && APPROVAL_MODES.has(m) ? m : null,
|
|
546
|
+
kill: data.kill === true,
|
|
547
|
+
policyVersion: typeof data.policyVersion === "string" && data.policyVersion.length > 0 ? data.policyVersion : null,
|
|
548
|
+
relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
|
|
549
|
+
};
|
|
550
|
+
return { command, mode };
|
|
551
|
+
} catch {
|
|
552
|
+
return { command: null, mode: null };
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
502
556
|
// src/wrapper/wsProtocol.ts
|
|
503
557
|
var PROTOCOL_VERSION = 1;
|
|
504
|
-
var TRANSCRIPT_TEXT_MAX = 4e3;
|
|
505
558
|
var encodeFrame = (frame) => JSON.stringify(frame);
|
|
506
559
|
var helloFrame = (fields) => ({ v: PROTOCOL_VERSION, t: "hello", ...fields });
|
|
507
|
-
var transcriptFrame = (seq, kind, text, meta) => ({
|
|
508
|
-
v: PROTOCOL_VERSION,
|
|
509
|
-
t: "transcript",
|
|
510
|
-
seq,
|
|
511
|
-
kind,
|
|
512
|
-
...text !== void 0 ? { text: text.slice(0, TRANSCRIPT_TEXT_MAX) } : {},
|
|
513
|
-
...meta ? { meta } : {}
|
|
514
|
-
});
|
|
515
560
|
var parse = (raw) => {
|
|
516
561
|
try {
|
|
517
562
|
const value = JSON.parse(raw);
|
|
@@ -524,21 +569,18 @@ var parse = (raw) => {
|
|
|
524
569
|
}
|
|
525
570
|
};
|
|
526
571
|
var isString = (v) => typeof v === "string";
|
|
527
|
-
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
528
572
|
var decodeServerFrame = (raw) => {
|
|
529
573
|
const f = parse(raw);
|
|
530
574
|
if (!f) return null;
|
|
531
575
|
switch (f.t) {
|
|
532
576
|
case "welcome":
|
|
533
|
-
return
|
|
577
|
+
return { v: PROTOCOL_VERSION, t: "welcome" };
|
|
534
578
|
case "ping":
|
|
535
579
|
return { v: PROTOCOL_VERSION, t: "ping" };
|
|
536
580
|
case "pong":
|
|
537
581
|
return { v: PROTOCOL_VERSION, t: "pong" };
|
|
538
582
|
case "command":
|
|
539
583
|
return isString(f.id) && isString(f.text) ? { v: PROTOCOL_VERSION, t: "command", id: f.id, text: f.text } : null;
|
|
540
|
-
case "resume":
|
|
541
|
-
return isFiniteNumber(f.afterSeq) ? { v: PROTOCOL_VERSION, t: "resume", afterSeq: f.afterSeq } : null;
|
|
542
584
|
case "error":
|
|
543
585
|
if (isString(f.code) && isString(f.message)) {
|
|
544
586
|
return { v: PROTOCOL_VERSION, t: "error", code: f.code, message: f.message, fatal: f.fatal === true };
|
|
@@ -552,7 +594,6 @@ var decodeServerFrame = (raw) => {
|
|
|
552
594
|
// src/wrapper/relayClient.ts
|
|
553
595
|
var DEFAULT_HEARTBEAT_MS = 2e4;
|
|
554
596
|
var DEFAULT_PONG_TIMEOUT_MS = 1e4;
|
|
555
|
-
var DEFAULT_MAX_OUTBOUND = 128;
|
|
556
597
|
var DEFAULT_MAX_SEEN_COMMANDS = 256;
|
|
557
598
|
var defaultBackoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4);
|
|
558
599
|
var defaultSocketFactory = (url) => {
|
|
@@ -573,17 +614,14 @@ var createRelayClient = (options) => {
|
|
|
573
614
|
const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
574
615
|
const pongTimeoutMs = options.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
|
|
575
616
|
const backoff = options.backoffMs ?? defaultBackoff;
|
|
576
|
-
const maxOutbound = Math.max(1, options.maxOutbound ?? DEFAULT_MAX_OUTBOUND);
|
|
577
617
|
const maxSeenCommands = Math.max(1, options.maxSeenCommands ?? DEFAULT_MAX_SEEN_COMMANDS);
|
|
578
618
|
let socket;
|
|
579
619
|
let isConnected = false;
|
|
580
620
|
let stopped = false;
|
|
581
|
-
let seq = 0;
|
|
582
621
|
let attempt = 0;
|
|
583
622
|
let reconnectTimer;
|
|
584
623
|
let heartbeatTimer;
|
|
585
624
|
let pongTimer;
|
|
586
|
-
const outbound = [];
|
|
587
625
|
const seenCommands = [];
|
|
588
626
|
const seenSet = /* @__PURE__ */ new Set();
|
|
589
627
|
const log = (message) => options.log?.(message);
|
|
@@ -612,20 +650,14 @@ var createRelayClient = (options) => {
|
|
|
612
650
|
} catch {
|
|
613
651
|
}
|
|
614
652
|
};
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const flushOutbound = () => {
|
|
624
|
-
while (outbound.length > 0 && isConnected) {
|
|
625
|
-
const frame = outbound.shift();
|
|
626
|
-
if (frame) sendFrame(frame);
|
|
627
|
-
}
|
|
628
|
-
};
|
|
653
|
+
const sendHello = () => sendFrame(
|
|
654
|
+
helloFrame({
|
|
655
|
+
apiKey: options.apiKey,
|
|
656
|
+
machineId: options.machineId,
|
|
657
|
+
agentType: options.agentType,
|
|
658
|
+
sessionId: options.getSessionId()
|
|
659
|
+
})
|
|
660
|
+
);
|
|
629
661
|
const armPong = () => {
|
|
630
662
|
if (pongTimer) clearTimeout(pongTimer);
|
|
631
663
|
pongTimer = setTimeout(() => {
|
|
@@ -646,7 +678,6 @@ var createRelayClient = (options) => {
|
|
|
646
678
|
switch (frame.t) {
|
|
647
679
|
case "welcome":
|
|
648
680
|
attempt = 0;
|
|
649
|
-
flushOutbound();
|
|
650
681
|
log("[pushary] relay connected");
|
|
651
682
|
break;
|
|
652
683
|
case "command":
|
|
@@ -665,9 +696,6 @@ var createRelayClient = (options) => {
|
|
|
665
696
|
if (pongTimer) clearTimeout(pongTimer);
|
|
666
697
|
pongTimer = void 0;
|
|
667
698
|
break;
|
|
668
|
-
case "resume":
|
|
669
|
-
log(`[pushary] relay signalled a gap; catch up after seq ${frame.afterSeq}`);
|
|
670
|
-
break;
|
|
671
699
|
case "error":
|
|
672
700
|
if (frame.fatal) {
|
|
673
701
|
log(`[pushary] relay fatal: ${frame.message}`);
|
|
@@ -725,15 +753,7 @@ var createRelayClient = (options) => {
|
|
|
725
753
|
socket = created;
|
|
726
754
|
created.onOpen(() => {
|
|
727
755
|
isConnected = true;
|
|
728
|
-
|
|
729
|
-
helloFrame({
|
|
730
|
-
apiKey: options.apiKey,
|
|
731
|
-
machineId: options.machineId,
|
|
732
|
-
agentType: options.agentType,
|
|
733
|
-
sessionId: options.getSessionId(),
|
|
734
|
-
lastSeq: seq
|
|
735
|
-
})
|
|
736
|
-
);
|
|
756
|
+
sendHello();
|
|
737
757
|
startHeartbeat();
|
|
738
758
|
});
|
|
739
759
|
created.onMessage((data) => onServerFrame(data));
|
|
@@ -752,22 +772,9 @@ var createRelayClient = (options) => {
|
|
|
752
772
|
connected() {
|
|
753
773
|
return isConnected;
|
|
754
774
|
},
|
|
755
|
-
sendTranscript(kind, text, meta) {
|
|
756
|
-
if (stopped) return;
|
|
757
|
-
seq++;
|
|
758
|
-
enqueueTranscript(transcriptFrame(seq, kind, text, meta));
|
|
759
|
-
},
|
|
760
775
|
reannounce() {
|
|
761
776
|
if (stopped || !isConnected) return;
|
|
762
|
-
|
|
763
|
-
helloFrame({
|
|
764
|
-
apiKey: options.apiKey,
|
|
765
|
-
machineId: options.machineId,
|
|
766
|
-
agentType: options.agentType,
|
|
767
|
-
sessionId: options.getSessionId(),
|
|
768
|
-
lastSeq: seq
|
|
769
|
-
})
|
|
770
|
-
);
|
|
777
|
+
sendHello();
|
|
771
778
|
},
|
|
772
779
|
stop() {
|
|
773
780
|
if (stopped) return;
|
|
@@ -779,6 +786,166 @@ var createRelayClient = (options) => {
|
|
|
779
786
|
};
|
|
780
787
|
};
|
|
781
788
|
|
|
789
|
+
// src/wrapper/stdinHandoff.ts
|
|
790
|
+
var isTty = () => Boolean(process.stdin.isTTY);
|
|
791
|
+
var createStdinHandoff = () => {
|
|
792
|
+
let listener;
|
|
793
|
+
let captured = false;
|
|
794
|
+
const removeListener = () => {
|
|
795
|
+
if (listener) {
|
|
796
|
+
process.stdin.off("data", listener);
|
|
797
|
+
listener = void 0;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
const setRaw = (on) => {
|
|
801
|
+
if (!isTty()) return;
|
|
802
|
+
try {
|
|
803
|
+
process.stdin.setRawMode(on);
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
const captureForWrapper = (onKey) => {
|
|
808
|
+
removeListener();
|
|
809
|
+
listener = onKey;
|
|
810
|
+
if (!isTty()) return;
|
|
811
|
+
try {
|
|
812
|
+
process.stdin.resume();
|
|
813
|
+
setRaw(true);
|
|
814
|
+
process.stdin.setEncoding("utf8");
|
|
815
|
+
process.stdin.on("data", listener);
|
|
816
|
+
captured = true;
|
|
817
|
+
} catch {
|
|
818
|
+
removeListener();
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
const releaseToChild = () => {
|
|
822
|
+
removeListener();
|
|
823
|
+
setRaw(false);
|
|
824
|
+
if (captured && isTty()) {
|
|
825
|
+
try {
|
|
826
|
+
process.stdin.pause();
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
captured = false;
|
|
831
|
+
};
|
|
832
|
+
return {
|
|
833
|
+
captureForWrapper,
|
|
834
|
+
releaseToChild,
|
|
835
|
+
dispose: releaseToChild
|
|
836
|
+
};
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// src/wrapper/args.ts
|
|
840
|
+
var resolveClaudeArgs = (argv) => {
|
|
841
|
+
const rest = argv.slice(2);
|
|
842
|
+
return rest[0] === "claude" ? rest.slice(1) : rest;
|
|
843
|
+
};
|
|
844
|
+
var extractRemoteFlag = (args2) => ({
|
|
845
|
+
remote: args2.includes("--remote"),
|
|
846
|
+
rest: args2.filter((arg) => arg !== "--remote")
|
|
847
|
+
});
|
|
848
|
+
var flagValue = (args2, index) => {
|
|
849
|
+
const inline = args2[index];
|
|
850
|
+
const eq = inline?.indexOf("=") ?? -1;
|
|
851
|
+
if (inline && eq > 0) return inline.slice(eq + 1);
|
|
852
|
+
const next = args2[index + 1];
|
|
853
|
+
return next && !next.startsWith("-") ? next : void 0;
|
|
854
|
+
};
|
|
855
|
+
var parseRemoteArgs = (args2) => {
|
|
856
|
+
let initialPrompt;
|
|
857
|
+
let resume;
|
|
858
|
+
let permissionMode;
|
|
859
|
+
for (let i = 0; i < args2.length; i++) {
|
|
860
|
+
const arg = args2[i];
|
|
861
|
+
if (arg === "-p" || arg === "--print" || arg?.startsWith("--print=")) {
|
|
862
|
+
initialPrompt = flagValue(args2, i);
|
|
863
|
+
} else if (arg === "-r" || arg === "--resume" || arg?.startsWith("--resume=")) {
|
|
864
|
+
resume = flagValue(args2, i);
|
|
865
|
+
} else if (arg === "--permission-mode" || arg?.startsWith("--permission-mode=")) {
|
|
866
|
+
permissionMode = flagValue(args2, i);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return { initialPrompt, resume, permissionMode };
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
// src/wrapper/localLeg.ts
|
|
873
|
+
var SIGNAL_EXIT_BASE = 128;
|
|
874
|
+
var SIGNAL_NUMBERS2 = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
875
|
+
var nativeArgs = (session) => {
|
|
876
|
+
const args2 = [...session.claudeArgs];
|
|
877
|
+
const id = session.getSessionId();
|
|
878
|
+
if (id && !session.userManagesSession) {
|
|
879
|
+
args2.push(session.isSessionCreated() ? "--resume" : "--session-id", id);
|
|
880
|
+
}
|
|
881
|
+
return args2;
|
|
882
|
+
};
|
|
883
|
+
var runLocalLeg = (session, deps = {}) => {
|
|
884
|
+
const spawn3 = deps.spawn ?? spawnClaude;
|
|
885
|
+
return new Promise((resolve) => {
|
|
886
|
+
const legAbort = new AbortController();
|
|
887
|
+
let exitReason = null;
|
|
888
|
+
let settled = false;
|
|
889
|
+
const finish = () => {
|
|
890
|
+
if (settled) return;
|
|
891
|
+
settled = true;
|
|
892
|
+
session.setActiveLeg(null);
|
|
893
|
+
resolve(exitReason ?? { type: "exit", code: 0 });
|
|
894
|
+
};
|
|
895
|
+
const control = {
|
|
896
|
+
onPhoneCommand: () => {
|
|
897
|
+
if (!exitReason) exitReason = "switch";
|
|
898
|
+
if (!legAbort.signal.aborted) legAbort.abort();
|
|
899
|
+
},
|
|
900
|
+
stop: (reason) => {
|
|
901
|
+
if (!exitReason) exitReason = { type: "exit", code: reason === "signal" ? 130 : 0 };
|
|
902
|
+
if (!legAbort.signal.aborted) legAbort.abort();
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
session.setActiveLeg(control);
|
|
906
|
+
if (session.input.bufferedCount > 0) {
|
|
907
|
+
exitReason = "switch";
|
|
908
|
+
finish();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
session.stdin.releaseToChild();
|
|
912
|
+
let child;
|
|
913
|
+
try {
|
|
914
|
+
child = spawn3(session.binary, nativeArgs(session), {
|
|
915
|
+
stdio: "inherit",
|
|
916
|
+
env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
|
|
917
|
+
signal: legAbort.signal
|
|
918
|
+
});
|
|
919
|
+
} catch {
|
|
920
|
+
exitReason = exitReason ?? { type: "exit", code: 127 };
|
|
921
|
+
finish();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
session.markSessionCreated();
|
|
925
|
+
child.on("error", () => {
|
|
926
|
+
if (!legAbort.signal.aborted && !exitReason) {
|
|
927
|
+
exitReason = { type: "exit", code: 127 };
|
|
928
|
+
}
|
|
929
|
+
finish();
|
|
930
|
+
});
|
|
931
|
+
child.on("exit", (code, signal) => {
|
|
932
|
+
if (signal === "SIGTERM" && legAbort.signal.aborted) {
|
|
933
|
+
} else if (!exitReason) {
|
|
934
|
+
if (session.input.bufferedCount > 0) {
|
|
935
|
+
exitReason = "switch";
|
|
936
|
+
} else if (typeof code === "number") {
|
|
937
|
+
exitReason = { type: "exit", code };
|
|
938
|
+
} else if (signal) {
|
|
939
|
+
exitReason = { type: "exit", code: SIGNAL_EXIT_BASE + (SIGNAL_NUMBERS2[signal] ?? 0) };
|
|
940
|
+
} else {
|
|
941
|
+
exitReason = { type: "exit", code: 0 };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
finish();
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
|
|
782
949
|
// src/wrapper/remoteLoop.ts
|
|
783
950
|
var errMsg = (err) => err instanceof Error ? err.message : String(err);
|
|
784
951
|
var DEFAULT_MAX_RESTARTS = 5;
|
|
@@ -846,41 +1013,89 @@ var runRemoteLoop = async (deps) => {
|
|
|
846
1013
|
return { exitCode: 130, totalCostUsd: totalCost };
|
|
847
1014
|
};
|
|
848
1015
|
|
|
849
|
-
// src/wrapper/
|
|
850
|
-
var
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1016
|
+
// src/wrapper/remoteLeg.ts
|
|
1017
|
+
var CTRL_RIGHT_BRACKET = "";
|
|
1018
|
+
var CTRL_C = "";
|
|
1019
|
+
var runRemoteLeg = async (session, sdk) => {
|
|
1020
|
+
const legAbort = new AbortController();
|
|
1021
|
+
let switchBack = false;
|
|
1022
|
+
const control = {
|
|
1023
|
+
// The running query already drains the shared input queue, so a freshly pushed
|
|
1024
|
+
// phone command is delivered without any action here.
|
|
1025
|
+
onPhoneCommand: () => {
|
|
1026
|
+
},
|
|
1027
|
+
stop: () => {
|
|
1028
|
+
if (!legAbort.signal.aborted) legAbort.abort();
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
session.setActiveLeg(control);
|
|
1032
|
+
session.stdin.captureForWrapper((data) => {
|
|
1033
|
+
if (data.includes(CTRL_RIGHT_BRACKET)) {
|
|
1034
|
+
switchBack = true;
|
|
1035
|
+
if (!legAbort.signal.aborted) legAbort.abort();
|
|
1036
|
+
} else if (data.includes(CTRL_C)) {
|
|
1037
|
+
if (!legAbort.signal.aborted) legAbort.abort();
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
process.stderr.write(
|
|
1041
|
+
"[pushary] remote \u2014 driving from your phone \xB7 press Ctrl-] for the local terminal\n"
|
|
1042
|
+
);
|
|
1043
|
+
let exitCode = 0;
|
|
1044
|
+
try {
|
|
1045
|
+
const result = await runRemoteLoop({
|
|
1046
|
+
sdk,
|
|
1047
|
+
input: session.input,
|
|
1048
|
+
canUseTool: session.approver.canUseTool,
|
|
1049
|
+
signal: legAbort.signal,
|
|
1050
|
+
// Resume the session the local leg (or a prior turn) created, so the
|
|
1051
|
+
// conversation is continuous across the switch. Undefined = fresh session.
|
|
1052
|
+
initialResume: session.getSessionId(),
|
|
1053
|
+
permissionMode: session.permissionMode,
|
|
1054
|
+
// The SDK REPLACES the subprocess env; spread process.env and mark the
|
|
1055
|
+
// recursion guard so a wrapper-spawned claude cannot re-enter the wrapper.
|
|
1056
|
+
env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
|
|
1057
|
+
cwd: process.cwd(),
|
|
1058
|
+
// Run the user's OWN claude (auth + version parity); Windows .cmd shims keep
|
|
1059
|
+
// the SDK's bundled binary since the SDK cannot spawn a shim directly.
|
|
1060
|
+
pathToClaudeCodeExecutable: needsShell(session.binary) ? void 0 : session.binary,
|
|
1061
|
+
onSessionId: (id) => session.setSessionId(id),
|
|
1062
|
+
onMessage: (message) => session.streamMessage(message),
|
|
1063
|
+
log: (message) => process.stderr.write(`${message}
|
|
1064
|
+
`)
|
|
1065
|
+
});
|
|
1066
|
+
exitCode = result.exitCode;
|
|
1067
|
+
} finally {
|
|
1068
|
+
session.stdin.releaseToChild();
|
|
1069
|
+
session.setActiveLeg(null);
|
|
1070
|
+
}
|
|
1071
|
+
return switchBack ? "switch" : { type: "exit", code: exitCode };
|
|
864
1072
|
};
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1073
|
+
|
|
1074
|
+
// src/wrapper/modeLoop.ts
|
|
1075
|
+
var DEFAULT_MIN_DWELL_MS = 300;
|
|
1076
|
+
var driveModeLoop = async (deps) => {
|
|
1077
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1078
|
+
const now = deps.now ?? (() => Date.now());
|
|
1079
|
+
const minDwell = deps.minDwellMs ?? DEFAULT_MIN_DWELL_MS;
|
|
1080
|
+
let mode = deps.startingMode;
|
|
1081
|
+
let lastSwitchAt = 0;
|
|
1082
|
+
while (true) {
|
|
1083
|
+
const result = mode === "local" ? await deps.runLocal() : await deps.runRemote();
|
|
1084
|
+
if (result !== "switch" || deps.isFinished()) {
|
|
1085
|
+
return result === "switch" ? 0 : result.code;
|
|
877
1086
|
}
|
|
1087
|
+
const since = now() - lastSwitchAt;
|
|
1088
|
+
if (since < minDwell) await sleep(minDwell - since);
|
|
1089
|
+
lastSwitchAt = now();
|
|
1090
|
+
mode = mode === "local" ? "remote" : "local";
|
|
878
1091
|
}
|
|
879
|
-
return { initialPrompt, resume, permissionMode };
|
|
880
1092
|
};
|
|
881
1093
|
|
|
882
|
-
// src/wrapper/
|
|
1094
|
+
// src/wrapper/dualMode.ts
|
|
883
1095
|
var INPUT_QUEUE_CAP = 32;
|
|
1096
|
+
var MIN_MODE_DWELL_MS = 300;
|
|
1097
|
+
var RELAY_FALLBACK_POLL_MS = 6e4;
|
|
1098
|
+
var SESSION_FLAG_RE = /^(-r|--resume|-c|--continue|--session-id)(=.*)?$/;
|
|
884
1099
|
var extractAssistantText = (message) => {
|
|
885
1100
|
const content = message.message?.content;
|
|
886
1101
|
if (typeof content === "string") return content || void 0;
|
|
@@ -890,69 +1105,50 @@ var extractAssistantText = (message) => {
|
|
|
890
1105
|
}
|
|
891
1106
|
return void 0;
|
|
892
1107
|
};
|
|
893
|
-
var
|
|
894
|
-
if (message.type === "assistant") {
|
|
895
|
-
const text = extractAssistantText(message);
|
|
896
|
-
if (text) process.stdout.write(`${text}
|
|
897
|
-
`);
|
|
898
|
-
} else if (message.type === "result") {
|
|
899
|
-
process.stderr.write("[pushary] turn complete \u2014 standing by for phone commands (Ctrl-C to exit)\n");
|
|
900
|
-
}
|
|
901
|
-
};
|
|
902
|
-
var streamTranscript = (relay, message) => {
|
|
903
|
-
if (message.type === "assistant") {
|
|
904
|
-
const text = extractAssistantText(message);
|
|
905
|
-
if (text) relay.sendTranscript("assistant", text);
|
|
906
|
-
} else if (message.type === "result") {
|
|
907
|
-
relay.sendTranscript(
|
|
908
|
-
"turn_end",
|
|
909
|
-
void 0,
|
|
910
|
-
typeof message.total_cost_usd === "number" ? { cost: message.total_cost_usd } : void 0
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
var runRemoteMode = async (binary, args2) => {
|
|
1108
|
+
var runDualMode = async (binary, args2, startingMode) => {
|
|
915
1109
|
let apiKey;
|
|
916
1110
|
try {
|
|
917
1111
|
apiKey = getApiKey();
|
|
918
1112
|
} catch {
|
|
919
1113
|
process.stderr.write(
|
|
920
|
-
"[pushary]
|
|
1114
|
+
"[pushary] phone control needs an API key. Run `npx @pushary/agent-hooks setup`. Running the normal passthrough for now.\n"
|
|
921
1115
|
);
|
|
922
1116
|
return { implemented: false };
|
|
923
1117
|
}
|
|
924
|
-
|
|
1118
|
+
let sdk;
|
|
1119
|
+
let remoteAvailable = true;
|
|
1120
|
+
let noticedUnavailable = false;
|
|
1121
|
+
const ensureSdk = async () => {
|
|
1122
|
+
if (sdk === void 0) {
|
|
1123
|
+
sdk = await ensureClaudeSdk((message) => process.stderr.write(`${message}
|
|
925
1124
|
`));
|
|
926
|
-
|
|
927
|
-
|
|
1125
|
+
if (!sdk) remoteAvailable = false;
|
|
1126
|
+
}
|
|
1127
|
+
return sdk;
|
|
1128
|
+
};
|
|
1129
|
+
if (startingMode === "remote" && !await ensureSdk()) {
|
|
1130
|
+
process.stderr.write("[pushary] phone control is unavailable; running the normal passthrough.\n");
|
|
928
1131
|
return { implemented: false };
|
|
929
1132
|
}
|
|
930
1133
|
const projectName = basename(process.cwd());
|
|
1134
|
+
const agentName = `Claude Code - ${projectName}`;
|
|
931
1135
|
const machineId = getMachineId();
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
1136
|
+
const stdin = createStdinHandoff();
|
|
1137
|
+
const { initialPrompt, resume, permissionMode } = parseRemoteArgs(args2);
|
|
1138
|
+
const userManagesSession = args2.some((a) => SESSION_FLAG_RE.test(a));
|
|
1139
|
+
let sessionId = resume;
|
|
1140
|
+
let sessionCreated = Boolean(resume);
|
|
1141
|
+
if (!sessionId && startingMode === "local" && !userManagesSession) {
|
|
1142
|
+
sessionId = crypto.randomUUID();
|
|
1143
|
+
}
|
|
935
1144
|
let totalCostUsd = 0;
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
return reportEvent(
|
|
940
|
-
{ event, agentType: "claude_code", agentName, action, sessionId, machineId },
|
|
941
|
-
{ maxAttempts: 2, timeoutMs: 4e3 }
|
|
942
|
-
).then(() => {
|
|
943
|
-
}).catch(() => {
|
|
944
|
-
});
|
|
945
|
-
};
|
|
946
|
-
const presenceTimer = setInterval(
|
|
947
|
-
() => void announcePresence("stop", "Standing by for phone commands"),
|
|
948
|
-
5 * 6e4
|
|
949
|
-
);
|
|
950
|
-
presenceTimer.unref?.();
|
|
1145
|
+
let killed = false;
|
|
1146
|
+
let finished = false;
|
|
1147
|
+
let activeLeg = null;
|
|
951
1148
|
const input = new BoundedInputQueue({
|
|
952
1149
|
cap: INPUT_QUEUE_CAP,
|
|
953
1150
|
onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; dropped ${total} stale instruction(s)
|
|
954
1151
|
`),
|
|
955
|
-
// Several instructions that piled up between turns are sent as one turn.
|
|
956
1152
|
coalesce: (items) => userMessage(items.map((m) => m.message.content).join("\n\n"))
|
|
957
1153
|
});
|
|
958
1154
|
const approver = createRemoteApprover({
|
|
@@ -961,16 +1157,17 @@ var runRemoteMode = async (binary, args2) => {
|
|
|
961
1157
|
projectName,
|
|
962
1158
|
getSessionId: () => sessionId
|
|
963
1159
|
});
|
|
964
|
-
const
|
|
1160
|
+
const initialModeState = await fetchModeState(apiKey).catch(() => null);
|
|
1161
|
+
const relayAdvertised = Boolean(initialModeState?.relayUrl);
|
|
965
1162
|
let relay;
|
|
966
|
-
if (
|
|
1163
|
+
if (initialModeState?.relayUrl) {
|
|
967
1164
|
relay = createRelayClient({
|
|
968
|
-
url:
|
|
1165
|
+
url: initialModeState.relayUrl,
|
|
969
1166
|
apiKey,
|
|
970
1167
|
machineId,
|
|
971
1168
|
agentType: "claude_code",
|
|
972
1169
|
getSessionId: () => sessionId,
|
|
973
|
-
onCommand: (command) =>
|
|
1170
|
+
onCommand: (command) => onPhoneCommand(command),
|
|
974
1171
|
onFatal: (message) => process.stderr.write(`[pushary] relay unavailable (${message}); using polling
|
|
975
1172
|
`),
|
|
976
1173
|
log: (message) => process.stderr.write(`${message}
|
|
@@ -978,84 +1175,144 @@ var runRemoteMode = async (binary, args2) => {
|
|
|
978
1175
|
});
|
|
979
1176
|
relay.start();
|
|
980
1177
|
}
|
|
1178
|
+
const announcePresence = (event, action) => {
|
|
1179
|
+
if (!sessionId) return Promise.resolve();
|
|
1180
|
+
return reportEvent(
|
|
1181
|
+
{ event, agentType: "claude_code", agentName, action, sessionId, machineId },
|
|
1182
|
+
{ maxAttempts: 2, timeoutMs: 4e3 }
|
|
1183
|
+
).then(() => {
|
|
1184
|
+
}).catch(() => {
|
|
1185
|
+
});
|
|
1186
|
+
};
|
|
1187
|
+
const setSessionId = (id) => {
|
|
1188
|
+
const isNew = sessionId !== id;
|
|
1189
|
+
sessionId = id;
|
|
1190
|
+
sessionCreated = true;
|
|
1191
|
+
relay?.reannounce();
|
|
1192
|
+
if (isNew) void announcePresence("session_start", "Remote session ready \u2014 reachable from your phone");
|
|
1193
|
+
};
|
|
1194
|
+
const onPhoneCommand = (command) => {
|
|
1195
|
+
if (!remoteAvailable) {
|
|
1196
|
+
if (!noticedUnavailable) {
|
|
1197
|
+
noticedUnavailable = true;
|
|
1198
|
+
process.stderr.write("[pushary] phone control is unavailable this run; the instruction was not delivered.\n");
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
input.push(userMessage(command));
|
|
1203
|
+
activeLeg?.onPhoneCommand();
|
|
1204
|
+
};
|
|
1205
|
+
const streamMessage = (message) => {
|
|
1206
|
+
if (message.type === "assistant") {
|
|
1207
|
+
const text = extractAssistantText(message);
|
|
1208
|
+
if (text) process.stdout.write(`${text}
|
|
1209
|
+
`);
|
|
1210
|
+
} else if (message.type === "result") {
|
|
1211
|
+
if (typeof message.total_cost_usd === "number") totalCostUsd = message.total_cost_usd;
|
|
1212
|
+
process.stderr.write("[pushary] turn complete \u2014 standing by for phone commands\n");
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
981
1215
|
const poller = startCommandPoller({
|
|
982
1216
|
apiKey,
|
|
983
1217
|
getSessionId: () => sessionId,
|
|
984
|
-
onCommand: (command) =>
|
|
985
|
-
// While the relay socket owns command delivery, the poller must not also drain
|
|
986
|
-
// the single-use queue (that would deliver the command twice); it still reads
|
|
987
|
-
// mode/kill. When the socket is down or absent, the poller is the source.
|
|
1218
|
+
onCommand: (command) => onPhoneCommand(command),
|
|
988
1219
|
shouldDrainCommands: () => !(relay?.connected() ?? false),
|
|
989
|
-
//
|
|
990
|
-
//
|
|
1220
|
+
// Unified poll: one request per tick for both command + mode/kill (half the
|
|
1221
|
+
// requests and the per-poll DB write of the old drain + mode pair).
|
|
1222
|
+
poll: pollAgent,
|
|
991
1223
|
fetchModeState,
|
|
1224
|
+
// Relay-primary: idle the safety-net poll at 60s (see RELAY_FALLBACK_POLL_MS).
|
|
1225
|
+
// No relay advertised -> the poller IS the command path, so keep the default.
|
|
1226
|
+
slowPollMs: relayAdvertised ? RELAY_FALLBACK_POLL_MS : void 0,
|
|
992
1227
|
onModeState: (state) => {
|
|
993
1228
|
if (state.kill && !killed) {
|
|
994
1229
|
killed = true;
|
|
1230
|
+
finished = true;
|
|
995
1231
|
process.stderr.write("[pushary] halted from Pushary \u2014 stopping the agent\n");
|
|
996
|
-
|
|
1232
|
+
activeLeg?.stop("kill");
|
|
997
1233
|
}
|
|
998
1234
|
},
|
|
999
1235
|
log: (message) => process.stderr.write(`${message}
|
|
1000
1236
|
`)
|
|
1001
1237
|
});
|
|
1002
1238
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
1003
|
-
const onSignal = () =>
|
|
1239
|
+
const onSignal = () => {
|
|
1240
|
+
finished = true;
|
|
1241
|
+
activeLeg?.stop("signal");
|
|
1242
|
+
};
|
|
1004
1243
|
for (const signal of signals) process.on(signal, onSignal);
|
|
1005
|
-
const
|
|
1244
|
+
const restoreStdin = () => stdin.dispose();
|
|
1245
|
+
process.once("exit", restoreStdin);
|
|
1246
|
+
const session = {
|
|
1247
|
+
apiKey,
|
|
1248
|
+
machineId,
|
|
1249
|
+
projectName,
|
|
1250
|
+
agentName,
|
|
1251
|
+
binary,
|
|
1252
|
+
input,
|
|
1253
|
+
approver,
|
|
1254
|
+
stdin,
|
|
1255
|
+
claudeArgs: args2,
|
|
1256
|
+
permissionMode,
|
|
1257
|
+
userManagesSession,
|
|
1258
|
+
getSessionId: () => sessionId,
|
|
1259
|
+
setSessionId,
|
|
1260
|
+
isSessionCreated: () => sessionCreated,
|
|
1261
|
+
markSessionCreated: () => {
|
|
1262
|
+
sessionCreated = true;
|
|
1263
|
+
},
|
|
1264
|
+
relayConnected: () => relay?.connected() ?? false,
|
|
1265
|
+
streamMessage,
|
|
1266
|
+
setActiveLeg: (control) => {
|
|
1267
|
+
activeLeg = control;
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1006
1270
|
try {
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
void announcePresence("session_start", "Remote session ready \u2014 reachable from your phone");
|
|
1029
|
-
},
|
|
1030
|
-
onCost: (usd) => {
|
|
1031
|
-
totalCostUsd = usd;
|
|
1032
|
-
},
|
|
1033
|
-
// Always echo the turn to the local terminal; also stream a bounded live
|
|
1034
|
-
// transcript to the phone when the relay is up (it presence-gates it).
|
|
1035
|
-
onMessage: (message) => {
|
|
1036
|
-
echoLocal(message);
|
|
1037
|
-
if (relay) streamTranscript(relay, message);
|
|
1271
|
+
if (sessionId && startingMode === "local") {
|
|
1272
|
+
void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
|
|
1273
|
+
}
|
|
1274
|
+
if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
|
|
1275
|
+
const exitCode = await driveModeLoop({
|
|
1276
|
+
startingMode,
|
|
1277
|
+
runLocal: () => runLocalLeg(session),
|
|
1278
|
+
runRemote: async () => {
|
|
1279
|
+
const ready = await ensureSdk();
|
|
1280
|
+
if (!ready) {
|
|
1281
|
+
remoteAvailable = false;
|
|
1282
|
+
input.clear();
|
|
1283
|
+
if (!noticedUnavailable) {
|
|
1284
|
+
noticedUnavailable = true;
|
|
1285
|
+
process.stderr.write(
|
|
1286
|
+
"[pushary] could not start phone control (offline?); staying in the terminal. Relaunch when online to enable.\n"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
return "switch";
|
|
1290
|
+
}
|
|
1291
|
+
return runRemoteLeg(session, ready);
|
|
1038
1292
|
},
|
|
1039
|
-
|
|
1040
|
-
|
|
1293
|
+
isFinished: () => finished,
|
|
1294
|
+
minDwellMs: MIN_MODE_DWELL_MS
|
|
1041
1295
|
});
|
|
1042
|
-
return { implemented: true, exitCode
|
|
1296
|
+
return { implemented: true, exitCode };
|
|
1043
1297
|
} finally {
|
|
1044
1298
|
for (const signal of signals) process.off(signal, onSignal);
|
|
1045
|
-
|
|
1299
|
+
process.removeListener("exit", restoreStdin);
|
|
1046
1300
|
relay?.stop();
|
|
1047
1301
|
poller.stop();
|
|
1048
1302
|
approver.teardown();
|
|
1049
1303
|
input.close();
|
|
1050
|
-
|
|
1304
|
+
stdin.dispose();
|
|
1305
|
+
await announcePresence("session_closed", "Session ended");
|
|
1051
1306
|
if (totalCostUsd > 0) {
|
|
1052
|
-
process.stderr.write(`[pushary]
|
|
1307
|
+
process.stderr.write(`[pushary] session cost: $${totalCostUsd.toFixed(4)}
|
|
1053
1308
|
`);
|
|
1054
1309
|
}
|
|
1055
1310
|
}
|
|
1056
1311
|
};
|
|
1057
1312
|
|
|
1058
1313
|
// src/wrapper/runClaudeWrapper.ts
|
|
1314
|
+
var hasPrintFlag = (args2) => args2.some((a) => a === "-p" || a === "--print" || a.startsWith("--print="));
|
|
1315
|
+
var isInteractiveTty = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1059
1316
|
var runClaudeWrapper = async (args2) => {
|
|
1060
1317
|
const binary = findClaudeBinary();
|
|
1061
1318
|
if (!binary) {
|
|
@@ -1066,10 +1323,11 @@ var runClaudeWrapper = async (args2) => {
|
|
|
1066
1323
|
}
|
|
1067
1324
|
const nested = process.env[WRAPPER_ACTIVE_ENV] === "1";
|
|
1068
1325
|
const { remote: wantRemote, rest } = extractRemoteFlag(args2);
|
|
1069
|
-
|
|
1326
|
+
const useDual = !nested && (wantRemote || isInteractiveTty() && !hasPrintFlag(rest));
|
|
1327
|
+
if (useDual) {
|
|
1070
1328
|
try {
|
|
1071
|
-
const
|
|
1072
|
-
if (
|
|
1329
|
+
const result = await runDualMode(binary, rest, wantRemote ? "remote" : "local");
|
|
1330
|
+
if (result.implemented) return result.exitCode ?? 0;
|
|
1073
1331
|
} catch {
|
|
1074
1332
|
}
|
|
1075
1333
|
}
|
package/dist/bin/pushary.js
CHANGED
|
@@ -26,8 +26,9 @@ 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
|
|
30
|
-
|
|
29
|
+
claude Run Claude Code through Pushary \u2014 the native terminal, and reachable from
|
|
30
|
+
your phone: send an instruction and it drives even a fully idle agent
|
|
31
|
+
(press Ctrl-] to take the terminal back). Add --remote to start headless.
|
|
31
32
|
doctor Verify your Pushary installation is working
|
|
32
33
|
clean Remove all Pushary configuration (--yes for non-interactive)
|
|
33
34
|
mode Switch approval mode (push_only, push_first, terminal_only)
|
|
@@ -48,6 +49,7 @@ Usage:
|
|
|
48
49
|
npx @pushary/agent-hooks@latest doctor
|
|
49
50
|
npx @pushary/agent-hooks@latest mode push_only --for 30m
|
|
50
51
|
npx @pushary/agent-hooks@latest wait 45
|
|
51
|
-
pushary claude
|
|
52
|
+
pushary claude # native terminal, reachable from your phone when idle
|
|
53
|
+
pushary claude --remote -p "start the refactor" # start headless, drive entirely from your phone
|
|
52
54
|
`);
|
|
53
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushary/agent-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.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 && bun test src/wrapper/spawnClaude.test.ts && bun test src/wrapper/wsProtocol.test.ts && bun test src/wrapper/relayClient.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 && bun test src/wrapper/modeLoop.test.ts && bun test src/wrapper/stdinHandoff.test.ts && bun test src/wrapper/localLeg.test.ts"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@inquirer/prompts": "^8.4.2",
|