@pushary/agent-hooks 0.89.10 → 0.89.12

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 (78) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +3 -2
  3. package/dist/bin/pushary-bell-hook.js +5 -5
  4. package/dist/bin/pushary-bell.js +11 -11
  5. package/dist/bin/pushary-claude.js +125 -36
  6. package/dist/bin/pushary-clean.js +39 -28
  7. package/dist/bin/pushary-codex-bridge.js +17 -10
  8. package/dist/bin/pushary-codex-hook.js +12 -11
  9. package/dist/bin/pushary-codex.js +8 -8
  10. package/dist/bin/pushary-connect.js +12 -12
  11. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  12. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  13. package/dist/bin/pushary-daemon.js +22 -22
  14. package/dist/bin/pushary-disconnect.js +9 -9
  15. package/dist/bin/pushary-doctor.js +59 -49
  16. package/dist/bin/pushary-elicitation-hook.js +5 -5
  17. package/dist/bin/pushary-gemini-bridge.js +17 -9
  18. package/dist/bin/pushary-gemini-hook.js +11 -10
  19. package/dist/bin/pushary-hook.js +14 -14
  20. package/dist/bin/pushary-login.js +11 -11
  21. package/dist/bin/pushary-logout.js +11 -11
  22. package/dist/bin/pushary-mode.js +5 -5
  23. package/dist/bin/pushary-notification-hook.js +8 -8
  24. package/dist/bin/pushary-opencode-hook.js +11 -10
  25. package/dist/bin/pushary-permission-denied-hook.js +12 -12
  26. package/dist/bin/pushary-permission-hook.js +12 -12
  27. package/dist/bin/pushary-post-hook.js +8 -8
  28. package/dist/bin/pushary-prompt-hook.js +8 -8
  29. package/dist/bin/pushary-session-end-hook.js +8 -8
  30. package/dist/bin/pushary-session-start-hook.js +8 -8
  31. package/dist/bin/pushary-setup.js +88 -60
  32. package/dist/bin/pushary-stats.js +4 -4
  33. package/dist/bin/pushary-status.js +14 -14
  34. package/dist/bin/pushary-stop-hook.js +8 -8
  35. package/dist/bin/pushary-stopfailure-hook.js +8 -8
  36. package/dist/bin/pushary-suggestions.js +2 -2
  37. package/dist/bin/pushary-upgrade.js +21 -21
  38. package/dist/bin/pushary-wait.js +5 -5
  39. package/dist/bin/pushary.js +1 -1
  40. package/dist/{chunk-253WC6O5.js → chunk-327YMKP5.js} +1 -1
  41. package/dist/{chunk-6XUABEX2.js → chunk-3OXHYPZG.js} +1 -1
  42. package/dist/{chunk-MZGWKIUZ.js → chunk-3XSP5SML.js} +7 -3
  43. package/dist/{chunk-XX2V4QS5.js → chunk-4LL6LG5X.js} +2 -2
  44. package/dist/{chunk-DKWM5T3P.js → chunk-5NDB3YOE.js} +2 -2
  45. package/dist/{chunk-TEZ2FZD6.js → chunk-6GU55AQQ.js} +2 -2
  46. package/dist/{chunk-Q223OM5J.js → chunk-6V7ZDH3A.js} +4 -4
  47. package/dist/{chunk-OLAGKKSR.js → chunk-6VXNA5JB.js} +16 -6
  48. package/dist/{chunk-34GN3IPX.js → chunk-7DK3ZH3N.js} +1 -1
  49. package/dist/{chunk-PZRWFWSR.js → chunk-7HYJPOGG.js} +7 -7
  50. package/dist/{chunk-TY5HVMXT.js → chunk-DHR4F2CR.js} +1 -1
  51. package/dist/{chunk-3HFK3UXM.js → chunk-FCUUVHTS.js} +1 -1
  52. package/dist/{chunk-NCVIWGVA.js → chunk-GKPURU7P.js} +1 -1
  53. package/dist/{chunk-UXHMWF4H.js → chunk-JHJHCMAF.js} +1 -1
  54. package/dist/chunk-JJV5Q4KG.js +20 -0
  55. package/dist/{chunk-TG3T3M4K.js → chunk-JVZX5YRJ.js} +6 -2
  56. package/dist/{chunk-6EKQZA6R.js → chunk-KVHP5ZME.js} +1 -1
  57. package/dist/{chunk-X4ZUUFKV.js → chunk-L4ZCFMWW.js} +63 -1
  58. package/dist/{chunk-2LPEZYMC.js → chunk-MFNQ5IMW.js} +1 -1
  59. package/dist/{chunk-KVE5RCQL.js → chunk-NBRM3FMD.js} +1 -1
  60. package/dist/{chunk-FZ73G2RO.js → chunk-NESKLV6R.js} +25 -6
  61. package/dist/{chunk-GTZLL7F4.js → chunk-OADGBOP7.js} +5 -5
  62. package/dist/{chunk-GSSFSVAQ.js → chunk-OCMOTJTW.js} +42 -24
  63. package/dist/{chunk-LURY4WNF.js → chunk-OTPJ4FUR.js} +1 -1
  64. package/dist/{chunk-QA72MKIT.js → chunk-OVEGEQSB.js} +5 -5
  65. package/dist/{chunk-3SQYKLBW.js → chunk-PS7DQ2SU.js} +46 -18
  66. package/dist/{chunk-UDBJHQMG.js → chunk-QFJZHVVL.js} +1 -1
  67. package/dist/chunk-QZCFVWA7.js +58 -0
  68. package/dist/{chunk-3XTFX4CL.js → chunk-RLQWNEPM.js} +3 -2
  69. package/dist/{chunk-3CUDMTL4.js → chunk-RNKPLUWC.js} +10 -3
  70. package/dist/{chunk-SYUIXXQW.js → chunk-VF3RTVWY.js} +1 -1
  71. package/dist/{chunk-TRVIRETT.js → chunk-VPLQFXCH.js} +1 -1
  72. package/dist/{chunk-IUN6VOKV.js → chunk-WDVQ6O4C.js} +2 -42
  73. package/dist/{chunk-IUWRVTQK.js → chunk-XREW7RQ3.js} +1 -1
  74. package/dist/{chunk-BLODMQKJ.js → chunk-Y4WVZIEI.js} +1 -1
  75. package/dist/src/index.d.ts +8 -1
  76. package/dist/src/index.js +16 -12
  77. package/package.json +2 -1
  78. /package/dist/{chunk-X6RXX2NF.js → chunk-3MCEES6Z.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.89.12
4
+
5
+ ### Phone control installs as a native service, without a VM
6
+
7
+ Fresh setup enables phone-start when a supported provider and the operating
8
+ system's own per-user service manager are available: launchd on macOS, systemd
9
+ on Linux, and Task Scheduler on Windows. Existing installs keep their stored
10
+ choice. Service replacement is transactional, verifies the new process before
11
+ committing, and restores the previous running or stopped state on failure.
12
+
13
+ Claude Code, Codex and Gemini sessions started from the phone now use the same
14
+ supervised control path. Setup checks both the local heartbeat and the server's
15
+ view of the machine before calling control ready.
16
+
17
+ ### Fresh and partial installs repair themselves
18
+
19
+ Setup now verifies every global command, its local JavaScript imports, and the
20
+ Cursor and VS Code runtime assets before reusing an installed version. A
21
+ same-version package missing any of them is reinstalled instead of failing the
22
+ same setup forever. Fresh installs also cover Codex on Windows and recover from
23
+ partially configured providers without disturbing unrelated settings.
24
+
25
+ ### The CLI and macOS app keep their own wiring
26
+
27
+ Cursor setup writes one gate per event even when the other installer ran first.
28
+ `pushary clean` removes the CLI gate while preserving the app's bridge, and
29
+ `pushary doctor` validates the MCP entry from the installer that actually wired
30
+ Cursor. This keeps mixed CLI/app machines working and avoids false repair
31
+ failures on app-only installs.
32
+
33
+ ### Transcript control works in every shell
34
+
35
+ Setup discloses encrypted transcript sync and its audited compliance-recovery
36
+ path before enabling it for a new install; older installs remain off until setup
37
+ runs again. `pushary setup --transcripts on|off` now provides the same persistent
38
+ choice in POSIX shells, PowerShell and Command Prompt. The existing
39
+ `PUSHARY_TRANSCRIPTS` environment override remains compatible.
40
+
41
+ ## 0.89.11
42
+
43
+ ### Phone-spawned Codex and Gemini sessions record their conversation again
44
+
45
+ Both bridges relay their turns by default and stop only when told to. The
46
+ launchers told them to stop on every run: the value they passed was an equality
47
+ test against an environment variable nobody sets, which is false whenever it is
48
+ absent and never the "unset" the bridge was looking for. The relay was therefore
49
+ never built, and a session started from the phone had nothing to show but its
50
+ task title. The opt-out is now passed only when it is asked for.
51
+
52
+ ### An agent standing by stops reading as working
53
+
54
+ `pushary claude` and the Gemini bridge reported that a session had started, that
55
+ it was still alive, and that it had ended, with nothing in between. Their
56
+ heartbeat kept resetting the clock that would otherwise have aged the session out
57
+ of "active", so the phone showed a working agent for one that had been waiting at
58
+ its prompt since its last reply. That is exactly when someone is looking at the
59
+ screen deciding whether to send it something. Both now report the turn boundary
60
+ they already detect, as the Codex bridge always has.
61
+
62
+ ### Taking a session over from your phone says so, and is ready when you ask
63
+
64
+ An instruction from the phone hands the terminal to Pushary so it can drive an
65
+ agent that would otherwise sit idle. That has not changed. The handover is now
66
+ announced before the session is signalled, instead of the terminal appearing to
67
+ lose its agent for no visible reason. The Agent SDK is resolved when the session
68
+ starts rather than during the handover, so the wait is gone, and a machine that
69
+ cannot reach npm reports the instruction as undelivered instead of accepting it
70
+ and then dropping it. An instruction that is dropped now says it was.
71
+
3
72
  ## 0.89.10
4
73
 
5
74
  ### Phone control delivery is crash-safe
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@pushary/agent-hooks.svg)](https://www.npmjs.com/package/@pushary/agent-hooks)
5
5
  [![license](https://img.shields.io/npm/l/@pushary/agent-hooks.svg)](./LICENSE)
6
6
 
7
- Push notifications, questions, and permission approvals for AI coding agents. Get a push when your agent finishes work, answer its questions from your phone, and approve risky commands before they run. Works with Claude Code, Codex, Cursor, VS Code, Gemini CLI, and Hermes.
7
+ Push notifications, questions, and permission approvals for AI agents. Get a push when your agent finishes work, answer its questions from your phone, and approve risky commands before they run. Works with Claude Code, Codex, Cursor, VS Code, OpenCode, Gemini CLI, and Hermes.
8
8
 
9
9
  Your agent froze waiting for a yes. This is how it reaches you.
10
10
 
@@ -110,6 +110,7 @@ npx @pushary/agent-hooks@latest doctor
110
110
  | Gemini CLI | yes | yes | yes |
111
111
  | Hermes | yes | yes | yes |
112
112
  | VS Code | yes | yes | yes |
113
+ | OpenCode | yes | yes | yes |
113
114
 
114
115
  Any other MCP client can use notify and ask by pointing at the Pushary MCP endpoint directly.
115
116
 
@@ -182,7 +183,7 @@ failing closed would let one malformed download silence every hook on the machin
182
183
  asserts this list matches it. Do not edit one without the other. -->
183
184
 
184
185
  ```
185
- setup Configure Claude Code, Codex, Gemini CLI, Hermes, Cursor, or VS Code with Pushary
186
+ setup Configure Claude Code, Codex, Gemini CLI, Hermes, Cursor, VS Code, or OpenCode with Pushary
186
187
  claude Run Claude Code through Pushary, reachable from your phone even when idle
187
188
  daemon Keep this machine reachable so your phone can START a new session
188
189
  status One screen: which key is in force, whether the server accepts it, what can receive an approval
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-TRVIRETT.js";
4
+ } from "../chunk-VPLQFXCH.js";
5
5
  import {
6
6
  readHookInput
7
7
  } from "../chunk-6CUUA7HO.js";
8
- import "../chunk-3CUDMTL4.js";
8
+ import "../chunk-RNKPLUWC.js";
9
9
  import "../chunk-BC3VCZ3E.js";
10
- import "../chunk-IUWRVTQK.js";
11
- import "../chunk-XX2V4QS5.js";
10
+ import "../chunk-XREW7RQ3.js";
11
+ import "../chunk-4LL6LG5X.js";
12
12
  import "../chunk-2UMNXADU.js";
13
- import "../chunk-X6RXX2NF.js";
13
+ import "../chunk-3MCEES6Z.js";
14
14
 
15
15
  // bin/pushary-bell-hook.ts
16
16
  var main = async () => {
@@ -5,38 +5,38 @@ import {
5
5
  liveAgentCount,
6
6
  ring,
7
7
  upgradeThreshold
8
- } from "../chunk-TRVIRETT.js";
8
+ } from "../chunk-VPLQFXCH.js";
9
9
  import {
10
10
  guardBinary
11
- } from "../chunk-SYUIXXQW.js";
12
- import "../chunk-X4ZUUFKV.js";
11
+ } from "../chunk-VF3RTVWY.js";
12
+ import "../chunk-L4ZCFMWW.js";
13
13
  import {
14
14
  claudeSettings
15
- } from "../chunk-QA72MKIT.js";
15
+ } from "../chunk-OVEGEQSB.js";
16
16
  import {
17
17
  createIo
18
18
  } from "../chunk-5RUIFDTP.js";
19
- import "../chunk-3CUDMTL4.js";
20
19
  import {
21
20
  parseFlags
22
21
  } from "../chunk-GMXKITVA.js";
23
22
  import {
24
23
  exitOnHelpFlag
25
- } from "../chunk-LURY4WNF.js";
26
- import "../chunk-3XTFX4CL.js";
27
- import "../chunk-BC3VCZ3E.js";
24
+ } from "../chunk-OTPJ4FUR.js";
25
+ import "../chunk-RLQWNEPM.js";
28
26
  import "../chunk-DINDL2CF.js";
29
- import "../chunk-IUWRVTQK.js";
27
+ import "../chunk-RNKPLUWC.js";
28
+ import "../chunk-BC3VCZ3E.js";
30
29
  import "../chunk-7QLSKOSU.js";
31
30
  import {
32
31
  EXIT
33
32
  } from "../chunk-KZERVKTD.js";
33
+ import "../chunk-XREW7RQ3.js";
34
34
  import {
35
35
  readJsonSafe,
36
36
  writeJsonAtomic
37
- } from "../chunk-XX2V4QS5.js";
37
+ } from "../chunk-4LL6LG5X.js";
38
38
  import "../chunk-2UMNXADU.js";
39
- import "../chunk-X6RXX2NF.js";
39
+ import "../chunk-3MCEES6Z.js";
40
40
 
41
41
  // src/bell/install.ts
42
42
  import { join } from "path";
@@ -1,24 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ensureDaemonRunning
4
- } from "../chunk-3SQYKLBW.js";
4
+ } from "../chunk-PS7DQ2SU.js";
5
5
  import {
6
6
  buildTranscriptRecords,
7
7
  decodePublicKey,
8
- fetchAccountPublicKey,
9
8
  generateSessionKey,
10
- uploadTranscriptRecords,
11
9
  wrapSessionKey
12
- } from "../chunk-IUN6VOKV.js";
10
+ } from "../chunk-WDVQ6O4C.js";
11
+ import {
12
+ fetchTranscriptRecipients,
13
+ uploadTranscriptRecords
14
+ } from "../chunk-QZCFVWA7.js";
15
+ import {
16
+ transcriptsEnabled
17
+ } from "../chunk-JJV5Q4KG.js";
13
18
  import {
14
19
  startProcessOwnership,
15
20
  withSpawnProcessOwnership
16
- } from "../chunk-TEZ2FZD6.js";
21
+ } from "../chunk-6GU55AQQ.js";
17
22
  import {
18
23
  needsShell,
19
24
  spawnClaude
20
25
  } from "../chunk-XHKBHWLX.js";
21
- import "../chunk-X4ZUUFKV.js";
26
+ import "../chunk-L4ZCFMWW.js";
22
27
  import {
23
28
  isDeferAnswer
24
29
  } from "../chunk-YHG74UFF.js";
@@ -27,7 +32,7 @@ import {
27
32
  getPolicy,
28
33
  repoKeyFor,
29
34
  reportEvent
30
- } from "../chunk-GTZLL7F4.js";
35
+ } from "../chunk-OADGBOP7.js";
31
36
  import {
32
37
  DEFAULT_SESSION,
33
38
  askUser,
@@ -39,23 +44,26 @@ import {
39
44
  removePendingCommand,
40
45
  savePendingCommand,
41
46
  waitForAnswer
42
- } from "../chunk-UXHMWF4H.js";
47
+ } from "../chunk-JHJHCMAF.js";
43
48
  import {
49
+ UNNAMED_ACTION,
44
50
  deriveToolTarget,
45
51
  describeToolCall,
46
52
  scopePathFor
47
- } from "../chunk-TG3T3M4K.js";
53
+ } from "../chunk-JVZX5YRJ.js";
48
54
  import {
49
55
  getMachineId
50
- } from "../chunk-DKWM5T3P.js";
51
- import "../chunk-QA72MKIT.js";
56
+ } from "../chunk-5NDB3YOE.js";
57
+ import "../chunk-OVEGEQSB.js";
52
58
  import "../chunk-DINDL2CF.js";
59
+ import "../chunk-RNKPLUWC.js";
60
+ import "../chunk-BC3VCZ3E.js";
53
61
  import "../chunk-BSZYIAZL.js";
54
62
  import "../chunk-SAF6HGAA.js";
55
- import "../chunk-IUWRVTQK.js";
56
63
  import "../chunk-7QLSKOSU.js";
57
64
  import "../chunk-KZERVKTD.js";
58
- import "../chunk-XX2V4QS5.js";
65
+ import "../chunk-XREW7RQ3.js";
66
+ import "../chunk-4LL6LG5X.js";
59
67
  import {
60
68
  getApiKey,
61
69
  getBaseUrl
@@ -63,7 +71,7 @@ import {
63
71
  import {
64
72
  KILL_REASON,
65
73
  resolveGate
66
- } from "../chunk-X6RXX2NF.js";
74
+ } from "../chunk-3MCEES6Z.js";
67
75
 
68
76
  // src/wrapper/claudeBinary.ts
69
77
  import { statSync } from "fs";
@@ -142,7 +150,6 @@ import { basename } from "path";
142
150
  import { homedir } from "os";
143
151
  import { join as join2, resolve } from "path";
144
152
  import { existsSync, readFileSync } from "fs";
145
- var transcriptsEnabled = () => process.env.PUSHARY_TRANSCRIPTS === "1";
146
153
  var claudeTranscriptPath = (cwd, sessionId) => {
147
154
  const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
148
155
  const configDir = process.env.CLAUDE_CONFIG_DIR || join2(homedir(), ".claude");
@@ -151,6 +158,8 @@ var claudeTranscriptPath = (cwd, sessionId) => {
151
158
  var createSyncState = () => ({
152
159
  sessionKey: null,
153
160
  wrappedKey: null,
161
+ providerWrappedKey: null,
162
+ providerKeyId: null,
154
163
  seen: /* @__PURE__ */ new Set(),
155
164
  keyUploaded: false,
156
165
  keyRejected: false
@@ -159,30 +168,77 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
159
168
  if (state.keyRejected) return state;
160
169
  let sessionKey = state.sessionKey;
161
170
  let wrappedKey = state.wrappedKey;
171
+ let providerWrappedKey = state.providerWrappedKey;
172
+ let providerKeyId = state.providerKeyId;
162
173
  if (!sessionKey) {
163
- const publicKey = await uploader.fetchAccountPublicKey();
164
- if (!publicKey) return state;
174
+ const recipients = await uploader.fetchTranscriptRecipients();
175
+ if (!recipients) return state;
165
176
  sessionKey = generateSessionKey();
166
- wrappedKey = wrapSessionKey(sessionKey, decodePublicKey(publicKey));
177
+ wrappedKey = wrapSessionKey(sessionKey, decodePublicKey(recipients.phonePublicKey));
178
+ if (recipients.provider) {
179
+ providerWrappedKey = wrapSessionKey(sessionKey, decodePublicKey(recipients.provider.publicKey));
180
+ providerKeyId = recipients.provider.keyId;
181
+ }
167
182
  }
168
183
  const built = buildTranscriptRecords(lines, sessionKey, state.seen);
169
184
  if (built.records.length === 0) {
170
- return { ...state, sessionKey, wrappedKey, seen: built.seen };
185
+ return { ...state, sessionKey, wrappedKey, providerWrappedKey, providerKeyId, seen: built.seen };
186
+ }
187
+ const seen = new Set(state.seen);
188
+ let keyUploaded = state.keyUploaded;
189
+ let keyRejected = false;
190
+ let rejected = 0;
191
+ for (const chunk of chunkRecords(built.records)) {
192
+ const result = await uploader.uploadRecords({
193
+ sessionId,
194
+ machineId,
195
+ dataEncryptionKey: keyUploaded ? void 0 : wrappedKey ?? void 0,
196
+ providerWrappedSessionKey: keyUploaded ? void 0 : providerWrappedKey ?? void 0,
197
+ providerKeyId: keyUploaded ? void 0 : providerKeyId ?? void 0,
198
+ records: chunk
199
+ });
200
+ keyUploaded = keyUploaded || result.keyRegistered;
201
+ if (result.keyConflict) {
202
+ keyRejected = true;
203
+ break;
204
+ }
205
+ if (!result.ok) break;
206
+ for (const id of result.settledIds ?? chunk.map((r) => r.localId)) seen.add(id);
207
+ rejected += result.rejectedIds.length;
208
+ }
209
+ if (rejected > 0) {
210
+ process.stderr.write(`[pushary] transcript: ${rejected} record${rejected === 1 ? "" : "s"} could not be stored and will not be retried.
211
+ `);
171
212
  }
172
- const result = await uploader.uploadRecords({
173
- sessionId,
174
- machineId,
175
- dataEncryptionKey: state.keyUploaded ? void 0 : wrappedKey ?? void 0,
176
- records: built.records
177
- });
178
213
  return {
179
214
  sessionKey,
180
215
  wrappedKey,
181
- seen: result.ok ? built.seen : state.seen,
182
- keyUploaded: state.keyUploaded || result.keyRegistered,
183
- keyRejected: result.keyConflict
216
+ providerWrappedKey,
217
+ providerKeyId,
218
+ seen,
219
+ keyUploaded,
220
+ keyRejected
184
221
  };
185
222
  };
223
+ var MAX_RECORDS_PER_BATCH = 200;
224
+ var MAX_BATCH_BYTES = 3e6;
225
+ var chunkRecords = (records) => {
226
+ const chunks = [];
227
+ let current = [];
228
+ let bytes = 0;
229
+ for (const record of records) {
230
+ const size = record.content.length;
231
+ if (current.length > 0 && (current.length >= MAX_RECORDS_PER_BATCH || bytes + size > MAX_BATCH_BYTES)) {
232
+ chunks.push(current);
233
+ current = [];
234
+ bytes = 0;
235
+ }
236
+ current.push(record);
237
+ bytes += size;
238
+ }
239
+ if (current.length > 0) chunks.push(current);
240
+ return chunks;
241
+ };
186
242
  var readLines = (path) => {
187
243
  try {
188
244
  return existsSync(path) ? readFileSync(path, "utf-8").split("\n") : [];
@@ -190,9 +246,11 @@ var readLines = (path) => {
190
246
  return [];
191
247
  }
192
248
  };
249
+ var IDLE_CEILING_MS = 6e4;
250
+ var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
193
251
  var startTranscriptSync = (opts) => {
194
252
  const uploader = {
195
- fetchAccountPublicKey: () => fetchAccountPublicKey(opts.apiKey),
253
+ fetchTranscriptRecipients: () => fetchTranscriptRecipients(opts.apiKey),
196
254
  uploadRecords: (params) => uploadTranscriptRecords(opts.apiKey, params)
197
255
  };
198
256
  const path = claudeTranscriptPath(opts.cwd, opts.sessionId);
@@ -209,12 +267,26 @@ var startTranscriptSync = (opts) => {
209
267
  inFlight = false;
210
268
  }
211
269
  };
212
- const timer = setInterval(() => void tick(), opts.intervalMs ?? 3e3);
213
- if (typeof timer.unref === "function") timer.unref();
270
+ const baseIntervalMs = opts.intervalMs ?? 3e3;
271
+ const maxIntervalMs = Math.max(baseIntervalMs, IDLE_CEILING_MS);
272
+ let timer;
273
+ const schedule = (delayMs) => {
274
+ if (!running) return;
275
+ timer = setTimeout(() => {
276
+ void (async () => {
277
+ const before = state.sessionKey;
278
+ await tick();
279
+ const idle = state.sessionKey === null && before === null;
280
+ schedule(nextInterval(delayMs, idle, baseIntervalMs, maxIntervalMs));
281
+ })();
282
+ }, delayMs);
283
+ if (typeof timer.unref === "function") timer.unref();
284
+ };
214
285
  void tick();
286
+ schedule(baseIntervalMs);
215
287
  return () => {
216
288
  running = false;
217
- clearInterval(timer);
289
+ if (timer) clearTimeout(timer);
218
290
  };
219
291
  };
220
292
 
@@ -342,9 +414,13 @@ var BoundedInputQueue = class {
342
414
  }
343
415
  // Drop every buffered item without closing the queue. Used when phone control
344
416
  // turns out to be unavailable (offline SDK setup) so a pending instruction does
345
- // not make the local leg switch again into a mode that cannot run.
417
+ // not make the local leg switch again into a mode that cannot run. Returns how
418
+ // many were dropped, because these are somebody's words and the caller has to
419
+ // be able to say they are gone.
346
420
  clear() {
421
+ const dropped = this.buffer.length;
347
422
  this.buffer.length = 0;
423
+ return dropped;
348
424
  }
349
425
  get isClosed() {
350
426
  return this.closed;
@@ -482,7 +558,7 @@ var createRemoteApprover = (deps) => {
482
558
  let question;
483
559
  try {
484
560
  question = await askUser2(deps.apiKey, {
485
- question: `Allow ${description}?`,
561
+ question: `Allow ${description || UNNAMED_ACTION}?`,
486
562
  type: "confirm",
487
563
  context: `Your agent wants to run this in ${deps.projectName} (you are away)`,
488
564
  agentName: `Claude Code - ${deps.projectName}`,
@@ -1128,7 +1204,12 @@ var runLocalLeg = (session, deps = {}) => {
1128
1204
  const control = {
1129
1205
  onPhoneCommand: () => {
1130
1206
  if (!exitReason) exitReason = "switch";
1131
- if (!legAbort.signal.aborted) legAbort.abort();
1207
+ if (!legAbort.signal.aborted) {
1208
+ process.stderr.write(
1209
+ "\n[pushary] instruction from your phone \u2014 taking this session over to run it (Ctrl-] hands the terminal back)\n"
1210
+ );
1211
+ legAbort.abort();
1212
+ }
1132
1213
  },
1133
1214
  stop: (reason) => {
1134
1215
  if (!exitReason) exitReason = { type: "exit", code: reason === "signal" ? 130 : 0 };
@@ -1482,6 +1563,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1482
1563
  } else if (message.type === "result") {
1483
1564
  if (typeof message.total_cost_usd === "number") totalCostUsd = message.total_cost_usd;
1484
1565
  process.stderr.write("[pushary] turn complete \u2014 standing by for phone commands\n");
1566
+ void announcePresence("stop", "Turn complete");
1485
1567
  }
1486
1568
  };
1487
1569
  const poller = startCommandPoller({
@@ -1546,6 +1628,7 @@ var runDualMode = async (binary, args2, startingMode) => {
1546
1628
  void announcePresence("session_start", "Session ready \u2014 reachable from your phone");
1547
1629
  ensurePresenceHeartbeat();
1548
1630
  ensureTranscriptSync();
1631
+ void ensureSdk();
1549
1632
  }
1550
1633
  if (startingMode === "remote" && initialPrompt) input.push(userMessage(initialPrompt));
1551
1634
  const exitCode = await driveModeLoop({
@@ -1555,13 +1638,19 @@ var runDualMode = async (binary, args2, startingMode) => {
1555
1638
  const ready = await ensureSdk();
1556
1639
  if (!ready) {
1557
1640
  remoteAvailable = false;
1558
- input.clear();
1641
+ const dropped = input.clear();
1559
1642
  if (!noticedUnavailable) {
1560
1643
  noticedUnavailable = true;
1561
1644
  process.stderr.write(
1562
1645
  "[pushary] could not start phone control (offline?); staying in the terminal. Relaunch when online to enable.\n"
1563
1646
  );
1564
1647
  }
1648
+ if (dropped > 0) {
1649
+ process.stderr.write(
1650
+ `[pushary] ${dropped} instruction(s) from your phone could not be delivered and were dropped.
1651
+ `
1652
+ );
1653
+ }
1565
1654
  return "switch";
1566
1655
  }
1567
1656
  return runRemoteLeg(session, ready);
@@ -3,13 +3,16 @@ import {
3
3
  HERMES_CONFIG_SCRIPT,
4
4
  resolveHermesPython,
5
5
  runHermesPythonScript
6
- } from "../chunk-UDBJHQMG.js";
6
+ } from "../chunk-QFJZHVVL.js";
7
7
  import {
8
8
  removeInstructionBlock,
9
9
  shortenHome,
10
10
  unregisterPluginLocation,
11
11
  vscodeSettingsTargets
12
- } from "../chunk-MZGWKIUZ.js";
12
+ } from "../chunk-3XSP5SML.js";
13
+ import {
14
+ pusharyHookExecutable
15
+ } from "../chunk-NESKLV6R.js";
13
16
  import {
14
17
  OPENCODE_PLUGIN_SIBLING,
15
18
  OPENCODE_PLUGIN_SIBLING_CONTENTS,
@@ -19,25 +22,25 @@ import {
19
22
  openCodePluginDir,
20
23
  openCodePluginFile,
21
24
  removeOpenCodeMcpServer
22
- } from "../chunk-34GN3IPX.js";
25
+ } from "../chunk-7DK3ZH3N.js";
23
26
  import {
24
27
  isClaudeMcpSnapshot,
25
28
  removeClaudeMcpServers,
26
29
  removeGeminiSettings,
27
30
  removePusharySettings,
28
31
  restoreClaudeMcpServer
29
- } from "../chunk-Q223OM5J.js";
30
- import "../chunk-6EKQZA6R.js";
31
- import "../chunk-SYUIXXQW.js";
32
+ } from "../chunk-6V7ZDH3A.js";
33
+ import "../chunk-KVHP5ZME.js";
34
+ import "../chunk-VF3RTVWY.js";
32
35
  import {
33
36
  disableDaemonService
34
- } from "../chunk-3SQYKLBW.js";
37
+ } from "../chunk-PS7DQ2SU.js";
35
38
  import "../chunk-XHKBHWLX.js";
36
39
  import {
37
40
  execNpm
38
- } from "../chunk-X4ZUUFKV.js";
39
- import "../chunk-TG3T3M4K.js";
40
- import "../chunk-DKWM5T3P.js";
41
+ } from "../chunk-L4ZCFMWW.js";
42
+ import "../chunk-JVZX5YRJ.js";
43
+ import "../chunk-5NDB3YOE.js";
41
44
  import {
42
45
  claudeJson,
43
46
  claudeMcpReceipt,
@@ -55,32 +58,33 @@ import {
55
58
  geminiMd,
56
59
  geminiSettings,
57
60
  hermesConfigReceipt,
61
+ isProtectedByScope,
58
62
  pusharyDir,
59
63
  removeCodexHookTrust,
60
64
  removeCodexHooks,
61
65
  vscodePluginDir
62
- } from "../chunk-QA72MKIT.js";
66
+ } from "../chunk-OVEGEQSB.js";
63
67
  import {
64
68
  rejectUnknownFlags
65
69
  } from "../chunk-GMXKITVA.js";
66
70
  import {
67
71
  exitOnHelpFlag
68
- } from "../chunk-LURY4WNF.js";
69
- import "../chunk-3XTFX4CL.js";
72
+ } from "../chunk-OTPJ4FUR.js";
73
+ import "../chunk-RLQWNEPM.js";
74
+ import "../chunk-DINDL2CF.js";
70
75
  import {
71
76
  removeClaudeAlias
72
77
  } from "../chunk-BC3VCZ3E.js";
73
- import "../chunk-DINDL2CF.js";
74
78
  import "../chunk-7QLSKOSU.js";
75
79
  import {
76
80
  EXIT
77
81
  } from "../chunk-KZERVKTD.js";
78
- import "../chunk-XX2V4QS5.js";
82
+ import "../chunk-4LL6LG5X.js";
79
83
  import "../chunk-2UMNXADU.js";
80
84
  import {
81
85
  CURSOR_GATE_EVENTS,
82
86
  HOOK_LOCATOR_BINARY
83
- } from "../chunk-X6RXX2NF.js";
87
+ } from "../chunk-3MCEES6Z.js";
84
88
 
85
89
  // bin/pushary-clean.ts
86
90
  import { existsSync, readFileSync, copyFileSync, readdirSync } from "fs";
@@ -146,9 +150,9 @@ var VSCODE_PLUGIN_DIR = vscodePluginDir();
146
150
  var PUSHARY_DIR = pusharyDir();
147
151
  var SHELL_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile"].map((f) => join(homedir(), f));
148
152
  var macAppInstalled = () => existsSync(join(PUSHARY_DIR, "bin", HOOK_LOCATOR_BINARY));
149
- var removalScope = () => ({
153
+ var removalScope = (keepMcpServer) => ({
150
154
  keepAppWiring: true,
151
- keepMcpServer: macAppInstalled()
155
+ keepMcpServer
152
156
  });
153
157
  var readJson = (path) => {
154
158
  try {
@@ -189,7 +193,7 @@ var guardedAliasRemoval = () => {
189
193
  var writeJson = (path, data) => {
190
194
  guardedWrite(path, JSON.stringify(data, null, 2) + "\n");
191
195
  };
192
- var removeCursorUserHook = () => {
196
+ var removeCursorUserHook = (scope) => {
193
197
  if (!existsSync(CURSOR_USER_HOOKS)) return "absent";
194
198
  let parsed;
195
199
  try {
@@ -202,7 +206,10 @@ var removeCursorUserHook = () => {
202
206
  let removedAny = false;
203
207
  for (const event of CURSOR_GATE_EVENTS) {
204
208
  const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
205
- const others = existing.filter((h) => !String(h.command ?? "").includes("pushary-gate"));
209
+ const others = existing.filter((h) => {
210
+ const command = String(h.command ?? "");
211
+ return pusharyHookExecutable(command) === null || isProtectedByScope(command, scope);
212
+ });
206
213
  if (others.length === existing.length) continue;
207
214
  removedAny = true;
208
215
  if (others.length === 0) delete hooks[event];
@@ -221,7 +228,10 @@ var removeCursorUserHook = () => {
221
228
  const afterHooks = after?.hooks ?? {};
222
229
  const stillThere = CURSOR_GATE_EVENTS.some((event) => {
223
230
  const entries = afterHooks[event];
224
- return Array.isArray(entries) && entries.some((h) => String(h?.command ?? "").includes("pushary-gate"));
231
+ return Array.isArray(entries) && entries.some((h) => {
232
+ const command = String(h?.command ?? "");
233
+ return pusharyHookExecutable(command) !== null && !isProtectedByScope(command, scope);
234
+ });
225
235
  });
226
236
  if (stillThere) return "failed";
227
237
  } catch {
@@ -229,13 +239,13 @@ var removeCursorUserHook = () => {
229
239
  }
230
240
  return "removed";
231
241
  };
232
- var cleanSettingsFile = (path, label) => {
242
+ var cleanSettingsFile = (path, label, scope) => {
233
243
  const data = readJson(path);
234
244
  if (!data) {
235
245
  console.log(` ${skip} ${label} ${dim("(not found)")}`);
236
246
  return;
237
247
  }
238
- const changed = removePusharySettings(data, removalScope());
248
+ const changed = removePusharySettings(data, scope);
239
249
  if (changed) {
240
250
  writeJson(path, data);
241
251
  console.log(` ${check} ${label} ${dim(did("cleaned"))}`);
@@ -280,6 +290,7 @@ var main = async () => {
280
290
  }
281
291
  console.log();
282
292
  const keepShared = macAppInstalled();
293
+ const scope = removalScope(keepShared);
283
294
  if (keepShared) {
284
295
  console.log(` ${dim("The Mac app is installed, so its wiring and the state it owns are left alone.")}`);
285
296
  console.log();
@@ -293,8 +304,8 @@ var main = async () => {
293
304
  process.exit(EXIT.FAILED);
294
305
  }
295
306
  console.log(` ${check} Background service ${dim(did("uninstalled"))}`);
296
- cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings");
297
- cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local");
307
+ cleanSettingsFile(CLAUDE_SETTINGS, "Claude Code settings", scope);
308
+ cleanSettingsFile(CLAUDE_SETTINGS_LOCAL, "Claude Code settings.local", scope);
298
309
  const claudeJson2 = keepShared ? null : readJson(CLAUDE_JSON);
299
310
  if (keepShared) {
300
311
  console.log(` ${skip} Claude Code MCP servers ${dim("(kept: the Mac app uses it)")}`);
@@ -332,7 +343,7 @@ var main = async () => {
332
343
  } else {
333
344
  console.log(` ${skip} Cursor MCP config ${dim("(not found)")}`);
334
345
  }
335
- const cursorGate = removeCursorUserHook();
346
+ const cursorGate = removeCursorUserHook(scope);
336
347
  if (cursorGate === "removed") {
337
348
  console.log(` ${check} Cursor gate ${dim("(removed from ~/.cursor/hooks.json)")}`);
338
349
  } else if (cursorGate === "absent") {
@@ -410,7 +421,7 @@ var main = async () => {
410
421
  console.log(` ${skip} Codex config ${dim("(not found)")}`);
411
422
  }
412
423
  if (codexHooksData) {
413
- if (removeCodexHooks(codexHooksData, removalScope())) {
424
+ if (removeCodexHooks(codexHooksData, scope)) {
414
425
  writeJson(codexHooksJson2, codexHooksData);
415
426
  console.log(` ${check} Codex hooks ${dim("(removed from ~/.codex/hooks.json)")}`);
416
427
  } else {
@@ -435,7 +446,7 @@ var main = async () => {
435
446
  const geminiSettingsPath = geminiSettings();
436
447
  const geminiSettings2 = readJson(geminiSettingsPath);
437
448
  if (geminiSettings2) {
438
- if (removeGeminiSettings(geminiSettings2, removalScope())) {
449
+ if (removeGeminiSettings(geminiSettings2, scope)) {
439
450
  writeJson(geminiSettingsPath, geminiSettings2);
440
451
  console.log(` ${check} Gemini CLI settings ${dim(did("cleaned"))}`);
441
452
  } else {