@pushary/agent-hooks 0.89.11 → 0.89.13

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 (80) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +4 -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 +225 -48
  6. package/dist/bin/pushary-clean.js +49 -29
  7. package/dist/bin/pushary-codex-bridge.js +17 -16
  8. package/dist/bin/pushary-codex-hook.js +11 -10
  9. package/dist/bin/pushary-codex.js +7 -7
  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 +4 -4
  17. package/dist/bin/pushary-gemini-bridge.js +16 -15
  18. package/dist/bin/pushary-gemini-hook.js +10 -9
  19. package/dist/bin/pushary-hook.js +13 -13
  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 +7 -7
  24. package/dist/bin/pushary-opencode-hook.js +10 -9
  25. package/dist/bin/pushary-permission-denied-hook.js +11 -11
  26. package/dist/bin/pushary-permission-hook.js +11 -11
  27. package/dist/bin/pushary-post-hook.js +7 -7
  28. package/dist/bin/pushary-prompt-hook.js +7 -7
  29. package/dist/bin/pushary-session-end-hook.js +7 -7
  30. package/dist/bin/pushary-session-start-hook.js +7 -7
  31. package/dist/bin/pushary-setup.js +87 -59
  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 +7 -7
  35. package/dist/bin/pushary-stopfailure-hook.js +7 -7
  36. package/dist/bin/pushary-suggestions.js +2 -2
  37. package/dist/bin/pushary-transcripts.d.ts +1 -0
  38. package/dist/bin/pushary-transcripts.js +79 -0
  39. package/dist/bin/pushary-upgrade.js +21 -21
  40. package/dist/bin/pushary-wait.js +5 -5
  41. package/dist/bin/pushary.js +3 -2
  42. package/dist/{chunk-6XUABEX2.js → chunk-3OXHYPZG.js} +1 -1
  43. package/dist/{chunk-QA72MKIT.js → chunk-3YGGEDJK.js} +5 -5
  44. package/dist/{chunk-IUN6VOKV.js → chunk-4IPSMEGT.js} +68 -46
  45. package/dist/{chunk-XX2V4QS5.js → chunk-4LL6LG5X.js} +2 -2
  46. package/dist/{chunk-J4K75V4D.js → chunk-5MNTWAAJ.js} +10 -9
  47. package/dist/{chunk-KVE5RCQL.js → chunk-5YEF55AX.js} +1 -1
  48. package/dist/{chunk-3SQYKLBW.js → chunk-7JZCADBR.js} +46 -18
  49. package/dist/{chunk-IUWRVTQK.js → chunk-ANMHIZZY.js} +1 -1
  50. package/dist/{chunk-6EKQZA6R.js → chunk-B2HND22S.js} +1 -1
  51. package/dist/{chunk-TRVIRETT.js → chunk-C56VHSXU.js} +1 -1
  52. package/dist/{chunk-TEZ2FZD6.js → chunk-CBGGUEKE.js} +2 -2
  53. package/dist/{chunk-3HFK3UXM.js → chunk-FOGW6O2Y.js} +1 -1
  54. package/dist/{chunk-3XTFX4CL.js → chunk-GMJDQDXU.js} +17 -2
  55. package/dist/{chunk-DKWM5T3P.js → chunk-JTBDBKHU.js} +2 -2
  56. package/dist/{chunk-Q223OM5J.js → chunk-JVLB5CAJ.js} +4 -4
  57. package/dist/{chunk-2LPEZYMC.js → chunk-KNLKHJZR.js} +1 -1
  58. package/dist/{chunk-X4ZUUFKV.js → chunk-L4ZCFMWW.js} +63 -1
  59. package/dist/chunk-M3UABYRT.js +20 -0
  60. package/dist/{chunk-PZRWFWSR.js → chunk-MNTZKFZI.js} +7 -7
  61. package/dist/{chunk-FZ73G2RO.js → chunk-MSTHT2M7.js} +25 -6
  62. package/dist/{chunk-TG3T3M4K.js → chunk-N4QJ3CLX.js} +6 -2
  63. package/dist/{chunk-3CUDMTL4.js → chunk-NBDU3R3G.js} +10 -3
  64. package/dist/{chunk-FXIL7RCL.js → chunk-OX35BSPV.js} +4 -4
  65. package/dist/{chunk-UDBJHQMG.js → chunk-QFJZHVVL.js} +1 -1
  66. package/dist/chunk-QZCFVWA7.js +58 -0
  67. package/dist/{chunk-TY5HVMXT.js → chunk-RAEPAUNJ.js} +1 -1
  68. package/dist/{chunk-NCVIWGVA.js → chunk-S4BLNILH.js} +1 -1
  69. package/dist/{chunk-MZGWKIUZ.js → chunk-VASZ6TWW.js} +7 -3
  70. package/dist/{chunk-SYUIXXQW.js → chunk-VF3RTVWY.js} +1 -1
  71. package/dist/{chunk-LURY4WNF.js → chunk-WHDSJ6PD.js} +1 -1
  72. package/dist/chunk-YKLCWWEI.js +76 -0
  73. package/dist/{chunk-253WC6O5.js → chunk-YXT55PYF.js} +1 -1
  74. package/dist/{chunk-BLODMQKJ.js → chunk-ZO43JTT3.js} +1 -1
  75. package/dist/{chunk-OLAGKKSR.js → chunk-ZP3SNUP7.js} +16 -6
  76. package/dist/{chunk-34GN3IPX.js → chunk-ZWEKONU6.js} +1 -1
  77. package/dist/src/index.d.ts +3 -1
  78. package/dist/src/index.js +15 -11
  79. package/package.json +4 -2
  80. /package/dist/{chunk-X6RXX2NF.js → chunk-QINYXLA5.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.89.13
4
+
5
+ ### Synced transcripts survive large turns and restarts safely
6
+
7
+ Oversized file reads and diffs are shortened before encryption instead of being
8
+ rejected by the server, with the removed UTF-8 byte count recorded in the turn.
9
+ Stable HMAC identifiers prevent transcript contents leaking through record ids,
10
+ and a resumed session reuses its persisted encryption key rather than conflicting
11
+ with the key already registered for that session.
12
+
13
+ Transcript recipients are now pinned on first use and later key changes stop sync
14
+ until the user explicitly runs `pushary transcripts trust --rotate`. First use
15
+ also fails closed when that pin cannot be stored, so no session key is wrapped to
16
+ an unrecorded recipient. New sessions can independently wrap their key for the
17
+ owner's phone and an optional audited compliance-recovery recipient; installations
18
+ without one remain compatible.
19
+
20
+ ## 0.89.12
21
+
22
+ ### Phone control installs as a native service, without a VM
23
+
24
+ Fresh setup enables phone-start when a supported provider and the operating
25
+ system's own per-user service manager are available: launchd on macOS, systemd
26
+ on Linux, and Task Scheduler on Windows. Existing installs keep their stored
27
+ choice. Service replacement is transactional, verifies the new process before
28
+ committing, and restores the previous running or stopped state on failure.
29
+
30
+ Claude Code, Codex and Gemini sessions started from the phone now use the same
31
+ supervised control path. Setup checks both the local heartbeat and the server's
32
+ view of the machine before calling control ready.
33
+
34
+ ### Fresh and partial installs repair themselves
35
+
36
+ Setup now verifies every global command, its local JavaScript imports, and the
37
+ Cursor and VS Code runtime assets before reusing an installed version. A
38
+ same-version package missing any of them is reinstalled instead of failing the
39
+ same setup forever. Fresh installs also cover Codex on Windows and recover from
40
+ partially configured providers without disturbing unrelated settings.
41
+
42
+ ### The CLI and macOS app keep their own wiring
43
+
44
+ Cursor setup writes one gate per event even when the other installer ran first.
45
+ `pushary clean` removes the CLI gate while preserving the app's bridge, and
46
+ `pushary doctor` validates the MCP entry from the installer that actually wired
47
+ Cursor. This keeps mixed CLI/app machines working and avoids false repair
48
+ failures on app-only installs.
49
+
50
+ ### Transcript control works in every shell
51
+
52
+ Setup discloses encrypted transcript sync and its audited compliance-recovery
53
+ path before enabling it for a new install; older installs remain off until setup
54
+ runs again. `pushary setup --transcripts on|off` now provides the same persistent
55
+ choice in POSIX shells, PowerShell and Command Prompt. The existing
56
+ `PUSHARY_TRANSCRIPTS` environment override remains compatible.
57
+
3
58
  ## 0.89.11
4
59
 
5
60
  ### Phone-spawned Codex and Gemini sessions record their conversation again
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
@@ -199,6 +200,7 @@ stats Show the approval moments your agents hit while not connected
199
200
  suggestions List rules mined from your own approvals
200
201
  upgrade Update the globally installed hooks to the latest version
201
202
  hook Run as a PreToolUse hook (reads stdin, writes stdout)
203
+ transcripts Show or re-trust the keys this machine seals transcripts to
202
204
  ```
203
205
 
204
206
  Run `pushary <command> --help` for one command's options.
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  handleBell
4
- } from "../chunk-TRVIRETT.js";
4
+ } from "../chunk-C56VHSXU.js";
5
5
  import {
6
6
  readHookInput
7
7
  } from "../chunk-6CUUA7HO.js";
8
- import "../chunk-3CUDMTL4.js";
8
+ import "../chunk-NBDU3R3G.js";
9
9
  import "../chunk-BC3VCZ3E.js";
10
- import "../chunk-IUWRVTQK.js";
11
- import "../chunk-XX2V4QS5.js";
10
+ import "../chunk-ANMHIZZY.js";
11
+ import "../chunk-4LL6LG5X.js";
12
12
  import "../chunk-2UMNXADU.js";
13
- import "../chunk-X6RXX2NF.js";
13
+ import "../chunk-QINYXLA5.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-C56VHSXU.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-3YGGEDJK.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-WHDSJ6PD.js";
25
+ import "../chunk-GMJDQDXU.js";
28
26
  import "../chunk-DINDL2CF.js";
29
- import "../chunk-IUWRVTQK.js";
27
+ import "../chunk-NBDU3R3G.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-ANMHIZZY.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-QINYXLA5.js";
40
40
 
41
41
  // src/bell/install.ts
42
42
  import { join } from "path";
@@ -1,24 +1,35 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ changedRecipientsMessage,
4
+ loadPin,
5
+ savePin,
6
+ verifyRecipients
7
+ } from "../chunk-YKLCWWEI.js";
2
8
  import {
3
9
  ensureDaemonRunning
4
- } from "../chunk-3SQYKLBW.js";
10
+ } from "../chunk-7JZCADBR.js";
5
11
  import {
6
12
  buildTranscriptRecords,
7
13
  decodePublicKey,
8
- fetchAccountPublicKey,
9
14
  generateSessionKey,
10
- uploadTranscriptRecords,
11
15
  wrapSessionKey
12
- } from "../chunk-IUN6VOKV.js";
16
+ } from "../chunk-4IPSMEGT.js";
17
+ import {
18
+ fetchTranscriptRecipients,
19
+ uploadTranscriptRecords
20
+ } from "../chunk-QZCFVWA7.js";
21
+ import {
22
+ transcriptsEnabled
23
+ } from "../chunk-M3UABYRT.js";
13
24
  import {
14
25
  startProcessOwnership,
15
26
  withSpawnProcessOwnership
16
- } from "../chunk-TEZ2FZD6.js";
27
+ } from "../chunk-CBGGUEKE.js";
17
28
  import {
18
29
  needsShell,
19
30
  spawnClaude
20
31
  } from "../chunk-XHKBHWLX.js";
21
- import "../chunk-X4ZUUFKV.js";
32
+ import "../chunk-L4ZCFMWW.js";
22
33
  import {
23
34
  isDeferAnswer
24
35
  } from "../chunk-YHG74UFF.js";
@@ -27,7 +38,7 @@ import {
27
38
  getPolicy,
28
39
  repoKeyFor,
29
40
  reportEvent
30
- } from "../chunk-FXIL7RCL.js";
41
+ } from "../chunk-OX35BSPV.js";
31
42
  import {
32
43
  DEFAULT_SESSION,
33
44
  askUser,
@@ -41,21 +52,24 @@ import {
41
52
  waitForAnswer
42
53
  } from "../chunk-JHJHCMAF.js";
43
54
  import {
55
+ UNNAMED_ACTION,
44
56
  deriveToolTarget,
45
57
  describeToolCall,
46
58
  scopePathFor
47
- } from "../chunk-TG3T3M4K.js";
59
+ } from "../chunk-N4QJ3CLX.js";
48
60
  import {
49
61
  getMachineId
50
- } from "../chunk-DKWM5T3P.js";
51
- import "../chunk-QA72MKIT.js";
62
+ } from "../chunk-JTBDBKHU.js";
63
+ import "../chunk-3YGGEDJK.js";
52
64
  import "../chunk-DINDL2CF.js";
65
+ import "../chunk-NBDU3R3G.js";
66
+ import "../chunk-BC3VCZ3E.js";
53
67
  import "../chunk-BSZYIAZL.js";
54
68
  import "../chunk-SAF6HGAA.js";
55
- import "../chunk-IUWRVTQK.js";
56
69
  import "../chunk-7QLSKOSU.js";
57
70
  import "../chunk-KZERVKTD.js";
58
- import "../chunk-XX2V4QS5.js";
71
+ import "../chunk-ANMHIZZY.js";
72
+ import "../chunk-4LL6LG5X.js";
59
73
  import {
60
74
  getApiKey,
61
75
  getBaseUrl
@@ -63,7 +77,7 @@ import {
63
77
  import {
64
78
  KILL_REASON,
65
79
  resolveGate
66
- } from "../chunk-X6RXX2NF.js";
80
+ } from "../chunk-QINYXLA5.js";
67
81
 
68
82
  // src/wrapper/claudeBinary.ts
69
83
  import { statSync } from "fs";
@@ -139,18 +153,88 @@ var runLocalPassthrough = (binary, args2) => {
139
153
  import { basename } from "path";
140
154
 
141
155
  // src/transcript-sync.ts
156
+ import { homedir as homedir2 } from "os";
157
+ import { join as join3, resolve } from "path";
158
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
159
+
160
+ // src/transcript-session-key.ts
161
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
142
162
  import { homedir } from "os";
143
- import { join as join2, resolve } from "path";
144
- import { existsSync, readFileSync } from "fs";
145
- var transcriptsEnabled = () => process.env.PUSHARY_TRANSCRIPTS === "1";
163
+ import { join as join2 } from "path";
164
+ var sessionDir = () => process.env.PUSHARY_SESSION_KEY_DIR?.trim() || join2(homedir(), ".pushary", "sessions");
165
+ var SESSION_KEY_TTL_MS = 48 * 60 * 60 * 1e3;
166
+ var SAFE_SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
167
+ var fileFor = (sessionId) => SAFE_SESSION_ID.test(sessionId) ? join2(sessionDir(), `${sessionId}.json`) : null;
168
+ var loadSessionKey = (sessionId) => {
169
+ const path = fileFor(sessionId);
170
+ if (!path || !existsSync(path)) return null;
171
+ try {
172
+ if (Date.now() - statSync2(path).mtimeMs > SESSION_KEY_TTL_MS) return null;
173
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
174
+ const key = typeof raw.sessionKey === "string" ? Buffer.from(raw.sessionKey, "base64") : null;
175
+ if (!key || key.length !== 32) return null;
176
+ return {
177
+ sessionKey: new Uint8Array(key),
178
+ wrappedKey: typeof raw.wrappedKey === "string" ? raw.wrappedKey : null,
179
+ providerWrappedKey: typeof raw.providerWrappedKey === "string" ? raw.providerWrappedKey : null,
180
+ providerKeyId: typeof raw.providerKeyId === "string" ? raw.providerKeyId : null,
181
+ keyUploaded: raw.keyUploaded === true
182
+ };
183
+ } catch {
184
+ return null;
185
+ }
186
+ };
187
+ var saveSessionKey = (sessionId, value) => {
188
+ const path = fileFor(sessionId);
189
+ if (!path) return;
190
+ try {
191
+ mkdirSync(sessionDir(), { recursive: true, mode: 448 });
192
+ writeFileSync(
193
+ path,
194
+ JSON.stringify({
195
+ sessionKey: Buffer.from(value.sessionKey).toString("base64"),
196
+ wrappedKey: value.wrappedKey,
197
+ providerWrappedKey: value.providerWrappedKey,
198
+ providerKeyId: value.providerKeyId,
199
+ keyUploaded: value.keyUploaded
200
+ }),
201
+ { mode: 384 }
202
+ );
203
+ } catch {
204
+ }
205
+ };
206
+ var sweepStaleSessionKeys = (now = Date.now()) => {
207
+ const dir = sessionDir();
208
+ if (!existsSync(dir)) return 0;
209
+ let removed = 0;
210
+ try {
211
+ for (const name of readdirSync(dir)) {
212
+ if (!name.endsWith(".json")) continue;
213
+ const path = join2(dir, name);
214
+ try {
215
+ if (now - statSync2(path).mtimeMs <= SESSION_KEY_TTL_MS) continue;
216
+ rmSync(path, { force: true });
217
+ removed += 1;
218
+ } catch {
219
+ }
220
+ }
221
+ } catch {
222
+ return removed;
223
+ }
224
+ return removed;
225
+ };
226
+
227
+ // src/transcript-sync.ts
146
228
  var claudeTranscriptPath = (cwd, sessionId) => {
147
229
  const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
148
- const configDir = process.env.CLAUDE_CONFIG_DIR || join2(homedir(), ".claude");
149
- return join2(configDir, "projects", projectId, `${sessionId}.jsonl`);
230
+ const configDir = process.env.CLAUDE_CONFIG_DIR || join3(homedir2(), ".claude");
231
+ return join3(configDir, "projects", projectId, `${sessionId}.jsonl`);
150
232
  };
151
233
  var createSyncState = () => ({
152
234
  sessionKey: null,
153
235
  wrappedKey: null,
236
+ providerWrappedKey: null,
237
+ providerKeyId: null,
154
238
  seen: /* @__PURE__ */ new Set(),
155
239
  keyUploaded: false,
156
240
  keyRejected: false
@@ -159,70 +243,163 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
159
243
  if (state.keyRejected) return state;
160
244
  let sessionKey = state.sessionKey;
161
245
  let wrappedKey = state.wrappedKey;
246
+ let providerWrappedKey = state.providerWrappedKey;
247
+ let providerKeyId = state.providerKeyId;
162
248
  if (!sessionKey) {
163
- const publicKey = await uploader.fetchAccountPublicKey();
164
- if (!publicKey) return state;
249
+ const recipients = await uploader.fetchTranscriptRecipients();
250
+ if (!recipients) return state;
165
251
  sessionKey = generateSessionKey();
166
- wrappedKey = wrapSessionKey(sessionKey, decodePublicKey(publicKey));
252
+ wrappedKey = wrapSessionKey(sessionKey, decodePublicKey(recipients.phonePublicKey));
253
+ if (recipients.provider) {
254
+ providerWrappedKey = wrapSessionKey(sessionKey, decodePublicKey(recipients.provider.publicKey));
255
+ providerKeyId = recipients.provider.keyId;
256
+ }
167
257
  }
168
258
  const built = buildTranscriptRecords(lines, sessionKey, state.seen);
169
259
  if (built.records.length === 0) {
170
- return { ...state, sessionKey, wrappedKey, seen: built.seen };
260
+ return { ...state, sessionKey, wrappedKey, providerWrappedKey, providerKeyId, seen: built.seen };
261
+ }
262
+ const seen = new Set(state.seen);
263
+ let keyUploaded = state.keyUploaded;
264
+ let keyRejected = false;
265
+ let rejected = 0;
266
+ for (const chunk of chunkRecords(built.records)) {
267
+ const result = await uploader.uploadRecords({
268
+ sessionId,
269
+ machineId,
270
+ dataEncryptionKey: keyUploaded ? void 0 : wrappedKey ?? void 0,
271
+ providerWrappedSessionKey: keyUploaded ? void 0 : providerWrappedKey ?? void 0,
272
+ providerKeyId: keyUploaded ? void 0 : providerKeyId ?? void 0,
273
+ records: chunk
274
+ });
275
+ keyUploaded = keyUploaded || result.keyRegistered;
276
+ if (result.keyConflict) {
277
+ keyRejected = true;
278
+ break;
279
+ }
280
+ if (!result.ok) break;
281
+ for (const id of result.settledIds ?? chunk.map((r) => r.localId)) seen.add(id);
282
+ rejected += result.rejectedIds.length;
283
+ }
284
+ if (rejected > 0) {
285
+ process.stderr.write(`[pushary] transcript: ${rejected} record${rejected === 1 ? "" : "s"} could not be stored and will not be retried.
286
+ `);
171
287
  }
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
288
  return {
179
289
  sessionKey,
180
290
  wrappedKey,
181
- seen: result.ok ? built.seen : state.seen,
182
- keyUploaded: state.keyUploaded || result.keyRegistered,
183
- keyRejected: result.keyConflict
291
+ providerWrappedKey,
292
+ providerKeyId,
293
+ seen,
294
+ keyUploaded,
295
+ keyRejected
184
296
  };
185
297
  };
298
+ var MAX_RECORDS_PER_BATCH = 200;
299
+ var MAX_BATCH_BYTES = 3e6;
300
+ var chunkRecords = (records) => {
301
+ const chunks = [];
302
+ let current = [];
303
+ let bytes = 0;
304
+ for (const record of records) {
305
+ const size = record.content.length;
306
+ if (current.length > 0 && (current.length >= MAX_RECORDS_PER_BATCH || bytes + size > MAX_BATCH_BYTES)) {
307
+ chunks.push(current);
308
+ current = [];
309
+ bytes = 0;
310
+ }
311
+ current.push(record);
312
+ bytes += size;
313
+ }
314
+ if (current.length > 0) chunks.push(current);
315
+ return chunks;
316
+ };
186
317
  var readLines = (path) => {
187
318
  try {
188
- return existsSync(path) ? readFileSync(path, "utf-8").split("\n") : [];
319
+ return existsSync2(path) ? readFileSync2(path, "utf-8").split("\n") : [];
189
320
  } catch {
190
321
  return [];
191
322
  }
192
323
  };
324
+ var IDLE_CEILING_MS = 6e4;
325
+ var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
193
326
  var startTranscriptSync = (opts) => {
327
+ let warnedAboutRecipients = false;
328
+ const fetchPinnedRecipients = async () => {
329
+ const offer = await fetchTranscriptRecipients(opts.apiKey);
330
+ if (!offer) return null;
331
+ const verdict = verifyRecipients(offer, loadPin());
332
+ if (verdict.kind === "first-use" && !savePin(verdict.pin)) return null;
333
+ if (verdict.kind === "changed") {
334
+ if (!warnedAboutRecipients) {
335
+ warnedAboutRecipients = true;
336
+ process.stderr.write(`${changedRecipientsMessage(verdict, offer)}
337
+ `);
338
+ }
339
+ return null;
340
+ }
341
+ return offer;
342
+ };
194
343
  const uploader = {
195
- fetchAccountPublicKey: () => fetchAccountPublicKey(opts.apiKey),
344
+ fetchTranscriptRecipients: fetchPinnedRecipients,
196
345
  uploadRecords: (params) => uploadTranscriptRecords(opts.apiKey, params)
197
346
  };
198
347
  const path = claudeTranscriptPath(opts.cwd, opts.sessionId);
199
- let state = createSyncState();
348
+ const persisted = loadSessionKey(opts.sessionId);
349
+ let state = persisted ? { ...createSyncState(), ...persisted } : createSyncState();
350
+ sweepStaleSessionKeys();
351
+ let persistedUploaded = persisted?.keyUploaded ?? false;
200
352
  let running = true;
201
353
  let inFlight = false;
202
354
  const tick = async () => {
203
355
  if (!running || inFlight) return;
204
356
  inFlight = true;
205
357
  try {
358
+ const before = state.sessionKey;
206
359
  state = await syncTick(readLines(path), state, uploader, opts.sessionId, opts.machineId);
360
+ if (state.sessionKey && (before !== state.sessionKey || state.keyUploaded !== persistedUploaded)) {
361
+ persistedUploaded = state.keyUploaded;
362
+ saveSessionKey(opts.sessionId, {
363
+ sessionKey: state.sessionKey,
364
+ wrappedKey: state.wrappedKey,
365
+ providerWrappedKey: state.providerWrappedKey,
366
+ providerKeyId: state.providerKeyId,
367
+ keyUploaded: state.keyUploaded
368
+ });
369
+ }
207
370
  } catch {
208
371
  } finally {
209
372
  inFlight = false;
210
373
  }
211
374
  };
212
- const timer = setInterval(() => void tick(), opts.intervalMs ?? 3e3);
213
- if (typeof timer.unref === "function") timer.unref();
375
+ const baseIntervalMs = opts.intervalMs ?? 3e3;
376
+ const maxIntervalMs = Math.max(baseIntervalMs, IDLE_CEILING_MS);
377
+ let timer;
378
+ const schedule = (delayMs) => {
379
+ if (!running) return;
380
+ timer = setTimeout(() => {
381
+ void (async () => {
382
+ const before = state.sessionKey;
383
+ await tick();
384
+ const idle = state.sessionKey === null && before === null;
385
+ schedule(nextInterval(delayMs, idle, baseIntervalMs, maxIntervalMs));
386
+ })();
387
+ }, delayMs);
388
+ if (typeof timer.unref === "function") timer.unref();
389
+ };
214
390
  void tick();
391
+ schedule(baseIntervalMs);
215
392
  return () => {
216
393
  running = false;
217
- clearInterval(timer);
394
+ if (timer) clearTimeout(timer);
218
395
  };
219
396
  };
220
397
 
221
398
  // src/wrapper/sdkLoader.ts
222
399
  import { spawn } from "child_process";
223
- import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
224
- import { homedir as homedir2 } from "os";
225
- import { join as join3 } from "path";
400
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
401
+ import { homedir as homedir3 } from "os";
402
+ import { join as join4 } from "path";
226
403
  import { pathToFileURL } from "url";
227
404
  var userMessage = (text) => ({
228
405
  type: "user",
@@ -232,8 +409,8 @@ var userMessage = (text) => ({
232
409
  var SDK_VERSION = "0.3.207";
233
410
  var SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
234
411
  var SDK_SPEC = `${SDK_PACKAGE}@${SDK_VERSION}`;
235
- var sdkCacheDir = () => process.env.PUSHARY_REMOTE_SDK_DIR?.trim() || join3(homedir2(), ".pushary", "remote-sdk");
236
- var cachedSdkEntry = () => join3(sdkCacheDir(), "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs");
412
+ var sdkCacheDir = () => process.env.PUSHARY_REMOTE_SDK_DIR?.trim() || join4(homedir3(), ".pushary", "remote-sdk");
413
+ var cachedSdkEntry = () => join4(sdkCacheDir(), "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs");
237
414
  var importSdk = async (specifierOrUrl) => {
238
415
  try {
239
416
  const mod = await import(specifierOrUrl);
@@ -248,7 +425,7 @@ var loadClaudeSdk = async () => {
248
425
  const fromResolution = await importSdk(SDK_PACKAGE);
249
426
  if (fromResolution) return fromResolution;
250
427
  const entry = cachedSdkEntry();
251
- if (existsSync2(entry)) return importSdk(pathToFileURL(entry).href);
428
+ if (existsSync3(entry)) return importSdk(pathToFileURL(entry).href);
252
429
  return null;
253
430
  };
254
431
  var ensureClaudeSdk = async (log) => {
@@ -256,10 +433,10 @@ var ensureClaudeSdk = async (log) => {
256
433
  if (present) return present;
257
434
  const dir = sdkCacheDir();
258
435
  try {
259
- mkdirSync(dir, { recursive: true });
260
- const pkgJson = join3(dir, "package.json");
261
- if (!existsSync2(pkgJson)) {
262
- writeFileSync(pkgJson, `${JSON.stringify({ name: "pushary-remote-sdk", private: true })}
436
+ mkdirSync2(dir, { recursive: true });
437
+ const pkgJson = join4(dir, "package.json");
438
+ if (!existsSync3(pkgJson)) {
439
+ writeFileSync2(pkgJson, `${JSON.stringify({ name: "pushary-remote-sdk", private: true })}
263
440
  `);
264
441
  }
265
442
  } catch {
@@ -486,7 +663,7 @@ var createRemoteApprover = (deps) => {
486
663
  let question;
487
664
  try {
488
665
  question = await askUser2(deps.apiKey, {
489
- question: `Allow ${description}?`,
666
+ question: `Allow ${description || UNNAMED_ACTION}?`,
490
667
  type: "confirm",
491
668
  context: `Your agent wants to run this in ${deps.projectName} (you are away)`,
492
669
  agentName: `Claude Code - ${deps.projectName}`,