pushary 1.9.73 → 1.9.76
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/CHANGELOG.md +20 -0
- package/dist/{api-JVWXPKEW.js → api-YDCHRRA4.js} +10 -10
- package/dist/bin/pushary-bell-hook.js +27 -25
- package/dist/bin/pushary-bell.js +14 -14
- package/dist/bin/pushary-claude.js +55 -82
- package/dist/bin/pushary-clean.js +37 -37
- package/dist/bin/pushary-codex-bridge.js +24 -24
- package/dist/bin/pushary-codex-hook.js +36 -34
- package/dist/bin/pushary-codex-session.js +8 -8
- package/dist/bin/pushary-codex.js +15 -15
- package/dist/bin/pushary-connect.js +26 -26
- package/dist/bin/pushary-cowork.js +5 -5
- package/dist/bin/pushary-daemon.js +1293 -63
- package/dist/bin/pushary-disconnect.js +31 -31
- package/dist/bin/pushary-display-hook.d.ts +1 -0
- package/dist/bin/pushary-display-hook.js +58 -0
- package/dist/bin/pushary-doctor.js +50 -50
- package/dist/bin/pushary-elicitation-hook.js +29 -27
- package/dist/bin/pushary-gemini-bridge.js +16 -16
- package/dist/bin/pushary-gemini-hook.js +31 -29
- package/dist/bin/pushary-hook.js +31 -29
- package/dist/bin/pushary-login.js +20 -20
- package/dist/bin/pushary-logout.js +30 -30
- package/dist/bin/pushary-mcp.js +14 -14
- package/dist/bin/pushary-mode.js +18 -18
- package/dist/bin/pushary-notification-hook.js +26 -24
- package/dist/bin/pushary-opencode-hook.js +30 -28
- package/dist/bin/pushary-permission-denied-hook.js +31 -29
- package/dist/bin/pushary-permission-hook.js +31 -29
- package/dist/bin/pushary-post-hook.js +32 -29
- package/dist/bin/pushary-prompt-hook.js +32 -29
- package/dist/bin/pushary-session-end-hook.js +28 -26
- package/dist/bin/pushary-session-start-hook.js +36 -34
- package/dist/bin/pushary-setup.js +66 -282
- package/dist/bin/pushary-stats.js +6 -6
- package/dist/bin/pushary-status.js +31 -31
- package/dist/bin/pushary-stop-hook.js +32 -26
- package/dist/bin/pushary-stopfailure-hook.js +26 -24
- package/dist/bin/pushary-suggestions.js +18 -18
- package/dist/bin/pushary-transcript-register.js +18 -16
- package/dist/bin/pushary-transcripts.js +20 -20
- package/dist/bin/pushary-upgrade.js +45 -45
- package/dist/bin/pushary-wait.js +18 -18
- package/dist/bin/pushary.js +7 -7
- package/dist/{chunk-F3WI32HG.js → chunk-2BK2EQOB.js} +7 -7
- package/dist/{chunk-ZBZBYUNZ.js → chunk-2QFYEB3P.js} +5 -5
- package/dist/{chunk-UIF7HWHX.js → chunk-3YU6NIIL.js} +4 -4
- package/dist/{chunk-L2FQBHIR.js → chunk-47TIRGS5.js} +2 -2
- package/dist/{chunk-AXZ67XVN.js → chunk-4LX5QUFV.js} +1 -1
- package/dist/{chunk-YZ6DMGZZ.js → chunk-57MQNSM5.js} +1 -1
- package/dist/{chunk-BLX6KRK5.js → chunk-5FV472Q2.js} +1 -1
- package/dist/{chunk-VBSPP4QY.js → chunk-5KPMDLXB.js} +3 -3
- package/dist/{chunk-S63VIM7E.js → chunk-5M5F4IWR.js} +2 -2
- package/dist/{chunk-7FROPPIH.js → chunk-5NW7VM35.js} +2 -2
- package/dist/{chunk-EITOMPFB.js → chunk-74ONBRPF.js} +4 -4
- package/dist/{chunk-7S46LCKD.js → chunk-7KYQ24FI.js} +3 -2
- package/dist/{chunk-NGNBX3YH.js → chunk-7LLC4R66.js} +4 -4
- package/dist/{chunk-AC5TVOAH.js → chunk-7SFP6Y5U.js} +373 -93
- package/dist/{chunk-CMYFV3VC.js → chunk-7ZOWQBHQ.js} +5 -5
- package/dist/{chunk-2QB7HJVJ.js → chunk-B2PFCS72.js} +2 -2
- package/dist/{chunk-UUCRWEEU.js → chunk-BUR2LYQI.js} +2 -2
- package/dist/{chunk-W47XZJSH.js → chunk-C2AR7QXU.js} +2 -2
- package/dist/{chunk-FK3JSDH5.js → chunk-CHIOUBPB.js} +13 -13
- package/dist/{chunk-TTGBS5JB.js → chunk-CMNB4IZO.js} +3 -3
- package/dist/{chunk-4UIR25GA.js → chunk-CUD6SYDG.js} +1 -1
- package/dist/{chunk-WJJVCDEU.js → chunk-DB3NOEDW.js} +6 -6
- package/dist/{chunk-PGJMSREY.js → chunk-DZF2M3LM.js} +8 -8
- package/dist/{chunk-2NG3ANMT.js → chunk-E2VJ7B7D.js} +9 -9
- package/dist/{chunk-S7VMHSJW.js → chunk-E7U4YM4A.js} +5 -5
- package/dist/{chunk-XKQ7CQGG.js → chunk-F7R7M5MG.js} +1 -1
- package/dist/{chunk-FAKEKFCI.js → chunk-FAGQAR6I.js} +5 -3
- package/dist/{chunk-3JJI5O5N.js → chunk-GII565PB.js} +1 -1
- package/dist/{chunk-WXV7J3DG.js → chunk-GJVV2EUC.js} +3 -3
- package/dist/{chunk-Z7AIYWZ2.js → chunk-IRLHN5UP.js} +236 -20
- package/dist/{chunk-NJSW2REO.js → chunk-IRN6JLSM.js} +4 -4
- package/dist/{chunk-JC7XUZST.js → chunk-ITLWEGLE.js} +31 -5
- package/dist/{chunk-7M4AA7QF.js → chunk-JNV5LOZH.js} +4 -4
- package/dist/{chunk-IN3MPEFA.js → chunk-JZ5KV2FK.js} +5 -5
- package/dist/{chunk-QAWWTRDD.js → chunk-KGWLDNNT.js} +4 -4
- package/dist/{chunk-O4EAMVSC.js → chunk-KWGOKO4E.js} +5 -5
- package/dist/{chunk-KDQLKND5.js → chunk-M5VJ3HXB.js} +6 -6
- package/dist/{chunk-R5PZPROX.js → chunk-M7JFGDGP.js} +1 -1
- package/dist/{chunk-N4VRSFRZ.js → chunk-MAWC4PJV.js} +5 -5
- package/dist/chunk-MHUKYYYR.js +239 -0
- package/dist/{chunk-56GK65Z6.js → chunk-MMVIKXCP.js} +3 -3
- package/dist/{chunk-CGWUVEOB.js → chunk-MTRZQR2R.js} +3 -3
- package/dist/{chunk-JYOR57ZW.js → chunk-NFUV25JZ.js} +3 -3
- package/dist/{chunk-EUHTC7K5.js → chunk-NJTAUEW6.js} +1 -1
- package/dist/{chunk-2JFEMKDG.js → chunk-NQA56AMZ.js} +6 -6
- package/dist/{chunk-AJWGZJFS.js → chunk-NVLKH5FS.js} +131 -28
- package/dist/{chunk-2HFC4W6J.js → chunk-PPD64MZW.js} +4 -3
- package/dist/{chunk-3RAWW5DH.js → chunk-QUGP76DQ.js} +6 -6
- package/dist/{chunk-PPKIL4EL.js → chunk-R3HXGHRQ.js} +1 -1
- package/dist/{chunk-3QBKMSOZ.js → chunk-S7C5NY4R.js} +1 -1
- package/dist/{chunk-EUEGCEA7.js → chunk-SWRNJ3BP.js} +3 -3
- package/dist/{chunk-RZVWEDIS.js → chunk-T2EJ3H6Y.js} +3 -3
- package/dist/{chunk-DT6VCMXS.js → chunk-TFYFQM5C.js} +2 -2
- package/dist/{chunk-GBR6E7AY.js → chunk-UEY4SHY2.js} +1 -1
- package/dist/{chunk-TFDMJHAB.js → chunk-UMBVIFP5.js} +1 -1
- package/dist/{chunk-TQ2L3ECX.js → chunk-USMUW3LI.js} +2 -2
- package/dist/{chunk-VJUDBZGC.js → chunk-USZJJWZL.js} +2 -2
- package/dist/{chunk-4LAD5A2Q.js → chunk-VELPOV2F.js} +4 -4
- package/dist/{chunk-JNTYPMVU.js → chunk-VZSSL6US.js} +10 -10
- package/dist/{chunk-KSLBS3II.js → chunk-W5AUTVVL.js} +6 -5
- package/dist/{chunk-OJ3ZD5VX.js → chunk-WK2L3XVQ.js} +1 -1
- package/dist/{chunk-PM2MHBMF.js → chunk-WLHHDDVA.js} +9 -4
- package/dist/{chunk-U3EBM73X.js → chunk-WRCM4XGY.js} +2 -2
- package/dist/{chunk-SQ5XCMPB.js → chunk-XJ5DYPTM.js} +4 -4
- package/dist/{chunk-PNXNWZ5P.js → chunk-XLGD5LDG.js} +3 -3
- package/dist/{chunk-7RX5TOU3.js → chunk-Y2CYSVWU.js} +6 -6
- package/dist/{chunk-AVHL2ZTS.js → chunk-Y4OBAK4I.js} +459 -172
- package/dist/{chunk-JWBZUTYF.js → chunk-YDPGBBV7.js} +5 -5
- package/dist/{chunk-FB262EOM.js → chunk-YLB4AGZJ.js} +1 -1
- package/dist/{chunk-QMPM3CAN.js → chunk-ZY25SNFQ.js} +1 -1
- package/dist/{codex-app-cards-switch-M7VH7AZH.js → codex-app-cards-switch-3OUTABVU.js} +11 -11
- package/dist/{codex-desktop-launch-6DASTBBA.js → codex-desktop-launch-JH3E2CIA.js} +1 -1
- package/dist/{codex-session-launch-2XSMFA4K.js → codex-session-launch-GR363OQN.js} +18 -18
- package/dist/{config-7QQOIFTW.js → config-R43UFHX7.js} +3 -3
- package/dist/{global-upgrade-XAKX26AN.js → global-upgrade-73GP4MZM.js} +10 -10
- package/dist/{identity-7AS5MBNM.js → identity-CA7CZGDL.js} +4 -4
- package/dist/{live-capabilities-DV4F7MIB.js → live-capabilities-RCGMQIIZ.js} +5 -5
- package/dist/{paths-GSMXYKT5.js → paths-H7VNNB46.js} +3 -3
- package/dist/{reapply-MR7UWBXR.js → reapply-3UIA3CUO.js} +27 -27
- package/dist/src/index.js +32 -30
- package/dist/{upgrade-lock-6OTHLCBZ.js → upgrade-lock-ZTLX5PDF.js} +4 -4
- package/dist/{usage-H53PO3VQ.js → usage-C53NVWC2.js} +3 -3
- package/package.json +4 -2
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
inspectConfiguredManagedProviders
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-VZSSL6US.js";
|
|
5
5
|
import {
|
|
6
6
|
HEARTBEAT_INTERVAL_MS,
|
|
7
7
|
sendHeartbeat
|
|
8
|
-
} from "../chunk-
|
|
9
|
-
import "../chunk-
|
|
8
|
+
} from "../chunk-2QFYEB3P.js";
|
|
9
|
+
import "../chunk-USZJJWZL.js";
|
|
10
10
|
import {
|
|
11
11
|
readSignedOutDiagnosis
|
|
12
|
-
} from "../chunk-
|
|
13
|
-
import
|
|
12
|
+
} from "../chunk-B2PFCS72.js";
|
|
13
|
+
import {
|
|
14
|
+
HERMES_INSTALL_MARKER_FILE,
|
|
15
|
+
defaultHermesHome,
|
|
16
|
+
parseHermesInstallMarker
|
|
17
|
+
} from "../chunk-FAGQAR6I.js";
|
|
14
18
|
import {
|
|
15
19
|
writeHuman
|
|
16
20
|
} from "../chunk-QCGNGEGE.js";
|
|
17
|
-
import "../chunk-
|
|
18
|
-
import "../chunk-
|
|
19
|
-
import "../chunk-
|
|
20
|
-
import "../chunk-
|
|
21
|
+
import "../chunk-W5AUTVVL.js";
|
|
22
|
+
import "../chunk-SWRNJ3BP.js";
|
|
23
|
+
import "../chunk-C2AR7QXU.js";
|
|
24
|
+
import "../chunk-QUGP76DQ.js";
|
|
21
25
|
import {
|
|
22
26
|
exitOnHelpFlag
|
|
23
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-WK2L3XVQ.js";
|
|
24
28
|
import {
|
|
25
29
|
codexBinaryIsBroken,
|
|
26
30
|
detectProviderLiveCapabilities,
|
|
@@ -29,8 +33,8 @@ import {
|
|
|
29
33
|
launchableProviders,
|
|
30
34
|
resolveControlPolicy,
|
|
31
35
|
trackProviderInventory
|
|
32
|
-
} from "../chunk-
|
|
33
|
-
import "../chunk-
|
|
36
|
+
} from "../chunk-74ONBRPF.js";
|
|
37
|
+
import "../chunk-NFUV25JZ.js";
|
|
34
38
|
import {
|
|
35
39
|
SESSION_REAP_SILENCE_MS,
|
|
36
40
|
describeIdleWindow,
|
|
@@ -38,16 +42,17 @@ import {
|
|
|
38
42
|
listProcessProofs,
|
|
39
43
|
ownsLiveProcess,
|
|
40
44
|
silentSpawnedProofs
|
|
41
|
-
} from "../chunk-
|
|
42
|
-
import "../chunk-
|
|
45
|
+
} from "../chunk-XLGD5LDG.js";
|
|
46
|
+
import "../chunk-UMBVIFP5.js";
|
|
43
47
|
import {
|
|
44
48
|
readLocalAutoUpdateReport
|
|
45
|
-
} from "../chunk-
|
|
49
|
+
} from "../chunk-CMNB4IZO.js";
|
|
46
50
|
import {
|
|
47
51
|
startRegisteredTranscripts
|
|
48
|
-
} from "../chunk-
|
|
49
|
-
import "../chunk-
|
|
50
|
-
import "../chunk-
|
|
52
|
+
} from "../chunk-7SFP6Y5U.js";
|
|
53
|
+
import "../chunk-YLB4AGZJ.js";
|
|
54
|
+
import "../chunk-MHUKYYYR.js";
|
|
55
|
+
import "../chunk-3YU6NIIL.js";
|
|
51
56
|
import {
|
|
52
57
|
DAEMON_LEASE_RENEW_INTERVAL_MS,
|
|
53
58
|
acquireDaemonLease,
|
|
@@ -64,52 +69,1273 @@ import {
|
|
|
64
69
|
requestDaemonHandoff,
|
|
65
70
|
stopDaemonRunning,
|
|
66
71
|
writeDaemonNeedsLogin
|
|
67
|
-
} from "../chunk-
|
|
68
|
-
import
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
} from "../chunk-DB3NOEDW.js";
|
|
73
|
+
import {
|
|
74
|
+
hookOwnerOf
|
|
75
|
+
} from "../chunk-KGWLDNNT.js";
|
|
76
|
+
import "../chunk-PPD64MZW.js";
|
|
77
|
+
import "../chunk-5FV472Q2.js";
|
|
71
78
|
import "../chunk-NMUI3ZWG.js";
|
|
72
|
-
import "../chunk-
|
|
73
|
-
import "../chunk-
|
|
79
|
+
import "../chunk-ITLWEGLE.js";
|
|
80
|
+
import "../chunk-XMSDA5WA.js";
|
|
81
|
+
import "../chunk-5M5F4IWR.js";
|
|
74
82
|
import {
|
|
75
83
|
spawnClaude
|
|
76
84
|
} from "../chunk-XHKBHWLX.js";
|
|
77
85
|
import {
|
|
78
86
|
readControlMode
|
|
79
|
-
} from "../chunk-
|
|
80
|
-
import
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
} from "../chunk-7LLC4R66.js";
|
|
88
|
+
import {
|
|
89
|
+
pendingCommandReceipts,
|
|
90
|
+
removePendingCommand
|
|
91
|
+
} from "../chunk-NVLKH5FS.js";
|
|
92
|
+
import "../chunk-ZY25SNFQ.js";
|
|
93
|
+
import "../chunk-7ZOWQBHQ.js";
|
|
83
94
|
import "../chunk-POFBE6ZY.js";
|
|
84
|
-
import "../chunk-
|
|
85
|
-
import "../chunk-
|
|
86
|
-
import "../chunk-
|
|
95
|
+
import "../chunk-JNV5LOZH.js";
|
|
96
|
+
import "../chunk-E7U4YM4A.js";
|
|
97
|
+
import "../chunk-KWGOKO4E.js";
|
|
87
98
|
import {
|
|
88
99
|
getMachineId
|
|
89
|
-
} from "../chunk-
|
|
90
|
-
import "../chunk-
|
|
100
|
+
} from "../chunk-BUR2LYQI.js";
|
|
101
|
+
import "../chunk-5NW7VM35.js";
|
|
91
102
|
import {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
import "../chunk-7S46LCKD.js";
|
|
103
|
+
claudeConfigDir,
|
|
104
|
+
codexHome
|
|
105
|
+
} from "../chunk-7KYQ24FI.js";
|
|
96
106
|
import {
|
|
97
|
-
resolveBinary
|
|
98
|
-
|
|
107
|
+
resolveBinary,
|
|
108
|
+
writeJsonAtomicDurable
|
|
109
|
+
} from "../chunk-T2EJ3H6Y.js";
|
|
99
110
|
import {
|
|
100
111
|
getApiKey,
|
|
101
112
|
getBaseUrl
|
|
102
|
-
} from "../chunk-
|
|
103
|
-
import "../chunk-
|
|
113
|
+
} from "../chunk-M7JFGDGP.js";
|
|
114
|
+
import "../chunk-PPY5N2E6.js";
|
|
115
|
+
import {
|
|
116
|
+
EXIT
|
|
117
|
+
} from "../chunk-KZERVKTD.js";
|
|
118
|
+
import "../chunk-R3HXGHRQ.js";
|
|
104
119
|
import "../chunk-QTOLNRET.js";
|
|
105
|
-
import
|
|
120
|
+
import {
|
|
121
|
+
CLAUDE_INBOX,
|
|
122
|
+
CLAUDE_INBOX_MAX_SESSIONS,
|
|
123
|
+
CLAUDE_INBOX_RECEIVER,
|
|
124
|
+
CLAUDE_INBOX_SETTLEMENTS,
|
|
125
|
+
CLAUDE_WAKE,
|
|
126
|
+
CLAUDE_WAKE_API,
|
|
127
|
+
COMMAND_RECOVERY,
|
|
128
|
+
NATIVE_INPUT,
|
|
129
|
+
NativeInputAgent,
|
|
130
|
+
claudeInboxAction,
|
|
131
|
+
claudeWakeDecision,
|
|
132
|
+
claudeWakeWaitingOn,
|
|
133
|
+
hermesPendingAdmissionId,
|
|
134
|
+
hermesReplayAdmissionId,
|
|
135
|
+
isClaudeInboxReceipt,
|
|
136
|
+
isCodexInputReceipt,
|
|
137
|
+
isCodexQueuedReceipt,
|
|
138
|
+
isCommandAcknowledgementReceipt,
|
|
139
|
+
nativeIdentifier,
|
|
140
|
+
nativeInputAction,
|
|
141
|
+
nativeRecord,
|
|
142
|
+
parseClaudeInboxClaim,
|
|
143
|
+
parseClaudeInboxPoll,
|
|
144
|
+
parseClaudeInboxSession,
|
|
145
|
+
parseHermesAdmission,
|
|
146
|
+
parseHermesReplayCursor,
|
|
147
|
+
qualifyHermesReceiptMode
|
|
148
|
+
} from "../chunk-Y4OBAK4I.js";
|
|
106
149
|
import "../chunk-MLKGABMK.js";
|
|
107
150
|
|
|
108
|
-
// src/
|
|
109
|
-
import {
|
|
110
|
-
import { existsSync, statSync } from "fs";
|
|
151
|
+
// src/native-host-receiver.ts
|
|
152
|
+
import { join as join2 } from "path";
|
|
111
153
|
import { homedir } from "os";
|
|
112
|
-
import {
|
|
154
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
155
|
+
|
|
156
|
+
// src/native-host.ts
|
|
157
|
+
import WebSocket from "ws";
|
|
158
|
+
import { createConnection } from "net";
|
|
159
|
+
import { createHash } from "crypto";
|
|
160
|
+
import {
|
|
161
|
+
readdirSync,
|
|
162
|
+
lstatSync,
|
|
163
|
+
realpathSync,
|
|
164
|
+
readFileSync,
|
|
165
|
+
mkdirSync,
|
|
166
|
+
openSync,
|
|
167
|
+
writeSync,
|
|
168
|
+
fsyncSync,
|
|
169
|
+
closeSync
|
|
170
|
+
} from "fs";
|
|
171
|
+
import { dirname, join, resolve } from "path";
|
|
172
|
+
var privateNode = (path, kind) => {
|
|
173
|
+
try {
|
|
174
|
+
const s = lstatSync(path);
|
|
175
|
+
return s.uid === process.getuid?.() && (s.mode & 63) === 0 && (kind === "socket" ? s.isSocket() : s.isDirectory());
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var ownerControlledDirectory = (path) => {
|
|
181
|
+
try {
|
|
182
|
+
const node = lstatSync(path);
|
|
183
|
+
return node.isDirectory() && node.uid === process.getuid?.() && (node.mode & 18) === 0;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var syncDirectory = (path) => {
|
|
189
|
+
const fd = openSync(path, "r");
|
|
190
|
+
try {
|
|
191
|
+
fsyncSync(fd);
|
|
192
|
+
} finally {
|
|
193
|
+
closeSync(fd);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var Rpc = class _Rpc {
|
|
197
|
+
constructor(socket, timeout) {
|
|
198
|
+
this.socket = socket;
|
|
199
|
+
this.timeout = timeout;
|
|
200
|
+
socket.on("message", (data) => {
|
|
201
|
+
let v;
|
|
202
|
+
try {
|
|
203
|
+
v = JSON.parse(data.toString());
|
|
204
|
+
} catch {
|
|
205
|
+
socket.terminate();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!nativeRecord(v) || typeof v.id !== "number") return;
|
|
209
|
+
const p = this.pending.get(v.id);
|
|
210
|
+
if (!p) return;
|
|
211
|
+
this.pending.delete(v.id);
|
|
212
|
+
clearTimeout(p.timer);
|
|
213
|
+
if (v.error) p.reject(new Error("native RPC refused"));
|
|
214
|
+
else p.resolve(v.result);
|
|
215
|
+
});
|
|
216
|
+
socket.on("close", () => this.fail());
|
|
217
|
+
socket.on("error", () => this.fail());
|
|
218
|
+
}
|
|
219
|
+
sequence = 0;
|
|
220
|
+
pending = /* @__PURE__ */ new Map();
|
|
221
|
+
fail() {
|
|
222
|
+
for (const p of this.pending.values()) {
|
|
223
|
+
clearTimeout(p.timer);
|
|
224
|
+
p.reject(new Error("native RPC disconnected"));
|
|
225
|
+
}
|
|
226
|
+
this.pending.clear();
|
|
227
|
+
}
|
|
228
|
+
static async connect(url, protocols, timeout) {
|
|
229
|
+
const socket = new WebSocket(url, [...protocols], {
|
|
230
|
+
handshakeTimeout: timeout,
|
|
231
|
+
maxPayload: NATIVE_INPUT.maxFrameBytes,
|
|
232
|
+
followRedirects: false
|
|
233
|
+
});
|
|
234
|
+
const rpc = new _Rpc(socket, timeout);
|
|
235
|
+
await new Promise((resolve3, reject) => {
|
|
236
|
+
socket.once("open", resolve3);
|
|
237
|
+
socket.once("error", reject);
|
|
238
|
+
});
|
|
239
|
+
if (protocols.length && socket.protocol !== protocols[0]) {
|
|
240
|
+
rpc.close();
|
|
241
|
+
throw new Error("native protocol mismatch");
|
|
242
|
+
}
|
|
243
|
+
return rpc;
|
|
244
|
+
}
|
|
245
|
+
request(method, params) {
|
|
246
|
+
const id = ++this.sequence;
|
|
247
|
+
return new Promise((resolve3, reject) => {
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
this.pending.delete(id);
|
|
250
|
+
reject(new Error("native RPC timeout"));
|
|
251
|
+
}, this.timeout);
|
|
252
|
+
this.pending.set(id, { resolve: resolve3, reject, timer });
|
|
253
|
+
this.socket.send(
|
|
254
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }),
|
|
255
|
+
(error) => {
|
|
256
|
+
if (error) {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
this.pending.delete(id);
|
|
259
|
+
reject(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
notify(method) {
|
|
266
|
+
this.socket.send(JSON.stringify({ method }));
|
|
267
|
+
}
|
|
268
|
+
close() {
|
|
269
|
+
this.fail();
|
|
270
|
+
this.socket.terminate();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
var controlRequest = (path, request, timeout) => new Promise((resolve3, reject) => {
|
|
274
|
+
const s = createConnection(path);
|
|
275
|
+
let buffer = "";
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
s.destroy();
|
|
278
|
+
reject(new Error("native control timeout"));
|
|
279
|
+
}, timeout);
|
|
280
|
+
s.on("connect", () => s.write(JSON.stringify(request) + "\n"));
|
|
281
|
+
s.on("error", reject);
|
|
282
|
+
s.on("close", () => {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
reject(new Error("native control closed"));
|
|
285
|
+
});
|
|
286
|
+
s.on("data", (chunk) => {
|
|
287
|
+
buffer += chunk.toString();
|
|
288
|
+
if (buffer.length > 65536) {
|
|
289
|
+
s.destroy();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!buffer.includes("\n")) return;
|
|
293
|
+
try {
|
|
294
|
+
resolve3(JSON.parse(buffer.slice(0, buffer.indexOf("\n"))));
|
|
295
|
+
} catch {
|
|
296
|
+
reject(new Error("invalid native control response"));
|
|
297
|
+
}
|
|
298
|
+
s.destroy();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
var NativeHostClient = class {
|
|
302
|
+
constructor(options) {
|
|
303
|
+
this.options = options;
|
|
304
|
+
this.timeout = options.timeoutMs ?? NATIVE_INPUT.rpcTimeoutMs;
|
|
305
|
+
}
|
|
306
|
+
timeout;
|
|
307
|
+
async connect() {
|
|
308
|
+
if ((this.options.platform ?? process.platform) !== "darwin")
|
|
309
|
+
throw new Error("native host platform unqualified");
|
|
310
|
+
const home = resolve(this.options.home);
|
|
311
|
+
if (this.options.agent === "codex" /* codex */) {
|
|
312
|
+
const published = join(home, NATIVE_INPUT.codexSocket);
|
|
313
|
+
const path2 = realpathSync(published);
|
|
314
|
+
if (lstatSync(published).uid !== process.getuid?.() || !privateNode(join(home, "app-server-control"), "directory") || !privateNode(dirname(path2), "directory") || !privateNode(path2, "socket"))
|
|
315
|
+
throw new Error("native control unavailable");
|
|
316
|
+
const rpc = await Rpc.connect(`ws+unix://${path2}:/`, [], this.timeout);
|
|
317
|
+
try {
|
|
318
|
+
const v2 = await rpc.request("initialize", {
|
|
319
|
+
clientInfo: { name: "pushary_native_input", version: "1" },
|
|
320
|
+
capabilities: { experimentalApi: true }
|
|
321
|
+
});
|
|
322
|
+
if (!nativeRecord(v2) || v2.codexHome !== home || typeof v2.userAgent !== "string" || !v2.userAgent.includes(`/${NATIVE_INPUT.codexVersion} `) || v2.platformOs !== "macos")
|
|
323
|
+
throw new Error("unqualified native host");
|
|
324
|
+
rpc.notify("initialized");
|
|
325
|
+
return { rpc, profile: home };
|
|
326
|
+
} catch (e) {
|
|
327
|
+
rpc.close();
|
|
328
|
+
throw e;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const path = join(home, NATIVE_INPUT.hermesSocket);
|
|
332
|
+
if (!ownerControlledDirectory(home) || !privateNode(path, "socket"))
|
|
333
|
+
throw new Error("native control unavailable");
|
|
334
|
+
const v = await controlRequest(
|
|
335
|
+
path,
|
|
336
|
+
{ protocol: 1, verb: "identify", id: 1 },
|
|
337
|
+
this.timeout
|
|
338
|
+
);
|
|
339
|
+
if (!nativeRecord(v)) throw new Error("invalid host");
|
|
340
|
+
const d = nativeRecord(v.result) ? v.result : v;
|
|
341
|
+
if (d.runtime_protocol !== 1 || d.state !== "ready" || !Array.isArray(d.served_profiles) || !nativeIdentifier(d.instance_id) || typeof d.api_origin !== "string")
|
|
342
|
+
throw new Error("unqualified native host");
|
|
343
|
+
const profiles = d.served_profiles.filter(
|
|
344
|
+
(p) => nativeRecord(p) && p.home === home
|
|
345
|
+
);
|
|
346
|
+
const profile = profiles[0];
|
|
347
|
+
if (profiles.length !== 1 || !nativeRecord(profile) || typeof profile.profile_id !== "string")
|
|
348
|
+
throw new Error("native profile mismatch");
|
|
349
|
+
const hermesReceiptMode = qualifyHermesReceiptMode(
|
|
350
|
+
d.code_sha,
|
|
351
|
+
d.capabilities
|
|
352
|
+
);
|
|
353
|
+
if (!hermesReceiptMode) throw new Error("native admission unavailable");
|
|
354
|
+
const url = new URL(d.api_origin);
|
|
355
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password || url.pathname !== "/")
|
|
356
|
+
throw new Error("nonlocal native host");
|
|
357
|
+
const t = await controlRequest(
|
|
358
|
+
path,
|
|
359
|
+
{
|
|
360
|
+
protocol: 1,
|
|
361
|
+
id: 1,
|
|
362
|
+
verb: "session-ticket",
|
|
363
|
+
params: {
|
|
364
|
+
profile_id: profile.profile_id,
|
|
365
|
+
instance_id: d.instance_id,
|
|
366
|
+
purpose: "interactive"
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
this.timeout
|
|
370
|
+
);
|
|
371
|
+
const ticket = nativeRecord(t) && nativeRecord(t.result) ? t.result : t;
|
|
372
|
+
if (!nativeRecord(ticket) || ticket.profile_id !== profile.profile_id || ticket.instance_id !== d.instance_id || ticket.runtime_protocol !== 1 || typeof ticket.ticket !== "string")
|
|
373
|
+
throw new Error("native ticket mismatch");
|
|
374
|
+
return {
|
|
375
|
+
rpc: await Rpc.connect(
|
|
376
|
+
`ws://${url.host}/api/ws`,
|
|
377
|
+
[
|
|
378
|
+
NATIVE_INPUT.hermesWebSocketProtocol,
|
|
379
|
+
NATIVE_INPUT.hermesTicketPrefix + ticket.ticket
|
|
380
|
+
],
|
|
381
|
+
this.timeout
|
|
382
|
+
),
|
|
383
|
+
profile: profile.profile_id,
|
|
384
|
+
hermesReceiptMode
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
async list(host) {
|
|
388
|
+
const { rpc } = host;
|
|
389
|
+
if (this.options.agent === "codex" /* codex */) {
|
|
390
|
+
const v2 = await rpc.request("thread/loaded/list", {
|
|
391
|
+
limit: NATIVE_INPUT.maxSessions
|
|
392
|
+
});
|
|
393
|
+
if (!nativeRecord(v2) || !Array.isArray(v2.data) || !v2.data.every(nativeIdentifier))
|
|
394
|
+
throw new Error("invalid loaded sessions");
|
|
395
|
+
const sessions = [];
|
|
396
|
+
for (const id of v2.data) {
|
|
397
|
+
const t = await this.thread(rpc, id, false);
|
|
398
|
+
if (t && typeof t.cwd === "string")
|
|
399
|
+
sessions.push({
|
|
400
|
+
sessionId: id,
|
|
401
|
+
cwd: t.cwd,
|
|
402
|
+
agent: this.options.agent
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return sessions;
|
|
406
|
+
}
|
|
407
|
+
const v = await rpc.request("session.list", {
|
|
408
|
+
limit: NATIVE_INPUT.maxSessions
|
|
409
|
+
});
|
|
410
|
+
if (!nativeRecord(v) || v.scope !== "live" || !Array.isArray(v.sessions))
|
|
411
|
+
throw new Error("invalid live sessions");
|
|
412
|
+
return v.sessions.filter(
|
|
413
|
+
(row) => nativeRecord(row) && nativeIdentifier(row.session_id) && (row.source === "cli" || row.source === "local")
|
|
414
|
+
).map((row) => ({
|
|
415
|
+
sessionId: String(row.session_id),
|
|
416
|
+
cwd: this.options.home,
|
|
417
|
+
agent: this.options.agent
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
async sessions() {
|
|
421
|
+
let host;
|
|
422
|
+
try {
|
|
423
|
+
host = await this.connect();
|
|
424
|
+
return await this.list(host);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this.options.report?.(
|
|
427
|
+
error instanceof Error ? error.message : "native discovery failed"
|
|
428
|
+
);
|
|
429
|
+
return [];
|
|
430
|
+
} finally {
|
|
431
|
+
host?.rpc.close();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async thread(rpc, id, includeTurns = false) {
|
|
435
|
+
const v = await rpc.request("thread/read", { threadId: id, includeTurns });
|
|
436
|
+
return nativeRecord(v) && nativeRecord(v.thread) && v.thread.id === id ? v.thread : null;
|
|
437
|
+
}
|
|
438
|
+
async hasReceipt(rpc, sessionId, commandId) {
|
|
439
|
+
for (const method of [
|
|
440
|
+
"thread/queue/list" /* queueList */,
|
|
441
|
+
"thread/items/list" /* itemsList */
|
|
442
|
+
]) {
|
|
443
|
+
let cursor;
|
|
444
|
+
for (let page = 0; page < NATIVE_INPUT.receiptMaxPages; page++) {
|
|
445
|
+
const result = await rpc.request(method, {
|
|
446
|
+
threadId: sessionId,
|
|
447
|
+
...method === "thread/items/list" /* itemsList */ ? { sortDirection: "desc" } : {},
|
|
448
|
+
limit: NATIVE_INPUT.receiptPageSize,
|
|
449
|
+
...cursor ? { cursor } : {}
|
|
450
|
+
});
|
|
451
|
+
if (!nativeRecord(result) || !Array.isArray(result.data))
|
|
452
|
+
throw new Error("invalid native receipt page");
|
|
453
|
+
if (method === "thread/queue/list" /* queueList */) {
|
|
454
|
+
if (result.data.some((row) => isCodexQueuedReceipt(row, commandId)))
|
|
455
|
+
return true;
|
|
456
|
+
} else {
|
|
457
|
+
const items = result.data.flatMap(
|
|
458
|
+
(row) => nativeRecord(row) && nativeRecord(row.item) ? [row.item] : []
|
|
459
|
+
);
|
|
460
|
+
if (isCodexInputReceipt(
|
|
461
|
+
{ id: sessionId, turns: [{ items }] },
|
|
462
|
+
sessionId,
|
|
463
|
+
commandId
|
|
464
|
+
))
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor)
|
|
468
|
+
break;
|
|
469
|
+
cursor = result.nextCursor;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
async hasHermesReceipt(host, intent, snapshot) {
|
|
475
|
+
if (host.hermesReceiptMode === "submission" /* submission */) {
|
|
476
|
+
const receipt2 = parseHermesAdmission(
|
|
477
|
+
await host.rpc.request("prompt.receipt" /* receipt */, {
|
|
478
|
+
session_id: intent.sessionId,
|
|
479
|
+
submission_id: intent.commandId
|
|
480
|
+
}),
|
|
481
|
+
host.profile,
|
|
482
|
+
intent.sessionId
|
|
483
|
+
);
|
|
484
|
+
return receipt2 !== null && receipt2.status !== "unknown" /* unknown */;
|
|
485
|
+
}
|
|
486
|
+
let admissionId = hermesPendingAdmissionId(
|
|
487
|
+
snapshot,
|
|
488
|
+
host.profile,
|
|
489
|
+
intent.sessionId,
|
|
490
|
+
intent.commandId
|
|
491
|
+
);
|
|
492
|
+
if (!admissionId && intent.hermesCursor) {
|
|
493
|
+
const replay = await host.rpc.request("session.events.since" /* eventsSince */, {
|
|
494
|
+
session_id: intent.sessionId,
|
|
495
|
+
...intent.hermesCursor
|
|
496
|
+
});
|
|
497
|
+
admissionId = hermesReplayAdmissionId(
|
|
498
|
+
replay,
|
|
499
|
+
intent.hermesCursor,
|
|
500
|
+
intent.sessionId,
|
|
501
|
+
intent.commandId
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (!admissionId) return false;
|
|
505
|
+
const receipt = parseHermesAdmission(
|
|
506
|
+
await host.rpc.request("prompt.receipt" /* receipt */, {
|
|
507
|
+
session_id: intent.sessionId,
|
|
508
|
+
admission_id: admissionId
|
|
509
|
+
}),
|
|
510
|
+
host.profile,
|
|
511
|
+
intent.sessionId
|
|
512
|
+
);
|
|
513
|
+
return receipt?.admissionId === admissionId && receipt.status !== "unknown" /* unknown */;
|
|
514
|
+
}
|
|
515
|
+
journalPath(sessionId, commandId) {
|
|
516
|
+
return join(
|
|
517
|
+
this.options.journalDir,
|
|
518
|
+
createHash("sha256").update(`${sessionId}\0${commandId}`).digest("hex") + ".json"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
readIntent(sessionId, commandId) {
|
|
522
|
+
try {
|
|
523
|
+
const file = this.journalPath(sessionId, commandId);
|
|
524
|
+
const node = lstatSync(file);
|
|
525
|
+
if (!node.isFile() || node.uid !== process.getuid?.() || node.size > 4096 || (node.mode & 63) !== 0)
|
|
526
|
+
throw new Error("unsafe intent");
|
|
527
|
+
const v = JSON.parse(readFileSync(file, "utf8"));
|
|
528
|
+
if (!nativeRecord(v) || v.version !== NATIVE_INPUT.journalVersion || v.agent !== this.options.agent || v.sessionId !== sessionId || v.commandId !== commandId || v.home !== resolve(this.options.home) || typeof v.submittedAtMs !== "number" || !Number.isSafeInteger(v.submittedAtMs) || typeof v.accepted !== "boolean" || v.hermesCursor !== void 0 && !parseHermesReplayCursor(v.hermesCursor))
|
|
529
|
+
throw new Error("invalid intent");
|
|
530
|
+
const hermesCursor = parseHermesReplayCursor(v.hermesCursor);
|
|
531
|
+
return {
|
|
532
|
+
version: v.version,
|
|
533
|
+
agent: v.agent,
|
|
534
|
+
sessionId,
|
|
535
|
+
commandId,
|
|
536
|
+
home: v.home,
|
|
537
|
+
submittedAtMs: v.submittedAtMs,
|
|
538
|
+
accepted: v.accepted,
|
|
539
|
+
settled: v.settled === true,
|
|
540
|
+
...hermesCursor ? { hermesCursor } : {}
|
|
541
|
+
};
|
|
542
|
+
} catch (e) {
|
|
543
|
+
if (e instanceof Error && "code" in e && e.code === "ENOENT") return null;
|
|
544
|
+
throw e;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
pendingSettlements() {
|
|
548
|
+
try {
|
|
549
|
+
return readdirSync(this.options.journalDir).filter((file) => /^[0-9a-f]{64}\.json$/.test(file)).flatMap((file) => {
|
|
550
|
+
try {
|
|
551
|
+
const raw = JSON.parse(
|
|
552
|
+
readFileSync(join(this.options.journalDir, file), "utf8")
|
|
553
|
+
);
|
|
554
|
+
if (!nativeRecord(raw) || !nativeIdentifier(raw.sessionId) || !nativeIdentifier(raw.commandId))
|
|
555
|
+
return [];
|
|
556
|
+
const intent = this.readIntent(raw.sessionId, raw.commandId);
|
|
557
|
+
return intent?.accepted && !intent.settled ? [{ sessionId: intent.sessionId, commandId: intent.commandId }] : [];
|
|
558
|
+
} catch {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
} catch {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
confirmSettlement(sessionId, commandId) {
|
|
567
|
+
const intent = this.readIntent(sessionId, commandId);
|
|
568
|
+
if (intent?.accepted)
|
|
569
|
+
writeJsonAtomicDurable(
|
|
570
|
+
this.journalPath(sessionId, commandId),
|
|
571
|
+
{ ...intent, settled: true },
|
|
572
|
+
384
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
writeIntent(sessionId, commandId, hermesCursor) {
|
|
576
|
+
mkdirSync(this.options.journalDir, { recursive: true, mode: 448 });
|
|
577
|
+
const fd = openSync(this.journalPath(sessionId, commandId), "wx", 384);
|
|
578
|
+
try {
|
|
579
|
+
writeSync(
|
|
580
|
+
fd,
|
|
581
|
+
JSON.stringify({
|
|
582
|
+
version: NATIVE_INPUT.journalVersion,
|
|
583
|
+
agent: this.options.agent,
|
|
584
|
+
sessionId,
|
|
585
|
+
commandId,
|
|
586
|
+
home: resolve(this.options.home),
|
|
587
|
+
submittedAtMs: Date.now(),
|
|
588
|
+
accepted: false,
|
|
589
|
+
...hermesCursor ? { hermesCursor } : {}
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
fsyncSync(fd);
|
|
593
|
+
} finally {
|
|
594
|
+
closeSync(fd);
|
|
595
|
+
}
|
|
596
|
+
syncDirectory(this.options.journalDir);
|
|
597
|
+
}
|
|
598
|
+
accept(intent) {
|
|
599
|
+
writeJsonAtomicDurable(
|
|
600
|
+
this.journalPath(intent.sessionId, intent.commandId),
|
|
601
|
+
{ ...intent, accepted: true },
|
|
602
|
+
384
|
|
603
|
+
);
|
|
604
|
+
return "accepted" /* accepted */;
|
|
605
|
+
}
|
|
606
|
+
async deliver(sessionId, command, signal) {
|
|
607
|
+
let intent;
|
|
608
|
+
try {
|
|
609
|
+
intent = this.readIntent(sessionId, command.id);
|
|
610
|
+
} catch {
|
|
611
|
+
return "outcome_unknown" /* outcomeUnknown */;
|
|
612
|
+
}
|
|
613
|
+
if (intent?.accepted) return "accepted" /* accepted */;
|
|
614
|
+
if (!intent && command.expiresAtMs <= Date.now())
|
|
615
|
+
return "expired" /* expired */;
|
|
616
|
+
let host;
|
|
617
|
+
let hermesCursor;
|
|
618
|
+
try {
|
|
619
|
+
host = await this.connect();
|
|
620
|
+
const { rpc } = host;
|
|
621
|
+
if (!(await this.list(host)).some((s) => s.sessionId === sessionId))
|
|
622
|
+
return intent ? "outcome_unknown" /* outcomeUnknown */ : "not_sent" /* notSent */;
|
|
623
|
+
if (this.options.agent === "codex" /* codex */) {
|
|
624
|
+
if (intent) {
|
|
625
|
+
if (await this.hasReceipt(rpc, sessionId, command.id))
|
|
626
|
+
return this.accept(intent);
|
|
627
|
+
} else {
|
|
628
|
+
const queue = await rpc.request("thread/queue/list" /* queueList */, {
|
|
629
|
+
threadId: sessionId,
|
|
630
|
+
limit: 1
|
|
631
|
+
});
|
|
632
|
+
if (!nativeRecord(queue) || !Array.isArray(queue.data))
|
|
633
|
+
throw new Error("native queue unavailable");
|
|
634
|
+
}
|
|
635
|
+
} else if (host.hermesReceiptMode === "submission" /* submission */) {
|
|
636
|
+
if (intent && await this.hasHermesReceipt(host, intent))
|
|
637
|
+
return this.accept(intent);
|
|
638
|
+
if (!intent) {
|
|
639
|
+
const snapshot = await rpc.request("session.resume" /* resume */, {
|
|
640
|
+
session_id: sessionId
|
|
641
|
+
});
|
|
642
|
+
if (!nativeRecord(snapshot) || snapshot.session_id !== sessionId)
|
|
643
|
+
throw new Error("native session mismatch");
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
const snapshot = await rpc.request("session.resume" /* resume */, {
|
|
647
|
+
session_id: sessionId
|
|
648
|
+
});
|
|
649
|
+
if (!nativeRecord(snapshot) || snapshot.session_id !== sessionId)
|
|
650
|
+
throw new Error("native session mismatch");
|
|
651
|
+
hermesCursor = parseHermesReplayCursor(snapshot) ?? void 0;
|
|
652
|
+
if (intent && await this.hasHermesReceipt(host, intent, snapshot))
|
|
653
|
+
return this.accept(intent);
|
|
654
|
+
if (!intent && !hermesCursor)
|
|
655
|
+
throw new Error("native replay cursor unavailable");
|
|
656
|
+
}
|
|
657
|
+
const action = nativeInputAction({
|
|
658
|
+
attempted: !!intent,
|
|
659
|
+
accepted: false,
|
|
660
|
+
nowMs: Date.now(),
|
|
661
|
+
expiresAtMs: Math.min(
|
|
662
|
+
command.expiresAtMs,
|
|
663
|
+
intent ? intent.submittedAtMs + NATIVE_INPUT.receiptWindowMs : Infinity
|
|
664
|
+
)
|
|
665
|
+
});
|
|
666
|
+
if (action === "expire" /* expire */) return "expired" /* expired */;
|
|
667
|
+
if (action === "outcome_unknown" /* outcomeUnknown */)
|
|
668
|
+
return "outcome_unknown" /* outcomeUnknown */;
|
|
669
|
+
if (action === "observe" /* observe */)
|
|
670
|
+
return "unconfirmed" /* unconfirmed */;
|
|
671
|
+
if (signal?.aborted) return "not_sent" /* notSent */;
|
|
672
|
+
if (!intent) {
|
|
673
|
+
this.writeIntent(sessionId, command.id, hermesCursor);
|
|
674
|
+
intent = this.readIntent(sessionId, command.id);
|
|
675
|
+
}
|
|
676
|
+
if (!intent) return "outcome_unknown" /* outcomeUnknown */;
|
|
677
|
+
if (this.options.agent === "hermes" /* hermes */) {
|
|
678
|
+
const receipt = parseHermesAdmission(
|
|
679
|
+
await rpc.request("prompt.submit" /* submit */, {
|
|
680
|
+
session_id: sessionId,
|
|
681
|
+
submission_id: command.id,
|
|
682
|
+
text: command.text
|
|
683
|
+
}),
|
|
684
|
+
host.profile,
|
|
685
|
+
sessionId
|
|
686
|
+
);
|
|
687
|
+
return receipt && receipt.status !== "unknown" /* unknown */ ? this.accept(intent) : "outcome_unknown" /* outcomeUnknown */;
|
|
688
|
+
}
|
|
689
|
+
const response = await rpc.request("thread/queue/add" /* queueAdd */, {
|
|
690
|
+
threadId: sessionId,
|
|
691
|
+
clientUserMessageId: command.id,
|
|
692
|
+
input: [{ type: "text", text: command.text, text_elements: [] }]
|
|
693
|
+
});
|
|
694
|
+
return nativeRecord(response) && isCodexQueuedReceipt(response.queuedSubmission, command.id) || await this.hasReceipt(rpc, sessionId, command.id) ? this.accept(intent) : "unconfirmed" /* unconfirmed */;
|
|
695
|
+
} catch (error) {
|
|
696
|
+
this.options.report?.(
|
|
697
|
+
error instanceof Error ? error.message : "native delivery failed"
|
|
698
|
+
);
|
|
699
|
+
return intent ? Date.now() >= Math.min(
|
|
700
|
+
command.expiresAtMs,
|
|
701
|
+
intent.submittedAtMs + NATIVE_INPUT.receiptWindowMs
|
|
702
|
+
) ? "outcome_unknown" /* outcomeUnknown */ : "unconfirmed" /* unconfirmed */ : "not_sent" /* notSent */;
|
|
703
|
+
} finally {
|
|
704
|
+
host?.rpc.close();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/hook-ownership-file.ts
|
|
710
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
711
|
+
var readHookOwner = (file) => {
|
|
712
|
+
try {
|
|
713
|
+
const value = JSON.parse(readFileSync2(file, "utf8"));
|
|
714
|
+
return nativeRecord(value) ? hookOwnerOf(value) : null;
|
|
715
|
+
} catch (error) {
|
|
716
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT" ? "none" : null;
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// src/native-input-receiver.ts
|
|
721
|
+
import { basename } from "path";
|
|
722
|
+
var startNativeInputReceiver = (apiKey, machineId, options) => {
|
|
723
|
+
const client = options.client;
|
|
724
|
+
const baseUrl = options.baseUrl ?? getBaseUrl();
|
|
725
|
+
const abort = new AbortController();
|
|
726
|
+
const busy = /* @__PURE__ */ new Set();
|
|
727
|
+
const heartbeats = /* @__PURE__ */ new Map();
|
|
728
|
+
const settlements = /* @__PURE__ */ new Map();
|
|
729
|
+
let scanning = false;
|
|
730
|
+
let cursor = 0;
|
|
731
|
+
const post = async (path, body) => {
|
|
732
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
733
|
+
method: "POST",
|
|
734
|
+
headers: {
|
|
735
|
+
"Content-Type": "application/json",
|
|
736
|
+
Authorization: `Bearer ${apiKey}`
|
|
737
|
+
},
|
|
738
|
+
body: JSON.stringify(body),
|
|
739
|
+
signal: AbortSignal.any([
|
|
740
|
+
abort.signal,
|
|
741
|
+
AbortSignal.timeout(CLAUDE_INBOX_RECEIVER.requestTimeoutMs)
|
|
742
|
+
])
|
|
743
|
+
});
|
|
744
|
+
if (!response.ok) throw new Error(`native inbox HTTP ${response.status}`);
|
|
745
|
+
return response.json();
|
|
746
|
+
};
|
|
747
|
+
const settle = async (commandId) => {
|
|
748
|
+
const pending = settlements.get(commandId);
|
|
749
|
+
if (!pending) return false;
|
|
750
|
+
try {
|
|
751
|
+
const receipt = await post("/api/agent/command/ack", {
|
|
752
|
+
...pending,
|
|
753
|
+
commandId,
|
|
754
|
+
machineId
|
|
755
|
+
});
|
|
756
|
+
if (!isCommandAcknowledgementReceipt(receipt))
|
|
757
|
+
throw new Error("native acknowledgement missing receipt");
|
|
758
|
+
client.confirmSettlement(pending.sessionId, commandId);
|
|
759
|
+
settlements.delete(commandId);
|
|
760
|
+
return true;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (!abort.signal.aborted)
|
|
763
|
+
options.report?.(
|
|
764
|
+
error instanceof Error ? error.message : "native receipt recovery failed"
|
|
765
|
+
);
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const receive = async (sessionId) => {
|
|
770
|
+
busy.add(sessionId);
|
|
771
|
+
try {
|
|
772
|
+
if ([...settlements.values()].some(
|
|
773
|
+
(receipt) => receipt.sessionId === sessionId
|
|
774
|
+
))
|
|
775
|
+
return;
|
|
776
|
+
const raw = await post("/api/agent/command/native", {
|
|
777
|
+
operation: "claim" /* claim */,
|
|
778
|
+
sessionId,
|
|
779
|
+
machineId
|
|
780
|
+
});
|
|
781
|
+
const command = parseClaudeInboxClaim(raw);
|
|
782
|
+
if (!command || abort.signal.aborted) return;
|
|
783
|
+
const outcome = await client.deliver(sessionId, command, abort.signal);
|
|
784
|
+
const settlement = CLAUDE_INBOX_SETTLEMENTS[outcome];
|
|
785
|
+
if (abort.signal.aborted || !settlement) return;
|
|
786
|
+
settlements.set(command.id, { sessionId, ...settlement });
|
|
787
|
+
await settle(command.id);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
if (!abort.signal.aborted)
|
|
790
|
+
options.report?.(
|
|
791
|
+
error instanceof Error ? error.message : "native inbox delivery failed"
|
|
792
|
+
);
|
|
793
|
+
} finally {
|
|
794
|
+
busy.delete(sessionId);
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const tick = async () => {
|
|
798
|
+
if (scanning || abort.signal.aborted) return;
|
|
799
|
+
scanning = true;
|
|
800
|
+
try {
|
|
801
|
+
for (const receipt of client.pendingSettlements()) {
|
|
802
|
+
settlements.set(receipt.commandId, {
|
|
803
|
+
sessionId: receipt.sessionId,
|
|
804
|
+
state: "delivered"
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
for (const commandId of [...settlements.keys()]) await settle(commandId);
|
|
808
|
+
const sessions = (await client.sessions()).filter(options.ownsSession).slice(0, CLAUDE_INBOX_MAX_SESSIONS);
|
|
809
|
+
if (!sessions.length) return;
|
|
810
|
+
for (const session of sessions) {
|
|
811
|
+
if (Date.now() - (heartbeats.get(session.sessionId) ?? 0) < CLAUDE_INBOX_RECEIVER.heartbeatMs)
|
|
812
|
+
continue;
|
|
813
|
+
await post("/api/agent/event", {
|
|
814
|
+
event: "heartbeat",
|
|
815
|
+
agentType: options.agentType ?? "claude_code",
|
|
816
|
+
agentName: `${options.agentName ?? "Claude Code"} - ${basename(session.cwd)}`,
|
|
817
|
+
sessionId: session.sessionId,
|
|
818
|
+
machineId,
|
|
819
|
+
cwd: session.cwd,
|
|
820
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
821
|
+
canDrainCommand: false
|
|
822
|
+
});
|
|
823
|
+
heartbeats.set(session.sessionId, Date.now());
|
|
824
|
+
}
|
|
825
|
+
const raw = await post("/api/agent/command/native", {
|
|
826
|
+
operation: "poll" /* poll */,
|
|
827
|
+
machineId,
|
|
828
|
+
sessionIds: sessions.map((session) => session.sessionId)
|
|
829
|
+
});
|
|
830
|
+
const pending = parseClaudeInboxPoll(raw);
|
|
831
|
+
if (!pending) throw new Error("native inbox protocol mismatch");
|
|
832
|
+
const rotated = [...pending.slice(cursor), ...pending.slice(0, cursor)];
|
|
833
|
+
cursor = pending.length ? (cursor + CLAUDE_INBOX_RECEIVER.maxConcurrent) % pending.length : 0;
|
|
834
|
+
for (const sessionId of rotated) {
|
|
835
|
+
if (abort.signal.aborted || busy.size >= CLAUDE_INBOX_RECEIVER.maxConcurrent)
|
|
836
|
+
break;
|
|
837
|
+
if (!busy.has(sessionId) && sessions.some((session) => session.sessionId === sessionId))
|
|
838
|
+
void receive(sessionId);
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
const message = error instanceof Error ? error.message : "native inbox polling failed";
|
|
842
|
+
if (!abort.signal.aborted) options.report?.(message);
|
|
843
|
+
} finally {
|
|
844
|
+
scanning = false;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const timer = setInterval(() => {
|
|
848
|
+
void tick();
|
|
849
|
+
}, options.pollMs ?? CLAUDE_INBOX_RECEIVER.pollMs);
|
|
850
|
+
timer.unref();
|
|
851
|
+
void tick();
|
|
852
|
+
return () => {
|
|
853
|
+
abort.abort();
|
|
854
|
+
clearInterval(timer);
|
|
855
|
+
};
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// src/native-host-receiver.ts
|
|
859
|
+
var ownsHermes = (home) => {
|
|
860
|
+
try {
|
|
861
|
+
return parseHermesInstallMarker(
|
|
862
|
+
JSON.parse(
|
|
863
|
+
readFileSync3(
|
|
864
|
+
join2(home, "plugins", "pushary", HERMES_INSTALL_MARKER_FILE),
|
|
865
|
+
"utf8"
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
)?.installedBy === "cli";
|
|
869
|
+
} catch {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
var startNativeHostReceivers = (apiKey, machineId, report) => {
|
|
874
|
+
const homes = {
|
|
875
|
+
["codex" /* codex */]: codexHome(),
|
|
876
|
+
["hermes" /* hermes */]: defaultHermesHome()
|
|
877
|
+
};
|
|
878
|
+
const stops = Object.values(NativeInputAgent).map((agent) => {
|
|
879
|
+
const home = homes[agent];
|
|
880
|
+
return startNativeInputReceiver(apiKey, machineId, {
|
|
881
|
+
client: new NativeHostClient({
|
|
882
|
+
agent,
|
|
883
|
+
home,
|
|
884
|
+
report,
|
|
885
|
+
journalDir: join2(homedir(), ".pushary", NATIVE_INPUT.journalDirectory)
|
|
886
|
+
}),
|
|
887
|
+
agentType: agent,
|
|
888
|
+
agentName: agent === "codex" /* codex */ ? "Codex" : "Hermes",
|
|
889
|
+
report,
|
|
890
|
+
ownsSession: () => agent === "codex" /* codex */ ? readHookOwner(join2(home, "hooks.json")) === "cli" : ownsHermes(home)
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
return () => {
|
|
894
|
+
for (const stop of stops) stop();
|
|
895
|
+
};
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/hook-command-receipts.ts
|
|
899
|
+
var reconcileCommandReceipts = async (apiKey, machineId, agentType, baseUrl, signal) => {
|
|
900
|
+
const namespace = `${apiKey}:${machineId}:${agentType}`;
|
|
901
|
+
for (const { sessionId, command } of pendingCommandReceipts(namespace)) {
|
|
902
|
+
signal?.throwIfAborted();
|
|
903
|
+
const state = command.delivered ? "delivered" : command.attempted ? "uncertain" : "failed";
|
|
904
|
+
const response = await fetch(`${baseUrl}/api/agent/command/ack`, {
|
|
905
|
+
method: "POST",
|
|
906
|
+
headers: {
|
|
907
|
+
"Content-Type": "application/json",
|
|
908
|
+
Authorization: `Bearer ${apiKey}`
|
|
909
|
+
},
|
|
910
|
+
body: JSON.stringify({
|
|
911
|
+
commandId: command.id,
|
|
912
|
+
sessionId,
|
|
913
|
+
machineId,
|
|
914
|
+
state,
|
|
915
|
+
failureCode: command.attempted ? "delivery_unconfirmed" : "expired"
|
|
916
|
+
}),
|
|
917
|
+
signal: AbortSignal.any([
|
|
918
|
+
AbortSignal.timeout(COMMAND_RECOVERY.acknowledgementTimeoutMs),
|
|
919
|
+
...signal ? [signal] : []
|
|
920
|
+
])
|
|
921
|
+
});
|
|
922
|
+
if (response.ok) removePendingCommand(sessionId, command.id, namespace);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// src/claude-inbox-receiver.ts
|
|
927
|
+
import { join as join4, basename as basename2 } from "path";
|
|
928
|
+
import { homedir as homedir2 } from "os";
|
|
929
|
+
|
|
930
|
+
// src/claude-inbox.ts
|
|
931
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
932
|
+
import { execFileSync } from "child_process";
|
|
933
|
+
import {
|
|
934
|
+
closeSync as closeSync2,
|
|
935
|
+
fsyncSync as fsyncSync2,
|
|
936
|
+
lstatSync as lstatSync2,
|
|
937
|
+
mkdirSync as mkdirSync2,
|
|
938
|
+
openSync as openSync2,
|
|
939
|
+
readFileSync as readFileSync4,
|
|
940
|
+
readdirSync as readdirSync2,
|
|
941
|
+
readSync,
|
|
942
|
+
statSync,
|
|
943
|
+
writeFileSync,
|
|
944
|
+
unlinkSync
|
|
945
|
+
} from "fs";
|
|
946
|
+
import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve2 } from "path";
|
|
947
|
+
import { Socket } from "net";
|
|
948
|
+
import { setTimeout as delay } from "timers/promises";
|
|
949
|
+
var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
950
|
+
var digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
951
|
+
var readObject = (file) => {
|
|
952
|
+
const stat = lstatSync2(file);
|
|
953
|
+
if (!stat.isFile() || stat.size > CLAUDE_INBOX.maxRecordBytes || stat.uid !== process.getuid?.())
|
|
954
|
+
throw new Error("Invalid inbox record");
|
|
955
|
+
return JSON.parse(readFileSync4(file, "utf8"));
|
|
956
|
+
};
|
|
957
|
+
var readKey = (file) => {
|
|
958
|
+
const value = readObject(file);
|
|
959
|
+
return object(value) && typeof value.peerToken === "string" && /^[0-9a-f]{32}$/.test(value.peerToken) && typeof value.procStart === "string" ? { peerToken: value.peerToken, procStart: value.procStart } : null;
|
|
960
|
+
};
|
|
961
|
+
var processStart = (pid) => {
|
|
962
|
+
if (process.platform !== "darwin") return null;
|
|
963
|
+
try {
|
|
964
|
+
return execFileSync("/bin/ps", ["-p", String(pid), "-o", "lstart="], {
|
|
965
|
+
encoding: "utf8",
|
|
966
|
+
env: { ...process.env, TZ: "UTC", LC_ALL: "C" },
|
|
967
|
+
timeout: CLAUDE_INBOX.connectTimeoutMs
|
|
968
|
+
}).trim();
|
|
969
|
+
} catch {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
var transcriptPath = (config, session) => join3(
|
|
974
|
+
config,
|
|
975
|
+
"projects",
|
|
976
|
+
resolve2(session.cwd).replace(/[^a-zA-Z0-9-]/g, "-"),
|
|
977
|
+
`${session.sessionId}.jsonl`
|
|
978
|
+
);
|
|
979
|
+
var transcriptSize = (file) => {
|
|
980
|
+
try {
|
|
981
|
+
return statSync(file).size;
|
|
982
|
+
} catch {
|
|
983
|
+
return 0;
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
var receiptPresent = (attempt) => {
|
|
987
|
+
let fd;
|
|
988
|
+
try {
|
|
989
|
+
fd = openSync2(attempt.transcript, "r");
|
|
990
|
+
const buffer = Buffer.alloc(CLAUDE_INBOX.maxTranscriptReadBytes);
|
|
991
|
+
const count = readSync(fd, buffer, 0, buffer.length, attempt.offset);
|
|
992
|
+
for (const line of buffer.subarray(0, count).toString("utf8").split("\n")) {
|
|
993
|
+
try {
|
|
994
|
+
if (isClaudeInboxReceipt(
|
|
995
|
+
JSON.parse(line),
|
|
996
|
+
attempt.sessionId,
|
|
997
|
+
attempt.commandId
|
|
998
|
+
))
|
|
999
|
+
return true;
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return false;
|
|
1004
|
+
} catch {
|
|
1005
|
+
return false;
|
|
1006
|
+
} finally {
|
|
1007
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
var connectInbox = (path) => new Promise((resolveConnection) => {
|
|
1011
|
+
const socket = new Socket();
|
|
1012
|
+
const timer = setTimeout(failed, CLAUDE_INBOX.connectTimeoutMs);
|
|
1013
|
+
function failed() {
|
|
1014
|
+
clearTimeout(timer);
|
|
1015
|
+
socket.destroy();
|
|
1016
|
+
resolveConnection(null);
|
|
1017
|
+
}
|
|
1018
|
+
socket.once("error", failed);
|
|
1019
|
+
socket.connect(path, () => {
|
|
1020
|
+
clearTimeout(timer);
|
|
1021
|
+
socket.removeListener("error", failed);
|
|
1022
|
+
socket.on("error", () => {
|
|
1023
|
+
});
|
|
1024
|
+
resolveConnection(socket);
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
var createAttempt = (file, attempt) => {
|
|
1028
|
+
mkdirSync2(dirname2(file), { recursive: true, mode: 448 });
|
|
1029
|
+
const fd = openSync2(file, "wx", 384);
|
|
1030
|
+
try {
|
|
1031
|
+
writeFileSync(fd, JSON.stringify(attempt));
|
|
1032
|
+
fsyncSync2(fd);
|
|
1033
|
+
} finally {
|
|
1034
|
+
closeSync2(fd);
|
|
1035
|
+
}
|
|
1036
|
+
const directory = openSync2(dirname2(file), "r");
|
|
1037
|
+
try {
|
|
1038
|
+
fsyncSync2(directory);
|
|
1039
|
+
} finally {
|
|
1040
|
+
closeSync2(directory);
|
|
1041
|
+
}
|
|
1042
|
+
return true;
|
|
1043
|
+
};
|
|
1044
|
+
var ClaudeInboxClient = class {
|
|
1045
|
+
options;
|
|
1046
|
+
constructor(options) {
|
|
1047
|
+
this.options = options;
|
|
1048
|
+
}
|
|
1049
|
+
sessions() {
|
|
1050
|
+
try {
|
|
1051
|
+
const sessions = readdirSync2(
|
|
1052
|
+
join3(this.options.configDir, "sessions")
|
|
1053
|
+
).filter((file) => /^\d+\.json$/.test(file));
|
|
1054
|
+
return sessions.flatMap((file) => {
|
|
1055
|
+
try {
|
|
1056
|
+
const session = parseClaudeInboxSession(
|
|
1057
|
+
readObject(join3(this.options.configDir, "sessions", file))
|
|
1058
|
+
);
|
|
1059
|
+
if (!session || file !== `${session.pid}.json` || !isAbsolute(session.cwd) || !isAbsolute(session.messagingSocketPath))
|
|
1060
|
+
return [];
|
|
1061
|
+
if ((this.options.processStart ?? processStart)(session.pid) !== session.procStart)
|
|
1062
|
+
return [];
|
|
1063
|
+
const socket = lstatSync2(session.messagingSocketPath);
|
|
1064
|
+
if (!socket.isSocket() || socket.uid !== process.getuid?.()) return [];
|
|
1065
|
+
return this.key(session) ? [session] : [];
|
|
1066
|
+
} catch {
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
} catch {
|
|
1071
|
+
return [];
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
key(session) {
|
|
1075
|
+
const canonicalSocket = session.messagingSocketPath.replace(
|
|
1076
|
+
/^\/private(?=\/(?:tmp|var|etc)\/)/,
|
|
1077
|
+
""
|
|
1078
|
+
);
|
|
1079
|
+
const key = readKey(
|
|
1080
|
+
join3(
|
|
1081
|
+
this.options.configDir,
|
|
1082
|
+
"sessions",
|
|
1083
|
+
`${session.pid}.${digest(canonicalSocket)}.key`
|
|
1084
|
+
)
|
|
1085
|
+
);
|
|
1086
|
+
return key?.procStart === session.procStart ? key : null;
|
|
1087
|
+
}
|
|
1088
|
+
attempts(version) {
|
|
1089
|
+
const directory = join3(this.options.journalDir, version);
|
|
1090
|
+
mkdirSync2(directory, { recursive: true, mode: 448 });
|
|
1091
|
+
const files = readdirSync2(directory).filter((file) => /^\d{13}-[0-9a-f-]{36}\.json$/.test(file)).sort().reverse();
|
|
1092
|
+
const attempts = files.slice(0, CLAUDE_WAKE.retainedAttempts).map((name) => {
|
|
1093
|
+
const file = join3(directory, name);
|
|
1094
|
+
const value = readObject(file);
|
|
1095
|
+
if (!object(value) || value.version !== CLAUDE_INBOX.journalVersion || typeof value.sessionId !== "string" || typeof value.commandId !== "string" || typeof value.transcript !== "string" || !resolve2(value.transcript).startsWith(
|
|
1096
|
+
resolve2(this.options.configDir, "projects") + "/"
|
|
1097
|
+
) || typeof value.offset !== "number" || !Number.isSafeInteger(value.offset) || value.offset < 0 || typeof value.accepted !== "boolean" || typeof value.submittedAtMs !== "number" || !Number.isSafeInteger(value.submittedAtMs))
|
|
1098
|
+
throw new Error("Invalid wake receipt");
|
|
1099
|
+
const attempt = {
|
|
1100
|
+
version: value.version,
|
|
1101
|
+
sessionId: value.sessionId,
|
|
1102
|
+
commandId: value.commandId,
|
|
1103
|
+
transcript: value.transcript,
|
|
1104
|
+
offset: value.offset,
|
|
1105
|
+
accepted: value.accepted,
|
|
1106
|
+
submittedAtMs: value.submittedAtMs
|
|
1107
|
+
};
|
|
1108
|
+
if (!attempt.accepted && receiptPresent(attempt)) {
|
|
1109
|
+
const accepted = { ...attempt, accepted: true };
|
|
1110
|
+
writeJsonAtomicDurable(file, accepted, 384);
|
|
1111
|
+
return { file, attempt: accepted };
|
|
1112
|
+
}
|
|
1113
|
+
return { file, attempt };
|
|
1114
|
+
});
|
|
1115
|
+
for (const name of files.slice(CLAUDE_WAKE.retainedAttempts))
|
|
1116
|
+
unlinkSync(join3(directory, name));
|
|
1117
|
+
return attempts;
|
|
1118
|
+
}
|
|
1119
|
+
async wake(sessionId, signal) {
|
|
1120
|
+
const matches = this.sessions().filter(
|
|
1121
|
+
(candidate) => candidate.sessionId === sessionId
|
|
1122
|
+
);
|
|
1123
|
+
const session = matches.length === 1 ? matches[0] : null;
|
|
1124
|
+
if (!session || signal?.aborted) return "wake_not_sent" /* notSent */;
|
|
1125
|
+
try {
|
|
1126
|
+
const attempts = this.attempts(session.version);
|
|
1127
|
+
const decision = claudeWakeDecision({
|
|
1128
|
+
sessionId,
|
|
1129
|
+
idle: session.status === "idle",
|
|
1130
|
+
nowMs: Date.now(),
|
|
1131
|
+
attempts: attempts.map((entry) => entry.attempt)
|
|
1132
|
+
});
|
|
1133
|
+
if (decision) return decision;
|
|
1134
|
+
const key = this.key(session);
|
|
1135
|
+
if (!key) return "wake_not_sent" /* notSent */;
|
|
1136
|
+
const socket = await connectInbox(session.messagingSocketPath);
|
|
1137
|
+
if (!socket) return "wake_not_sent" /* notSent */;
|
|
1138
|
+
if (signal?.aborted || (this.options.processStart ?? processStart)(session.pid) !== session.procStart) {
|
|
1139
|
+
socket.destroy();
|
|
1140
|
+
return "wake_not_sent" /* notSent */;
|
|
1141
|
+
}
|
|
1142
|
+
const commandId = randomUUID();
|
|
1143
|
+
const transcript = transcriptPath(this.options.configDir, session);
|
|
1144
|
+
const attempt = {
|
|
1145
|
+
version: CLAUDE_INBOX.journalVersion,
|
|
1146
|
+
sessionId,
|
|
1147
|
+
commandId,
|
|
1148
|
+
transcript,
|
|
1149
|
+
offset: transcriptSize(transcript),
|
|
1150
|
+
accepted: false,
|
|
1151
|
+
submittedAtMs: Date.now()
|
|
1152
|
+
};
|
|
1153
|
+
const file = join3(
|
|
1154
|
+
this.options.journalDir,
|
|
1155
|
+
session.version,
|
|
1156
|
+
`${attempt.submittedAtMs}-${commandId}.json`
|
|
1157
|
+
);
|
|
1158
|
+
try {
|
|
1159
|
+
createAttempt(file, attempt);
|
|
1160
|
+
} catch {
|
|
1161
|
+
socket.destroy();
|
|
1162
|
+
return "wake_not_sent" /* notSent */;
|
|
1163
|
+
}
|
|
1164
|
+
const frame = {
|
|
1165
|
+
type: "user",
|
|
1166
|
+
session_id: sessionId,
|
|
1167
|
+
uuid: commandId,
|
|
1168
|
+
msg_id: commandId,
|
|
1169
|
+
from: CLAUDE_INBOX.sender,
|
|
1170
|
+
priority: "next",
|
|
1171
|
+
message: { role: "user", content: CLAUDE_WAKE.message }
|
|
1172
|
+
};
|
|
1173
|
+
socket.end(
|
|
1174
|
+
JSON.stringify({ type: "auth", token: key.peerToken }) + "\n" + JSON.stringify(frame) + "\n"
|
|
1175
|
+
);
|
|
1176
|
+
socket.setTimeout(CLAUDE_INBOX.connectTimeoutMs, () => socket.destroy());
|
|
1177
|
+
const outcome = await this.observe(
|
|
1178
|
+
file,
|
|
1179
|
+
attempt,
|
|
1180
|
+
attempt.submittedAtMs + CLAUDE_WAKE.receiptWindowMs
|
|
1181
|
+
);
|
|
1182
|
+
return outcome === "accepted" /* accepted */ ? "wake_accepted" /* accepted */ : "wake_unconfirmed" /* unconfirmed */;
|
|
1183
|
+
} catch {
|
|
1184
|
+
return "wake_not_sent" /* notSent */;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async observe(file, attempt, expiresAtMs) {
|
|
1188
|
+
const deliveryDeadline = Math.min(
|
|
1189
|
+
expiresAtMs,
|
|
1190
|
+
attempt.submittedAtMs + CLAUDE_INBOX.receiptWindowMs
|
|
1191
|
+
);
|
|
1192
|
+
if (claudeInboxAction({
|
|
1193
|
+
attempted: true,
|
|
1194
|
+
accepted: attempt.accepted,
|
|
1195
|
+
expiresAtMs: deliveryDeadline,
|
|
1196
|
+
nowMs: Date.now()
|
|
1197
|
+
}) === "acknowledge" /* acknowledge */)
|
|
1198
|
+
return "accepted" /* accepted */;
|
|
1199
|
+
const deadline = Math.min(
|
|
1200
|
+
deliveryDeadline,
|
|
1201
|
+
Date.now() + (this.options.receiptWindowMs ?? CLAUDE_INBOX.receiptWindowMs)
|
|
1202
|
+
);
|
|
1203
|
+
do {
|
|
1204
|
+
if (receiptPresent(attempt)) {
|
|
1205
|
+
try {
|
|
1206
|
+
writeJsonAtomicDurable(file, { ...attempt, accepted: true }, 384);
|
|
1207
|
+
} catch {
|
|
1208
|
+
return "unconfirmed" /* unconfirmed */;
|
|
1209
|
+
}
|
|
1210
|
+
return "accepted" /* accepted */;
|
|
1211
|
+
}
|
|
1212
|
+
if (Date.now() >= deadline) break;
|
|
1213
|
+
await delay(this.options.receiptPollMs ?? CLAUDE_INBOX.receiptPollMs);
|
|
1214
|
+
} while (Date.now() <= deadline);
|
|
1215
|
+
return deliveryDeadline <= Date.now() ? "outcome_unknown" /* outcomeUnknown */ : "unconfirmed" /* unconfirmed */;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// src/claude-inbox-receiver.ts
|
|
1220
|
+
var ownsSession = (session) => {
|
|
1221
|
+
const files = [
|
|
1222
|
+
join4(claudeConfigDir(), "settings.json"),
|
|
1223
|
+
join4(session.cwd, ".claude", "settings.json"),
|
|
1224
|
+
join4(session.cwd, ".claude", "settings.local.json")
|
|
1225
|
+
];
|
|
1226
|
+
const owners = files.map(readHookOwner);
|
|
1227
|
+
return owners.includes("cli") && !owners.includes("app") && !owners.includes(null);
|
|
1228
|
+
};
|
|
1229
|
+
var startClaudeInboxReceiver = (apiKey, machineId, options = {}) => {
|
|
1230
|
+
const client = options.client ?? new ClaudeInboxClient({
|
|
1231
|
+
configDir: claudeConfigDir(),
|
|
1232
|
+
journalDir: join4(homedir2(), ".pushary", CLAUDE_WAKE.directory)
|
|
1233
|
+
});
|
|
1234
|
+
const baseUrl = options.baseUrl ?? getBaseUrl();
|
|
1235
|
+
const abort = new AbortController();
|
|
1236
|
+
let scanning = false;
|
|
1237
|
+
let cursor = 0;
|
|
1238
|
+
const heartbeats = /* @__PURE__ */ new Map();
|
|
1239
|
+
const post = async (body, path = CLAUDE_WAKE_API) => {
|
|
1240
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
1241
|
+
method: "POST",
|
|
1242
|
+
headers: {
|
|
1243
|
+
"Content-Type": "application/json",
|
|
1244
|
+
Authorization: `Bearer ${apiKey}`
|
|
1245
|
+
},
|
|
1246
|
+
body: JSON.stringify(body),
|
|
1247
|
+
signal: AbortSignal.any([
|
|
1248
|
+
abort.signal,
|
|
1249
|
+
AbortSignal.timeout(CLAUDE_INBOX_RECEIVER.requestTimeoutMs)
|
|
1250
|
+
])
|
|
1251
|
+
});
|
|
1252
|
+
if (!response.ok) throw new Error(`Claude wake HTTP ${response.status}`);
|
|
1253
|
+
return response.json();
|
|
1254
|
+
};
|
|
1255
|
+
const report = async (sessionId, waitingOn) => {
|
|
1256
|
+
await post({
|
|
1257
|
+
operation: "report" /* report */,
|
|
1258
|
+
sessionId,
|
|
1259
|
+
machineId,
|
|
1260
|
+
waitingOn
|
|
1261
|
+
});
|
|
1262
|
+
};
|
|
1263
|
+
const tick = async () => {
|
|
1264
|
+
if (scanning || abort.signal.aborted) return;
|
|
1265
|
+
scanning = true;
|
|
1266
|
+
try {
|
|
1267
|
+
await reconcileCommandReceipts(
|
|
1268
|
+
apiKey,
|
|
1269
|
+
machineId,
|
|
1270
|
+
"claude_code",
|
|
1271
|
+
baseUrl,
|
|
1272
|
+
abort.signal
|
|
1273
|
+
);
|
|
1274
|
+
const sessions = client.sessions().filter(options.ownsSession ?? ownsSession).slice(0, CLAUDE_INBOX_MAX_SESSIONS);
|
|
1275
|
+
if (!sessions.length) return;
|
|
1276
|
+
for (const session of sessions) {
|
|
1277
|
+
if (Date.now() - (heartbeats.get(session.sessionId) ?? 0) < CLAUDE_INBOX_RECEIVER.heartbeatMs)
|
|
1278
|
+
continue;
|
|
1279
|
+
await post(
|
|
1280
|
+
{
|
|
1281
|
+
event: "heartbeat",
|
|
1282
|
+
agentType: "claude_code",
|
|
1283
|
+
agentName: `Claude Code - ${basename2(session.cwd)}`,
|
|
1284
|
+
sessionId: session.sessionId,
|
|
1285
|
+
machineId,
|
|
1286
|
+
cwd: session.cwd,
|
|
1287
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1288
|
+
canDrainCommand: false,
|
|
1289
|
+
skipCommandDrain: true
|
|
1290
|
+
},
|
|
1291
|
+
"/api/agent/event"
|
|
1292
|
+
);
|
|
1293
|
+
heartbeats.set(session.sessionId, Date.now());
|
|
1294
|
+
}
|
|
1295
|
+
const raw = await post({
|
|
1296
|
+
operation: "poll" /* poll */,
|
|
1297
|
+
machineId,
|
|
1298
|
+
sessionIds: sessions.map((session) => session.sessionId)
|
|
1299
|
+
});
|
|
1300
|
+
const pending = parseClaudeInboxPoll(raw);
|
|
1301
|
+
if (!pending) throw new Error("Claude wake protocol mismatch");
|
|
1302
|
+
const rotated = [...pending.slice(cursor), ...pending.slice(0, cursor)];
|
|
1303
|
+
const batch = rotated.slice(0, CLAUDE_INBOX_RECEIVER.maxConcurrent);
|
|
1304
|
+
cursor = pending.length ? (cursor + batch.length) % pending.length : 0;
|
|
1305
|
+
await Promise.all(
|
|
1306
|
+
batch.map(async (sessionId) => {
|
|
1307
|
+
if (!sessions.some((session) => session.sessionId === sessionId) || abort.signal.aborted)
|
|
1308
|
+
return;
|
|
1309
|
+
await report(sessionId, "waking" /* waking */);
|
|
1310
|
+
const outcome = await client.wake(sessionId, abort.signal);
|
|
1311
|
+
await report(sessionId, claudeWakeWaitingOn(outcome));
|
|
1312
|
+
})
|
|
1313
|
+
);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
if (!abort.signal.aborted)
|
|
1316
|
+
options.report?.(
|
|
1317
|
+
error instanceof Error ? error.message : "Claude wake failed"
|
|
1318
|
+
);
|
|
1319
|
+
} finally {
|
|
1320
|
+
scanning = false;
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
const timer = setInterval(() => {
|
|
1324
|
+
void tick();
|
|
1325
|
+
}, options.pollMs ?? CLAUDE_INBOX_RECEIVER.pollMs);
|
|
1326
|
+
timer.unref();
|
|
1327
|
+
void tick();
|
|
1328
|
+
return () => {
|
|
1329
|
+
abort.abort();
|
|
1330
|
+
clearInterval(timer);
|
|
1331
|
+
};
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
// src/spawn-daemon.ts
|
|
1335
|
+
import { execFileSync as execFileSync2, spawn } from "child_process";
|
|
1336
|
+
import { existsSync, statSync as statSync2 } from "fs";
|
|
1337
|
+
import { homedir as homedir3 } from "os";
|
|
1338
|
+
import { basename as basename3, dirname as dirname3, join as join5 } from "path";
|
|
113
1339
|
|
|
114
1340
|
// src/stop-requests.ts
|
|
115
1341
|
var STOP_CAPABILITY = "stop";
|
|
@@ -292,13 +1518,13 @@ var resolveSpawnDirectory = (requested, options = {}) => {
|
|
|
292
1518
|
const fallbackDir = options.fallbackDir ?? process.cwd();
|
|
293
1519
|
const value = requested?.trim();
|
|
294
1520
|
if (!value) return { ok: true, cwd: fallbackDir };
|
|
295
|
-
const homeDir = options.homeDir ??
|
|
296
|
-
const cwd = value === "~" ? homeDir : value.startsWith("~/") || value.startsWith("~\\") ?
|
|
1521
|
+
const homeDir = options.homeDir ?? homedir3();
|
|
1522
|
+
const cwd = value === "~" ? homeDir : value.startsWith("~/") || value.startsWith("~\\") ? join5(homeDir, value.slice(2)) : value;
|
|
297
1523
|
if (!(options.exists ?? existsSync)(cwd)) {
|
|
298
1524
|
return { ok: false, message: `Directory does not exist: ${value}` };
|
|
299
1525
|
}
|
|
300
1526
|
try {
|
|
301
|
-
if (!(options.isDirectory ?? ((path) =>
|
|
1527
|
+
if (!(options.isDirectory ?? ((path) => statSync2(path).isDirectory()))(cwd)) {
|
|
302
1528
|
return { ok: false, message: `Not a directory: ${value}` };
|
|
303
1529
|
}
|
|
304
1530
|
} catch {
|
|
@@ -390,9 +1616,9 @@ var runSpawnDaemon = async () => {
|
|
|
390
1616
|
describedProviders = description;
|
|
391
1617
|
});
|
|
392
1618
|
describedProviders = describeLaunchableProviders(inventory.current.capabilities);
|
|
393
|
-
const siblingEntry = process.argv[1] ?
|
|
394
|
-
const codexBridgeEntry = process.argv[1] ?
|
|
395
|
-
const geminiBridgeEntry = process.argv[1] ?
|
|
1619
|
+
const siblingEntry = process.argv[1] ? join5(dirname3(process.argv[1]), "pushary.js") : "";
|
|
1620
|
+
const codexBridgeEntry = process.argv[1] ? join5(dirname3(process.argv[1]), "pushary-codex-bridge.js") : "pushary-codex-bridge";
|
|
1621
|
+
const geminiBridgeEntry = process.argv[1] ? join5(dirname3(process.argv[1]), "pushary-gemini-bridge.js") : "pushary-gemini-bridge";
|
|
396
1622
|
const spawnDirect = siblingEntry.length > 0 && existsSync(siblingEntry);
|
|
397
1623
|
const existing = readDaemonStatus();
|
|
398
1624
|
let handoff;
|
|
@@ -492,7 +1718,7 @@ var runSpawnDaemon = async () => {
|
|
|
492
1718
|
if (!plan) {
|
|
493
1719
|
return Promise.reject(new Error(FLAG_SHAPED_PROMPT_MESSAGE));
|
|
494
1720
|
}
|
|
495
|
-
return new Promise((
|
|
1721
|
+
return new Promise((resolve3, reject) => {
|
|
496
1722
|
try {
|
|
497
1723
|
const child = plan.direct ? spawn(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env }) : spawnClaude(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env });
|
|
498
1724
|
let started = false;
|
|
@@ -521,8 +1747,8 @@ var runSpawnDaemon = async () => {
|
|
|
521
1747
|
});
|
|
522
1748
|
}
|
|
523
1749
|
child.unref();
|
|
524
|
-
log(`spawn ${req.id}: launched a phone-requested ${plan.agent} session in ${
|
|
525
|
-
|
|
1750
|
+
log(`spawn ${req.id}: launched a phone-requested ${plan.agent} session in ${basename3(cwd)} (pid ${child.pid ?? "unknown"})`);
|
|
1751
|
+
resolve3(typeof child.pid === "number" ? child.pid : null);
|
|
526
1752
|
});
|
|
527
1753
|
} catch (err) {
|
|
528
1754
|
reject(err);
|
|
@@ -530,7 +1756,7 @@ var runSpawnDaemon = async () => {
|
|
|
530
1756
|
});
|
|
531
1757
|
};
|
|
532
1758
|
const kill = process.platform === "win32" ? (pid) => {
|
|
533
|
-
|
|
1759
|
+
execFileSync2("taskkill", windowsTaskkillArgs(pid), { stdio: "ignore" });
|
|
534
1760
|
} : process.kill.bind(process);
|
|
535
1761
|
const stopDeps = {
|
|
536
1762
|
kill: (pid, signal) => {
|
|
@@ -542,7 +1768,7 @@ var runSpawnDaemon = async () => {
|
|
|
542
1768
|
return launched || ownsLiveProcess(request);
|
|
543
1769
|
},
|
|
544
1770
|
selfPid: process.pid,
|
|
545
|
-
sleep: (ms) => new Promise((
|
|
1771
|
+
sleep: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms)),
|
|
546
1772
|
isAlive: (pid) => {
|
|
547
1773
|
try {
|
|
548
1774
|
process.kill(pid, 0);
|
|
@@ -600,7 +1826,7 @@ var runSpawnDaemon = async () => {
|
|
|
600
1826
|
completedStops.delete(acknowledgement.stopId);
|
|
601
1827
|
break;
|
|
602
1828
|
}
|
|
603
|
-
if (attempt < 2) await new Promise((
|
|
1829
|
+
if (attempt < 2) await new Promise((resolve3) => setTimeout(resolve3, 200 * (attempt + 1)));
|
|
604
1830
|
}
|
|
605
1831
|
}
|
|
606
1832
|
async function apply() {
|
|
@@ -743,8 +1969,12 @@ var runSpawnDaemon = async () => {
|
|
|
743
1969
|
if (!stopped) timer = setTimeout(tick, jitter(nextDelay()));
|
|
744
1970
|
};
|
|
745
1971
|
const stopTranscripts = startRegisteredTranscripts(apiKey, machineId);
|
|
1972
|
+
const stopNativeHosts = startNativeHostReceivers(apiKey, machineId, log);
|
|
1973
|
+
const stopClaudeInbox = startClaudeInboxReceiver(apiKey, machineId, { report: log });
|
|
746
1974
|
const teardown = () => {
|
|
747
1975
|
stopTranscripts();
|
|
1976
|
+
stopClaudeInbox();
|
|
1977
|
+
stopNativeHosts();
|
|
748
1978
|
if (stopped) return;
|
|
749
1979
|
stopped = true;
|
|
750
1980
|
if (timer) clearTimeout(timer);
|
|
@@ -767,7 +1997,7 @@ var runSpawnDaemon = async () => {
|
|
|
767
1997
|
return;
|
|
768
1998
|
}
|
|
769
1999
|
const result = await sendHeartbeat(apiKey, {
|
|
770
|
-
repoKey:
|
|
2000
|
+
repoKey: basename3(process.cwd()),
|
|
771
2001
|
capabilities: [
|
|
772
2002
|
...launchAdvertisement(inventory.current.capabilities),
|
|
773
2003
|
STOP_CAPABILITY,
|
|
@@ -799,24 +2029,24 @@ var runSpawnDaemon = async () => {
|
|
|
799
2029
|
log(hasSpawnProvider(inventory.current.capabilities) ? `daemon online (pid ${process.pid}): this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.` : `daemon online (pid ${process.pid}): no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.`);
|
|
800
2030
|
timer = setTimeout(tick, 0);
|
|
801
2031
|
const signals = daemonSignals();
|
|
802
|
-
await new Promise((
|
|
2032
|
+
await new Promise((resolve3) => {
|
|
803
2033
|
const onSignal = () => finish();
|
|
804
2034
|
for (const signal of signals) process.on(signal, onSignal);
|
|
805
2035
|
finish = () => {
|
|
806
2036
|
teardown();
|
|
807
2037
|
for (const signal of signals) process.off(signal, onSignal);
|
|
808
|
-
|
|
2038
|
+
resolve3();
|
|
809
2039
|
};
|
|
810
2040
|
});
|
|
811
2041
|
return 0;
|
|
812
2042
|
};
|
|
813
2043
|
|
|
814
2044
|
// src/daemon-sessions.ts
|
|
815
|
-
import { execFileSync as
|
|
2045
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
816
2046
|
var readResidentSetKb = (pid) => {
|
|
817
2047
|
if (process.platform === "win32") return null;
|
|
818
2048
|
try {
|
|
819
|
-
const output =
|
|
2049
|
+
const output = execFileSync3("ps", ["-o", "rss=", "-p", String(pid)], {
|
|
820
2050
|
encoding: "utf8",
|
|
821
2051
|
stdio: ["ignore", "pipe", "ignore"]
|
|
822
2052
|
});
|