pushary 1.8.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +1943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/data/SKILL.md +591 -0
  5. package/data/cowork/SKILL.md +77 -0
  6. package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
  7. package/data/cursor-plugin/CHANGELOG.md +70 -0
  8. package/data/cursor-plugin/CONTRIBUTING.md +40 -0
  9. package/data/cursor-plugin/LICENSE +21 -0
  10. package/data/cursor-plugin/README.md +113 -0
  11. package/data/cursor-plugin/SECURITY.md +33 -0
  12. package/data/cursor-plugin/assets/logo.png +0 -0
  13. package/data/cursor-plugin/commands/notify-when-done.md +14 -0
  14. package/data/cursor-plugin/commands/pushary-test.md +13 -0
  15. package/data/cursor-plugin/hooks/hooks.json +75 -0
  16. package/data/cursor-plugin/mcp.json +11 -0
  17. package/data/cursor-plugin/rules/pushary.mdc +40 -0
  18. package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
  19. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
  20. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  21. package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
  22. package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
  23. package/data/vscode-plugin/.mcp.json +11 -0
  24. package/data/vscode-plugin/CHANGELOG.md +49 -0
  25. package/data/vscode-plugin/CONTRIBUTING.md +40 -0
  26. package/data/vscode-plugin/LICENSE +21 -0
  27. package/data/vscode-plugin/README.md +128 -0
  28. package/data/vscode-plugin/SECURITY.md +56 -0
  29. package/data/vscode-plugin/assets/logo.png +0 -0
  30. package/data/vscode-plugin/commands/notify-when-done.md +14 -0
  31. package/data/vscode-plugin/commands/pushary-test.md +13 -0
  32. package/data/vscode-plugin/hooks/hooks.json +60 -0
  33. package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
  34. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  35. package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
  36. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  37. package/dist/bin/pushary-bell-hook.js +46 -0
  38. package/dist/bin/pushary-bell.d.ts +1 -0
  39. package/dist/bin/pushary-bell.js +146 -0
  40. package/dist/bin/pushary-claude.d.ts +1 -0
  41. package/dist/bin/pushary-claude.js +1916 -0
  42. package/dist/bin/pushary-clean.d.ts +1 -0
  43. package/dist/bin/pushary-clean.js +1300 -0
  44. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  45. package/dist/bin/pushary-codex-bridge.js +599 -0
  46. package/dist/bin/pushary-codex-hook.d.ts +1 -0
  47. package/dist/bin/pushary-codex-hook.js +886 -0
  48. package/dist/bin/pushary-codex.d.ts +1 -0
  49. package/dist/bin/pushary-codex.js +125 -0
  50. package/dist/bin/pushary-connect.d.ts +1 -0
  51. package/dist/bin/pushary-connect.js +100 -0
  52. package/dist/bin/pushary-cowork.d.ts +1 -0
  53. package/dist/bin/pushary-cowork.js +36 -0
  54. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  55. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  56. package/dist/bin/pushary-daemon.d.ts +1 -0
  57. package/dist/bin/pushary-daemon.js +878 -0
  58. package/dist/bin/pushary-disconnect.d.ts +1 -0
  59. package/dist/bin/pushary-disconnect.js +264 -0
  60. package/dist/bin/pushary-doctor.d.ts +1 -0
  61. package/dist/bin/pushary-doctor.js +1433 -0
  62. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  63. package/dist/bin/pushary-elicitation-hook.js +149 -0
  64. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  65. package/dist/bin/pushary-gemini-bridge.js +339 -0
  66. package/dist/bin/pushary-gemini-hook.d.ts +1 -0
  67. package/dist/bin/pushary-gemini-hook.js +381 -0
  68. package/dist/bin/pushary-hook.d.ts +1 -0
  69. package/dist/bin/pushary-hook.js +95 -0
  70. package/dist/bin/pushary-login.d.ts +1 -0
  71. package/dist/bin/pushary-login.js +198 -0
  72. package/dist/bin/pushary-logout.d.ts +1 -0
  73. package/dist/bin/pushary-logout.js +147 -0
  74. package/dist/bin/pushary-mcp.d.ts +1 -0
  75. package/dist/bin/pushary-mcp.js +114 -0
  76. package/dist/bin/pushary-mode.d.ts +1 -0
  77. package/dist/bin/pushary-mode.js +130 -0
  78. package/dist/bin/pushary-notification-hook.d.ts +1 -0
  79. package/dist/bin/pushary-notification-hook.js +56 -0
  80. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  81. package/dist/bin/pushary-opencode-hook.js +338 -0
  82. package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
  83. package/dist/bin/pushary-permission-denied-hook.js +60 -0
  84. package/dist/bin/pushary-permission-hook.d.ts +1 -0
  85. package/dist/bin/pushary-permission-hook.js +65 -0
  86. package/dist/bin/pushary-post-hook.d.ts +1 -0
  87. package/dist/bin/pushary-post-hook.js +70 -0
  88. package/dist/bin/pushary-prompt-hook.d.ts +1 -0
  89. package/dist/bin/pushary-prompt-hook.js +61 -0
  90. package/dist/bin/pushary-session-end-hook.d.ts +1 -0
  91. package/dist/bin/pushary-session-end-hook.js +59 -0
  92. package/dist/bin/pushary-session-start-hook.d.ts +1 -0
  93. package/dist/bin/pushary-session-start-hook.js +72 -0
  94. package/dist/bin/pushary-setup.d.ts +1 -0
  95. package/dist/bin/pushary-setup.js +2660 -0
  96. package/dist/bin/pushary-stats.d.ts +1 -0
  97. package/dist/bin/pushary-stats.js +44 -0
  98. package/dist/bin/pushary-status.d.ts +1 -0
  99. package/dist/bin/pushary-status.js +262 -0
  100. package/dist/bin/pushary-stop-hook.d.ts +1 -0
  101. package/dist/bin/pushary-stop-hook.js +63 -0
  102. package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
  103. package/dist/bin/pushary-stopfailure-hook.js +56 -0
  104. package/dist/bin/pushary-suggestions.d.ts +1 -0
  105. package/dist/bin/pushary-suggestions.js +100 -0
  106. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  107. package/dist/bin/pushary-transcript-register.js +40 -0
  108. package/dist/bin/pushary-transcripts.d.ts +1 -0
  109. package/dist/bin/pushary-transcripts.js +77 -0
  110. package/dist/bin/pushary-upgrade.d.ts +1 -0
  111. package/dist/bin/pushary-upgrade.js +599 -0
  112. package/dist/bin/pushary-wait.d.ts +1 -0
  113. package/dist/bin/pushary-wait.js +122 -0
  114. package/dist/bin/pushary.d.ts +1 -0
  115. package/dist/bin/pushary.js +74 -0
  116. package/dist/chunk-2ABTSGFT.js +173 -0
  117. package/dist/chunk-2OY3ZZ5N.js +124 -0
  118. package/dist/chunk-3EGEA4KH.js +44 -0
  119. package/dist/chunk-3EVPNIBE.js +185 -0
  120. package/dist/chunk-5OP5MQI7.js +45 -0
  121. package/dist/chunk-6J2JC2RT.js +149 -0
  122. package/dist/chunk-7QLSKOSU.js +19 -0
  123. package/dist/chunk-7Z7Q4GGS.js +88 -0
  124. package/dist/chunk-A2A2YTMG.js +835 -0
  125. package/dist/chunk-AHV4LNB5.js +377 -0
  126. package/dist/chunk-AXPCESYO.js +197 -0
  127. package/dist/chunk-C2WCPBF7.js +86 -0
  128. package/dist/chunk-CJVVKOSR.js +73 -0
  129. package/dist/chunk-CULUZTWQ.js +2117 -0
  130. package/dist/chunk-CVQYGMSR.js +142 -0
  131. package/dist/chunk-CVWG2N3Y.js +16 -0
  132. package/dist/chunk-D2FX3EPW.js +15 -0
  133. package/dist/chunk-DB3ONZB4.js +44 -0
  134. package/dist/chunk-DE4Y6TCC.js +160 -0
  135. package/dist/chunk-E4FJTUH4.js +249 -0
  136. package/dist/chunk-EPCXUP3P.js +41 -0
  137. package/dist/chunk-EX3GAPA4.js +44 -0
  138. package/dist/chunk-EYK3GSRH.js +143 -0
  139. package/dist/chunk-GMXKITVA.js +70 -0
  140. package/dist/chunk-GPEGOGRJ.js +49 -0
  141. package/dist/chunk-GU3EJCVV.js +43 -0
  142. package/dist/chunk-HQ6G3B5R.js +59 -0
  143. package/dist/chunk-HZVVNGEW.js +22 -0
  144. package/dist/chunk-IPVKH2ST.js +231 -0
  145. package/dist/chunk-J5TIV3J6.js +238 -0
  146. package/dist/chunk-JU6W3XAU.js +64 -0
  147. package/dist/chunk-JYG4B33G.js +227 -0
  148. package/dist/chunk-JZTHGAPA.js +503 -0
  149. package/dist/chunk-KGDWDSNL.js +102 -0
  150. package/dist/chunk-KVQBWI5T.js +99 -0
  151. package/dist/chunk-KZERVKTD.js +16 -0
  152. package/dist/chunk-LXQ5FBBD.js +47 -0
  153. package/dist/chunk-M5ICYGHT.js +138 -0
  154. package/dist/chunk-M5JDZ45W.js +943 -0
  155. package/dist/chunk-MDEPU45E.js +1226 -0
  156. package/dist/chunk-MPWXGONV.js +157 -0
  157. package/dist/chunk-N6IOSZOE.js +185 -0
  158. package/dist/chunk-NGDD6TXY.js +192 -0
  159. package/dist/chunk-NQTHFF4W.js +2830 -0
  160. package/dist/chunk-OKXS7WDZ.js +68 -0
  161. package/dist/chunk-OLQRRFBX.js +21 -0
  162. package/dist/chunk-P7WQEVWA.js +307 -0
  163. package/dist/chunk-POFBE6ZY.js +12 -0
  164. package/dist/chunk-PQ4K74WL.js +30 -0
  165. package/dist/chunk-PUZZELE6.js +37 -0
  166. package/dist/chunk-Q7E4I6UA.js +767 -0
  167. package/dist/chunk-QCGNGEGE.js +15 -0
  168. package/dist/chunk-R7437YI3.js +151 -0
  169. package/dist/chunk-RHR6NARM.js +305 -0
  170. package/dist/chunk-SAXBC4AZ.js +60 -0
  171. package/dist/chunk-SGZJ5LYH.js +76 -0
  172. package/dist/chunk-SIFVFP6T.js +70 -0
  173. package/dist/chunk-SLFABHW3.js +150 -0
  174. package/dist/chunk-T7E75EKA.js +67 -0
  175. package/dist/chunk-TY4JT7W3.js +38 -0
  176. package/dist/chunk-U4TMAANR.js +111 -0
  177. package/dist/chunk-UI36QSBI.js +177 -0
  178. package/dist/chunk-VQKRG3AC.js +339 -0
  179. package/dist/chunk-WE2J62NC.js +1131 -0
  180. package/dist/chunk-XHKBHWLX.js +14 -0
  181. package/dist/chunk-ZSZSSJCV.js +275 -0
  182. package/dist/chunk-ZVSKMPBZ.js +135 -0
  183. package/dist/reapply-CW3MA66V.js +34 -0
  184. package/dist/src/index.d.ts +204 -0
  185. package/dist/src/index.js +64 -0
  186. package/package.json +108 -0
@@ -0,0 +1,1916 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ WRAPPER_ACTIVE_ENV,
4
+ findClaudeBinary
5
+ } from "../chunk-PQ4K74WL.js";
6
+ import {
7
+ SESSION_IDLE_TIMEOUT_MS,
8
+ describeIdleWindow,
9
+ startProcessOwnership,
10
+ withSpawnProcessOwnership
11
+ } from "../chunk-ZSZSSJCV.js";
12
+ import {
13
+ announceSubscriptionRequired,
14
+ isDeferAnswer
15
+ } from "../chunk-5OP5MQI7.js";
16
+ import {
17
+ claudeTranscriptPath,
18
+ loadSessionKey,
19
+ registerTranscript,
20
+ registeredTranscriptEnabled
21
+ } from "../chunk-JZTHGAPA.js";
22
+ import "../chunk-SGZJ5LYH.js";
23
+ import "../chunk-EYK3GSRH.js";
24
+ import "../chunk-ZVSKMPBZ.js";
25
+ import "../chunk-C2WCPBF7.js";
26
+ import "../chunk-6J2JC2RT.js";
27
+ import {
28
+ ensureDaemonRunning
29
+ } from "../chunk-WE2J62NC.js";
30
+ import "../chunk-3EVPNIBE.js";
31
+ import "../chunk-OKXS7WDZ.js";
32
+ import {
33
+ encryptRecord
34
+ } from "../chunk-MPWXGONV.js";
35
+ import "../chunk-SAXBC4AZ.js";
36
+ import {
37
+ transcriptsEnabled
38
+ } from "../chunk-OLQRRFBX.js";
39
+ import {
40
+ needsShell,
41
+ spawnClaude
42
+ } from "../chunk-XHKBHWLX.js";
43
+ import {
44
+ UNNAMED_ACTION,
45
+ deriveToolTarget,
46
+ describeToolCall,
47
+ fetchModeState,
48
+ getPolicy,
49
+ phoneDrivenEnv,
50
+ repoKeyFor,
51
+ reportEvent,
52
+ scopePathFor
53
+ } from "../chunk-CULUZTWQ.js";
54
+ import {
55
+ DEFAULT_SESSION,
56
+ markPendingCommandDelivered,
57
+ markPendingCommandFailed,
58
+ readPendingCommand,
59
+ readPendingCommandById,
60
+ removePendingCommand,
61
+ savePendingCommand
62
+ } from "../chunk-N6IOSZOE.js";
63
+ import "../chunk-EPCXUP3P.js";
64
+ import {
65
+ DECISION_STOP_REASON,
66
+ UNVERIFIABLE_APPROVAL_REASON,
67
+ askUser,
68
+ cancelQuestion,
69
+ isDecisionStopped,
70
+ isLocalQuestionHandoff,
71
+ nextLongPollMs,
72
+ waitForAnswer,
73
+ withdrawQuestion
74
+ } from "../chunk-NGDD6TXY.js";
75
+ import "../chunk-CVQYGMSR.js";
76
+ import "../chunk-POFBE6ZY.js";
77
+ import "../chunk-GMXKITVA.js";
78
+ import "../chunk-P7WQEVWA.js";
79
+ import {
80
+ SubscriptionRequiredError
81
+ } from "../chunk-M5ICYGHT.js";
82
+ import "../chunk-2ABTSGFT.js";
83
+ import "../chunk-2OY3ZZ5N.js";
84
+ import {
85
+ getMachineId
86
+ } from "../chunk-LXQ5FBBD.js";
87
+ import "../chunk-7QLSKOSU.js";
88
+ import "../chunk-KZERVKTD.js";
89
+ import "../chunk-DB3ONZB4.js";
90
+ import "../chunk-A2A2YTMG.js";
91
+ import "../chunk-T7E75EKA.js";
92
+ import {
93
+ getApiKey,
94
+ getBaseUrl
95
+ } from "../chunk-GPEGOGRJ.js";
96
+ import {
97
+ NPX_LATEST_COMMAND
98
+ } from "../chunk-E4FJTUH4.js";
99
+ import {
100
+ KILL_REASON,
101
+ redactSecretsFromContent,
102
+ resolveGate
103
+ } from "../chunk-NQTHFF4W.js";
104
+
105
+ // src/wrapper/localPassthrough.ts
106
+ var SIGNAL_NUMBERS = {
107
+ SIGHUP: 1,
108
+ SIGINT: 2,
109
+ SIGQUIT: 3,
110
+ SIGKILL: 9,
111
+ SIGTERM: 15
112
+ };
113
+ var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
114
+ var runLocalPassthrough = (binary, args2) => {
115
+ return new Promise((resolve) => {
116
+ let child;
117
+ try {
118
+ child = spawnClaude(binary, args2, {
119
+ stdio: "inherit",
120
+ env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" }
121
+ });
122
+ } catch {
123
+ resolve(127);
124
+ return;
125
+ }
126
+ const forward = (signal) => {
127
+ try {
128
+ child.kill(signal);
129
+ } catch {
130
+ }
131
+ };
132
+ for (const signal of FORWARDED_SIGNALS) process.on(signal, forward);
133
+ const cleanup = () => {
134
+ for (const signal of FORWARDED_SIGNALS) process.off(signal, forward);
135
+ };
136
+ child.on("error", () => {
137
+ cleanup();
138
+ resolve(127);
139
+ });
140
+ child.on("exit", (code2, signal) => {
141
+ cleanup();
142
+ if (typeof code2 === "number") resolve(code2);
143
+ else if (signal) resolve(128 + (SIGNAL_NUMBERS[signal] ?? 0));
144
+ else resolve(0);
145
+ });
146
+ });
147
+ };
148
+
149
+ // src/wrapper/dualMode.ts
150
+ import { existsSync as existsSync2 } from "fs";
151
+
152
+ // src/transcript-draft.ts
153
+ var redactDraft = (text, complete = false) => {
154
+ let end = complete ? text.length : text.lastIndexOf("\n") + 1;
155
+ const pem = /-----BEGIN [^\n]*PRIVATE KEY-----/g;
156
+ for (const match of text.matchAll(pem)) {
157
+ if (!/-----END [^\n]*PRIVATE KEY-----/.test(text.slice(match.index, end))) end = Math.min(end, match.index);
158
+ }
159
+ const quoted = /(?:secret|token|password|passwd|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)\s*[=:]\s*(["'])/gi;
160
+ for (const match of text.matchAll(quoted)) {
161
+ const close = text.indexOf(match[1], match.index + match[0].length);
162
+ if (close < 0 || close >= end) end = Math.min(end, match.index);
163
+ }
164
+ return redactSecretsFromContent(text.slice(0, end));
165
+ };
166
+ var DraftText = class {
167
+ messageId = "";
168
+ text = "";
169
+ complete = false;
170
+ interrupted = false;
171
+ add(message) {
172
+ if (message.parent_tool_use_id) return;
173
+ const event = message.event;
174
+ if (message.type === "stream_event" && event?.type === "message_start") {
175
+ this.messageId = event.message?.id ?? "";
176
+ this.text = "";
177
+ this.complete = false;
178
+ this.interrupted = false;
179
+ } else if (message.type === "stream_event" && event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
180
+ this.text = (this.text + (event.delta.text ?? "")).slice(0, 32 * 1024);
181
+ } else if (message.type === "assistant") {
182
+ const body = message.message;
183
+ if (body?.id === this.messageId && Array.isArray(body.content)) {
184
+ this.text = body.content.filter((block) => block.type === "text").map((block) => block.text).join("").slice(0, 32 * 1024);
185
+ this.complete = true;
186
+ }
187
+ }
188
+ }
189
+ interrupt() {
190
+ if (!this.complete) this.interrupted = true;
191
+ }
192
+ snapshot() {
193
+ const text = redactDraft(this.text, this.complete);
194
+ return this.messageId && text ? { messageId: this.messageId, text, status: this.interrupted ? "interrupted" : this.complete ? "complete" : "streaming" } : null;
195
+ }
196
+ };
197
+ var startTranscriptDraft = (apiKey, getSessionId) => {
198
+ const text = new DraftText();
199
+ const startedAt = Date.now();
200
+ let revision = 0;
201
+ let stopped = false;
202
+ let pending;
203
+ let ending = false;
204
+ let sent = "";
205
+ const flush = async () => {
206
+ if (stopped || pending || !transcriptsEnabled()) return;
207
+ const sessionId = getSessionId();
208
+ const snapshot = text.snapshot();
209
+ if (!sessionId || !snapshot || !registeredTranscriptEnabled(apiKey, sessionId)) return;
210
+ const raw = JSON.stringify(snapshot);
211
+ if (raw === sent) return;
212
+ const key = loadSessionKey(sessionId);
213
+ if (!key?.keyUploaded) return;
214
+ pending = (async () => {
215
+ try {
216
+ const response = await fetch(`${getBaseUrl()}/api/agent/transcript`, {
217
+ method: "POST",
218
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
219
+ body: JSON.stringify({ sessionId, draft: { startedAt, revision: ++revision, content: encryptRecord(raw, key.sessionKey) } }),
220
+ signal: AbortSignal.timeout(5e3)
221
+ });
222
+ if (response.ok) sent = raw;
223
+ } catch {
224
+ }
225
+ })();
226
+ try {
227
+ await pending;
228
+ } finally {
229
+ pending = void 0;
230
+ }
231
+ };
232
+ const timer = setInterval(() => void flush(), 800);
233
+ timer.unref();
234
+ return {
235
+ add: (message) => {
236
+ if (!ending) text.add(message);
237
+ },
238
+ stop: async () => {
239
+ if (ending) return;
240
+ ending = true;
241
+ clearInterval(timer);
242
+ text.interrupt();
243
+ await pending;
244
+ await flush();
245
+ stopped = true;
246
+ }
247
+ };
248
+ };
249
+
250
+ // src/wrapper/dualMode.ts
251
+ import { basename } from "path";
252
+
253
+ // src/wrapper/sdkLoader.ts
254
+ import { spawn } from "child_process";
255
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
256
+ import { homedir } from "os";
257
+ import { join } from "path";
258
+ import { pathToFileURL } from "url";
259
+ var userMessage = (text) => ({
260
+ type: "user",
261
+ message: { role: "user", content: text },
262
+ parent_tool_use_id: null
263
+ });
264
+ var SDK_VERSION = "0.3.207";
265
+ var SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
266
+ var SDK_SPEC = `${SDK_PACKAGE}@${SDK_VERSION}`;
267
+ var sdkCacheDir = () => process.env.PUSHARY_REMOTE_SDK_DIR?.trim() || join(homedir(), ".pushary", "remote-sdk");
268
+ var cachedSdkEntry = () => join(sdkCacheDir(), "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs");
269
+ var importSdk = async (specifierOrUrl) => {
270
+ try {
271
+ const mod = await import(specifierOrUrl);
272
+ if (typeof mod.query === "function") return mod;
273
+ if (mod.default && typeof mod.default.query === "function") return mod.default;
274
+ return null;
275
+ } catch {
276
+ return null;
277
+ }
278
+ };
279
+ var loadClaudeSdk = async () => {
280
+ const fromResolution = await importSdk(SDK_PACKAGE);
281
+ if (fromResolution) return fromResolution;
282
+ const entry = cachedSdkEntry();
283
+ if (existsSync(entry)) return importSdk(pathToFileURL(entry).href);
284
+ return null;
285
+ };
286
+ var ensureClaudeSdk = async (log) => {
287
+ const present = await loadClaudeSdk();
288
+ if (present) return present;
289
+ const dir = sdkCacheDir();
290
+ try {
291
+ mkdirSync(dir, { recursive: true });
292
+ const pkgJson = join(dir, "package.json");
293
+ if (!existsSync(pkgJson)) {
294
+ writeFileSync(pkgJson, `${JSON.stringify({ name: "pushary-remote-sdk", private: true })}
295
+ `);
296
+ }
297
+ } catch {
298
+ return null;
299
+ }
300
+ log?.("[pushary] setting up phone control (one-time, fetching the Claude Agent SDK)...");
301
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
302
+ const installed = await new Promise((resolve) => {
303
+ const child = spawn(
304
+ npmCmd,
305
+ ["install", "--no-optional", "--no-audit", "--no-fund", "--loglevel=error", SDK_SPEC],
306
+ { cwd: dir, stdio: "ignore", shell: process.platform === "win32" }
307
+ );
308
+ child.on("error", () => resolve(false));
309
+ child.on("close", (code2) => resolve(code2 === 0));
310
+ });
311
+ if (!installed) {
312
+ log?.(`[pushary] could not auto-install the SDK. Install it manually: npm i -g ${SDK_SPEC}`);
313
+ return null;
314
+ }
315
+ return loadClaudeSdk();
316
+ };
317
+
318
+ // src/wrapper/messageQueue.ts
319
+ var BoundedInputQueue = class {
320
+ buffer = [];
321
+ waiter;
322
+ closed = false;
323
+ generation = 0;
324
+ cap;
325
+ onDrop;
326
+ coalesce;
327
+ pushedCount = 0;
328
+ deliveredCount = 0;
329
+ droppedCount = 0;
330
+ constructor(options = {}) {
331
+ this.cap = Math.max(1, options.cap ?? 32);
332
+ this.onDrop = options.onDrop;
333
+ this.coalesce = options.coalesce;
334
+ }
335
+ push(item) {
336
+ if (this.closed) return false;
337
+ this.pushedCount++;
338
+ if (this.waiter) {
339
+ const resolve = this.waiter;
340
+ this.waiter = void 0;
341
+ this.deliveredCount++;
342
+ resolve({ value: item, done: false });
343
+ return true;
344
+ }
345
+ if (this.buffer.length >= this.cap) {
346
+ this.droppedCount++;
347
+ this.onDrop?.(item, this.droppedCount);
348
+ return false;
349
+ }
350
+ this.buffer.push(item);
351
+ return true;
352
+ }
353
+ // Mark done. Any parked consumer completes; further push() is ignored. The
354
+ // buffer is cleared so nothing is retained after shutdown.
355
+ close() {
356
+ if (this.closed) return;
357
+ this.closed = true;
358
+ this.buffer.length = 0;
359
+ if (this.waiter) {
360
+ const resolve = this.waiter;
361
+ this.waiter = void 0;
362
+ resolve({ value: void 0, done: true });
363
+ }
364
+ }
365
+ // End the CURRENT consumer without closing the queue, so a fresh query can
366
+ // attach after a restart and keep draining the same buffered items. Resolving
367
+ // the parked waiter as done lets the abandoned query's for-await unwind cleanly
368
+ // instead of hanging on a promise that would otherwise never settle.
369
+ detachConsumer() {
370
+ this.generation++;
371
+ if (this.waiter) {
372
+ const resolve = this.waiter;
373
+ this.waiter = void 0;
374
+ resolve({ value: void 0, done: true });
375
+ }
376
+ }
377
+ // Drop every buffered item without closing the queue. Used when phone control
378
+ // turns out to be unavailable (offline SDK setup) so a pending instruction does
379
+ // not make the local leg switch again into a mode that cannot run. Returns how
380
+ // many were dropped, because these are somebody's words and the caller has to
381
+ // be able to say they are gone.
382
+ clear() {
383
+ const dropped = this.buffer.length;
384
+ this.buffer.length = 0;
385
+ return dropped;
386
+ }
387
+ get isClosed() {
388
+ return this.closed;
389
+ }
390
+ get bufferedCount() {
391
+ return this.buffer.length;
392
+ }
393
+ get stats() {
394
+ return { pushed: this.pushedCount, delivered: this.deliveredCount, dropped: this.droppedCount };
395
+ }
396
+ [Symbol.asyncIterator]() {
397
+ const generation = this.generation;
398
+ return {
399
+ next: () => {
400
+ if (generation !== this.generation || this.closed) {
401
+ return Promise.resolve({ value: void 0, done: true });
402
+ }
403
+ if (this.buffer.length > 0) {
404
+ if (this.coalesce && this.buffer.length > 1) {
405
+ const items = this.buffer.splice(0, this.buffer.length);
406
+ this.deliveredCount += items.length;
407
+ return Promise.resolve({ value: this.coalesce(items), done: false });
408
+ }
409
+ const value = this.buffer.shift();
410
+ this.deliveredCount++;
411
+ return Promise.resolve({ value, done: false });
412
+ }
413
+ if (this.closed) {
414
+ return Promise.resolve({ value: void 0, done: true });
415
+ }
416
+ if (this.waiter) {
417
+ return Promise.reject(
418
+ new Error("BoundedInputQueue: concurrent consumers are not supported")
419
+ );
420
+ }
421
+ return new Promise((resolve) => {
422
+ this.waiter = resolve;
423
+ });
424
+ },
425
+ return: () => {
426
+ if (generation === this.generation) this.detachConsumer();
427
+ return Promise.resolve({ value: void 0, done: true });
428
+ }
429
+ };
430
+ }
431
+ };
432
+
433
+ // src/wrapper/approver.ts
434
+ var DEFAULT_MAX_CONCURRENT = 16;
435
+ var NO_LOCAL_APPROVAL_UI = "This remote session has no local approval UI. Continue in an interactive session or choose a mode that permits remote approval.";
436
+ var SUBSCRIPTION_REQUIRED_NO_LOCAL_APPROVAL_UI = "Your Pushary workspace has no active subscription, so approvals cannot reach your phone, and this remote session has no local approval UI. Continue in an interactive session.";
437
+ var DEFAULT_APPROVAL_WINDOW_MS = 5 * 6e4;
438
+ var MAX_APPROVAL_WINDOW_MS = 30 * 6e4;
439
+ var allow = (input) => ({
440
+ behavior: "allow",
441
+ updatedInput: input
442
+ });
443
+ var deny = (message) => ({ behavior: "deny", message });
444
+ var createRemoteApprover = (deps) => {
445
+ const fetchModeState2 = deps.fetchModeState ?? fetchModeState;
446
+ const getPolicy2 = deps.getPolicy ?? getPolicy;
447
+ const askUser2 = deps.askUser ?? askUser;
448
+ const waitForAnswer2 = deps.waitForAnswer ?? waitForAnswer;
449
+ const cancelQuestion2 = deps.cancelQuestion ?? cancelQuestion;
450
+ const now = deps.now ?? (() => Date.now());
451
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
452
+ const maxConcurrent = Math.max(1, deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT);
453
+ const pending = /* @__PURE__ */ new Set();
454
+ let tornDown = false;
455
+ const windowFor = (policy) => {
456
+ const base = policy.timeoutSeconds > 0 ? policy.timeoutSeconds * 1e3 : deps.approvalWindowMs ?? DEFAULT_APPROVAL_WINDOW_MS;
457
+ const ceiling = policy.timeoutAction === "wait" ? MAX_APPROVAL_WINDOW_MS : base;
458
+ return Math.min(Math.max(base, 15e3), Math.max(ceiling, 15e3));
459
+ };
460
+ const applyTimeout = (policy, input) => policy.timeoutAction === "approve" ? allow(input) : deny("No approval received in time");
461
+ const waitForPhone = async (entry, correlationId, windowMs) => {
462
+ const deadline = now() + windowMs;
463
+ while (now() < deadline && !entry.controller.signal.aborted) {
464
+ const hold = nextLongPollMs(deadline, now());
465
+ if (hold === null) {
466
+ await sleep(Math.min(500, Math.max(0, deadline - now())));
467
+ continue;
468
+ }
469
+ const abortPromise = new Promise((resolve) => {
470
+ entry.controller.signal.addEventListener("abort", () => resolve({ answered: false, aborted: true }), {
471
+ once: true
472
+ });
473
+ });
474
+ let outcome;
475
+ try {
476
+ outcome = await Promise.race([
477
+ waitForAnswer2(deps.apiKey, correlationId, hold, AbortSignal.any([entry.controller.signal, AbortSignal.timeout(Math.max(1, deadline - now()))])),
478
+ abortPromise
479
+ ]);
480
+ } catch {
481
+ return { answered: false, status: "unavailable" };
482
+ }
483
+ if ("aborted" in outcome) return { answered: false, status: "cancelled" };
484
+ if (isDecisionStopped(outcome) || outcome.answered) return outcome;
485
+ await sleep(Math.min(500, Math.max(0, deadline - now())));
486
+ }
487
+ return { answered: false };
488
+ };
489
+ const canUseTool = async (toolName, input, options) => {
490
+ if (toolName.startsWith("mcp__pushary__")) return allow(input);
491
+ if (tornDown) return deny("Remote session is shutting down");
492
+ if (pending.size >= maxConcurrent) {
493
+ deps.log?.(`[pushary] too many concurrent approvals (${pending.size}); denying ${toolName}`);
494
+ return deny("Too many pending approvals; try again");
495
+ }
496
+ const controller = new AbortController();
497
+ const entry = { controller };
498
+ let answered = false;
499
+ pending.add(entry);
500
+ const outerAbort = () => controller.abort();
501
+ if (options?.signal) {
502
+ if (options.signal.aborted) controller.abort();
503
+ else options.signal.addEventListener("abort", outerAbort, { once: true });
504
+ }
505
+ try {
506
+ const sessionId = deps.getSessionId?.();
507
+ const modeState = await fetchModeState2(deps.apiKey, sessionId);
508
+ const policyConfig = await getPolicy2(deps.apiKey, modeState.policyVersion, "claude_code", true, sessionId);
509
+ const scopePath = modeState.scope ? scopePathFor(toolName, input, deps.cwd) : void 0;
510
+ const verdict = resolveGate({
511
+ modeState,
512
+ config: policyConfig,
513
+ toolName,
514
+ toolInputs: [input],
515
+ scopePaths: scopePath ? [scopePath] : [],
516
+ cwd: deps.cwd,
517
+ repoKey: repoKeyFor(deps.cwd),
518
+ sessionId
519
+ });
520
+ if (verdict.kind === "kill") return deny(KILL_REASON);
521
+ if (verdict.kind === "allow") return allow(input);
522
+ if (verdict.kind === "deny") return deny(verdict.reason);
523
+ if (verdict.kind === "defer") return deny("Approval state unavailable; denied by default");
524
+ const policy = verdict.policy;
525
+ const description = describeToolCall(toolName, input, "hook");
526
+ const toolTarget = deriveToolTarget(toolName, input);
527
+ let question;
528
+ try {
529
+ question = await askUser2(deps.apiKey, {
530
+ question: `Allow ${description || UNNAMED_ACTION}?`,
531
+ type: "confirm",
532
+ context: `Your agent wants to run this in ${deps.projectName} (you are away)`,
533
+ agentName: `Claude Code - ${deps.projectName}`,
534
+ sessionId,
535
+ machineId: deps.machineId,
536
+ toolName,
537
+ toolTarget,
538
+ repoKey: repoKeyFor(deps.cwd)
539
+ });
540
+ } catch (error) {
541
+ if (!(error instanceof SubscriptionRequiredError)) return deny(UNVERIFIABLE_APPROVAL_REASON);
542
+ announceSubscriptionRequired(error.upgradeUrl, sessionId);
543
+ return deny(SUBSCRIPTION_REQUIRED_NO_LOCAL_APPROVAL_UI);
544
+ }
545
+ if (isDecisionStopped(question)) return deny(DECISION_STOP_REASON);
546
+ entry.correlationId = question.correlationId;
547
+ const localHandoff = isLocalQuestionHandoff(question);
548
+ let answer = question.answered ? { ...question, answered: true } : localHandoff ? { answered: false } : await waitForPhone(entry, question.correlationId, windowFor(policy));
549
+ if (isDecisionStopped(answer) || entry.controller.signal.aborted) return deny(DECISION_STOP_REASON);
550
+ if (!answer.answered) {
551
+ answer = await withdrawQuestion(deps.apiKey, question.correlationId, { cancelQuestion: cancelQuestion2, waitForAnswer: waitForAnswer2 });
552
+ entry.correlationId = void 0;
553
+ if (isDecisionStopped(answer)) return deny(DECISION_STOP_REASON);
554
+ }
555
+ answered = answer.answered;
556
+ if (answer.answered) {
557
+ if (isDeferAnswer(answer.value)) return deny("Deferred \u2014 handle it on the machine");
558
+ return answer.value === "yes" ? allow(input) : deny("Denied from your phone");
559
+ }
560
+ if (localHandoff) return deny(NO_LOCAL_APPROVAL_UI);
561
+ return applyTimeout(policy, input);
562
+ } catch {
563
+ return deny("Approval unavailable; denied by default");
564
+ } finally {
565
+ if (options?.signal) options.signal.removeEventListener("abort", outerAbort);
566
+ if (entry.correlationId && !answered && !tornDown) {
567
+ await cancelQuestion2(deps.apiKey, entry.correlationId).catch(() => {
568
+ });
569
+ }
570
+ pending.delete(entry);
571
+ }
572
+ };
573
+ const teardown = () => {
574
+ tornDown = true;
575
+ for (const entry of pending) {
576
+ entry.controller.abort();
577
+ if (entry.correlationId) {
578
+ void cancelQuestion2(deps.apiKey, entry.correlationId).catch(() => {
579
+ });
580
+ }
581
+ }
582
+ pending.clear();
583
+ };
584
+ return {
585
+ canUseTool,
586
+ teardown,
587
+ pendingCount: () => pending.size
588
+ };
589
+ };
590
+
591
+ // src/wrapper/authRejection.ts
592
+ var isAuthRejection = (status) => status === 401 || status === 403;
593
+
594
+ // src/wrapper/drainClient.ts
595
+ var drainPendingCommand = async (apiKey, sessionId, signal, baseUrl = getBaseUrl()) => {
596
+ try {
597
+ const response = await fetch(`${baseUrl}/api/agent/command/drain`, {
598
+ method: "POST",
599
+ headers: {
600
+ "Content-Type": "application/json",
601
+ Authorization: `Bearer ${apiKey}`
602
+ },
603
+ body: JSON.stringify(sessionId ? { sessionId } : {}),
604
+ signal
605
+ });
606
+ if (!response.ok) return { command: null, rejected: isAuthRejection(response.status) };
607
+ const data = await response.json();
608
+ const command = typeof data.command === "string" && data.command.length > 0 ? data.command : null;
609
+ return { command, rejected: false };
610
+ } catch {
611
+ return { command: null, rejected: false };
612
+ }
613
+ };
614
+
615
+ // src/wrapper/commandPoller.ts
616
+ var SLOW_POLL_MS = 3e4;
617
+ var FAST_POLL_MS = 2e3;
618
+ var FAST_DECAY_TICKS = 3;
619
+ var MAX_ERROR_BACKOFF_MS = 12e4;
620
+ var MAX_ERROR_STREAK = 6;
621
+ var REQUEST_TIMEOUT_MS = 1e4;
622
+ var PollRejectedError = class extends Error {
623
+ };
624
+ var EMPTY_DRAIN = { command: null, rejected: false };
625
+ var startCommandPoller = (opts) => {
626
+ const drain = opts.drain ?? drainPendingCommand;
627
+ const slow = opts.slowPollMs ?? SLOW_POLL_MS;
628
+ const fast = opts.fastPollMs ?? FAST_POLL_MS;
629
+ const requestTimeout = opts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
630
+ let stopped = false;
631
+ let timer;
632
+ let inflight;
633
+ let idleTicks = 0;
634
+ let errorStreak = 0;
635
+ let authRejectionReported = false;
636
+ const reportAuthRejection = () => {
637
+ if (authRejectionReported) return;
638
+ authRejectionReported = true;
639
+ try {
640
+ opts.onAuthRejected?.();
641
+ } catch {
642
+ return;
643
+ }
644
+ };
645
+ const jitter = (ms) => {
646
+ const r = opts.jitter ? opts.jitter() : Math.random();
647
+ return Math.max(1, Math.round(ms * (0.85 + r * 0.3)));
648
+ };
649
+ const nextDelay = () => {
650
+ if (errorStreak > 0) {
651
+ return Math.min(slow * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
652
+ }
653
+ return idleTicks >= FAST_DECAY_TICKS ? slow : fast;
654
+ };
655
+ const schedule = (delayMs) => {
656
+ if (stopped) return;
657
+ timer = setTimeout(tick, jitter(delayMs));
658
+ };
659
+ const fetchMode = async () => {
660
+ if (!opts.fetchModeState || !opts.onModeState) return;
661
+ try {
662
+ const state = await opts.fetchModeState(opts.apiKey, opts.getSessionId?.());
663
+ try {
664
+ opts.onModeState(state);
665
+ } catch {
666
+ }
667
+ } catch {
668
+ }
669
+ };
670
+ const tick = async () => {
671
+ if (stopped) return;
672
+ const controller = new AbortController();
673
+ inflight = controller;
674
+ const timeout = setTimeout(() => controller.abort(), requestTimeout);
675
+ try {
676
+ const wantCommands = opts.shouldDrainCommands ? opts.shouldDrainCommands() : true;
677
+ let command;
678
+ if (opts.poll) {
679
+ const result = await opts.poll(opts.apiKey, opts.getSessionId?.(), wantCommands, controller.signal);
680
+ if (result.rejected) {
681
+ reportAuthRejection();
682
+ throw new PollRejectedError();
683
+ }
684
+ command = result.command;
685
+ if (result.mode && opts.onModeState) {
686
+ try {
687
+ opts.onModeState(result.mode);
688
+ } catch {
689
+ }
690
+ }
691
+ } else {
692
+ const [drained] = await Promise.all([
693
+ wantCommands ? drain(opts.apiKey, opts.getSessionId?.(), controller.signal) : Promise.resolve(EMPTY_DRAIN),
694
+ fetchMode()
695
+ ]);
696
+ if (drained.rejected) {
697
+ reportAuthRejection();
698
+ throw new PollRejectedError();
699
+ }
700
+ command = drained.command;
701
+ }
702
+ errorStreak = 0;
703
+ if (command) {
704
+ idleTicks = 0;
705
+ const text = typeof command === "string" ? command : command.text;
706
+ let accepted = false;
707
+ try {
708
+ accepted = await opts.onCommand(text) !== false;
709
+ } catch {
710
+ }
711
+ if (typeof command !== "string") {
712
+ const state = accepted ? "delivered" : "failed";
713
+ if (command.acknowledge) {
714
+ await command.acknowledge(state).catch(() => false);
715
+ } else if (opts.ackCommand) {
716
+ await opts.ackCommand(opts.apiKey, command.id, state).catch(() => false);
717
+ }
718
+ }
719
+ } else {
720
+ idleTicks++;
721
+ }
722
+ } catch {
723
+ errorStreak = Math.min(errorStreak + 1, MAX_ERROR_STREAK);
724
+ } finally {
725
+ clearTimeout(timeout);
726
+ inflight = void 0;
727
+ }
728
+ schedule(nextDelay());
729
+ };
730
+ schedule(fast);
731
+ opts.log?.("[pushary] command poller active");
732
+ return {
733
+ stop() {
734
+ stopped = true;
735
+ if (timer) clearTimeout(timer);
736
+ inflight?.abort();
737
+ inflight = void 0;
738
+ }
739
+ };
740
+ };
741
+
742
+ // src/wrapper/pollClient.ts
743
+ var APPROVAL_MODES = /* @__PURE__ */ new Set(["push_only", "terminal_only", "push_first", "notify_only"]);
744
+ var pollAgent = async (apiKey, sessionId, wantCommand, signal, baseUrl = getBaseUrl(), machineId, controlMode, takeoverDeclined) => {
745
+ try {
746
+ const sessionKey = sessionId || DEFAULT_SESSION;
747
+ const receiptNamespace = `${apiKey}:${machineId}:claude_code`;
748
+ let storedCommand = machineId ? readPendingCommand(sessionKey, receiptNamespace) : null;
749
+ let acknowledgedCommandId = null;
750
+ const acknowledge = async (id, state) => {
751
+ const persisted = state === "delivered" ? markPendingCommandDelivered(sessionKey, id, receiptNamespace) : markPendingCommandFailed(sessionKey, id, receiptNamespace);
752
+ if (!persisted) return false;
753
+ try {
754
+ const acknowledged = await fetch(`${baseUrl}/api/agent/command/ack`, {
755
+ method: "POST",
756
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
757
+ body: JSON.stringify({
758
+ commandId: id,
759
+ sessionId: sessionId ?? null,
760
+ machineId,
761
+ state,
762
+ ...state === "failed" ? { failureCode: "consumer_rejected" } : {}
763
+ }),
764
+ signal
765
+ });
766
+ if (acknowledged.ok) removePendingCommand(sessionKey, id, receiptNamespace);
767
+ return acknowledged.ok;
768
+ } catch {
769
+ return false;
770
+ }
771
+ };
772
+ if (storedCommand?.delivered || storedCommand?.failed) {
773
+ const acknowledged = await acknowledge(
774
+ storedCommand.id,
775
+ storedCommand.failed ? "failed" : "delivered"
776
+ );
777
+ if (acknowledged) {
778
+ acknowledgedCommandId = storedCommand.id;
779
+ storedCommand = null;
780
+ }
781
+ }
782
+ const response = await fetch(`${baseUrl}/api/agent/poll`, {
783
+ method: "POST",
784
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
785
+ body: JSON.stringify({
786
+ ...sessionId ? { sessionId } : {},
787
+ ...controlMode ? { controlMode } : {},
788
+ ...takeoverDeclined ? { takeoverDeclined } : {},
789
+ ...machineId ? { machineId } : {},
790
+ ...wantCommand && !storedCommand ? {} : { drain: false },
791
+ ...wantCommand && !storedCommand && machineId ? { ackMode: true } : {}
792
+ }),
793
+ signal
794
+ });
795
+ if (!response.ok) return { command: null, mode: null, rejected: isAuthRejection(response.status) };
796
+ const data = await response.json();
797
+ const claimedCommand = (id, text) => ({
798
+ id,
799
+ text,
800
+ acknowledge: (state) => acknowledge(id, state)
801
+ });
802
+ let command = typeof data.command === "string" && data.command.length > 0 ? data.command : null;
803
+ if (storedCommand) {
804
+ command = storedCommand.delivered || storedCommand.failed ? null : claimedCommand(storedCommand.id, storedCommand.text);
805
+ } else if (data.command && typeof data.command === "object") {
806
+ const claimed = data.command;
807
+ if (typeof claimed.id === "string" && typeof claimed.text === "string" && claimed.id !== acknowledgedCommandId) {
808
+ const saved = savePendingCommand(sessionKey, {
809
+ id: claimed.id,
810
+ text: claimed.text,
811
+ expiresAtMs: typeof claimed.expiresAtMs === "number" ? claimed.expiresAtMs : Date.now() + 36e5,
812
+ delivered: false
813
+ }, receiptNamespace);
814
+ command = saved ? claimedCommand(claimed.id, claimed.text) : null;
815
+ }
816
+ }
817
+ const m = data.override?.mode;
818
+ const mode = {
819
+ mode: typeof m === "string" && APPROVAL_MODES.has(m) ? m : null,
820
+ kill: data.kill === true,
821
+ policyVersion: typeof data.policyVersion === "string" && data.policyVersion.length > 0 ? data.policyVersion : null,
822
+ relayUrl: typeof data.relayUrl === "string" && data.relayUrl.length > 0 ? data.relayUrl : null
823
+ };
824
+ return { command, mode, rejected: false, ...data.takeoverRequested === true ? { takeoverRequested: true } : {} };
825
+ } catch {
826
+ return { command: null, mode: null, rejected: false };
827
+ }
828
+ };
829
+
830
+ // src/wrapper/wsProtocol.ts
831
+ var PROTOCOL_VERSION = 1;
832
+ var encodeFrame = (frame) => JSON.stringify(frame);
833
+ var helloFrame = (fields) => ({ v: PROTOCOL_VERSION, t: "hello", ...fields });
834
+ var parse = (raw) => {
835
+ try {
836
+ const value = JSON.parse(raw);
837
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
838
+ const obj = value;
839
+ if (obj.v !== PROTOCOL_VERSION) return null;
840
+ return obj;
841
+ } catch {
842
+ return null;
843
+ }
844
+ };
845
+ var isString = (v) => typeof v === "string";
846
+ var decodeServerFrame = (raw) => {
847
+ const f = parse(raw);
848
+ if (!f) return null;
849
+ switch (f.t) {
850
+ case "welcome":
851
+ return { v: PROTOCOL_VERSION, t: "welcome" };
852
+ case "ping":
853
+ return { v: PROTOCOL_VERSION, t: "ping" };
854
+ case "pong":
855
+ return { v: PROTOCOL_VERSION, t: "pong" };
856
+ case "command":
857
+ return isString(f.id) && isString(f.text) ? { v: PROTOCOL_VERSION, t: "command", id: f.id, text: f.text } : null;
858
+ case "error":
859
+ if (isString(f.code) && isString(f.message)) {
860
+ return { v: PROTOCOL_VERSION, t: "error", code: f.code, message: f.message, fatal: f.fatal === true };
861
+ }
862
+ return null;
863
+ default:
864
+ return null;
865
+ }
866
+ };
867
+
868
+ // src/wrapper/relayClient.ts
869
+ var DEFAULT_HEARTBEAT_MS = 2e4;
870
+ var DEFAULT_PONG_TIMEOUT_MS = 1e4;
871
+ var DEFAULT_MAX_SEEN_COMMANDS = 256;
872
+ var COMMAND_RECEIPT_TTL_MS = 36e5;
873
+ var defaultBackoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4);
874
+ var defaultSocketFactory = (url) => {
875
+ const WS = globalThis.WebSocket;
876
+ if (!WS) throw new Error("no WebSocket in this runtime");
877
+ const socket = new WS(url);
878
+ return {
879
+ send: (data) => socket.send(data),
880
+ close: () => socket.close(),
881
+ onOpen: (cb) => socket.addEventListener("open", () => cb()),
882
+ onMessage: (cb) => socket.addEventListener("message", (event) => cb(String(event.data))),
883
+ onClose: (cb) => socket.addEventListener("close", () => cb()),
884
+ onError: (cb) => socket.addEventListener("error", (event) => cb(event))
885
+ };
886
+ };
887
+ var createRelayClient = (options) => {
888
+ const factory = options.socketFactory ?? defaultSocketFactory;
889
+ const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
890
+ const pongTimeoutMs = options.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
891
+ const backoff = options.backoffMs ?? defaultBackoff;
892
+ const maxSeenCommands = Math.max(1, options.maxSeenCommands ?? DEFAULT_MAX_SEEN_COMMANDS);
893
+ let socket;
894
+ let isConnected = false;
895
+ let stopped = false;
896
+ let attempt = 0;
897
+ let reconnectTimer;
898
+ let heartbeatTimer;
899
+ let pongTimer;
900
+ const seenCommands = [];
901
+ const seenSet = /* @__PURE__ */ new Set();
902
+ const pendingCommands = /* @__PURE__ */ new Map();
903
+ const log = (message) => options.log?.(message);
904
+ const receiptNamespace = `${options.apiKey}:${options.machineId}:${options.agentType}`;
905
+ const acknowledgeStored = (id) => {
906
+ const receipt = readPendingCommandById(
907
+ options.getSessionId() || DEFAULT_SESSION,
908
+ id,
909
+ receiptNamespace
910
+ );
911
+ if (!receipt || !receipt.delivered && !receipt.failed) return false;
912
+ sendFrame(receipt.failed ? { v: PROTOCOL_VERSION, t: "ack", id, state: "failed", failureCode: "consumer_rejected" } : { v: PROTOCOL_VERSION, t: "ack", id, state: "delivered" });
913
+ return true;
914
+ };
915
+ const clearTimers = () => {
916
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
917
+ if (pongTimer) clearTimeout(pongTimer);
918
+ if (reconnectTimer) clearTimeout(reconnectTimer);
919
+ heartbeatTimer = void 0;
920
+ pongTimer = void 0;
921
+ reconnectTimer = void 0;
922
+ };
923
+ const rememberCommand = (id) => {
924
+ if (seenSet.has(id)) return false;
925
+ seenSet.add(id);
926
+ seenCommands.push(id);
927
+ while (seenCommands.length > maxSeenCommands) {
928
+ const evicted = seenCommands.shift();
929
+ if (evicted) seenSet.delete(evicted);
930
+ }
931
+ return true;
932
+ };
933
+ const sendFrame = (frame) => {
934
+ if (!socket || !isConnected) return;
935
+ try {
936
+ socket.send(encodeFrame(frame));
937
+ } catch {
938
+ }
939
+ };
940
+ const sendHello = () => sendFrame(
941
+ helloFrame({
942
+ apiKey: options.apiKey,
943
+ machineId: options.machineId,
944
+ agentType: options.agentType,
945
+ sessionId: options.getSessionId()
946
+ })
947
+ );
948
+ const armPong = () => {
949
+ if (pongTimer) clearTimeout(pongTimer);
950
+ pongTimer = setTimeout(() => {
951
+ log("[pushary] relay heartbeat timed out; reconnecting");
952
+ dropAndReconnect();
953
+ }, pongTimeoutMs);
954
+ };
955
+ const startHeartbeat = () => {
956
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
957
+ heartbeatTimer = setInterval(() => {
958
+ sendFrame({ v: PROTOCOL_VERSION, t: "ping" });
959
+ armPong();
960
+ }, heartbeatMs);
961
+ };
962
+ const onServerFrame = async (raw) => {
963
+ const frame = decodeServerFrame(raw);
964
+ if (!frame) return;
965
+ switch (frame.t) {
966
+ case "welcome":
967
+ attempt = 0;
968
+ log("[pushary] relay connected");
969
+ break;
970
+ case "command":
971
+ if (pendingCommands.has(frame.id)) {
972
+ await pendingCommands.get(frame.id);
973
+ acknowledgeStored(frame.id);
974
+ break;
975
+ }
976
+ if (seenSet.has(frame.id)) {
977
+ sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id, state: "delivered" });
978
+ break;
979
+ }
980
+ if (acknowledgeStored(frame.id)) break;
981
+ const sessionKey = options.getSessionId() || DEFAULT_SESSION;
982
+ if (!savePendingCommand(sessionKey, {
983
+ id: frame.id,
984
+ text: frame.text,
985
+ expiresAtMs: Date.now() + COMMAND_RECEIPT_TTL_MS,
986
+ delivered: false,
987
+ failed: true
988
+ }, receiptNamespace)) {
989
+ acknowledgeStored(frame.id);
990
+ break;
991
+ }
992
+ let completePending;
993
+ pendingCommands.set(frame.id, new Promise((resolve) => {
994
+ completePending = resolve;
995
+ }));
996
+ let accepted = false;
997
+ try {
998
+ const result = options.onCommand(frame.text);
999
+ accepted = (result instanceof Promise ? await result : result) !== false;
1000
+ } catch {
1001
+ }
1002
+ if (accepted && markPendingCommandDelivered(sessionKey, frame.id, receiptNamespace)) {
1003
+ rememberCommand(frame.id);
1004
+ sendFrame({ v: PROTOCOL_VERSION, t: "ack", id: frame.id, state: "delivered" });
1005
+ } else {
1006
+ sendFrame({
1007
+ v: PROTOCOL_VERSION,
1008
+ t: "ack",
1009
+ id: frame.id,
1010
+ state: "failed",
1011
+ failureCode: "consumer_rejected"
1012
+ });
1013
+ }
1014
+ completePending();
1015
+ pendingCommands.delete(frame.id);
1016
+ break;
1017
+ case "ping":
1018
+ sendFrame({ v: PROTOCOL_VERSION, t: "pong" });
1019
+ break;
1020
+ case "pong":
1021
+ if (pongTimer) clearTimeout(pongTimer);
1022
+ pongTimer = void 0;
1023
+ break;
1024
+ case "error":
1025
+ if (frame.fatal) {
1026
+ log(`[pushary] relay fatal: ${frame.message}`);
1027
+ fatal(frame.message);
1028
+ } else {
1029
+ log(`[pushary] relay error: ${frame.message}`);
1030
+ }
1031
+ break;
1032
+ }
1033
+ };
1034
+ const dropSocket = () => {
1035
+ isConnected = false;
1036
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
1037
+ if (pongTimer) clearTimeout(pongTimer);
1038
+ heartbeatTimer = void 0;
1039
+ pongTimer = void 0;
1040
+ if (socket) {
1041
+ try {
1042
+ socket.close();
1043
+ } catch {
1044
+ }
1045
+ }
1046
+ socket = void 0;
1047
+ };
1048
+ const scheduleReconnect = () => {
1049
+ if (stopped) return;
1050
+ if (reconnectTimer) return;
1051
+ const delay = backoff(attempt);
1052
+ attempt++;
1053
+ const jittered = Math.max(1, Math.round(delay * (0.85 + Math.random() * 0.3)));
1054
+ reconnectTimer = setTimeout(() => {
1055
+ reconnectTimer = void 0;
1056
+ connect();
1057
+ }, jittered);
1058
+ };
1059
+ const dropAndReconnect = () => {
1060
+ dropSocket();
1061
+ scheduleReconnect();
1062
+ };
1063
+ const fatal = (message) => {
1064
+ stopped = true;
1065
+ clearTimers();
1066
+ dropSocket();
1067
+ options.onFatal?.(message);
1068
+ };
1069
+ const connect = () => {
1070
+ if (stopped) return;
1071
+ let created;
1072
+ try {
1073
+ created = factory(options.url);
1074
+ } catch (error) {
1075
+ fatal(error instanceof Error ? error.message : String(error));
1076
+ return;
1077
+ }
1078
+ socket = created;
1079
+ created.onOpen(() => {
1080
+ isConnected = true;
1081
+ sendHello();
1082
+ startHeartbeat();
1083
+ });
1084
+ created.onMessage((data) => onServerFrame(data));
1085
+ created.onClose(() => {
1086
+ if (stopped) return;
1087
+ dropAndReconnect();
1088
+ });
1089
+ created.onError(() => {
1090
+ });
1091
+ };
1092
+ return {
1093
+ start() {
1094
+ if (stopped) return;
1095
+ connect();
1096
+ },
1097
+ connected() {
1098
+ return isConnected;
1099
+ },
1100
+ reannounce() {
1101
+ if (stopped || !isConnected) return;
1102
+ sendHello();
1103
+ },
1104
+ stop() {
1105
+ if (stopped) return;
1106
+ stopped = true;
1107
+ sendFrame({ v: PROTOCOL_VERSION, t: "bye" });
1108
+ clearTimers();
1109
+ dropSocket();
1110
+ }
1111
+ };
1112
+ };
1113
+
1114
+ // src/wrapper/stdinHandoff.ts
1115
+ var isTty = () => Boolean(process.stdin.isTTY);
1116
+ var createStdinHandoff = () => {
1117
+ let listener;
1118
+ let captured = false;
1119
+ const removeListener = () => {
1120
+ if (listener) {
1121
+ process.stdin.off("data", listener);
1122
+ listener = void 0;
1123
+ }
1124
+ };
1125
+ const setRaw = (on) => {
1126
+ if (!isTty()) return;
1127
+ try {
1128
+ process.stdin.setRawMode(on);
1129
+ } catch {
1130
+ }
1131
+ };
1132
+ const captureForWrapper = (onKey) => {
1133
+ removeListener();
1134
+ listener = onKey;
1135
+ if (!isTty()) return;
1136
+ try {
1137
+ process.stdin.resume();
1138
+ setRaw(true);
1139
+ process.stdin.setEncoding("utf8");
1140
+ process.stdin.on("data", listener);
1141
+ captured = true;
1142
+ } catch {
1143
+ removeListener();
1144
+ }
1145
+ };
1146
+ const releaseToChild = () => {
1147
+ removeListener();
1148
+ setRaw(false);
1149
+ if (captured && isTty()) {
1150
+ try {
1151
+ process.stdin.pause();
1152
+ } catch {
1153
+ }
1154
+ }
1155
+ captured = false;
1156
+ };
1157
+ return {
1158
+ captureForWrapper,
1159
+ releaseToChild,
1160
+ dispose: releaseToChild
1161
+ };
1162
+ };
1163
+
1164
+ // src/wrapper/args.ts
1165
+ var resolveClaudeArgs = (argv) => {
1166
+ const rest = argv.slice(2);
1167
+ return rest[0] === "claude" ? rest.slice(1) : rest;
1168
+ };
1169
+ var extractRemoteFlag = (args2) => ({
1170
+ remote: args2.includes("--remote"),
1171
+ rest: args2.filter((arg) => arg !== "--remote")
1172
+ });
1173
+ var flagValue = (args2, index) => {
1174
+ const inline = args2[index];
1175
+ const eq = inline?.indexOf("=") ?? -1;
1176
+ if (inline && eq > 0) return inline.slice(eq + 1);
1177
+ const next = args2[index + 1];
1178
+ return next && !next.startsWith("-") ? next : void 0;
1179
+ };
1180
+ var PASS_THROUGH_VALUE_FLAGS = /* @__PURE__ */ new Set(["--cwd", "--add-dir"]);
1181
+ var parseRemoteArgs = (args2) => {
1182
+ let initialPrompt;
1183
+ let resume;
1184
+ let permissionMode;
1185
+ let model;
1186
+ let settings;
1187
+ for (let i = 0; i < args2.length; i++) {
1188
+ const arg = args2[i];
1189
+ if (arg === "--model" || arg === "-m" || arg?.startsWith("--model=")) {
1190
+ model = flagValue(args2, i);
1191
+ if (!arg.includes("=")) i++;
1192
+ } else if (arg === "--settings" || arg?.startsWith("--settings=")) {
1193
+ settings = flagValue(args2, i);
1194
+ if (!arg.includes("=")) i++;
1195
+ } else if (PASS_THROUGH_VALUE_FLAGS.has(arg ?? "")) {
1196
+ i++;
1197
+ } else if (arg === "-p" || arg === "--print" || arg?.startsWith("--print=")) {
1198
+ initialPrompt = flagValue(args2, i);
1199
+ } else if (arg === "-r" || arg === "--resume" || arg?.startsWith("--resume=")) {
1200
+ resume = flagValue(args2, i);
1201
+ } else if (arg === "--permission-mode" || arg?.startsWith("--permission-mode=")) {
1202
+ permissionMode = flagValue(args2, i);
1203
+ }
1204
+ }
1205
+ return { initialPrompt, resume, permissionMode, ...model ? { model } : {}, ...settings ? { settings } : {} };
1206
+ };
1207
+ var remoteCompatibilityIssue = (args2) => {
1208
+ const values = /* @__PURE__ */ new Set(["--model", "-m", "--settings", "--permission-mode", "--resume", "-r", "--session-id", "-p", "--print", "--cwd", "--add-dir"]);
1209
+ const switches = /* @__PURE__ */ new Set(["--continue", "-c", "--verbose", "--debug", "--dangerously-skip-permissions"]);
1210
+ for (let index = 0; index < args2.length; index++) {
1211
+ const arg = args2[index];
1212
+ if (!arg.startsWith("-")) continue;
1213
+ const flag = arg.split("=")[0];
1214
+ if (switches.has(flag)) continue;
1215
+ if (!values.has(flag)) return `Phone takeover does not yet preserve ${flag}; keep using this terminal.`;
1216
+ if (!arg.includes("=") && args2[index + 1] && !args2[index + 1].startsWith("-")) index++;
1217
+ }
1218
+ return null;
1219
+ };
1220
+
1221
+ // src/wrapper/localLeg.ts
1222
+ import { readFileSync } from "fs";
1223
+ import { homedir as homedir2 } from "os";
1224
+ import { join as join2 } from "path";
1225
+ var SIGNAL_EXIT_BASE = 128;
1226
+ var SIGNAL_NUMBERS2 = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
1227
+ var nativeArgs = (session) => {
1228
+ let args2 = [...session.claudeArgs];
1229
+ const id = session.getSessionId();
1230
+ if (id && session.userManagesSession && session.isSessionCreated()) {
1231
+ args2 = args2.filter((arg, index, all) => {
1232
+ if (/^(--resume|-r|--session-id)(=|$)|^(--continue|-c)$/.test(arg)) return false;
1233
+ return !/^(--resume|-r|--session-id)$/.test(all[index - 1] ?? "") || arg.startsWith("-");
1234
+ });
1235
+ return [...args2, "--resume", id];
1236
+ }
1237
+ if (id && !session.userManagesSession) {
1238
+ args2.push(session.isSessionCreated() ? "--resume" : "--session-id", id);
1239
+ }
1240
+ return args2;
1241
+ };
1242
+ var runLocalLeg = (session, deps = {}) => {
1243
+ const spawn2 = deps.spawn ?? spawnClaude;
1244
+ return new Promise((resolve) => {
1245
+ const legAbort = new AbortController();
1246
+ let exitReason = null;
1247
+ let settled = false;
1248
+ let identityTimer;
1249
+ const finish = () => {
1250
+ if (settled) return;
1251
+ settled = true;
1252
+ if (identityTimer) clearInterval(identityTimer);
1253
+ session.setActiveLeg(null);
1254
+ resolve(exitReason ?? { type: "exit", code: 0 });
1255
+ };
1256
+ const control = {
1257
+ onPhoneCommand: () => {
1258
+ if (!exitReason) exitReason = "switch";
1259
+ if (!legAbort.signal.aborted) {
1260
+ process.stderr.write(
1261
+ "\n[pushary] phone takeover requested \u2014 resuming this conversation with phone control (Ctrl-] hands the terminal back)\n"
1262
+ );
1263
+ legAbort.abort();
1264
+ }
1265
+ },
1266
+ stop: (reason) => {
1267
+ if (!exitReason) exitReason = { type: "exit", code: reason === "signal" ? 130 : 0 };
1268
+ if (!legAbort.signal.aborted) legAbort.abort();
1269
+ }
1270
+ };
1271
+ session.setActiveLeg(control);
1272
+ if (session.input.bufferedCount > 0) {
1273
+ exitReason = "switch";
1274
+ finish();
1275
+ return;
1276
+ }
1277
+ session.stdin.releaseToChild();
1278
+ let child;
1279
+ try {
1280
+ child = spawn2(session.binary, nativeArgs(session), {
1281
+ stdio: "inherit",
1282
+ env: { ...process.env, [WRAPPER_ACTIVE_ENV]: "1" },
1283
+ signal: legAbort.signal
1284
+ });
1285
+ } catch {
1286
+ exitReason = exitReason ?? { type: "exit", code: 127 };
1287
+ finish();
1288
+ return;
1289
+ }
1290
+ const spawnedAt = Date.now();
1291
+ const readIdentity = () => {
1292
+ if (!child.pid) return;
1293
+ try {
1294
+ const root = process.env.CLAUDE_CONFIG_DIR || join2(homedir2(), ".claude");
1295
+ const record = JSON.parse(readFileSync(join2(root, "sessions", `${child.pid}.json`), "utf8"));
1296
+ if (record.pid !== child.pid || record.cwd !== process.cwd() || record.startedAt < spawnedAt - 5e3 || typeof record.startedAt !== "number" || !/^[a-f0-9-]{36}$/i.test(record.sessionId)) return;
1297
+ if (record.sessionId !== session.getSessionId()) session.setSessionId(record.sessionId);
1298
+ session.markSessionCreated();
1299
+ } catch {
1300
+ }
1301
+ };
1302
+ session.markSessionCreated();
1303
+ readIdentity();
1304
+ identityTimer = setInterval(readIdentity, 500);
1305
+ identityTimer.unref();
1306
+ child.on("error", (error) => {
1307
+ if (legAbort.signal.aborted && error.name === "AbortError") return;
1308
+ if (!legAbort.signal.aborted && !exitReason) {
1309
+ exitReason = { type: "exit", code: 127 };
1310
+ }
1311
+ finish();
1312
+ });
1313
+ child.on("exit", (code2, signal) => {
1314
+ if (signal === "SIGTERM" && legAbort.signal.aborted) {
1315
+ } else if (!exitReason) {
1316
+ if (session.input.bufferedCount > 0) {
1317
+ exitReason = "switch";
1318
+ } else if (typeof code2 === "number") {
1319
+ exitReason = { type: "exit", code: code2 };
1320
+ } else if (signal) {
1321
+ exitReason = { type: "exit", code: SIGNAL_EXIT_BASE + (SIGNAL_NUMBERS2[signal] ?? 0) };
1322
+ } else {
1323
+ exitReason = { type: "exit", code: 0 };
1324
+ }
1325
+ }
1326
+ finish();
1327
+ });
1328
+ });
1329
+ };
1330
+
1331
+ // src/wrapper/remoteLoop.ts
1332
+ var errMsg = (err) => err instanceof Error ? err.message : String(err);
1333
+ var scheduleWithTimer = (fire, delayMs) => {
1334
+ const timer = setTimeout(fire, delayMs);
1335
+ timer.unref?.();
1336
+ return () => clearTimeout(timer);
1337
+ };
1338
+ var isTurnActivity = (message) => message.type !== "system" && message.type !== "result";
1339
+ var isInit = (message) => message.type === "system" && message.subtype === "init";
1340
+ var DEFAULT_MAX_RESTARTS = 5;
1341
+ var runRemoteLoop = async (deps) => {
1342
+ const maxRestarts = deps.maxRestarts ?? DEFAULT_MAX_RESTARTS;
1343
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1344
+ const backoff = deps.backoffMs ?? ((attempt) => Math.min(1e3 * 2 ** attempt, 15e3));
1345
+ const idleTimeoutMs = deps.idleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
1346
+ const scheduleIdle = deps.scheduleIdle ?? scheduleWithTimer;
1347
+ let resumeId = deps.initialResume;
1348
+ let totalCost = 0;
1349
+ let restarts = 0;
1350
+ let idleExpired = false;
1351
+ let cancelIdle;
1352
+ const disarmIdle = () => {
1353
+ cancelIdle?.();
1354
+ cancelIdle = void 0;
1355
+ };
1356
+ const ended = (exitCode, endedBy) => ({ exitCode, totalCostUsd: totalCost, endedBy });
1357
+ while (!deps.signal.aborted) {
1358
+ deps.input.detachConsumer();
1359
+ let query;
1360
+ try {
1361
+ query = deps.sdk.query({
1362
+ prompt: deps.input,
1363
+ options: {
1364
+ resume: resumeId,
1365
+ includePartialMessages: true,
1366
+ permissionMode: deps.permissionMode,
1367
+ model: deps.model,
1368
+ settings: deps.settings,
1369
+ canUseTool: deps.canUseTool,
1370
+ env: deps.env,
1371
+ cwd: deps.cwd,
1372
+ pathToClaudeCodeExecutable: deps.pathToClaudeCodeExecutable
1373
+ }
1374
+ });
1375
+ } catch (err) {
1376
+ deps.log?.(`[pushary] remote mode: query failed to start: ${errMsg(err)}`);
1377
+ return ended(1, "failed");
1378
+ }
1379
+ const onAbort = () => {
1380
+ deps.input.detachConsumer();
1381
+ if (query.close) query.close();
1382
+ else void query.interrupt?.().catch(() => {
1383
+ });
1384
+ };
1385
+ if (deps.signal.aborted) onAbort();
1386
+ else deps.signal.addEventListener("abort", onAbort, { once: true });
1387
+ let turnOpen = false;
1388
+ const stillResting = (deliveredAtRest) => !turnOpen && deps.input.bufferedCount === 0 && deps.input.stats.delivered === deliveredAtRest;
1389
+ const armIdle = () => {
1390
+ disarmIdle();
1391
+ const deliveredAtRest = deps.input.stats.delivered;
1392
+ cancelIdle = scheduleIdle(() => {
1393
+ cancelIdle = void 0;
1394
+ if (!stillResting(deliveredAtRest)) return;
1395
+ idleExpired = true;
1396
+ deps.log?.(`[pushary] remote mode: no phone instruction for ${describeIdleWindow(idleTimeoutMs)}, ending the session`);
1397
+ onAbort();
1398
+ }, idleTimeoutMs);
1399
+ };
1400
+ const restsAfter = (message) => deps.input.bufferedCount === 0 && (message.type === "result" || isInit(message) && !turnOpen);
1401
+ try {
1402
+ for await (const message of query) {
1403
+ deps.onMessage?.(message);
1404
+ if (isInit(message) && typeof message.session_id === "string") {
1405
+ if (resumeId && message.session_id !== resumeId) {
1406
+ disarmIdle();
1407
+ deps.signal.removeEventListener("abort", onAbort);
1408
+ deps.input.detachConsumer();
1409
+ query.close?.();
1410
+ deps.log?.("[pushary] refused a different session ID during resume");
1411
+ return ended(1, "failed");
1412
+ }
1413
+ resumeId = message.session_id;
1414
+ deps.onSessionId?.(resumeId);
1415
+ }
1416
+ if (message.type === "result" && typeof message.total_cost_usd === "number") {
1417
+ totalCost = message.total_cost_usd;
1418
+ deps.onCost?.(totalCost);
1419
+ }
1420
+ if (message.type === "result") turnOpen = false;
1421
+ else if (isTurnActivity(message)) turnOpen = true;
1422
+ if (restsAfter(message)) armIdle();
1423
+ }
1424
+ disarmIdle();
1425
+ deps.signal.removeEventListener("abort", onAbort);
1426
+ query.close?.();
1427
+ if (idleExpired) return ended(0, "idle");
1428
+ return deps.signal.aborted ? ended(130, "aborted") : ended(0, "input-closed");
1429
+ } catch (err) {
1430
+ disarmIdle();
1431
+ deps.signal.removeEventListener("abort", onAbort);
1432
+ query.close?.();
1433
+ if (idleExpired) return ended(0, "idle");
1434
+ if (deps.signal.aborted) return ended(130, "aborted");
1435
+ restarts++;
1436
+ if (restarts > maxRestarts) {
1437
+ deps.log?.(`[pushary] remote mode: too many restarts, giving up: ${errMsg(err)}`);
1438
+ return ended(1, "failed");
1439
+ }
1440
+ deps.log?.(
1441
+ `[pushary] remote mode: transient error, resuming session (attempt ${restarts}/${maxRestarts}): ${errMsg(err)}`
1442
+ );
1443
+ await sleep(backoff(restarts));
1444
+ }
1445
+ }
1446
+ return ended(130, "aborted");
1447
+ };
1448
+
1449
+ // src/wrapper/remoteLeg.ts
1450
+ var CTRL_RIGHT_BRACKET = "";
1451
+ var CTRL_C = "";
1452
+ var terminalAttended = () => Boolean(process.stdin.isTTY);
1453
+ var runRemoteLeg = async (session, sdk, deps = {}) => {
1454
+ const legAbort = new AbortController();
1455
+ const launchArgs = parseRemoteArgs(session.claudeArgs);
1456
+ const attended = deps.attended ?? terminalAttended();
1457
+ let switchBack = false;
1458
+ let idleEnded = false;
1459
+ const control = {
1460
+ // The running query already drains the shared input queue, so a freshly pushed
1461
+ // phone command is delivered without any action here.
1462
+ onPhoneCommand: () => {
1463
+ },
1464
+ stop: () => {
1465
+ if (!legAbort.signal.aborted) legAbort.abort();
1466
+ }
1467
+ };
1468
+ session.setActiveLeg(control);
1469
+ session.stdin.captureForWrapper((data) => {
1470
+ if (data.includes(CTRL_RIGHT_BRACKET)) {
1471
+ switchBack = true;
1472
+ if (!legAbort.signal.aborted) legAbort.abort();
1473
+ } else if (data.includes(CTRL_C)) {
1474
+ if (!legAbort.signal.aborted) legAbort.abort();
1475
+ }
1476
+ });
1477
+ process.stderr.write(
1478
+ "[pushary] remote \u2014 driving from your phone \xB7 press Ctrl-] for the local terminal\n"
1479
+ );
1480
+ let exitCode = 0;
1481
+ try {
1482
+ const result = await runRemoteLoop({
1483
+ sdk,
1484
+ input: session.input,
1485
+ canUseTool: session.approver.canUseTool,
1486
+ signal: legAbort.signal,
1487
+ // Resume the session the local leg (or a prior turn) created, so the
1488
+ // conversation is continuous across the switch. Undefined = fresh session.
1489
+ initialResume: session.getSessionId(),
1490
+ permissionMode: session.permissionMode,
1491
+ model: launchArgs.model,
1492
+ settings: launchArgs.settings,
1493
+ // The SDK REPLACES the subprocess env; spread process.env and mark the
1494
+ // recursion guard so a wrapper-spawned claude cannot re-enter the wrapper.
1495
+ env: phoneDrivenEnv({ ...process.env, [WRAPPER_ACTIVE_ENV]: "1" }),
1496
+ cwd: process.cwd(),
1497
+ // Run the user's OWN claude (auth + version parity); Windows .cmd shims keep
1498
+ // the SDK's bundled binary since the SDK cannot spawn a shim directly.
1499
+ pathToClaudeCodeExecutable: needsShell(session.binary) ? void 0 : session.binary,
1500
+ onSessionId: (id) => session.setSessionId(id),
1501
+ onMessage: (message) => session.streamMessage(message),
1502
+ log: (message) => process.stderr.write(`${message}
1503
+ `),
1504
+ idleTimeoutMs: deps.idleTimeoutMs,
1505
+ scheduleIdle: deps.scheduleIdle
1506
+ });
1507
+ exitCode = result.exitCode;
1508
+ idleEnded = result.endedBy === "idle";
1509
+ } finally {
1510
+ session.stdin.releaseToChild();
1511
+ session.setActiveLeg(null);
1512
+ }
1513
+ if (idleEnded && attended) {
1514
+ process.stderr.write("[pushary] handing the terminal back to local mode\n");
1515
+ return "switch";
1516
+ }
1517
+ return switchBack ? "switch" : { type: "exit", code: exitCode };
1518
+ };
1519
+
1520
+ // src/wrapper/modeLoop.ts
1521
+ var DEFAULT_MIN_DWELL_MS = 300;
1522
+ var driveModeLoop = async (deps) => {
1523
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1524
+ const now = deps.now ?? (() => Date.now());
1525
+ const minDwell = deps.minDwellMs ?? DEFAULT_MIN_DWELL_MS;
1526
+ let mode = deps.startingMode;
1527
+ let lastSwitchAt = 0;
1528
+ while (true) {
1529
+ const result = mode === "local" ? await deps.runLocal() : await deps.runRemote();
1530
+ if (result !== "switch" || deps.isFinished()) {
1531
+ return result === "switch" ? 0 : result.code;
1532
+ }
1533
+ const since = now() - lastSwitchAt;
1534
+ if (since < minDwell) await sleep(minDwell - since);
1535
+ lastSwitchAt = now();
1536
+ mode = mode === "local" ? "remote" : "local";
1537
+ }
1538
+ };
1539
+
1540
+ // src/wrapper/dualMode.ts
1541
+ var INPUT_QUEUE_CAP = 32;
1542
+ var MIN_MODE_DWELL_MS = 300;
1543
+ var PRESENCE_HEARTBEAT_MS = 6e4;
1544
+ var SESSION_FLAG_RE = /^(-r|--resume|-c|--continue|--session-id)(=.*)?$/;
1545
+ var extractAssistantText = (message) => {
1546
+ const content = message.message?.content;
1547
+ if (typeof content === "string") return content || void 0;
1548
+ if (Array.isArray(content)) {
1549
+ const text = content.filter((block) => !!block && typeof block === "object").filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
1550
+ return text || void 0;
1551
+ }
1552
+ return void 0;
1553
+ };
1554
+ var runDualMode = async (binary, args2, startingMode) => {
1555
+ const compatibilityIssue = remoteCompatibilityIssue(args2);
1556
+ if (startingMode === "remote" && compatibilityIssue) {
1557
+ process.stderr.write(`[pushary] ${compatibilityIssue}
1558
+ `);
1559
+ return { implemented: false };
1560
+ }
1561
+ let apiKey;
1562
+ try {
1563
+ apiKey = getApiKey();
1564
+ } catch {
1565
+ process.stderr.write(
1566
+ `[pushary] phone control needs an API key. Run \`${NPX_LATEST_COMMAND} setup\`. Running the normal passthrough for now.
1567
+ `
1568
+ );
1569
+ return { implemented: false };
1570
+ }
1571
+ let sdk;
1572
+ let sdkLoading;
1573
+ let remoteAvailable = true;
1574
+ let noticedUnavailable = false;
1575
+ const ensureSdk = async () => {
1576
+ if (sdk === void 0) {
1577
+ sdkLoading ??= ensureClaudeSdk((message) => process.stderr.write(`${message}
1578
+ `));
1579
+ sdk = await sdkLoading;
1580
+ if (!sdk) remoteAvailable = false;
1581
+ }
1582
+ return sdk;
1583
+ };
1584
+ if (startingMode === "remote" && !await ensureSdk()) {
1585
+ process.stderr.write("[pushary] phone control is unavailable; running the normal passthrough.\n");
1586
+ return { implemented: false };
1587
+ }
1588
+ const projectName = basename(process.cwd());
1589
+ const agentName = `Claude Code - ${projectName}`;
1590
+ const machineId = getMachineId();
1591
+ const stdin = createStdinHandoff();
1592
+ const verbose = process.env.PUSHARY_VERBOSE === "1";
1593
+ const verboseLog = (message) => {
1594
+ if (verbose) process.stderr.write(`${message}
1595
+ `);
1596
+ };
1597
+ const { initialPrompt, resume, permissionMode } = parseRemoteArgs(args2);
1598
+ const userManagesSession = args2.some((a) => SESSION_FLAG_RE.test(a));
1599
+ const explicitIdIndex = args2.findIndex((arg) => arg === "--session-id" || arg.startsWith("--session-id="));
1600
+ const explicitId = explicitIdIndex < 0 ? void 0 : args2[explicitIdIndex].split("=")[1] ?? args2[explicitIdIndex + 1];
1601
+ let sessionId = resume ?? explicitId;
1602
+ let sessionCreated = Boolean(resume);
1603
+ if (!sessionId && startingMode === "local" && !userManagesSession) {
1604
+ sessionId = crypto.randomUUID();
1605
+ }
1606
+ let totalCostUsd = 0;
1607
+ let killed = false;
1608
+ let finished = false;
1609
+ let activeLeg = null;
1610
+ let controlMode = startingMode;
1611
+ let takeoverPending = false;
1612
+ let takeoverDecline;
1613
+ const input = new BoundedInputQueue({
1614
+ cap: INPUT_QUEUE_CAP,
1615
+ onDrop: (_dropped, total) => process.stderr.write(`[pushary] input queue full; refused ${total} instruction(s)
1616
+ `),
1617
+ coalesce: (items) => userMessage(items.map((m) => m.message.content).join("\n\n"))
1618
+ });
1619
+ const approver = createRemoteApprover({
1620
+ apiKey,
1621
+ machineId,
1622
+ projectName,
1623
+ cwd: process.cwd(),
1624
+ getSessionId: () => sessionId
1625
+ });
1626
+ const initialModeState = await fetchModeState(apiKey).catch(() => null);
1627
+ let relay;
1628
+ const startRelay = () => {
1629
+ if (!initialModeState?.relayUrl || relay) return;
1630
+ relay = createRelayClient({
1631
+ url: initialModeState.relayUrl,
1632
+ apiKey,
1633
+ machineId,
1634
+ agentType: "claude_code",
1635
+ getSessionId: () => sessionId,
1636
+ onCommand: (command) => onPhoneCommand(command),
1637
+ onFatal: (message) => verboseLog(`[pushary] relay unavailable (${message}); using polling`),
1638
+ log: verboseLog
1639
+ });
1640
+ relay.start();
1641
+ };
1642
+ let ownership;
1643
+ let ownershipSessionId;
1644
+ const ensureOwnership = async () => {
1645
+ if (!sessionId || ownershipSessionId === sessionId) return ownership?.ready;
1646
+ ownership?.stop();
1647
+ ownershipSessionId = sessionId;
1648
+ ownership = startProcessOwnership({
1649
+ sessionId,
1650
+ pid: process.pid,
1651
+ ...process.env.PUSHARY_SPAWN_ID ? { spawnId: process.env.PUSHARY_SPAWN_ID } : {}
1652
+ });
1653
+ await ownership.ready;
1654
+ };
1655
+ const announcePresence = async (event, action) => {
1656
+ if (!sessionId) return Promise.resolve();
1657
+ try {
1658
+ await ensureOwnership();
1659
+ ownership?.touch();
1660
+ await reportEvent(
1661
+ { event, agentType: "claude_code", agentName, action, sessionId, machineId, pid: process.pid },
1662
+ { maxAttempts: event === "session_start" ? 4 : 2, timeoutMs: 4e3, requireOk: event === "session_start" }
1663
+ );
1664
+ } catch {
1665
+ }
1666
+ };
1667
+ let presenceHeartbeat;
1668
+ let transcriptStarted;
1669
+ const ensureTranscriptSync = () => {
1670
+ if (transcriptStarted === sessionId || !transcriptsEnabled() || !sessionId) return;
1671
+ transcriptStarted = sessionId;
1672
+ registerTranscript("claude", { session_id: sessionId, cwd: process.cwd() });
1673
+ };
1674
+ const ensurePresenceHeartbeat = () => {
1675
+ if (presenceHeartbeat || !sessionId) return;
1676
+ presenceHeartbeat = setInterval(
1677
+ () => void announcePresence("heartbeat", "Session online"),
1678
+ PRESENCE_HEARTBEAT_MS
1679
+ );
1680
+ presenceHeartbeat.unref();
1681
+ };
1682
+ const setSessionId = (id) => {
1683
+ const isNew = sessionId !== id;
1684
+ sessionId = id;
1685
+ sessionCreated = true;
1686
+ relay?.reannounce();
1687
+ if (isNew) void announcePresence("session_start", "Remote session ready \u2014 reachable from your phone");
1688
+ ensurePresenceHeartbeat();
1689
+ ensureTranscriptSync();
1690
+ };
1691
+ const onPhoneCommand = async (command) => {
1692
+ if (controlMode !== "remote") return false;
1693
+ if (!sessionId || finished || !await ensureSdk() || !remoteAvailable) {
1694
+ if (!noticedUnavailable) {
1695
+ noticedUnavailable = true;
1696
+ process.stderr.write("[pushary] phone control is unavailable this run; the instruction was not delivered.\n");
1697
+ }
1698
+ return false;
1699
+ }
1700
+ if (finished || !input.push(userMessage(command))) return false;
1701
+ activeLeg?.onPhoneCommand();
1702
+ return true;
1703
+ };
1704
+ let draft;
1705
+ const streamMessage = (message) => {
1706
+ draft?.add(message);
1707
+ if (message.type === "assistant") {
1708
+ const text = extractAssistantText(message);
1709
+ if (text) process.stdout.write(`${text}
1710
+ `);
1711
+ } else if (message.type === "result") {
1712
+ if (typeof message.total_cost_usd === "number") totalCostUsd = message.total_cost_usd;
1713
+ process.stderr.write("[pushary] turn complete \u2014 standing by for phone commands\n");
1714
+ void announcePresence("stop", "Turn complete");
1715
+ }
1716
+ };
1717
+ const declineTakeover = (reason, message) => {
1718
+ takeoverDecline = reason;
1719
+ process.stderr.write(`[pushary] ${message}
1720
+ `);
1721
+ };
1722
+ const takeOver = async () => {
1723
+ if (controlMode !== "local" || takeoverPending || finished) return;
1724
+ if (compatibilityIssue) {
1725
+ declineTakeover("unsupported_flags", compatibilityIssue);
1726
+ return;
1727
+ }
1728
+ takeoverPending = true;
1729
+ try {
1730
+ if (!sessionId || !existsSync2(claudeTranscriptPath(process.cwd(), sessionId))) {
1731
+ declineTakeover("no_transcript", "phone takeover needs a turn in this session first; your terminal and queued instructions are unchanged.");
1732
+ return;
1733
+ }
1734
+ if (!await ensureSdk()) {
1735
+ declineTakeover("sdk_unavailable", "phone takeover could not start the Agent SDK; your terminal and queued instructions are unchanged.");
1736
+ return;
1737
+ }
1738
+ if (!finished && controlMode === "local") activeLeg?.onPhoneCommand();
1739
+ } finally {
1740
+ takeoverPending = false;
1741
+ }
1742
+ };
1743
+ const poller = startCommandPoller({
1744
+ apiKey,
1745
+ getSessionId: () => sessionId,
1746
+ onCommand: (command) => onPhoneCommand(command),
1747
+ shouldDrainCommands: () => controlMode === "remote" && Boolean(sessionId) && !(relay?.connected() ?? false),
1748
+ // Unified poll: one request per tick for both command + mode/kill (half the
1749
+ // requests and the per-poll DB write of the old drain + mode pair).
1750
+ poll: async (key, id, wantCommand, signal) => {
1751
+ const declined = takeoverDecline;
1752
+ takeoverDecline = void 0;
1753
+ const result = await pollAgent(key, id, wantCommand, signal, void 0, machineId, controlMode, declined);
1754
+ if (result.takeoverRequested) void takeOver();
1755
+ return result;
1756
+ },
1757
+ fetchModeState,
1758
+ // Observe explicit takeover promptly even when the command queue is idle.
1759
+ // Not faster than this: the poll runs for the life of every wrapper, including
1760
+ // local mode where it cannot return a command, and each tick costs a handful
1761
+ // of Redis round trips. Takeover already sits behind a confirm and a biometric.
1762
+ slowPollMs: 15e3,
1763
+ onAuthRejected: () => {
1764
+ process.stderr.write(
1765
+ "[pushary] this key was rejected \u2014 remote stop and phone messages are NOT active; run `pushary login` to reconnect\n"
1766
+ );
1767
+ },
1768
+ onModeState: (state) => {
1769
+ if (state.kill && !killed) {
1770
+ killed = true;
1771
+ finished = true;
1772
+ process.stderr.write("[pushary] halted from Pushary \u2014 stopping the agent\n");
1773
+ activeLeg?.stop("kill");
1774
+ }
1775
+ },
1776
+ log: verboseLog
1777
+ });
1778
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
1779
+ const onSignal = () => {
1780
+ finished = true;
1781
+ activeLeg?.stop("signal");
1782
+ };
1783
+ for (const signal of signals) process.on(signal, onSignal);
1784
+ const restoreStdin = () => stdin.dispose();
1785
+ process.once("exit", restoreStdin);
1786
+ const session = {
1787
+ apiKey,
1788
+ machineId,
1789
+ projectName,
1790
+ agentName,
1791
+ binary,
1792
+ input,
1793
+ approver,
1794
+ stdin,
1795
+ claudeArgs: args2,
1796
+ permissionMode,
1797
+ userManagesSession,
1798
+ getSessionId: () => sessionId,
1799
+ setSessionId,
1800
+ isSessionCreated: () => sessionCreated,
1801
+ markSessionCreated: () => {
1802
+ sessionCreated = true;
1803
+ },
1804
+ relayConnected: () => relay?.connected() ?? false,
1805
+ streamMessage,
1806
+ setActiveLeg: (control) => {
1807
+ activeLeg = control;
1808
+ }
1809
+ };
1810
+ try {
1811
+ if (sessionId && startingMode === "local") {
1812
+ process.stderr.write(
1813
+ "[pushary] phone messages queue for the next turn. Choose Continue on phone in the app for idle control (Ctrl-] returns the terminal).\n"
1814
+ );
1815
+ void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
1816
+ ensurePresenceHeartbeat();
1817
+ ensureTranscriptSync();
1818
+ void ensureSdk();
1819
+ }
1820
+ if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
1821
+ const exitCode = await driveModeLoop({
1822
+ startingMode,
1823
+ runLocal: async () => {
1824
+ controlMode = "local";
1825
+ await draft?.stop();
1826
+ draft = void 0;
1827
+ relay?.stop();
1828
+ relay = void 0;
1829
+ return runLocalLeg(session);
1830
+ },
1831
+ runRemote: async () => {
1832
+ controlMode = "remote";
1833
+ startRelay();
1834
+ const ready = await ensureSdk();
1835
+ if (!ready) {
1836
+ remoteAvailable = false;
1837
+ const dropped = input.clear();
1838
+ if (!noticedUnavailable) {
1839
+ noticedUnavailable = true;
1840
+ process.stderr.write(
1841
+ "[pushary] could not start phone control (offline?); staying in the terminal. Relaunch when online to enable.\n"
1842
+ );
1843
+ }
1844
+ if (dropped > 0) {
1845
+ process.stderr.write(
1846
+ `[pushary] ${dropped} instruction(s) from your phone could not be delivered and were dropped.
1847
+ `
1848
+ );
1849
+ }
1850
+ return "switch";
1851
+ }
1852
+ draft = startTranscriptDraft(apiKey, () => sessionId);
1853
+ return runRemoteLeg(session, ready);
1854
+ },
1855
+ isFinished: () => finished,
1856
+ minDwellMs: MIN_MODE_DWELL_MS
1857
+ });
1858
+ return { implemented: true, exitCode };
1859
+ } finally {
1860
+ for (const signal of signals) process.off(signal, onSignal);
1861
+ process.removeListener("exit", restoreStdin);
1862
+ await draft?.stop();
1863
+ relay?.stop();
1864
+ poller.stop();
1865
+ if (presenceHeartbeat) clearInterval(presenceHeartbeat);
1866
+ approver.teardown();
1867
+ input.close();
1868
+ stdin.dispose();
1869
+ if (sessionId) registerTranscript("claude", { session_id: sessionId, cwd: process.cwd() }, void 0, { ended: true });
1870
+ await announcePresence("session_closed", "Session ended");
1871
+ ownership?.stop();
1872
+ if (totalCostUsd > 0) {
1873
+ process.stderr.write(`[pushary] session cost: $${totalCostUsd.toFixed(4)}
1874
+ `);
1875
+ }
1876
+ }
1877
+ };
1878
+
1879
+ // src/wrapper/runClaudeWrapper.ts
1880
+ var hasPrintFlag = (args2) => args2.some((a) => a === "-p" || a === "--print" || a.startsWith("--print="));
1881
+ var isInteractiveTty = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
1882
+ var runClaudeWrapper = async (args2) => {
1883
+ const binary = findClaudeBinary();
1884
+ if (!binary) {
1885
+ process.stderr.write(
1886
+ "[pushary] Could not find the `claude` binary on your PATH. Install Claude Code, or set PUSHARY_CLAUDE_BIN to its full path.\n"
1887
+ );
1888
+ return 127;
1889
+ }
1890
+ const nested = process.env[WRAPPER_ACTIVE_ENV] === "1";
1891
+ const { remote: wantRemote, rest } = extractRemoteFlag(args2);
1892
+ const useDual = !nested && (wantRemote || isInteractiveTty() && !hasPrintFlag(rest));
1893
+ if (useDual) {
1894
+ try {
1895
+ const result = await runDualMode(binary, rest, wantRemote ? "remote" : "local");
1896
+ if (result.implemented) return result.exitCode ?? 0;
1897
+ } catch {
1898
+ }
1899
+ }
1900
+ return runLocalPassthrough(binary, rest);
1901
+ };
1902
+
1903
+ // bin/pushary-claude.ts
1904
+ var args = resolveClaudeArgs(process.argv);
1905
+ var spawnId = process.env.PUSHARY_SPAWN_ID?.trim() || void 0;
1906
+ var code = await withSpawnProcessOwnership(spawnId, async () => {
1907
+ await ensureDaemonRunning();
1908
+ return runClaudeWrapper(args);
1909
+ }).catch((err) => {
1910
+ process.stderr.write(
1911
+ `[pushary] wrapper error: ${err instanceof Error ? err.message : String(err)}
1912
+ `
1913
+ );
1914
+ return 1;
1915
+ });
1916
+ process.exit(code);