@pushary/agent-hooks 0.43.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 +451 -174
- package/dist/bin/pushary-codex-hook.js +13 -15
- package/dist/bin/pushary-codex.js +2 -4
- package/dist/bin/pushary-gemini-hook.js +8 -10
- package/dist/bin/pushary-hook.js +2 -3
- package/dist/bin/pushary-notification-hook.js +1 -2
- package/dist/bin/pushary-permission-denied-hook.js +2 -3
- package/dist/bin/pushary-permission-hook.js +2 -3
- package/dist/bin/pushary-post-hook.js +1 -2
- package/dist/bin/pushary-prompt-hook.js +1 -2
- package/dist/bin/pushary-session-end-hook.js +1 -2
- package/dist/bin/pushary-session-start-hook.js +1 -2
- package/dist/bin/pushary-setup.js +1 -2
- package/dist/bin/pushary-stop-hook.js +1 -2
- package/dist/bin/pushary-stopfailure-hook.js +1 -2
- package/dist/bin/pushary.js +5 -3
- package/dist/{chunk-V2TSLWTV.js → chunk-E3Q3KYZF.js} +5 -7
- package/dist/{chunk-NRCQ5UUR.js → chunk-VZNP4R5I.js} +377 -49
- package/dist/src/index.js +6 -8
- package/package.json +2 -2
- package/dist/chunk-ETDXSKR5.js +0 -355
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
fetchModeState,
|
|
11
11
|
getMachineId,
|
|
12
12
|
getPolicy,
|
|
13
|
+
reportEvent,
|
|
13
14
|
resolvePolicy,
|
|
14
15
|
waitForAnswer
|
|
15
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-VZNP4R5I.js";
|
|
16
17
|
import "../chunk-DWED7BS3.js";
|
|
17
18
|
import "../chunk-Z5PL3K7C.js";
|
|
18
19
|
import {
|
|
@@ -100,7 +101,7 @@ var runLocalPassthrough = (binary, args2) => {
|
|
|
100
101
|
});
|
|
101
102
|
};
|
|
102
103
|
|
|
103
|
-
// src/wrapper/
|
|
104
|
+
// src/wrapper/dualMode.ts
|
|
104
105
|
import { basename } from "path";
|
|
105
106
|
|
|
106
107
|
// src/wrapper/sdkLoader.ts
|
|
@@ -150,7 +151,7 @@ var ensureClaudeSdk = async (log) => {
|
|
|
150
151
|
} catch {
|
|
151
152
|
return null;
|
|
152
153
|
}
|
|
153
|
-
log?.("[pushary] setting up
|
|
154
|
+
log?.("[pushary] setting up phone control (one-time, fetching the Claude Agent SDK)...");
|
|
154
155
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
155
156
|
const installed = await new Promise((resolve) => {
|
|
156
157
|
const child = spawn2(
|
|
@@ -224,6 +225,12 @@ var BoundedInputQueue = class {
|
|
|
224
225
|
resolve({ value: void 0, done: true });
|
|
225
226
|
}
|
|
226
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
|
+
}
|
|
227
234
|
get isClosed() {
|
|
228
235
|
return this.closed;
|
|
229
236
|
}
|
|
@@ -329,6 +336,7 @@ var createRemoteApprover = (deps) => {
|
|
|
329
336
|
}
|
|
330
337
|
const controller = new AbortController();
|
|
331
338
|
const entry = { controller };
|
|
339
|
+
let answered = false;
|
|
332
340
|
pending.add(entry);
|
|
333
341
|
const outerAbort = () => controller.abort();
|
|
334
342
|
if (options?.signal) {
|
|
@@ -365,6 +373,7 @@ var createRemoteApprover = (deps) => {
|
|
|
365
373
|
entry.correlationId = question.correlationId;
|
|
366
374
|
if (question.noDevices) return applyTimeout(policy, input);
|
|
367
375
|
const answer = await waitForPhone(entry, question.correlationId, windowFor(policy));
|
|
376
|
+
answered = answer.answered;
|
|
368
377
|
if (answer.answered) {
|
|
369
378
|
if (isDeferAnswer(answer.value)) return deny("Deferred \u2014 handle it on the machine");
|
|
370
379
|
return answer.value === "yes" ? allow(input) : deny("Denied from your phone");
|
|
@@ -374,6 +383,10 @@ var createRemoteApprover = (deps) => {
|
|
|
374
383
|
return deny("Approval unavailable; denied by default");
|
|
375
384
|
} finally {
|
|
376
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
|
+
}
|
|
377
390
|
pending.delete(entry);
|
|
378
391
|
}
|
|
379
392
|
};
|
|
@@ -464,10 +477,23 @@ var startCommandPoller = (opts) => {
|
|
|
464
477
|
const timeout = setTimeout(() => controller.abort(), requestTimeout);
|
|
465
478
|
try {
|
|
466
479
|
const wantCommands = opts.shouldDrainCommands ? opts.shouldDrainCommands() : true;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
497
|
errorStreak = 0;
|
|
472
498
|
if (command) {
|
|
473
499
|
idleTicks = 0;
|
|
@@ -498,19 +524,39 @@ var startCommandPoller = (opts) => {
|
|
|
498
524
|
};
|
|
499
525
|
};
|
|
500
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
|
+
|
|
501
556
|
// src/wrapper/wsProtocol.ts
|
|
502
557
|
var PROTOCOL_VERSION = 1;
|
|
503
|
-
var TRANSCRIPT_TEXT_MAX = 4e3;
|
|
504
558
|
var encodeFrame = (frame) => JSON.stringify(frame);
|
|
505
559
|
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
560
|
var parse = (raw) => {
|
|
515
561
|
try {
|
|
516
562
|
const value = JSON.parse(raw);
|
|
@@ -523,21 +569,18 @@ var parse = (raw) => {
|
|
|
523
569
|
}
|
|
524
570
|
};
|
|
525
571
|
var isString = (v) => typeof v === "string";
|
|
526
|
-
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
527
572
|
var decodeServerFrame = (raw) => {
|
|
528
573
|
const f = parse(raw);
|
|
529
574
|
if (!f) return null;
|
|
530
575
|
switch (f.t) {
|
|
531
576
|
case "welcome":
|
|
532
|
-
return
|
|
577
|
+
return { v: PROTOCOL_VERSION, t: "welcome" };
|
|
533
578
|
case "ping":
|
|
534
579
|
return { v: PROTOCOL_VERSION, t: "ping" };
|
|
535
580
|
case "pong":
|
|
536
581
|
return { v: PROTOCOL_VERSION, t: "pong" };
|
|
537
582
|
case "command":
|
|
538
583
|
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
584
|
case "error":
|
|
542
585
|
if (isString(f.code) && isString(f.message)) {
|
|
543
586
|
return { v: PROTOCOL_VERSION, t: "error", code: f.code, message: f.message, fatal: f.fatal === true };
|
|
@@ -551,7 +594,6 @@ var decodeServerFrame = (raw) => {
|
|
|
551
594
|
// src/wrapper/relayClient.ts
|
|
552
595
|
var DEFAULT_HEARTBEAT_MS = 2e4;
|
|
553
596
|
var DEFAULT_PONG_TIMEOUT_MS = 1e4;
|
|
554
|
-
var DEFAULT_MAX_OUTBOUND = 128;
|
|
555
597
|
var DEFAULT_MAX_SEEN_COMMANDS = 256;
|
|
556
598
|
var defaultBackoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4);
|
|
557
599
|
var defaultSocketFactory = (url) => {
|
|
@@ -572,17 +614,14 @@ var createRelayClient = (options) => {
|
|
|
572
614
|
const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
573
615
|
const pongTimeoutMs = options.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
|
|
574
616
|
const backoff = options.backoffMs ?? defaultBackoff;
|
|
575
|
-
const maxOutbound = Math.max(1, options.maxOutbound ?? DEFAULT_MAX_OUTBOUND);
|
|
576
617
|
const maxSeenCommands = Math.max(1, options.maxSeenCommands ?? DEFAULT_MAX_SEEN_COMMANDS);
|
|
577
618
|
let socket;
|
|
578
619
|
let isConnected = false;
|
|
579
620
|
let stopped = false;
|
|
580
|
-
let seq = 0;
|
|
581
621
|
let attempt = 0;
|
|
582
622
|
let reconnectTimer;
|
|
583
623
|
let heartbeatTimer;
|
|
584
624
|
let pongTimer;
|
|
585
|
-
const outbound = [];
|
|
586
625
|
const seenCommands = [];
|
|
587
626
|
const seenSet = /* @__PURE__ */ new Set();
|
|
588
627
|
const log = (message) => options.log?.(message);
|
|
@@ -611,20 +650,14 @@ var createRelayClient = (options) => {
|
|
|
611
650
|
} catch {
|
|
612
651
|
}
|
|
613
652
|
};
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const flushOutbound = () => {
|
|
623
|
-
while (outbound.length > 0 && isConnected) {
|
|
624
|
-
const frame = outbound.shift();
|
|
625
|
-
if (frame) sendFrame(frame);
|
|
626
|
-
}
|
|
627
|
-
};
|
|
653
|
+
const sendHello = () => sendFrame(
|
|
654
|
+
helloFrame({
|
|
655
|
+
apiKey: options.apiKey,
|
|
656
|
+
machineId: options.machineId,
|
|
657
|
+
agentType: options.agentType,
|
|
658
|
+
sessionId: options.getSessionId()
|
|
659
|
+
})
|
|
660
|
+
);
|
|
628
661
|
const armPong = () => {
|
|
629
662
|
if (pongTimer) clearTimeout(pongTimer);
|
|
630
663
|
pongTimer = setTimeout(() => {
|
|
@@ -645,7 +678,6 @@ var createRelayClient = (options) => {
|
|
|
645
678
|
switch (frame.t) {
|
|
646
679
|
case "welcome":
|
|
647
680
|
attempt = 0;
|
|
648
|
-
flushOutbound();
|
|
649
681
|
log("[pushary] relay connected");
|
|
650
682
|
break;
|
|
651
683
|
case "command":
|
|
@@ -664,9 +696,6 @@ var createRelayClient = (options) => {
|
|
|
664
696
|
if (pongTimer) clearTimeout(pongTimer);
|
|
665
697
|
pongTimer = void 0;
|
|
666
698
|
break;
|
|
667
|
-
case "resume":
|
|
668
|
-
log(`[pushary] relay signalled a gap; catch up after seq ${frame.afterSeq}`);
|
|
669
|
-
break;
|
|
670
699
|
case "error":
|
|
671
700
|
if (frame.fatal) {
|
|
672
701
|
log(`[pushary] relay fatal: ${frame.message}`);
|
|
@@ -724,15 +753,7 @@ var createRelayClient = (options) => {
|
|
|
724
753
|
socket = created;
|
|
725
754
|
created.onOpen(() => {
|
|
726
755
|
isConnected = true;
|
|
727
|
-
|
|
728
|
-
helloFrame({
|
|
729
|
-
apiKey: options.apiKey,
|
|
730
|
-
machineId: options.machineId,
|
|
731
|
-
agentType: options.agentType,
|
|
732
|
-
sessionId: options.getSessionId(),
|
|
733
|
-
lastSeq: seq
|
|
734
|
-
})
|
|
735
|
-
);
|
|
756
|
+
sendHello();
|
|
736
757
|
startHeartbeat();
|
|
737
758
|
});
|
|
738
759
|
created.onMessage((data) => onServerFrame(data));
|
|
@@ -751,22 +772,9 @@ var createRelayClient = (options) => {
|
|
|
751
772
|
connected() {
|
|
752
773
|
return isConnected;
|
|
753
774
|
},
|
|
754
|
-
sendTranscript(kind, text, meta) {
|
|
755
|
-
if (stopped) return;
|
|
756
|
-
seq++;
|
|
757
|
-
enqueueTranscript(transcriptFrame(seq, kind, text, meta));
|
|
758
|
-
},
|
|
759
775
|
reannounce() {
|
|
760
776
|
if (stopped || !isConnected) return;
|
|
761
|
-
|
|
762
|
-
helloFrame({
|
|
763
|
-
apiKey: options.apiKey,
|
|
764
|
-
machineId: options.machineId,
|
|
765
|
-
agentType: options.agentType,
|
|
766
|
-
sessionId: options.getSessionId(),
|
|
767
|
-
lastSeq: seq
|
|
768
|
-
})
|
|
769
|
-
);
|
|
777
|
+
sendHello();
|
|
770
778
|
},
|
|
771
779
|
stop() {
|
|
772
780
|
if (stopped) return;
|
|
@@ -778,6 +786,166 @@ var createRelayClient = (options) => {
|
|
|
778
786
|
};
|
|
779
787
|
};
|
|
780
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
|
+
|
|
781
949
|
// src/wrapper/remoteLoop.ts
|
|
782
950
|
var errMsg = (err) => err instanceof Error ? err.message : String(err);
|
|
783
951
|
var DEFAULT_MAX_RESTARTS = 5;
|
|
@@ -845,41 +1013,89 @@ var runRemoteLoop = async (deps) => {
|
|
|
845
1013
|
return { exitCode: 130, totalCostUsd: totalCost };
|
|
846
1014
|
};
|
|
847
1015
|
|
|
848
|
-
// src/wrapper/
|
|
849
|
-
var
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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 };
|
|
863
1072
|
};
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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;
|
|
876
1086
|
}
|
|
1087
|
+
const since = now() - lastSwitchAt;
|
|
1088
|
+
if (since < minDwell) await sleep(minDwell - since);
|
|
1089
|
+
lastSwitchAt = now();
|
|
1090
|
+
mode = mode === "local" ? "remote" : "local";
|
|
877
1091
|
}
|
|
878
|
-
return { initialPrompt, resume, permissionMode };
|
|
879
1092
|
};
|
|
880
1093
|
|
|
881
|
-
// src/wrapper/
|
|
1094
|
+
// src/wrapper/dualMode.ts
|
|
882
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)(=.*)?$/;
|
|
883
1099
|
var extractAssistantText = (message) => {
|
|
884
1100
|
const content = message.message?.content;
|
|
885
1101
|
if (typeof content === "string") return content || void 0;
|
|
@@ -889,54 +1105,50 @@ var extractAssistantText = (message) => {
|
|
|
889
1105
|
}
|
|
890
1106
|
return void 0;
|
|
891
1107
|
};
|
|
892
|
-
var
|
|
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) => {
|
|
1108
|
+
var runDualMode = async (binary, args2, startingMode) => {
|
|
914
1109
|
let apiKey;
|
|
915
1110
|
try {
|
|
916
1111
|
apiKey = getApiKey();
|
|
917
1112
|
} catch {
|
|
918
1113
|
process.stderr.write(
|
|
919
|
-
"[pushary]
|
|
1114
|
+
"[pushary] phone control needs an API key. Run `npx @pushary/agent-hooks setup`. Running the normal passthrough for now.\n"
|
|
920
1115
|
);
|
|
921
1116
|
return { implemented: false };
|
|
922
1117
|
}
|
|
923
|
-
|
|
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}
|
|
924
1124
|
`));
|
|
925
|
-
|
|
926
|
-
|
|
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");
|
|
927
1131
|
return { implemented: false };
|
|
928
1132
|
}
|
|
929
1133
|
const projectName = basename(process.cwd());
|
|
1134
|
+
const agentName = `Claude Code - ${projectName}`;
|
|
930
1135
|
const machineId = getMachineId();
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
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
|
+
}
|
|
934
1144
|
let totalCostUsd = 0;
|
|
1145
|
+
let killed = false;
|
|
1146
|
+
let finished = false;
|
|
1147
|
+
let activeLeg = null;
|
|
935
1148
|
const input = new BoundedInputQueue({
|
|
936
1149
|
cap: INPUT_QUEUE_CAP,
|
|
937
1150
|
onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; dropped ${total} stale instruction(s)
|
|
938
1151
|
`),
|
|
939
|
-
// Several instructions that piled up between turns are sent as one turn.
|
|
940
1152
|
coalesce: (items) => userMessage(items.map((m) => m.message.content).join("\n\n"))
|
|
941
1153
|
});
|
|
942
1154
|
const approver = createRemoteApprover({
|
|
@@ -945,16 +1157,17 @@ var runRemoteMode = async (binary, args2) => {
|
|
|
945
1157
|
projectName,
|
|
946
1158
|
getSessionId: () => sessionId
|
|
947
1159
|
});
|
|
948
|
-
const
|
|
1160
|
+
const initialModeState = await fetchModeState(apiKey).catch(() => null);
|
|
1161
|
+
const relayAdvertised = Boolean(initialModeState?.relayUrl);
|
|
949
1162
|
let relay;
|
|
950
|
-
if (
|
|
1163
|
+
if (initialModeState?.relayUrl) {
|
|
951
1164
|
relay = createRelayClient({
|
|
952
|
-
url:
|
|
1165
|
+
url: initialModeState.relayUrl,
|
|
953
1166
|
apiKey,
|
|
954
1167
|
machineId,
|
|
955
1168
|
agentType: "claude_code",
|
|
956
1169
|
getSessionId: () => sessionId,
|
|
957
|
-
onCommand: (command) =>
|
|
1170
|
+
onCommand: (command) => onPhoneCommand(command),
|
|
958
1171
|
onFatal: (message) => process.stderr.write(`[pushary] relay unavailable (${message}); using polling
|
|
959
1172
|
`),
|
|
960
1173
|
log: (message) => process.stderr.write(`${message}
|
|
@@ -962,81 +1175,144 @@ var runRemoteMode = async (binary, args2) => {
|
|
|
962
1175
|
});
|
|
963
1176
|
relay.start();
|
|
964
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
|
+
};
|
|
965
1215
|
const poller = startCommandPoller({
|
|
966
1216
|
apiKey,
|
|
967
1217
|
getSessionId: () => sessionId,
|
|
968
|
-
onCommand: (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.
|
|
1218
|
+
onCommand: (command) => onPhoneCommand(command),
|
|
972
1219
|
shouldDrainCommands: () => !(relay?.connected() ?? false),
|
|
973
|
-
//
|
|
974
|
-
//
|
|
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,
|
|
975
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,
|
|
976
1227
|
onModeState: (state) => {
|
|
977
1228
|
if (state.kill && !killed) {
|
|
978
1229
|
killed = true;
|
|
1230
|
+
finished = true;
|
|
979
1231
|
process.stderr.write("[pushary] halted from Pushary \u2014 stopping the agent\n");
|
|
980
|
-
|
|
1232
|
+
activeLeg?.stop("kill");
|
|
981
1233
|
}
|
|
982
1234
|
},
|
|
983
1235
|
log: (message) => process.stderr.write(`${message}
|
|
984
1236
|
`)
|
|
985
1237
|
});
|
|
986
1238
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
987
|
-
const onSignal = () =>
|
|
1239
|
+
const onSignal = () => {
|
|
1240
|
+
finished = true;
|
|
1241
|
+
activeLeg?.stop("signal");
|
|
1242
|
+
};
|
|
988
1243
|
for (const signal of signals) process.on(signal, onSignal);
|
|
989
|
-
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
|
+
};
|
|
990
1270
|
try {
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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);
|
|
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);
|
|
1021
1292
|
},
|
|
1022
|
-
|
|
1023
|
-
|
|
1293
|
+
isFinished: () => finished,
|
|
1294
|
+
minDwellMs: MIN_MODE_DWELL_MS
|
|
1024
1295
|
});
|
|
1025
|
-
return { implemented: true, exitCode
|
|
1296
|
+
return { implemented: true, exitCode };
|
|
1026
1297
|
} finally {
|
|
1027
1298
|
for (const signal of signals) process.off(signal, onSignal);
|
|
1299
|
+
process.removeListener("exit", restoreStdin);
|
|
1028
1300
|
relay?.stop();
|
|
1029
1301
|
poller.stop();
|
|
1030
1302
|
approver.teardown();
|
|
1031
1303
|
input.close();
|
|
1304
|
+
stdin.dispose();
|
|
1305
|
+
await announcePresence("session_closed", "Session ended");
|
|
1032
1306
|
if (totalCostUsd > 0) {
|
|
1033
|
-
process.stderr.write(`[pushary]
|
|
1307
|
+
process.stderr.write(`[pushary] session cost: $${totalCostUsd.toFixed(4)}
|
|
1034
1308
|
`);
|
|
1035
1309
|
}
|
|
1036
1310
|
}
|
|
1037
1311
|
};
|
|
1038
1312
|
|
|
1039
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);
|
|
1040
1316
|
var runClaudeWrapper = async (args2) => {
|
|
1041
1317
|
const binary = findClaudeBinary();
|
|
1042
1318
|
if (!binary) {
|
|
@@ -1047,10 +1323,11 @@ var runClaudeWrapper = async (args2) => {
|
|
|
1047
1323
|
}
|
|
1048
1324
|
const nested = process.env[WRAPPER_ACTIVE_ENV] === "1";
|
|
1049
1325
|
const { remote: wantRemote, rest } = extractRemoteFlag(args2);
|
|
1050
|
-
|
|
1326
|
+
const useDual = !nested && (wantRemote || isInteractiveTty() && !hasPrintFlag(rest));
|
|
1327
|
+
if (useDual) {
|
|
1051
1328
|
try {
|
|
1052
|
-
const
|
|
1053
|
-
if (
|
|
1329
|
+
const result = await runDualMode(binary, rest, wantRemote ? "remote" : "local");
|
|
1330
|
+
if (result.implemented) return result.exitCode ?? 0;
|
|
1054
1331
|
} catch {
|
|
1055
1332
|
}
|
|
1056
1333
|
}
|