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