pushary 1.9.76 → 1.9.78

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 (125) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/{api-YDCHRRA4.js → api-4WCUP47K.js} +10 -10
  3. package/dist/bin/pushary-bell-hook.js +25 -25
  4. package/dist/bin/pushary-bell.js +10 -10
  5. package/dist/bin/pushary-claude.js +26 -26
  6. package/dist/bin/pushary-clean.js +34 -34
  7. package/dist/bin/pushary-codex-bridge.js +21 -21
  8. package/dist/bin/pushary-codex-hook.js +30 -30
  9. package/dist/bin/pushary-codex-session.js +8 -8
  10. package/dist/bin/pushary-codex.js +13 -13
  11. package/dist/bin/pushary-connect.js +26 -26
  12. package/dist/bin/pushary-cowork.js +5 -5
  13. package/dist/bin/pushary-daemon.js +122 -66
  14. package/dist/bin/pushary-disconnect.js +25 -25
  15. package/dist/bin/pushary-display-hook.js +16 -16
  16. package/dist/bin/pushary-doctor.js +42 -42
  17. package/dist/bin/pushary-elicitation-hook.js +25 -25
  18. package/dist/bin/pushary-gemini-bridge.js +15 -15
  19. package/dist/bin/pushary-gemini-hook.js +27 -27
  20. package/dist/bin/pushary-hook.js +29 -29
  21. package/dist/bin/pushary-login.js +20 -20
  22. package/dist/bin/pushary-logout.js +24 -24
  23. package/dist/bin/pushary-mcp.js +11 -11
  24. package/dist/bin/pushary-mode.js +15 -15
  25. package/dist/bin/pushary-notification-hook.js +24 -24
  26. package/dist/bin/pushary-opencode-hook.js +26 -26
  27. package/dist/bin/pushary-permission-denied-hook.js +29 -29
  28. package/dist/bin/pushary-permission-hook.js +29 -29
  29. package/dist/bin/pushary-post-hook.js +24 -24
  30. package/dist/bin/pushary-prompt-hook.js +24 -24
  31. package/dist/bin/pushary-session-end-hook.js +26 -26
  32. package/dist/bin/pushary-session-start-hook.js +34 -34
  33. package/dist/bin/pushary-setup.js +58 -55
  34. package/dist/bin/pushary-stats.js +5 -5
  35. package/dist/bin/pushary-status.js +31 -31
  36. package/dist/bin/pushary-stop-hook.js +24 -24
  37. package/dist/bin/pushary-stopfailure-hook.js +24 -24
  38. package/dist/bin/pushary-suggestions.js +15 -15
  39. package/dist/bin/pushary-transcript-register.js +46 -18
  40. package/dist/bin/pushary-transcripts.js +19 -19
  41. package/dist/bin/pushary-upgrade.js +40 -40
  42. package/dist/bin/pushary-wait.js +15 -15
  43. package/dist/bin/pushary.js +7 -7
  44. package/dist/{chunk-TFYFQM5C.js → chunk-22FLBWJH.js} +2 -2
  45. package/dist/{chunk-KGWLDNNT.js → chunk-3XH4YLCN.js} +4 -4
  46. package/dist/{chunk-5FV472Q2.js → chunk-42Y7IZSV.js} +1 -1
  47. package/dist/{chunk-PPD64MZW.js → chunk-4DOJLAGR.js} +1 -1
  48. package/dist/{chunk-CUD6SYDG.js → chunk-4FYY6YW3.js} +1 -1
  49. package/dist/{chunk-BUR2LYQI.js → chunk-4ZSQPGAU.js} +2 -2
  50. package/dist/{chunk-MMVIKXCP.js → chunk-56POFDRB.js} +3 -3
  51. package/dist/{chunk-JNV5LOZH.js → chunk-5F3L3QOO.js} +4 -4
  52. package/dist/{chunk-WK2L3XVQ.js → chunk-5QOMMZFZ.js} +1 -1
  53. package/dist/{chunk-USZJJWZL.js → chunk-5WU6534E.js} +2 -2
  54. package/dist/{chunk-DZF2M3LM.js → chunk-5YHI2FN4.js} +6 -6
  55. package/dist/{chunk-ZY25SNFQ.js → chunk-6HDYWRCP.js} +1 -1
  56. package/dist/{chunk-47TIRGS5.js → chunk-6WOG2SCC.js} +1 -0
  57. package/dist/{chunk-4LX5QUFV.js → chunk-74NZ3CJQ.js} +1 -1
  58. package/dist/{chunk-W5AUTVVL.js → chunk-74R4IZ7N.js} +5 -5
  59. package/dist/{chunk-VZSSL6US.js → chunk-7BMBXTN4.js} +14 -14
  60. package/dist/{chunk-DB3NOEDW.js → chunk-7KMI2G24.js} +6 -6
  61. package/dist/{chunk-UEY4SHY2.js → chunk-AOGM34LL.js} +1 -1
  62. package/dist/{chunk-2QFYEB3P.js → chunk-B4MQSH3R.js} +5 -5
  63. package/dist/{chunk-F7R7M5MG.js → chunk-BE5BA3GF.js} +1 -1
  64. package/dist/{chunk-74ONBRPF.js → chunk-BM3AT5AF.js} +4 -4
  65. package/dist/{chunk-JZ5KV2FK.js → chunk-C5LCOXAC.js} +5 -5
  66. package/dist/{chunk-XLGD5LDG.js → chunk-CHBK47DU.js} +3 -3
  67. package/dist/{chunk-USMUW3LI.js → chunk-DLOUKVLM.js} +2 -2
  68. package/dist/{chunk-UMBVIFP5.js → chunk-DQCQQ2PY.js} +1 -1
  69. package/dist/{chunk-MTRZQR2R.js → chunk-EDHOHS3C.js} +3 -3
  70. package/dist/{chunk-M5VJ3HXB.js → chunk-FMDP5KWV.js} +6 -6
  71. package/dist/{chunk-CHIOUBPB.js → chunk-IVBQ5ISS.js} +13 -13
  72. package/dist/{chunk-5M5F4IWR.js → chunk-IVY6LYTO.js} +1 -1
  73. package/dist/{chunk-WLHHDDVA.js → chunk-JLBLEFBK.js} +3 -3
  74. package/dist/{chunk-WRCM4XGY.js → chunk-JM4WO3JT.js} +2 -2
  75. package/dist/{chunk-GJVV2EUC.js → chunk-KJYZXX7X.js} +5 -3
  76. package/dist/{chunk-NJTAUEW6.js → chunk-L2S7KWMF.js} +1 -1
  77. package/dist/{chunk-5KPMDLXB.js → chunk-LXSGNGK5.js} +3 -3
  78. package/dist/{chunk-VELPOV2F.js → chunk-MJXP5MFI.js} +4 -4
  79. package/dist/{chunk-YLB4AGZJ.js → chunk-MPZFQP6O.js} +1 -1
  80. package/dist/{chunk-E7U4YM4A.js → chunk-NFK5ZCLK.js} +3 -3
  81. package/dist/{chunk-7LLC4R66.js → chunk-NH7C6QBI.js} +4 -4
  82. package/dist/{chunk-Y4OBAK4I.js → chunk-OFJUHEEG.js} +8 -5
  83. package/dist/{chunk-IRLHN5UP.js → chunk-OFQACQDL.js} +27 -7
  84. package/dist/{chunk-CMNB4IZO.js → chunk-OOODSAHW.js} +3 -3
  85. package/dist/{chunk-7ZOWQBHQ.js → chunk-PYW7VWVZ.js} +5 -5
  86. package/dist/{chunk-M7JFGDGP.js → chunk-Q53QJQLY.js} +1 -1
  87. package/dist/{chunk-R3HXGHRQ.js → chunk-RCDZ2XZ6.js} +1 -1
  88. package/dist/{chunk-XJ5DYPTM.js → chunk-RDU5HWKU.js} +4 -4
  89. package/dist/{chunk-MAWC4PJV.js → chunk-RIVCTSYV.js} +5 -5
  90. package/dist/{chunk-E2VJ7B7D.js → chunk-RJEG3TUZ.js} +6 -6
  91. package/dist/{chunk-QUGP76DQ.js → chunk-SV2C7PZV.js} +6 -6
  92. package/dist/{chunk-7SFP6Y5U.js → chunk-T2MJ5H2V.js} +603 -162
  93. package/dist/{chunk-3YU6NIIL.js → chunk-TDLK3ZU2.js} +4 -4
  94. package/dist/{chunk-C2AR7QXU.js → chunk-TKQGDSO4.js} +2 -2
  95. package/dist/{chunk-2BK2EQOB.js → chunk-TNNB7P6Z.js} +7 -7
  96. package/dist/{chunk-7KYQ24FI.js → chunk-TQYSBYYZ.js} +4 -4
  97. package/dist/{chunk-IRN6JLSM.js → chunk-TRTAYW7Z.js} +4 -4
  98. package/dist/{chunk-SWRNJ3BP.js → chunk-VIIQLZJS.js} +3 -3
  99. package/dist/{chunk-NFUV25JZ.js → chunk-VYELSTZK.js} +3 -3
  100. package/dist/{chunk-S7C5NY4R.js → chunk-VZHDSUXK.js} +1 -1
  101. package/dist/{chunk-FAGQAR6I.js → chunk-WHTWHHE3.js} +478 -161
  102. package/dist/{chunk-57MQNSM5.js → chunk-WJTUCQ47.js} +1 -1
  103. package/dist/{chunk-KWGOKO4E.js → chunk-XCBKGQ6N.js} +3 -3
  104. package/dist/{chunk-T2EJ3H6Y.js → chunk-Y4I5YX3L.js} +3 -3
  105. package/dist/{chunk-GII565PB.js → chunk-YH3RAWMV.js} +1 -1
  106. package/dist/{chunk-NQA56AMZ.js → chunk-YJK3LBAJ.js} +6 -6
  107. package/dist/{chunk-B2PFCS72.js → chunk-YOPFR22Y.js} +2 -2
  108. package/dist/{chunk-5NW7VM35.js → chunk-YP35CA5L.js} +2 -2
  109. package/dist/{chunk-ITLWEGLE.js → chunk-Z5VZWDGL.js} +15 -5
  110. package/dist/{chunk-Y2CYSVWU.js → chunk-Z6DZHUCO.js} +6 -6
  111. package/dist/{chunk-YDPGBBV7.js → chunk-ZFS2Y5GK.js} +5 -5
  112. package/dist/{chunk-NVLKH5FS.js → chunk-ZM3ELQPE.js} +9 -9
  113. package/dist/{codex-app-cards-switch-3OUTABVU.js → codex-app-cards-switch-GUZ7XMPB.js} +11 -11
  114. package/dist/{codex-desktop-launch-JH3E2CIA.js → codex-desktop-launch-Q5UINXU6.js} +1 -1
  115. package/dist/{codex-session-launch-GR363OQN.js → codex-session-launch-4G5AFXXU.js} +18 -18
  116. package/dist/{config-R43UFHX7.js → config-725HOJFK.js} +3 -3
  117. package/dist/{global-upgrade-73GP4MZM.js → global-upgrade-AXTMEH3I.js} +10 -10
  118. package/dist/{identity-CA7CZGDL.js → identity-HLIZDGEN.js} +4 -4
  119. package/dist/{live-capabilities-RCGMQIIZ.js → live-capabilities-A74OIHWN.js} +5 -5
  120. package/dist/{paths-H7VNNB46.js → paths-ZHDLQD25.js} +3 -3
  121. package/dist/{reapply-3UIA3CUO.js → reapply-RC2AMGCQ.js} +27 -27
  122. package/dist/src/index.js +28 -28
  123. package/dist/{upgrade-lock-ZTLX5PDF.js → upgrade-lock-4B3D3OCT.js} +4 -4
  124. package/dist/{usage-C53NVWC2.js → usage-VVPEFYKS.js} +3 -3
  125. package/package.json +1 -1
@@ -3,16 +3,16 @@ import {
3
3
  loadPin,
4
4
  savePin,
5
5
  verifyRecipients
6
- } from "./chunk-YLB4AGZJ.js";
6
+ } from "./chunk-MPZFQP6O.js";
7
7
  import {
8
8
  SESSION_HISTORY_RETENTION_DAYS
9
9
  } from "./chunk-MHUKYYYR.js";
10
10
  import {
11
11
  ensureDaemonRunning
12
- } from "./chunk-DB3NOEDW.js";
12
+ } from "./chunk-7KMI2G24.js";
13
13
  import {
14
14
  hookOwnerOf
15
- } from "./chunk-KGWLDNNT.js";
15
+ } from "./chunk-3XH4YLCN.js";
16
16
  import {
17
17
  buildTranscriptRecords,
18
18
  decodePublicKey,
@@ -23,39 +23,42 @@ import {
23
23
  recordSealerFor,
24
24
  uploadTranscriptRecords,
25
25
  wrapSessionKey
26
- } from "./chunk-ITLWEGLE.js";
26
+ } from "./chunk-Z5VZWDGL.js";
27
27
  import {
28
28
  external_exports
29
29
  } from "./chunk-XMSDA5WA.js";
30
30
  import {
31
31
  transcriptEnvironmentPreference,
32
32
  transcriptsEnabled
33
- } from "./chunk-5M5F4IWR.js";
33
+ } from "./chunk-IVY6LYTO.js";
34
34
  import {
35
35
  readTranscriptPreference
36
- } from "./chunk-7LLC4R66.js";
36
+ } from "./chunk-NH7C6QBI.js";
37
37
  import {
38
38
  claudeConfigDir,
39
39
  codexHome,
40
40
  pusharyDir
41
- } from "./chunk-7KYQ24FI.js";
41
+ } from "./chunk-TQYSBYYZ.js";
42
42
  import {
43
- writeJsonAtomic
44
- } from "./chunk-T2EJ3H6Y.js";
43
+ writeJsonAtomic,
44
+ writeJsonAtomicDurable
45
+ } from "./chunk-Y4I5YX3L.js";
45
46
  import {
46
47
  getApiKey,
47
48
  getBaseUrl
48
- } from "./chunk-M7JFGDGP.js";
49
+ } from "./chunk-Q53QJQLY.js";
49
50
  import {
50
51
  PENDING_COMMAND_PREFIX,
51
52
  SESSION_HISTORY_BACKFILL_LIMIT_BYTES,
53
+ canonicalJsonText,
54
+ isRecord,
52
55
  redactSecretsFromContent
53
- } from "./chunk-Y4OBAK4I.js";
56
+ } from "./chunk-OFJUHEEG.js";
54
57
 
55
58
  // src/transcript-registration.ts
56
- import { createHash } from "crypto";
57
- import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
58
- import { basename, isAbsolute, join as join4, resolve as resolve2, sep } from "path";
59
+ import { createHash as createHash4 } from "crypto";
60
+ import { readFileSync as readFileSync6, readdirSync as readdirSync2, realpathSync, statSync as statSync3, unlinkSync as unlinkSync2 } from "fs";
61
+ import { basename, dirname as dirname2, isAbsolute, join as join5, relative, resolve as resolve2, sep } from "path";
59
62
 
60
63
  // src/claude-display-draft.ts
61
64
  import { closeSync, fstatSync, lstatSync, openSync, readFileSync, readSync, unlinkSync } from "fs";
@@ -202,18 +205,17 @@ var readDisplayDraft = (sessionId, runtime) => {
202
205
  };
203
206
 
204
207
  // src/transcript-session-key.ts
205
- import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync, writeFileSync } from "fs";
208
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync } from "fs";
206
209
  import { homedir } from "os";
207
210
  import { join as join2 } from "path";
208
211
  var sessionKeyDirectory = () => process.env.PUSHARY_SESSION_KEY_DIR?.trim() || join2(homedir(), ".pushary", "sessions");
209
- var SESSION_KEY_TTL_MS = 48 * 60 * 60 * 1e3;
212
+ var SESSION_DISPLAY_TTL_MS = 48 * 60 * 60 * 1e3;
210
213
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
211
214
  var fileFor2 = (sessionId, directory) => SAFE_SESSION_ID.test(sessionId) ? join2(directory, `${sessionId}.json`) : null;
212
215
  var loadSessionKey = (sessionId, directory = sessionKeyDirectory()) => {
213
216
  const path = fileFor2(sessionId, directory);
214
217
  if (!path || !existsSync(path)) return null;
215
218
  try {
216
- if (Date.now() - statSync(path).mtimeMs > SESSION_KEY_TTL_MS) return null;
217
219
  const raw = JSON.parse(readFileSync2(path, "utf-8"));
218
220
  const key = typeof raw.sessionKey === "string" ? Buffer.from(raw.sessionKey, "base64") : null;
219
221
  if (!key || key.length !== 32) return null;
@@ -231,33 +233,31 @@ var loadSessionKey = (sessionId, directory = sessionKeyDirectory()) => {
231
233
  };
232
234
  var saveSessionKey = (sessionId, value, directory = sessionKeyDirectory()) => {
233
235
  const path = fileFor2(sessionId, directory);
234
- if (!path) return;
236
+ if (!path) return false;
235
237
  try {
236
238
  mkdirSync(directory, { recursive: true, mode: 448 });
237
- writeFileSync(
238
- path,
239
- JSON.stringify({
240
- sessionKey: Buffer.from(value.sessionKey).toString("base64"),
241
- wrappedKey: value.wrappedKey,
242
- providerWrappedKey: value.providerWrappedKey,
243
- providerKeyId: value.providerKeyId,
244
- compressRecords: value.compressRecords,
245
- keyUploaded: value.keyUploaded
246
- }),
247
- { mode: 384 }
248
- );
239
+ writeJsonAtomicDurable(path, {
240
+ sessionKey: Buffer.from(value.sessionKey).toString("base64"),
241
+ wrappedKey: value.wrappedKey,
242
+ providerWrappedKey: value.providerWrappedKey,
243
+ providerKeyId: value.providerKeyId,
244
+ compressRecords: value.compressRecords,
245
+ keyUploaded: value.keyUploaded
246
+ }, 384);
247
+ return true;
249
248
  } catch {
249
+ return false;
250
250
  }
251
251
  };
252
- var sweepStaleSessionKeys = (now = Date.now(), dir = sessionKeyDirectory()) => {
252
+ var sweepStaleSessionDisplays = (now = Date.now(), dir = sessionKeyDirectory()) => {
253
253
  if (!existsSync(dir)) return 0;
254
254
  let removed = 0;
255
255
  try {
256
256
  for (const name of readdirSync(dir)) {
257
- if (!name.endsWith(".json") && !name.endsWith(".display")) continue;
257
+ if (!name.endsWith(".display")) continue;
258
258
  const path = join2(dir, name);
259
259
  try {
260
- if (now - statSync(path).mtimeMs <= SESSION_KEY_TTL_MS) continue;
260
+ if (now - statSync(path).mtimeMs <= SESSION_DISPLAY_TTL_MS) continue;
261
261
  rmSync(path, { force: true });
262
262
  removed += 1;
263
263
  } catch {
@@ -288,8 +288,8 @@ var startDraftPublisher = (options) => {
288
288
  if (!key?.keyUploaded) return;
289
289
  const draft = options.snapshot(key.sessionKey);
290
290
  if (!draft) return;
291
- const identity = JSON.stringify([sessionId, draft.startedAt, draft.revision]);
292
- if (identity === sent) return;
291
+ const identity2 = JSON.stringify([sessionId, draft.startedAt, draft.revision]);
292
+ if (identity2 === sent) return;
293
293
  const response = await fetch(`${options.baseUrl}/api/agent/transcript`, {
294
294
  method: "POST",
295
295
  headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json" },
@@ -301,7 +301,7 @@ var startDraftPublisher = (options) => {
301
301
  signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5e3)])
302
302
  });
303
303
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
304
- sent = identity;
304
+ sent = identity2;
305
305
  retryAt = 0;
306
306
  retryMs = 1e3;
307
307
  reportedFailure = false;
@@ -364,6 +364,11 @@ var startDisplayPublisher = (options) => {
364
364
  }).stop;
365
365
  };
366
366
 
367
+ // src/transcript-reader.ts
368
+ import { createHash as createHash3 } from "crypto";
369
+ import { join as join3 } from "path";
370
+ import { closeSync as closeSync3, fstatSync as fstatSync3, openSync as openSync3, readFileSync as readFileSync5, readSync as readSync2, statSync as statSync2 } from "fs";
371
+
367
372
  // src/transcript-source.ts
368
373
  var normalizeTranscriptLines = (lines, source) => {
369
374
  if (source === "claude") return [...lines];
@@ -416,13 +421,456 @@ var transcriptLinesSince = (lines, oldestLineMs, dropCompactionSummaries) => lin
416
421
  return !stampedBefore(record, oldestLineMs) && !(dropCompactionSummaries && isCompactionSummary(record));
417
422
  });
418
423
 
424
+ // src/transcript-snapshot-file.ts
425
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3 } from "fs";
426
+
427
+ // src/transcript-snapshot-source.ts
428
+ import { createHash } from "crypto";
429
+ var identity = external_exports.string().min(1).max(256);
430
+ var timestamp = external_exports.string().refine((value) => Number.isFinite(Date.parse(value)));
431
+ var geminiTool = external_exports.object({ id: identity, name: external_exports.string(), args: external_exports.record(external_exports.string(), external_exports.unknown()), result: external_exports.unknown().optional(), status: external_exports.string() });
432
+ var geminiMessage = external_exports.object({
433
+ id: identity,
434
+ type: external_exports.enum(["user", "gemini", "info", "warning", "error"]),
435
+ timestamp,
436
+ content: external_exports.unknown(),
437
+ displayContent: external_exports.unknown().optional(),
438
+ toolCalls: external_exports.array(geminiTool).optional()
439
+ });
440
+ var toolPatch = external_exports.object({ id: identity, result: external_exports.unknown().optional() });
441
+ var messagePatch = external_exports.object({ id: identity, content: external_exports.unknown().optional(), toolCalls: external_exports.array(toolPatch).optional() });
442
+ var patchSchema = messagePatch.partial().extend({ updates: external_exports.array(messagePatch).optional(), removeIds: external_exports.array(identity).optional(), orderIds: external_exports.array(identity).optional() });
443
+ var metadataSchema = external_exports.object({ sessionId: identity, kind: external_exports.enum(["main", "subagent"]).optional() });
444
+ var textParts = (value) => {
445
+ if (typeof value === "string") return [{ type: "text", text: value }];
446
+ if (!Array.isArray(value)) return [];
447
+ return value.flatMap((part) => {
448
+ if (typeof part === "string") return [{ type: "text", text: part }];
449
+ return isRecord(part) && part.thought !== true && typeof part.text === "string" ? [{ type: "text", text: part.text }] : [];
450
+ });
451
+ };
452
+ var geminiResult = (value) => {
453
+ if (typeof value === "string") return value;
454
+ if (!Array.isArray(value)) return null;
455
+ const parts = value.flatMap((part) => {
456
+ if (!isRecord(part) || part.thought === true) return [];
457
+ if (typeof part.text === "string") return [part.text];
458
+ if (isRecord(part.functionResponse) && isRecord(part.functionResponse.response)) return [JSON.stringify(part.functionResponse.response)];
459
+ return [];
460
+ });
461
+ return parts.join("\n");
462
+ };
463
+ var geminiConversationMessage = (message) => {
464
+ if (message.type !== "user" && message.type !== "gemini") return [];
465
+ const content = textParts(message.displayContent ?? message.content);
466
+ for (const tool of message.toolCalls ?? []) {
467
+ content.push({ type: "tool_use", id: tool.id, name: tool.name, input: tool.args });
468
+ if (tool.result !== void 0 && tool.result !== null) content.push({ type: "tool_result", tool_use_id: tool.id, content: geminiResult(tool.result), is_error: tool.status === "error" });
469
+ }
470
+ return [{ id: message.id, role: message.type === "user" ? "user" : "assistant", timestamp: message.timestamp, content }];
471
+ };
472
+ var applyMessagePatch = (messages, patch) => {
473
+ const previous = messages.get(patch.id);
474
+ if (!previous) return;
475
+ const updates = new Map(patch.toolCalls?.map((tool) => [tool.id, tool]));
476
+ const toolCalls = previous.toolCalls?.map((tool) => {
477
+ const update = updates.get(tool.id);
478
+ return update && Object.hasOwn(update, "result") ? { ...tool, result: update.result } : tool;
479
+ });
480
+ messages.set(patch.id, { ...previous, ...Object.hasOwn(patch, "content") ? { content: patch.content } : {}, toolCalls });
481
+ };
482
+ var applyGeminiPatch = (messages, value) => {
483
+ const patch = patchSchema.parse(value);
484
+ if (patch.id) applyMessagePatch(messages, { ...patch, id: patch.id });
485
+ for (const update of patch.updates ?? []) applyMessagePatch(messages, update);
486
+ for (const id of patch.removeIds ?? []) messages.delete(id);
487
+ for (const id of patch.orderIds ?? []) {
488
+ const message = messages.get(id);
489
+ if (message) {
490
+ messages.delete(id);
491
+ messages.set(id, message);
492
+ }
493
+ }
494
+ };
495
+ var replaceMessages = (messages, value) => {
496
+ const replacement = external_exports.array(geminiMessage).parse(value);
497
+ messages.clear();
498
+ for (const message of replacement) messages.set(message.id, message);
499
+ };
500
+ var rewindMessages = (messages, id) => {
501
+ if (!messages.has(id)) {
502
+ messages.clear();
503
+ return;
504
+ }
505
+ let remove = false;
506
+ for (const key of messages.keys()) {
507
+ if (key === id) remove = true;
508
+ if (remove) messages.delete(key);
509
+ }
510
+ };
511
+ var parseGeminiSnapshot = (text, sessionId) => {
512
+ let records;
513
+ try {
514
+ records = [JSON.parse(text)];
515
+ } catch {
516
+ records = text.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
517
+ }
518
+ const messages = /* @__PURE__ */ new Map();
519
+ let identified = false;
520
+ for (const record of records) {
521
+ if (!isRecord(record)) throw new Error("Invalid Gemini history record");
522
+ if ("sessionId" in record) {
523
+ const metadata = metadataSchema.parse(record);
524
+ if (metadata.sessionId !== sessionId || metadata.kind === "subagent") throw new Error("Gemini history identity mismatch");
525
+ identified = true;
526
+ if ("messages" in record) replaceMessages(messages, record.messages);
527
+ } else if ("$rewindTo" in record) rewindMessages(messages, identity.parse(record.$rewindTo));
528
+ else if ("$patch" in record) applyGeminiPatch(messages, record.$patch);
529
+ else if ("$set" in record) {
530
+ if (!isRecord(record.$set)) throw new Error("Invalid Gemini checkpoint");
531
+ if ("sessionId" in record.$set && record.$set.sessionId !== sessionId) throw new Error("Gemini checkpoint identity mismatch");
532
+ if (record.$set.kind === "subagent") throw new Error("Gemini subagent history is not a main session");
533
+ if ("messages" in record.$set) replaceMessages(messages, record.$set.messages);
534
+ } else {
535
+ const message = geminiMessage.parse(record);
536
+ messages.set(message.id, message);
537
+ }
538
+ }
539
+ if (!identified) throw new Error("Gemini history metadata unavailable");
540
+ return [...messages.values()].flatMap(geminiConversationMessage);
541
+ };
542
+ var cursorBlock = external_exports.discriminatedUnion("type", [
543
+ external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }),
544
+ external_exports.object({ type: external_exports.literal("tool_use"), name: external_exports.string(), input: external_exports.record(external_exports.string(), external_exports.unknown()) })
545
+ ]);
546
+ var cursorMessage = external_exports.object({ role: external_exports.enum(["user", "assistant"]), message: external_exports.object({ content: external_exports.array(cursorBlock) }) });
547
+ var parseCursorSnapshot = (text) => {
548
+ const messages = [];
549
+ const occurrences = /* @__PURE__ */ new Map();
550
+ for (const line of text.split("\n").filter((line2) => line2.trim())) {
551
+ const record = JSON.parse(line);
552
+ if (isRecord(record) && (record.type === "metadata" || record.type === "turn_ended")) continue;
553
+ const message = cursorMessage.parse(record);
554
+ const fingerprint = createHash("sha256").update(canonicalJsonText(message)).digest("hex");
555
+ const occurrence = occurrences.get(fingerprint) ?? 0;
556
+ occurrences.set(fingerprint, occurrence + 1);
557
+ messages.push({ id: `cursor:${fingerprint}:${occurrence}`, role: message.role, content: message.message.content });
558
+ }
559
+ return messages;
560
+ };
561
+ var historyBlock = external_exports.discriminatedUnion("type", [
562
+ external_exports.object({ type: external_exports.literal("text"), text: external_exports.string() }),
563
+ external_exports.object({ type: external_exports.literal("tool_use"), id: identity, name: external_exports.string(), input: external_exports.record(external_exports.string(), external_exports.unknown()) }),
564
+ external_exports.object({ type: external_exports.literal("tool_result"), tool_use_id: identity, content: external_exports.unknown(), is_error: external_exports.boolean().optional() })
565
+ ]);
566
+ var hermesSnapshot = external_exports.object({
567
+ version: external_exports.literal(1),
568
+ sessionId: identity,
569
+ completed: external_exports.boolean().default(false),
570
+ updatedAtMs: external_exports.number().int().positive(),
571
+ error: external_exports.string().optional(),
572
+ messages: external_exports.array(external_exports.object({ id: identity, role: external_exports.enum(["user", "assistant"]), timestamp, content: external_exports.array(historyBlock) })).max(5e3)
573
+ });
574
+ var TranscriptSourceUnavailable = class extends Error {
575
+ };
576
+ var parseHermesSnapshot = (text, sessionId, nowMs) => {
577
+ const snapshot = hermesSnapshot.parse(JSON.parse(text));
578
+ if (snapshot.sessionId !== sessionId) throw new Error("Hermes history identity mismatch");
579
+ if (snapshot.error || !snapshot.completed && nowMs - snapshot.updatedAtMs > 18e4 || snapshot.updatedAtMs > nowMs + 5e3) throw new TranscriptSourceUnavailable("Hermes history exporter is unavailable");
580
+ return snapshot.messages;
581
+ };
582
+
583
+ // src/transcript-snapshot-file.ts
584
+ var MAXIMUM_SNAPSHOT_MESSAGES = 5e3;
585
+ var readTranscriptSnapshot = (path, source, sessionId) => {
586
+ const descriptor = openSync2(path, "r");
587
+ try {
588
+ const before = fstatSync2(descriptor);
589
+ if (!before.isFile() || before.size === 0 || before.size > SESSION_HISTORY_BACKFILL_LIMIT_BYTES) throw new Error("History snapshot is empty or exceeds its size limit");
590
+ const text = readFileSync3(descriptor, "utf8");
591
+ const after = fstatSync2(descriptor);
592
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs) throw new Error("History snapshot changed while reading");
593
+ let messages;
594
+ switch (source) {
595
+ case "gemini":
596
+ messages = parseGeminiSnapshot(text, sessionId);
597
+ break;
598
+ case "cursor":
599
+ messages = parseCursorSnapshot(text);
600
+ break;
601
+ case "hermes":
602
+ messages = parseHermesSnapshot(text, sessionId, Date.now());
603
+ break;
604
+ }
605
+ if (messages.length > MAXIMUM_SNAPSHOT_MESSAGES) throw new Error("History snapshot exceeds its message limit");
606
+ return messages;
607
+ } finally {
608
+ closeSync2(descriptor);
609
+ }
610
+ };
611
+
612
+ // src/transcript-snapshot-journal.ts
613
+ import { lstatSync as lstatSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
614
+ import { dirname } from "path";
615
+
616
+ // src/transcript-projection.ts
617
+ import { createHash as createHash2 } from "crypto";
618
+ var projectTranscriptSnapshot = (previous, messages) => {
619
+ const revision = (previous?.revision ?? 0) + 1;
620
+ const entries = /* @__PURE__ */ Object.create(null);
621
+ const lines = [];
622
+ for (const [order, message] of messages.entries()) {
623
+ if (!message.id || message.id.length > 256 || Object.hasOwn(entries, message.id)) throw new Error("Invalid or duplicate history message identity");
624
+ const record = { timestamp: message.timestamp, observedAtMs: message.observedAtMs, historyMessage: { role: message.role, content: message.content } };
625
+ const digest = createHash2("sha256").update(JSON.stringify(record)).digest("hex");
626
+ entries[message.id] = { digest, order };
627
+ const earlier = previous?.entries[message.id];
628
+ if (earlier?.digest === digest && earlier.order === order) continue;
629
+ lines.push(JSON.stringify({ ...record, historySource: { id: message.id, order, removed: false }, historyRevision: revision }));
630
+ }
631
+ for (const [id, entry] of Object.entries(previous?.entries ?? {})) {
632
+ if (Object.hasOwn(entries, id)) continue;
633
+ lines.push(JSON.stringify({ historySource: { id, order: entry.order, removed: true }, historyRevision: revision }));
634
+ }
635
+ return { lines, next: { revision: lines.length ? revision : previous?.revision ?? 0, entries } };
636
+ };
637
+
638
+ // src/transcript-snapshot-journal.ts
639
+ var MAXIMUM_JOURNAL_BYTES = 64 * 1024 * 1024;
640
+ var projectionSchema = external_exports.object({
641
+ revision: external_exports.number().int().nonnegative(),
642
+ entries: external_exports.record(external_exports.string(), external_exports.object({ digest: external_exports.string().regex(/^[a-f0-9]{64}$/), order: external_exports.number().int().nonnegative() }))
643
+ });
644
+ var journalSchema = external_exports.object({
645
+ version: external_exports.literal(1),
646
+ binding: external_exports.string(),
647
+ epochStartedAtMs: external_exports.number(),
648
+ initialized: external_exports.boolean(),
649
+ observedAtMs: external_exports.record(external_exports.string(), external_exports.number().nonnegative()).default({}),
650
+ excludedIds: external_exports.array(external_exports.string()),
651
+ projection: projectionSchema.nullable(),
652
+ pending: external_exports.object({ lines: external_exports.array(external_exports.string()), next: projectionSchema }).nullable()
653
+ });
654
+ var TranscriptSnapshotJournal = class {
655
+ constructor(path, binding, epochStartedAtMs) {
656
+ this.path = path;
657
+ const info = lstatSync2(path, { throwIfNoEntry: false });
658
+ if (!info) {
659
+ this.current = { version: 1, binding, epochStartedAtMs, initialized: false, observedAtMs: {}, excludedIds: [], projection: null, pending: null };
660
+ return;
661
+ }
662
+ if (!info.isFile() || info.nlink !== 1 || info.size > MAXIMUM_JOURNAL_BYTES || process.getuid && (info.uid !== process.getuid() || (info.mode & 511) !== 384)) throw new Error("Unsafe history checkpoint");
663
+ const saved = journalSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
664
+ if (saved.binding !== binding) throw new Error("History checkpoint belongs to another source");
665
+ this.current = saved.epochStartedAtMs === epochStartedAtMs ? saved : { ...saved, epochStartedAtMs, initialized: false, observedAtMs: {}, pending: null, excludedIds: [], projection: { revision: Math.max(saved.projection?.revision ?? 0, saved.pending?.next.revision ?? 0), entries: {} } };
666
+ }
667
+ current;
668
+ prepare(messages, oldestMessageMs, nowMs = Date.now()) {
669
+ const pending = this.pendingLinesSince(oldestMessageMs);
670
+ if (pending) return pending;
671
+ const identities = new Set(messages.map((message) => message.id));
672
+ const excluded = new Set(this.current.excludedIds.filter((id) => identities.has(id)));
673
+ if (!this.current.initialized) {
674
+ for (const message of messages) if (!message.timestamp) excluded.add(message.id);
675
+ }
676
+ const observedAtMs = Object.fromEntries(Object.entries(this.current.observedAtMs).filter(([id]) => identities.has(id)));
677
+ for (const message of messages) {
678
+ if (!message.timestamp && !excluded.has(message.id) && !Object.hasOwn(observedAtMs, message.id)) observedAtMs[message.id] = nowMs;
679
+ }
680
+ const allowed = messages.filter((message) => !excluded.has(message.id) && (message.timestamp ? Date.parse(message.timestamp) : observedAtMs[message.id] ?? 0) >= oldestMessageMs).map((message) => message.timestamp ? message : { ...message, observedAtMs: observedAtMs[message.id] });
681
+ const projected = projectTranscriptSnapshot(this.current.projection, allowed);
682
+ if (projected.lines.length === 0) {
683
+ const metadataChanged = excluded.size !== this.current.excludedIds.length || Object.keys(observedAtMs).length !== Object.keys(this.current.observedAtMs).length;
684
+ if (!this.current.initialized || metadataChanged) this.store({ ...this.current, initialized: true, observedAtMs, excludedIds: [...excluded], projection: projected.next, pending: null });
685
+ return [];
686
+ }
687
+ this.store({ ...this.current, initialized: true, observedAtMs, excludedIds: [...excluded], pending: { lines: [...projected.lines], next: projected.next } });
688
+ return projected.lines;
689
+ }
690
+ pendingLinesSince(oldestMessageMs) {
691
+ const pending = this.current.pending;
692
+ if (!pending) return null;
693
+ const lines = pending.lines.filter((line) => {
694
+ const record = JSON.parse(line);
695
+ if (!record || typeof record !== "object") throw new Error("Invalid pending history record");
696
+ if ("timestamp" in record) return typeof record.timestamp === "string" && Date.parse(record.timestamp) >= oldestMessageMs;
697
+ return !("observedAtMs" in record) || typeof record.observedAtMs === "number" && record.observedAtMs >= oldestMessageMs;
698
+ });
699
+ if (lines.length !== pending.lines.length) this.store({ ...this.current, pending: { ...pending, lines } });
700
+ return lines;
701
+ }
702
+ settle() {
703
+ if (!this.current.pending) return;
704
+ this.store({ ...this.current, projection: this.current.pending.next, pending: null });
705
+ }
706
+ store(next) {
707
+ if (Buffer.byteLength(JSON.stringify(next)) > MAXIMUM_JOURNAL_BYTES) throw new Error("History checkpoint exceeds its size limit");
708
+ mkdirSync2(dirname(this.path), { recursive: true, mode: 448 });
709
+ writeJsonAtomicDurable(this.path, next, 384);
710
+ this.current = journalSchema.parse(next);
711
+ }
712
+ };
713
+
714
+ // src/transcript-reader.ts
715
+ var readPositionsKeptInMemory = { load: () => /* @__PURE__ */ new Map(), save: () => {
716
+ } };
717
+ var startTranscriptCaptureEpoch = (startedAtMs, positions = readPositionsKeptInMemory, backfill = null) => ({ startedAtMs, cutoffs: new Map(positions.load()), positions, backfill });
718
+ var recordedBirthtime = (info) => process.platform === "darwin" && info.birthtimeMs > 0 ? info.birthtimeMs : null;
719
+ var sightingOf = (info, seenAtMs) => info ? { identity: `${info.dev}:${info.ino}`, sizeBytes: info.size, birthtimeMs: recordedBirthtime(info), seenAtMs } : { identity: null, sizeBytes: 0, birthtimeMs: null, seenAtMs };
720
+ var storeCutoff = (epoch, path, cutoff) => {
721
+ epoch.cutoffs.set(path, cutoff);
722
+ epoch.positions.save(epoch.cutoffs);
723
+ };
724
+ var recordSighting = (epoch, path, sighting) => {
725
+ const previous = epoch.cutoffs.get(path);
726
+ const cutoff = nextTranscriptCutoff(previous, sighting, epoch.startedAtMs, epoch.backfill);
727
+ if (cutoff !== previous) storeCutoff(epoch, path, cutoff);
728
+ return cutoff;
729
+ };
730
+ var recordSettledPosition = (epoch, path, readFrom, settledThroughOffset) => {
731
+ const current = epoch.cutoffs.get(path);
732
+ const advanced = advancedTranscriptCutoff(current, readFrom, settledThroughOffset);
733
+ if (advanced !== void 0 && advanced !== current) storeCutoff(epoch, path, advanced);
734
+ };
735
+ var forgetUnregisteredTranscripts = (epoch, registeredPaths) => {
736
+ const unregistered = [...epoch.cutoffs.keys()].filter((path) => !registeredPaths.has(path));
737
+ if (unregistered.length === 0) return;
738
+ for (const path of unregistered) epoch.cutoffs.delete(path);
739
+ epoch.positions.save(epoch.cutoffs);
740
+ };
741
+ var statOrNull = (path) => {
742
+ try {
743
+ return statSync2(path);
744
+ } catch {
745
+ return null;
746
+ }
747
+ };
748
+ var sightTranscript = (epoch, path, seenAtMs) => recordSighting(epoch, path, sightingOf(statOrNull(path), seenAtMs));
749
+ var LINE_FEED = 10;
750
+ var MAXIMUM_TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
751
+ var nothingUnread = (cutoff) => ({ lines: [], cutoff, completeThroughOffset: cutoff.offset, reachedEnd: true });
752
+ var readThroughALineEnd = (descriptor, offset, unread) => {
753
+ let window = Math.min(unread, MAXIMUM_TRANSCRIPT_READ_BYTES);
754
+ for (; ; ) {
755
+ const buffer = Buffer.alloc(window);
756
+ const received = buffer.subarray(0, readSync2(descriptor, buffer, 0, window, offset));
757
+ if (window >= unread || received.length < window || received.includes(LINE_FEED)) return received;
758
+ window = Math.min(unread, window * 2);
759
+ }
760
+ };
761
+ var readLinesSinceCutoff = (epoch, path, seenAtMs) => {
762
+ let descriptor;
763
+ try {
764
+ descriptor = openSync3(path, "r");
765
+ } catch {
766
+ return nothingUnread(sightTranscript(epoch, path, seenAtMs));
767
+ }
768
+ try {
769
+ const info = fstatSync3(descriptor);
770
+ const cutoff = recordSighting(epoch, path, sightingOf(info, seenAtMs));
771
+ const unread = info.size - cutoff.offset;
772
+ if (unread <= 0) return nothingUnread(cutoff);
773
+ const received = readThroughALineEnd(descriptor, cutoff.offset, unread);
774
+ const completeBytes = received.lastIndexOf(LINE_FEED) + 1;
775
+ const reachedEnd = received.length >= unread;
776
+ return {
777
+ lines: (reachedEnd ? received : received.subarray(0, completeBytes)).toString("utf8").split("\n"),
778
+ cutoff,
779
+ completeThroughOffset: cutoff.offset + completeBytes,
780
+ reachedEnd
781
+ };
782
+ } finally {
783
+ closeSync3(descriptor);
784
+ }
785
+ };
786
+ var linesSeenDuringEpoch = (epoch, path, source, seenAtMs, retentionDays) => {
787
+ const read2 = readLinesSinceCutoff(epoch, path, seenAtMs);
788
+ const oldestLineMs = oldestTranscriptLineMs(epoch.startedAtMs, epoch.backfill, retentionDays, seenAtMs);
789
+ return {
790
+ ...read2,
791
+ lines: transcriptLinesSince(normalizeTranscriptLines(read2.lines, source), oldestLineMs, dropsCompactionSummaries(read2.cutoff, epoch.backfill))
792
+ };
793
+ };
794
+ var sourceVersion = (path) => {
795
+ const info = statOrNull(path);
796
+ if (!info?.isFile()) throw new TranscriptSourceUnavailable("Conversation source unavailable");
797
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}`;
798
+ };
799
+ var appendOnlyReader = (path, source, epoch) => ({
800
+ requiresRevisions: false,
801
+ read(retentionDays, nowMs) {
802
+ const version = sourceVersion(path);
803
+ const read2 = epoch ? linesSeenDuringEpoch(epoch, path, source, nowMs, retentionDays) : null;
804
+ const lines = read2?.lines ?? normalizeTranscriptLines(readFileSync5(path, "utf8").split("\n"), source);
805
+ return {
806
+ lines,
807
+ acknowledge() {
808
+ if (epoch && read2) recordSettledPosition(epoch, path, read2.cutoff, read2.completeThroughOffset);
809
+ if ((read2?.reachedEnd ?? true) && sourceVersion(path) === version) return "caught_up";
810
+ return !read2 || read2.completeThroughOffset > read2.cutoff.offset ? "more" : "unchanged";
811
+ }
812
+ };
813
+ }
814
+ });
815
+ var snapshotReader = (options, source) => {
816
+ const { path, epoch } = options;
817
+ const binding = createHash3("sha256").update(JSON.stringify([options.baseUrl, options.apiKey, options.machineId, options.sessionId, source, path])).digest("hex");
818
+ const journalPath = join3(options.keyDirectory, "snapshots", binding + ".json");
819
+ const epochStart = epoch?.startedAtMs ?? 0;
820
+ const backfill = epoch ? epoch.backfill : SESSION_HISTORY_BACKFILL;
821
+ let journal;
822
+ return {
823
+ requiresRevisions: true,
824
+ read(retentionDays, nowMs) {
825
+ const checkpoint = journal ??= new TranscriptSnapshotJournal(journalPath, binding, backfill ? 0 : epochStart);
826
+ const oldestMessage = oldestTranscriptLineMs(epochStart, backfill, retentionDays, nowMs);
827
+ const pending = checkpoint.pendingLinesSince(oldestMessage);
828
+ const version = pending === null ? sourceVersion(path) : null;
829
+ const lines = pending ?? checkpoint.prepare(readTranscriptSnapshot(path, source, options.sessionId), oldestMessage, nowMs);
830
+ return {
831
+ lines,
832
+ acknowledge() {
833
+ checkpoint.settle();
834
+ return version === null || sourceVersion(path) !== version ? "more" : "caught_up";
835
+ }
836
+ };
837
+ }
838
+ };
839
+ };
840
+ var createTranscriptReader = (options) => options.source === "claude" || options.source === "codex" ? appendOnlyReader(options.path, options.source, options.epoch) : snapshotReader(options, options.source);
841
+
842
+ // src/transcript-health.ts
843
+ var HEALTH_REFRESH_MS = 6e4;
844
+ var transcriptHealthReporter = (target) => {
845
+ let reported;
846
+ let reportedAt = 0;
847
+ let warned = false;
848
+ return async (state) => {
849
+ if (!target.machineId || state === reported && Date.now() - reportedAt < HEALTH_REFRESH_MS) return;
850
+ try {
851
+ const response = await fetch(`${target.baseUrl}/api/agent/transcript/status`, {
852
+ method: "POST",
853
+ headers: { Authorization: `Bearer ${target.apiKey}`, "Content-Type": "application/json" },
854
+ body: JSON.stringify({ sessionId: target.sessionId, machineId: target.machineId, captureState: state }),
855
+ signal: AbortSignal.timeout(2e3)
856
+ });
857
+ if (!response.ok) throw new Error("History status rejected");
858
+ reported = state;
859
+ reportedAt = Date.now();
860
+ warned = false;
861
+ } catch {
862
+ if (!warned) process.stderr.write("[pushary] History sync status could not reach the server.\n");
863
+ warned = true;
864
+ }
865
+ };
866
+ };
867
+
419
868
  // src/transcript-sync.ts
420
- import { join as join3, resolve } from "path";
421
- import { closeSync as closeSync2, existsSync as existsSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3, readSync as readSync2, statSync as statSync2 } from "fs";
869
+ import { join as join4, resolve } from "path";
422
870
  var claudeTranscriptPath = (cwd, sessionId) => {
423
871
  const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
424
872
  const configDir = claudeConfigDir();
425
- return join3(configDir, "projects", projectId, `${sessionId}.jsonl`);
873
+ return join4(configDir, "projects", projectId, `${sessionId}.jsonl`);
426
874
  };
427
875
  var createSyncState = () => ({
428
876
  sessionKey: null,
@@ -434,6 +882,7 @@ var createSyncState = () => ({
434
882
  keyUploaded: false,
435
883
  keyRejected: false,
436
884
  keyUnavailable: false,
885
+ rejectedRecords: 0,
437
886
  unsettled: 0,
438
887
  retentionDays: null
439
888
  });
@@ -502,6 +951,7 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
502
951
  keyUploaded,
503
952
  keyRejected,
504
953
  keyUnavailable,
954
+ rejectedRecords: state.rejectedRecords + rejected,
505
955
  unsettled: built.records.filter((record) => !seen.has(record.localId)).length,
506
956
  retentionDays: keyed.retentionDays
507
957
  };
@@ -525,106 +975,34 @@ var chunkRecords = (records) => {
525
975
  if (current.length > 0) chunks.push(current);
526
976
  return chunks;
527
977
  };
528
- var readLines = (path) => {
529
- try {
530
- return existsSync2(path) ? readFileSync3(path, "utf-8").split("\n") : [];
531
- } catch {
532
- return [];
533
- }
534
- };
535
978
  var IDLE_CEILING_MS = 6e4;
536
- var readPositionsKeptInMemory = { load: () => /* @__PURE__ */ new Map(), save: () => {
537
- } };
538
- var startTranscriptCaptureEpoch = (startedAtMs, positions = readPositionsKeptInMemory, backfill = null) => ({ startedAtMs, cutoffs: new Map(positions.load()), positions, backfill });
539
- var recordedBirthtime = (info) => process.platform === "darwin" && info.birthtimeMs > 0 ? info.birthtimeMs : null;
540
- var sightingOf = (info, seenAtMs) => info ? { identity: `${info.dev}:${info.ino}`, sizeBytes: info.size, birthtimeMs: recordedBirthtime(info), seenAtMs } : { identity: null, sizeBytes: 0, birthtimeMs: null, seenAtMs };
541
- var storeCutoff = (epoch, path, cutoff) => {
542
- epoch.cutoffs.set(path, cutoff);
543
- epoch.positions.save(epoch.cutoffs);
544
- };
545
- var recordSighting = (epoch, path, sighting) => {
546
- const previous = epoch.cutoffs.get(path);
547
- const cutoff = nextTranscriptCutoff(previous, sighting, epoch.startedAtMs, epoch.backfill);
548
- if (cutoff !== previous) storeCutoff(epoch, path, cutoff);
549
- return cutoff;
550
- };
551
- var recordSettledPosition = (epoch, path, readFrom, settledThroughOffset) => {
552
- const current = epoch.cutoffs.get(path);
553
- const advanced = advancedTranscriptCutoff(current, readFrom, settledThroughOffset);
554
- if (advanced !== void 0 && advanced !== current) storeCutoff(epoch, path, advanced);
555
- };
556
- var forgetUnregisteredTranscripts = (epoch, registeredPaths) => {
557
- const unregistered = [...epoch.cutoffs.keys()].filter((path) => !registeredPaths.has(path));
558
- if (unregistered.length === 0) return;
559
- for (const path of unregistered) epoch.cutoffs.delete(path);
560
- epoch.positions.save(epoch.cutoffs);
561
- };
562
- var statOrNull = (path) => {
563
- try {
564
- return statSync2(path);
565
- } catch {
566
- return null;
567
- }
568
- };
569
- var sightTranscript = (epoch, path, seenAtMs) => recordSighting(epoch, path, sightingOf(statOrNull(path), seenAtMs));
570
- var LINE_FEED = 10;
571
- var MAXIMUM_TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
979
+ var RECIPIENT_REFRESH_MS = 6e4;
572
980
  var CATCH_UP_DELAY_MS = 50;
573
981
  var STALLED_BACKOFF_FACTOR = 4;
574
- var nothingUnread = (cutoff) => ({ lines: [], cutoff, completeThroughOffset: cutoff.offset, reachedEnd: true });
575
- var readThroughALineEnd = (descriptor, offset, unread) => {
576
- let window = Math.min(unread, MAXIMUM_TRANSCRIPT_READ_BYTES);
577
- for (; ; ) {
578
- const buffer = Buffer.alloc(window);
579
- const received = buffer.subarray(0, readSync2(descriptor, buffer, 0, window, offset));
580
- if (window >= unread || received.length < window || received.includes(LINE_FEED)) return received;
581
- window = Math.min(unread, window * 2);
582
- }
583
- };
584
- var readLinesSinceCutoff = (epoch, path, seenAtMs) => {
585
- let descriptor;
586
- try {
587
- descriptor = openSync2(path, "r");
588
- } catch {
589
- return nothingUnread(sightTranscript(epoch, path, seenAtMs));
590
- }
591
- try {
592
- const info = fstatSync2(descriptor);
593
- const cutoff = recordSighting(epoch, path, sightingOf(info, seenAtMs));
594
- const unread = info.size - cutoff.offset;
595
- if (unread <= 0) return nothingUnread(cutoff);
596
- const received = readThroughALineEnd(descriptor, cutoff.offset, unread);
597
- const completeBytes = received.lastIndexOf(LINE_FEED) + 1;
598
- const reachedEnd = received.length >= unread;
599
- return {
600
- lines: (reachedEnd ? received : received.subarray(0, completeBytes)).toString("utf8").split("\n"),
601
- cutoff,
602
- completeThroughOffset: cutoff.offset + completeBytes,
603
- reachedEnd
604
- };
605
- } finally {
606
- closeSync2(descriptor);
607
- }
608
- };
609
- var linesSeenDuringEpoch = (epoch, path, source, seenAtMs, retentionDays) => {
610
- const read2 = readLinesSinceCutoff(epoch, path, seenAtMs);
611
- const oldestLineMs = oldestTranscriptLineMs(epoch.startedAtMs, epoch.backfill, retentionDays, seenAtMs);
612
- return {
613
- ...read2,
614
- lines: transcriptLinesSince(normalizeTranscriptLines(read2.lines, source), oldestLineMs, dropsCompactionSummaries(read2.cutoff, epoch.backfill))
615
- };
616
- };
617
982
  var nothingLeftToUpload = (state) => state.sessionKey !== null && !state.keyRejected && state.unsettled === 0;
618
983
  var uploadStalled = (before, after) => after.sessionKey !== null && !after.keyRejected && after.unsettled > 0 && after.seen.size === before.seen.size;
984
+ var captureHealth = (state, stalled, catchingUp) => {
985
+ if (state.keyRejected || state.keyUnavailable || state.rejectedRecords > 0 || stalled) return "capture_failed";
986
+ return catchingUp ? "catching_up" : "capturing";
987
+ };
619
988
  var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
620
989
  var startTranscriptSync = (opts) => {
990
+ const reportHealth = transcriptHealthReporter({ ...opts, baseUrl: opts.baseUrl ?? getBaseUrl() });
991
+ let recipientFailure = "capture_failed";
621
992
  let warnedAboutRecipients = false;
993
+ let verifiedRecipients = null;
994
+ let recipientsVerifiedAt = 0;
622
995
  const fetchPinnedRecipients = async () => {
623
- const offer = await fetchTranscriptRecipients(opts.apiKey, opts.baseUrl);
996
+ if (verifiedRecipients && Date.now() - recipientsVerifiedAt < RECIPIENT_REFRESH_MS) return verifiedRecipients;
997
+ verifiedRecipients = null;
998
+ const offer = await fetchTranscriptRecipients(opts.apiKey, opts.baseUrl, (reason) => {
999
+ recipientFailure = reason;
1000
+ });
624
1001
  if (!offer) return null;
625
1002
  const verdict = verifyRecipients(offer, loadPin(opts.storage?.recipientPinFile));
626
1003
  if (verdict.kind === "first-use" && !savePin(verdict.pin, opts.storage?.recipientPinFile)) return null;
627
1004
  if (verdict.kind === "changed") {
1005
+ recipientFailure = "phone_key_changed";
628
1006
  if (!warnedAboutRecipients) {
629
1007
  warnedAboutRecipients = true;
630
1008
  process.stderr.write(`${changedRecipientsMessage(verdict, offer)}
@@ -632,6 +1010,8 @@ var startTranscriptSync = (opts) => {
632
1010
  }
633
1011
  return null;
634
1012
  }
1013
+ verifiedRecipients = offer;
1014
+ recipientsVerifiedAt = Date.now();
635
1015
  return offer;
636
1016
  };
637
1017
  const uploader = {
@@ -639,16 +1019,15 @@ var startTranscriptSync = (opts) => {
639
1019
  uploadRecords: (params) => (opts.enabled ? opts.enabled() : transcriptsEnabled()) ? uploadTranscriptRecords(opts.apiKey, params, opts.baseUrl) : Promise.resolve({ ok: false, keyRegistered: false, keyConflict: false, keyUnavailable: false, settledIds: null, rejectedIds: [] })
640
1020
  };
641
1021
  const path = opts.path ?? claudeTranscriptPath(opts.cwd, opts.sessionId);
1022
+ const reader = createTranscriptReader({ ...opts, source: opts.source ?? "claude", path, baseUrl: opts.baseUrl ?? getBaseUrl(), keyDirectory: opts.storage?.sessionKeyDirectory ?? sessionKeyDirectory() });
642
1023
  const persisted = loadSessionKey(opts.sessionId, opts.storage?.sessionKeyDirectory);
643
1024
  let state = persisted ? { ...createSyncState(), ...persisted } : createSyncState();
644
- sweepStaleSessionKeys(Date.now(), opts.storage?.sessionKeyDirectory);
1025
+ sweepStaleSessionDisplays(Date.now(), opts.storage?.sessionKeyDirectory);
645
1026
  let persistedKey = persisted?.sessionKey ?? null;
646
1027
  let persistedUploaded = persisted?.keyUploaded ?? false;
647
1028
  const persistSessionKey = () => {
648
1029
  if (!state.sessionKey || state.sessionKey === persistedKey && state.keyUploaded === persistedUploaded) return;
649
- persistedKey = state.sessionKey;
650
- persistedUploaded = state.keyUploaded;
651
- saveSessionKey(opts.sessionId, {
1030
+ const saved = saveSessionKey(opts.sessionId, {
652
1031
  sessionKey: state.sessionKey,
653
1032
  wrappedKey: state.wrappedKey,
654
1033
  providerWrappedKey: state.providerWrappedKey,
@@ -656,6 +1035,9 @@ var startTranscriptSync = (opts) => {
656
1035
  compressRecords: state.compressRecords,
657
1036
  keyUploaded: state.keyUploaded
658
1037
  }, opts.storage?.sessionKeyDirectory);
1038
+ if (!saved) throw new Error("Could not persist transcript encryption key");
1039
+ persistedKey = state.sessionKey;
1040
+ persistedUploaded = state.keyUploaded;
659
1041
  };
660
1042
  let running = true;
661
1043
  let inFlight = false;
@@ -668,27 +1050,42 @@ var startTranscriptSync = (opts) => {
668
1050
  try {
669
1051
  const ending = opts.ending?.() === true;
670
1052
  if (state.keyRejected) {
1053
+ await reportHealth("capture_failed");
671
1054
  if (ending) opts.onDrained?.();
672
1055
  return;
673
1056
  }
674
- state = await withSessionKey(state, uploader);
675
- if (!state.sessionKey) return;
1057
+ const recipients = await fetchPinnedRecipients();
1058
+ if (!recipients) {
1059
+ await reportHealth(recipientFailure);
1060
+ return;
1061
+ }
1062
+ if (reader.requiresRevisions && recipients.phoneReadsTranscriptRevisions !== true) {
1063
+ await reportHealth("phone_upgrade_required");
1064
+ return;
1065
+ }
1066
+ state = await withSessionKey({ ...state, retentionDays: recipients.retentionDays }, uploader);
1067
+ if (!state.sessionKey) {
1068
+ await reportHealth(recipientFailure);
1069
+ return;
1070
+ }
676
1071
  persistSessionKey();
677
1072
  const previous = state;
678
- const modified = existsSync2(path) ? statSync2(path).mtimeMs : null;
679
1073
  const source = opts.source ?? "claude";
680
- const retentionDays = state.retentionDays ?? SESSION_HISTORY_RETENTION_DAYS.standard;
681
- const read2 = opts.epoch ? linesSeenDuringEpoch(opts.epoch, path, source, Date.now(), retentionDays) : null;
682
- const lines = read2?.lines ?? normalizeTranscriptLines(readLines(path), source);
683
- state = await syncTick(lines, state, uploader, opts.sessionId, opts.machineId);
1074
+ const batch = reader.read(state.retentionDays ?? SESSION_HISTORY_RETENTION_DAYS.standard, Date.now());
1075
+ state = await syncTick(batch.lines, state, uploader, opts.sessionId, opts.machineId);
684
1076
  stalled = uploadStalled(previous, state);
685
1077
  persistSessionKey();
686
1078
  const settled = nothingLeftToUpload(state);
687
- if (opts.epoch && read2 && settled) recordSettledPosition(opts.epoch, path, read2.cutoff, read2.completeThroughOffset);
688
- catchingUp = settled && read2 !== null && !read2.reachedEnd && read2.completeThroughOffset > read2.cutoff.offset;
689
- const readEverything = read2?.reachedEnd ?? true;
690
- if (ending && opts.ending?.() && existsSync2(path) && statSync2(path).mtimeMs === modified && settled && readEverything) opts.onDrained?.();
691
- } catch {
1079
+ const progress = settled ? batch.acknowledge() : "unchanged";
1080
+ catchingUp = progress === "more";
1081
+ const health = captureHealth(state, stalled, catchingUp);
1082
+ await reportHealth(source === "cursor" && health === "capturing" ? "capturing_limited" : health);
1083
+ if (ending && opts.ending?.() && progress === "caught_up") {
1084
+ await reportHealth(health === "capture_failed" ? health : source === "cursor" ? "synced_limited" : "synced");
1085
+ opts.onDrained?.();
1086
+ }
1087
+ } catch (error) {
1088
+ await reportHealth(error instanceof TranscriptSourceUnavailable ? "source_unavailable" : "capture_failed");
692
1089
  } finally {
693
1090
  inFlight = false;
694
1091
  }
@@ -726,15 +1123,32 @@ var startTranscriptSync = (opts) => {
726
1123
 
727
1124
  // src/transcript-registration.ts
728
1125
  import { homedir as homedir2 } from "os";
729
- var defaultRegistryDirectory = () => process.env.PUSHARY_TRANSCRIPT_REGISTRY_DIR || join4(pusharyDir(), "transcript-capture");
1126
+ var storedRegistrationSchema = external_exports.object({
1127
+ sessionId: external_exports.string().min(1).max(128),
1128
+ cwd: external_exports.string(),
1129
+ path: external_exports.string(),
1130
+ source: external_exports.enum(["claude", "codex", "gemini", "cursor", "hermes"]),
1131
+ root: external_exports.string().optional(),
1132
+ account: external_exports.string().regex(/^[a-f0-9]{64}$/),
1133
+ consentOverride: external_exports.boolean().nullable().default(null),
1134
+ expectedSiteId: external_exports.string().min(1).optional(),
1135
+ expectedBaseUrl: external_exports.string().url().optional(),
1136
+ verifiedSiteId: external_exports.string().min(1).optional(),
1137
+ endedAt: external_exports.number().positive().optional(),
1138
+ drained: external_exports.boolean().optional()
1139
+ });
1140
+ var defaultRegistryDirectory = () => process.env.PUSHARY_TRANSCRIPT_REGISTRY_DIR || join5(pusharyDir(), "transcript-capture");
730
1141
  var cliRegistry = (apiKey = getApiKey()) => ({ apiKey, baseUrl: getBaseUrl(), directory: defaultRegistryDirectory() });
731
- var accountFor = (registry) => createHash("sha256").update(`${registry.baseUrl}\0${registry.apiKey}`).digest("hex");
1142
+ var accountFor = (registry) => createHash4("sha256").update(`${registry.baseUrl}\0${registry.apiKey}`).digest("hex");
732
1143
  var TTL = 48 * 60 * 60 * 1e3;
733
- var read = (path) => JSON.parse(readFileSync4(path, "utf8"));
1144
+ var read = (path) => {
1145
+ if (statSync3(path).size > 16384) throw new Error("History registration exceeds its size limit");
1146
+ return storedRegistrationSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
1147
+ };
734
1148
  var consent = (record) => record.consentOverride ?? readTranscriptPreference() ?? true;
735
1149
  var withoutTrailingSlash = (url) => url.replace(/\/+$/, "");
736
1150
  var sameServer = (url, registry) => withoutTrailingSlash(url) === withoutTrailingSlash(registry.baseUrl);
737
- var fileFor3 = (registry, sessionId) => join4(registry.directory, `${createHash("sha256").update(accountFor(registry) + sessionId).digest("hex")}.json`);
1151
+ var fileFor3 = (registry, sessionId) => join5(registry.directory, `${createHash4("sha256").update(accountFor(registry) + sessionId).digest("hex")}.json`);
738
1152
  var permitted = (registry, raw) => raw.account === accountFor(registry) && consent(raw) && !raw.drained && (raw.source !== "claude" || !!raw.expectedSiteId || !macOwnsClaudeHooks(raw.cwd)) && (!raw.expectedSiteId || raw.expectedSiteId === raw.verifiedSiteId && !!raw.expectedBaseUrl && sameServer(raw.expectedBaseUrl, registry));
739
1153
  var registeredTranscriptEnabled = (apiKey, sessionId, registry = cliRegistry(apiKey)) => {
740
1154
  try {
@@ -744,39 +1158,56 @@ var registeredTranscriptEnabled = (apiKey, sessionId, registry = cliRegistry(api
744
1158
  }
745
1159
  };
746
1160
  var macOwnsClaudeHooks = (cwd) => [
747
- join4(claudeConfigDir(), "settings.json"),
748
- join4(cwd, ".claude", "settings.json"),
749
- join4(cwd, ".claude", "settings.local.json")
1161
+ join5(claudeConfigDir(), "settings.json"),
1162
+ join5(cwd, ".claude", "settings.json"),
1163
+ join5(cwd, ".claude", "settings.local.json")
750
1164
  ].some((path) => {
751
1165
  try {
752
- return hookOwnerOf(JSON.parse(readFileSync4(path, "utf8"))) === "app";
1166
+ return hookOwnerOf(JSON.parse(readFileSync6(path, "utf8"))) === "app";
753
1167
  } catch {
754
1168
  return false;
755
1169
  }
756
1170
  });
757
- var TRANSCRIPT_ROOT_NAME = { claude: "projects", codex: "sessions" };
1171
+ var TRANSCRIPT_ROOT_NAME = { claude: "projects", codex: "sessions", gemini: "tmp", cursor: "projects", hermes: "hermes-history" };
758
1172
  var transcriptRoots = (source) => {
759
1173
  const home = homedir2();
760
- if (source === "codex") return [join4(codexHome(), TRANSCRIPT_ROOT_NAME.codex)];
1174
+ if (source === "codex") return [join5(codexHome(), TRANSCRIPT_ROOT_NAME.codex)];
1175
+ if (source === "cursor") return [join5(home, ".cursor", TRANSCRIPT_ROOT_NAME.cursor)];
1176
+ if (source === "hermes") return [join5(home, ".pushary", TRANSCRIPT_ROOT_NAME.hermes)];
1177
+ if (source === "gemini") return [join5(process.env.GEMINI_CLI_HOME || home, ".gemini", "tmp"), join5(home, ".cache", ".gemini", "tmp")];
761
1178
  const configured = process.env.CLAUDE_CONFIG_DIR;
762
- return [join4(configured || join4(home, ".claude"), TRANSCRIPT_ROOT_NAME.claude), join4(home, ".claude", TRANSCRIPT_ROOT_NAME.claude)];
1179
+ return [join5(configured || join5(home, ".claude"), TRANSCRIPT_ROOT_NAME.claude), join5(home, ".claude", TRANSCRIPT_ROOT_NAME.claude)];
763
1180
  };
764
1181
  var isAgentTranscriptRoot = (root, source) => isAbsolute(root) && resolve2(root) === root && basename(root) === TRANSCRIPT_ROOT_NAME[source];
765
1182
  var acceptedTranscriptRoots = (source, registeredRoot) => typeof registeredRoot === "string" && isAgentTranscriptRoot(registeredRoot, source) ? [...transcriptRoots(source), registeredRoot] : transcriptRoots(source);
1183
+ var resolvedSourcePath = (path) => {
1184
+ const absolute = resolve2(path);
1185
+ try {
1186
+ return realpathSync(absolute);
1187
+ } catch {
1188
+ const parent = dirname2(absolute);
1189
+ return parent === absolute ? absolute : join5(resolvedSourcePath(parent), basename(absolute));
1190
+ }
1191
+ };
766
1192
  var transcriptRootContaining = (candidate, roots) => {
767
- const target = resolve2(candidate);
768
- return roots.map((root) => resolve2(root)).find((root) => target.startsWith(root + sep)) ?? null;
1193
+ const target = resolvedSourcePath(candidate);
1194
+ return roots.map((root) => resolve2(root)).find((root) => target.startsWith(resolvedSourcePath(root) + sep)) ?? null;
769
1195
  };
770
1196
  var captureRegistration = (source, value, registeredRoot) => {
771
1197
  const input = value;
772
- if (source !== "claude" && source !== "codex") return null;
773
- if (input.agent_id || typeof input.session_id !== "string" || !/^[\w-]{1,128}$/.test(input.session_id)) return null;
1198
+ if (source !== "claude" && source !== "codex" && source !== "gemini" && source !== "cursor" && source !== "hermes") return null;
1199
+ const sessionId = input.session_id ?? input.conversation_id;
1200
+ if (input.agent_id || typeof sessionId !== "string" || !/^[\w-]{1,128}$/.test(sessionId)) return null;
774
1201
  if (typeof input.cwd !== "string" || !isAbsolute(input.cwd)) return null;
775
- const path = input.transcript_path ?? input.agent_transcript_path ?? (source === "claude" ? claudeTranscriptPath(input.cwd, input.session_id) : null);
776
- if (typeof path !== "string" || !isAbsolute(path) || !path.endsWith(".jsonl")) return null;
1202
+ const path = input.transcript_path ?? input.agent_transcript_path ?? (source === "claude" ? claudeTranscriptPath(input.cwd, sessionId) : null);
1203
+ if (typeof path !== "string" || !isAbsolute(path) || !(path.endsWith(".jsonl") || (source === "gemini" || source === "hermes") && path.endsWith(".json"))) return null;
777
1204
  const root = transcriptRootContaining(path, acceptedTranscriptRoots(source, registeredRoot));
778
1205
  if (root === null) return null;
779
- return { sessionId: input.session_id, cwd: input.cwd, path, source, root };
1206
+ const segments = relative(root, path).split(sep);
1207
+ if (source === "hermes" && (segments.length !== 2 || !/^[a-f0-9]{64}$/.test(segments[0] ?? "") || basename(path) !== `${sessionId}.json`)) return null;
1208
+ if (source === "gemini" && (segments.length !== 3 || segments[1] !== "chats" || !basename(path).startsWith("session-"))) return null;
1209
+ if (source === "cursor" && (segments[1] !== "agent-transcripts" || basename(path) !== `${sessionId}.jsonl` || !(segments.length === 3 || segments.length === 4 && basename(dirname2(path)) === sessionId))) return null;
1210
+ return { sessionId, cwd: input.cwd, path, source, root };
780
1211
  };
781
1212
  var registerTranscript = (source, input, startDaemon = () => {
782
1213
  void ensureDaemonRunning({ waitTimeoutMs: 0 }).catch(() => {
@@ -793,10 +1224,15 @@ var registerTranscript = (source, input, startDaemon = () => {
793
1224
  previous = read(path);
794
1225
  } catch {
795
1226
  }
1227
+ if (previous?.account === account && (resolvedSourcePath(previous.path) !== resolvedSourcePath(registration.path) || previous.source !== registration.source)) {
1228
+ process.stderr.write("[pushary] History registration refused a conflicting source for this session.\n");
1229
+ return;
1230
+ }
796
1231
  if (source === "claude" && !options.siteId && !previous?.expectedSiteId && macOwnsClaudeHooks(registration.cwd)) return;
797
1232
  const ending = options.ended || input.hook_event_name === "SessionEnd" && process.env.PUSHARY_WRAPPER_ACTIVE !== "1";
798
1233
  const stored = {
799
1234
  ...registration,
1235
+ path: previous?.account === account ? previous.path : registration.path,
800
1236
  account,
801
1237
  consentOverride: options.consentOverride ?? transcriptEnvironmentPreference(),
802
1238
  // A wrapper refresh must not erase the Mac's account binding.
@@ -838,7 +1274,7 @@ var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
838
1274
  try {
839
1275
  let unreadable = 0;
840
1276
  const files = readdirSync2(registry.directory).filter((name) => name.endsWith(".json")).flatMap((name) => {
841
- const path = join4(registry.directory, name);
1277
+ const path = join5(registry.directory, name);
842
1278
  try {
843
1279
  const time = statSync3(path).mtimeMs;
844
1280
  if (Date.now() - time > TTL) {
@@ -887,13 +1323,18 @@ var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
887
1323
  for (const [id, registration] of wanted) {
888
1324
  if (active.has(id)) continue;
889
1325
  const current = () => read(registration.registrationPath);
1326
+ const snapshotKeys = join5(registry.directory, "snapshot-keys", accountFor(registry));
1327
+ const keyStorage = storage ?? (registration.source === "claude" || registration.source === "codex" ? void 0 : {
1328
+ sessionKeyDirectory: snapshotKeys,
1329
+ recipientPinFile: join5(snapshotKeys, "recipients.json")
1330
+ });
890
1331
  active.set(id, { path: registration.path, stop: startTranscriptSync({
891
1332
  apiKey,
892
1333
  machineId,
893
1334
  ...registration,
894
1335
  intervalMs,
895
1336
  baseUrl: registry.baseUrl,
896
- storage,
1337
+ storage: keyStorage,
897
1338
  epoch,
898
1339
  enabled: () => {
899
1340
  try {