@pushary/agent-hooks 1.9.75 → 1.9.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/{api-L3BIISGS.js → api-IGMCPM35.js} +10 -10
- package/dist/bin/pushary-bell-hook.js +26 -25
- package/dist/bin/pushary-bell.js +10 -10
- package/dist/bin/pushary-claude.js +27 -26
- package/dist/bin/pushary-clean.js +33 -33
- package/dist/bin/pushary-codex-bridge.js +21 -21
- package/dist/bin/pushary-codex-hook.js +31 -30
- package/dist/bin/pushary-codex-session.js +8 -8
- package/dist/bin/pushary-codex.js +13 -13
- package/dist/bin/pushary-connect.js +26 -26
- package/dist/bin/pushary-cowork.js +5 -5
- package/dist/bin/pushary-daemon.js +123 -66
- package/dist/bin/pushary-disconnect.js +25 -25
- package/dist/bin/pushary-display-hook.js +17 -16
- package/dist/bin/pushary-doctor.js +41 -41
- package/dist/bin/pushary-elicitation-hook.js +26 -25
- package/dist/bin/pushary-gemini-bridge.js +15 -15
- package/dist/bin/pushary-gemini-hook.js +28 -27
- package/dist/bin/pushary-hook.js +30 -29
- package/dist/bin/pushary-login.js +20 -20
- package/dist/bin/pushary-logout.js +24 -24
- package/dist/bin/pushary-mcp.js +11 -11
- package/dist/bin/pushary-mode.js +15 -15
- package/dist/bin/pushary-notification-hook.js +25 -24
- package/dist/bin/pushary-opencode-hook.js +27 -26
- package/dist/bin/pushary-permission-denied-hook.js +30 -29
- package/dist/bin/pushary-permission-hook.js +30 -29
- package/dist/bin/pushary-post-hook.js +25 -24
- package/dist/bin/pushary-prompt-hook.js +25 -24
- package/dist/bin/pushary-session-end-hook.js +27 -26
- package/dist/bin/pushary-session-start-hook.js +35 -34
- package/dist/bin/pushary-setup.js +50 -267
- package/dist/bin/pushary-stats.js +5 -5
- package/dist/bin/pushary-status.js +31 -31
- package/dist/bin/pushary-stop-hook.js +25 -24
- package/dist/bin/pushary-stopfailure-hook.js +25 -24
- package/dist/bin/pushary-suggestions.js +15 -15
- package/dist/bin/pushary-transcript-register.js +47 -18
- package/dist/bin/pushary-transcripts.js +19 -19
- package/dist/bin/pushary-upgrade.js +39 -39
- package/dist/bin/pushary-wait.js +15 -15
- package/dist/bin/pushary.js +7 -7
- package/dist/{chunk-JCZWWBBA.js → chunk-2IOQHB3C.js} +5 -5
- package/dist/{chunk-UIS332AZ.js → chunk-2OWG667E.js} +1 -1
- package/dist/{chunk-UEJTWQVF.js → chunk-2YRNRYZW.js} +1 -1
- package/dist/{chunk-UU3CDLIX.js → chunk-3UYMJZXV.js} +2 -2
- package/dist/{chunk-HXV2SY2V.js → chunk-4ZXFOT44.js} +2 -2
- package/dist/{chunk-L2FQBHIR.js → chunk-6WOG2SCC.js} +3 -2
- package/dist/{chunk-6BFLDHPM.js → chunk-6XOEYBWJ.js} +3 -3
- package/dist/{chunk-SOHJ37NA.js → chunk-7AQEWD2J.js} +3 -3
- package/dist/{chunk-VGHWK5Z2.js → chunk-7CU7OGWP.js} +263 -5
- package/dist/{chunk-GAAN3Y7V.js → chunk-7HYNAA6B.js} +1 -1
- package/dist/{chunk-EVBG5YUO.js → chunk-A3BF7SGM.js} +3 -3
- package/dist/{chunk-AUDYAVNM.js → chunk-ABAWD6QM.js} +3 -3
- package/dist/{chunk-Z7ORL6SC.js → chunk-BUBC4R5J.js} +2 -2
- package/dist/{chunk-RPKFXNOJ.js → chunk-CIMFZIGP.js} +2 -2
- package/dist/{chunk-SIJHPGUW.js → chunk-CKUFXQUJ.js} +3 -3
- package/dist/{chunk-P4TWTQ6V.js → chunk-CS5PJXL7.js} +6 -6
- package/dist/{chunk-LRYZXY4V.js → chunk-CU65AFT4.js} +1 -1
- package/dist/{chunk-M7BXAWUC.js → chunk-D5NLXF72.js} +3 -3
- package/dist/{chunk-LIDUPDEY.js → chunk-DYDA77FS.js} +4 -4
- package/dist/{chunk-4DH54LS7.js → chunk-EFHCTIFT.js} +1 -1
- package/dist/{chunk-EOGVWCLF.js → chunk-ESHMWZXX.js} +5 -5
- package/dist/{chunk-T6RWREWU.js → chunk-EV47UGDK.js} +1 -1
- package/dist/{chunk-UBANQ4EZ.js → chunk-FBLVK2WR.js} +13 -13
- package/dist/{chunk-FEC3JUIH.js → chunk-FXQCSU4L.js} +3 -3
- package/dist/{chunk-I73UQU7K.js → chunk-GI3PX2JU.js} +1 -1
- package/dist/{chunk-SFJOGZQV.js → chunk-GL34UITI.js} +3 -3
- package/dist/{chunk-4T25BBBO.js → chunk-GOJO2OLP.js} +4 -4
- package/dist/{chunk-32JJ5KV3.js → chunk-GV7FB5L3.js} +4 -4
- package/dist/{chunk-3SAF4QXU.js → chunk-H4GOJIOK.js} +4 -4
- package/dist/{chunk-QDUDXJAF.js → chunk-HGVUTO67.js} +3 -3
- package/dist/{chunk-53D6FFVE.js → chunk-IHT33GZN.js} +1 -1
- package/dist/{chunk-QRS7FQHE.js → chunk-INBG473K.js} +8 -2
- package/dist/{chunk-E5XYFIYL.js → chunk-J2NPFRDI.js} +6 -6
- package/dist/{chunk-DCHZ5JOI.js → chunk-JJPBWBAF.js} +6 -6
- package/dist/{chunk-DX65IDBI.js → chunk-JLYVYHD2.js} +1 -1
- package/dist/{chunk-DARPCQPY.js → chunk-K2MW33DG.js} +2 -2
- package/dist/{chunk-JFVLMWKY.js → chunk-KUI2KGRZ.js} +9 -9
- package/dist/{chunk-BGEUAJYL.js → chunk-LD5LSJ2B.js} +2 -2
- package/dist/{chunk-JOMG6J64.js → chunk-LIEPMKTX.js} +1 -1
- package/dist/chunk-MHUKYYYR.js +239 -0
- package/dist/{chunk-I6TIZMM3.js → chunk-MQBQRXTN.js} +5 -5
- package/dist/{chunk-DOG3BQFO.js → chunk-MRTGCXG4.js} +6 -6
- package/dist/{chunk-EH5OLIP2.js → chunk-NHIEDRZR.js} +236 -20
- package/dist/{chunk-PV5XEBFI.js → chunk-NNV2XS3Y.js} +6 -6
- package/dist/{chunk-WL3V2UV4.js → chunk-OJ4EEE2B.js} +6 -6
- package/dist/{chunk-D4X4BQVQ.js → chunk-OXA2P5NJ.js} +10 -10
- package/dist/{chunk-BRG4XS2H.js → chunk-P4IFREOO.js} +1 -1
- package/dist/{chunk-MD4UJICD.js → chunk-P7CGVHH5.js} +5 -5
- package/dist/{chunk-ZX3KEXO3.js → chunk-PAPUUZOP.js} +626 -164
- package/dist/{chunk-PGLNE556.js → chunk-PGZFXNRJ.js} +1 -1
- package/dist/{chunk-3PPL64AG.js → chunk-PKHNZTHY.js} +9 -4
- package/dist/{chunk-NCTABQMC.js → chunk-PUF3BL6C.js} +1 -1
- package/dist/{chunk-EM2RSWDA.js → chunk-PZWC3JXW.js} +5 -5
- package/dist/{chunk-2J4IVUEA.js → chunk-PZXQUV3P.js} +4 -4
- package/dist/{chunk-7O54GE5C.js → chunk-QBCV27CM.js} +3 -3
- package/dist/{chunk-SNUHX5GQ.js → chunk-QGOY3HNV.js} +6 -6
- package/dist/{chunk-7FNSQMQ2.js → chunk-QS2IKPQW.js} +4 -4
- package/dist/{chunk-CI6MDBQB.js → chunk-QVP6F6BX.js} +7 -7
- package/dist/{chunk-CLHRSK5L.js → chunk-RYDT6Y32.js} +4 -4
- package/dist/{chunk-HRRGIVIL.js → chunk-S2UHLCF5.js} +3 -3
- package/dist/{chunk-R7V6BPEV.js → chunk-SNHAXVCJ.js} +1 -1
- package/dist/{chunk-4GWKKUQ5.js → chunk-SUOW4HJB.js} +5 -5
- package/dist/{chunk-TPYZFYQ5.js → chunk-TK3XMPG2.js} +4 -4
- package/dist/{chunk-XO7LP3JC.js → chunk-UBQ3TG3B.js} +1 -1
- package/dist/{chunk-SHW6KK2K.js → chunk-UU2CQFSJ.js} +2 -2
- package/dist/{chunk-5EQOUZYE.js → chunk-UUX6Q3CB.js} +2 -2
- package/dist/{chunk-SONZCJY5.js → chunk-WFSE7NRM.js} +23 -6
- package/dist/{chunk-DUOBFECC.js → chunk-XYNQA6YC.js} +1 -1
- package/dist/{chunk-KDUWFGY2.js → chunk-Y5YJLBML.js} +4 -4
- package/dist/{chunk-2DB7TBA7.js → chunk-ZZFYV26G.js} +2 -2
- package/dist/{codex-app-cards-switch-CN3G7GZW.js → codex-app-cards-switch-LTZCEFYB.js} +11 -11
- package/dist/{codex-desktop-launch-GFZXCDSL.js → codex-desktop-launch-DBJBV66U.js} +1 -1
- package/dist/{codex-session-launch-4F2UGQYE.js → codex-session-launch-6RTU73OP.js} +18 -18
- package/dist/{config-BLHDUDCS.js → config-YREUMA6Z.js} +3 -3
- package/dist/{global-upgrade-IZEC22BQ.js → global-upgrade-EYLSJXXN.js} +10 -10
- package/dist/{identity-3HGTEP5Q.js → identity-WIM6NVLP.js} +4 -4
- package/dist/{live-capabilities-AZK664T7.js → live-capabilities-SU3BEP2Z.js} +5 -5
- package/dist/{paths-ROMZUSEX.js → paths-XV3FXWOM.js} +3 -3
- package/dist/{reapply-IJYRR4YK.js → reapply-Q7QC3MWL.js} +27 -27
- package/dist/src/index.js +29 -28
- package/dist/{upgrade-lock-FGEZPX56.js → upgrade-lock-A2ALHHUG.js} +4 -4
- package/dist/{usage-HCURE44F.js → usage-UNEATRXG.js} +3 -3
- package/package.json +1 -1
|
@@ -3,13 +3,16 @@ import {
|
|
|
3
3
|
loadPin,
|
|
4
4
|
savePin,
|
|
5
5
|
verifyRecipients
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EV47UGDK.js";
|
|
7
|
+
import {
|
|
8
|
+
SESSION_HISTORY_RETENTION_DAYS
|
|
9
|
+
} from "./chunk-MHUKYYYR.js";
|
|
7
10
|
import {
|
|
8
11
|
ensureDaemonRunning
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JJPBWBAF.js";
|
|
10
13
|
import {
|
|
11
14
|
hookOwnerOf
|
|
12
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-TK3XMPG2.js";
|
|
13
16
|
import {
|
|
14
17
|
buildTranscriptRecords,
|
|
15
18
|
decodePublicKey,
|
|
@@ -20,38 +23,42 @@ import {
|
|
|
20
23
|
recordSealerFor,
|
|
21
24
|
uploadTranscriptRecords,
|
|
22
25
|
wrapSessionKey
|
|
23
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-WFSE7NRM.js";
|
|
24
27
|
import {
|
|
25
28
|
external_exports
|
|
26
29
|
} from "./chunk-XMSDA5WA.js";
|
|
27
30
|
import {
|
|
28
31
|
transcriptEnvironmentPreference,
|
|
29
32
|
transcriptsEnabled
|
|
30
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-CIMFZIGP.js";
|
|
31
34
|
import {
|
|
32
35
|
readTranscriptPreference
|
|
33
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-DYDA77FS.js";
|
|
34
37
|
import {
|
|
35
38
|
claudeConfigDir,
|
|
36
39
|
codexHome,
|
|
37
40
|
pusharyDir
|
|
38
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-PZXQUV3P.js";
|
|
39
42
|
import {
|
|
40
|
-
writeJsonAtomic
|
|
41
|
-
|
|
43
|
+
writeJsonAtomic,
|
|
44
|
+
writeJsonAtomicDurable
|
|
45
|
+
} from "./chunk-ABAWD6QM.js";
|
|
42
46
|
import {
|
|
43
47
|
getApiKey,
|
|
44
48
|
getBaseUrl
|
|
45
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-IHT33GZN.js";
|
|
46
50
|
import {
|
|
47
51
|
PENDING_COMMAND_PREFIX,
|
|
52
|
+
SESSION_HISTORY_BACKFILL_LIMIT_BYTES,
|
|
53
|
+
canonicalJsonText,
|
|
54
|
+
isRecord,
|
|
48
55
|
redactSecretsFromContent
|
|
49
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-INBG473K.js";
|
|
50
57
|
|
|
51
58
|
// src/transcript-registration.ts
|
|
52
|
-
import { createHash } from "crypto";
|
|
53
|
-
import { readFileSync as
|
|
54
|
-
import { basename, isAbsolute, join as
|
|
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";
|
|
55
62
|
|
|
56
63
|
// src/claude-display-draft.ts
|
|
57
64
|
import { closeSync, fstatSync, lstatSync, openSync, readFileSync, readSync, unlinkSync } from "fs";
|
|
@@ -198,18 +205,17 @@ var readDisplayDraft = (sessionId, runtime) => {
|
|
|
198
205
|
};
|
|
199
206
|
|
|
200
207
|
// src/transcript-session-key.ts
|
|
201
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync
|
|
208
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync } from "fs";
|
|
202
209
|
import { homedir } from "os";
|
|
203
210
|
import { join as join2 } from "path";
|
|
204
211
|
var sessionKeyDirectory = () => process.env.PUSHARY_SESSION_KEY_DIR?.trim() || join2(homedir(), ".pushary", "sessions");
|
|
205
|
-
var
|
|
212
|
+
var SESSION_DISPLAY_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
206
213
|
var SAFE_SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
207
214
|
var fileFor2 = (sessionId, directory) => SAFE_SESSION_ID.test(sessionId) ? join2(directory, `${sessionId}.json`) : null;
|
|
208
215
|
var loadSessionKey = (sessionId, directory = sessionKeyDirectory()) => {
|
|
209
216
|
const path = fileFor2(sessionId, directory);
|
|
210
217
|
if (!path || !existsSync(path)) return null;
|
|
211
218
|
try {
|
|
212
|
-
if (Date.now() - statSync(path).mtimeMs > SESSION_KEY_TTL_MS) return null;
|
|
213
219
|
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
214
220
|
const key = typeof raw.sessionKey === "string" ? Buffer.from(raw.sessionKey, "base64") : null;
|
|
215
221
|
if (!key || key.length !== 32) return null;
|
|
@@ -227,33 +233,31 @@ var loadSessionKey = (sessionId, directory = sessionKeyDirectory()) => {
|
|
|
227
233
|
};
|
|
228
234
|
var saveSessionKey = (sessionId, value, directory = sessionKeyDirectory()) => {
|
|
229
235
|
const path = fileFor2(sessionId, directory);
|
|
230
|
-
if (!path) return;
|
|
236
|
+
if (!path) return false;
|
|
231
237
|
try {
|
|
232
238
|
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}),
|
|
243
|
-
{ mode: 384 }
|
|
244
|
-
);
|
|
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;
|
|
245
248
|
} catch {
|
|
249
|
+
return false;
|
|
246
250
|
}
|
|
247
251
|
};
|
|
248
|
-
var
|
|
252
|
+
var sweepStaleSessionDisplays = (now = Date.now(), dir = sessionKeyDirectory()) => {
|
|
249
253
|
if (!existsSync(dir)) return 0;
|
|
250
254
|
let removed = 0;
|
|
251
255
|
try {
|
|
252
256
|
for (const name of readdirSync(dir)) {
|
|
253
|
-
if (!name.endsWith(".
|
|
257
|
+
if (!name.endsWith(".display")) continue;
|
|
254
258
|
const path = join2(dir, name);
|
|
255
259
|
try {
|
|
256
|
-
if (now - statSync(path).mtimeMs <=
|
|
260
|
+
if (now - statSync(path).mtimeMs <= SESSION_DISPLAY_TTL_MS) continue;
|
|
257
261
|
rmSync(path, { force: true });
|
|
258
262
|
removed += 1;
|
|
259
263
|
} catch {
|
|
@@ -284,8 +288,8 @@ var startDraftPublisher = (options) => {
|
|
|
284
288
|
if (!key?.keyUploaded) return;
|
|
285
289
|
const draft = options.snapshot(key.sessionKey);
|
|
286
290
|
if (!draft) return;
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
291
|
+
const identity2 = JSON.stringify([sessionId, draft.startedAt, draft.revision]);
|
|
292
|
+
if (identity2 === sent) return;
|
|
289
293
|
const response = await fetch(`${options.baseUrl}/api/agent/transcript`, {
|
|
290
294
|
method: "POST",
|
|
291
295
|
headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json" },
|
|
@@ -297,7 +301,7 @@ var startDraftPublisher = (options) => {
|
|
|
297
301
|
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5e3)])
|
|
298
302
|
});
|
|
299
303
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
300
|
-
sent =
|
|
304
|
+
sent = identity2;
|
|
301
305
|
retryAt = 0;
|
|
302
306
|
retryMs = 1e3;
|
|
303
307
|
reportedFailure = false;
|
|
@@ -360,6 +364,11 @@ var startDisplayPublisher = (options) => {
|
|
|
360
364
|
}).stop;
|
|
361
365
|
};
|
|
362
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
|
+
|
|
363
372
|
// src/transcript-source.ts
|
|
364
373
|
var normalizeTranscriptLines = (lines, source) => {
|
|
365
374
|
if (source === "claude") return [...lines];
|
|
@@ -376,11 +385,14 @@ var normalizeTranscriptLines = (lines, source) => {
|
|
|
376
385
|
});
|
|
377
386
|
};
|
|
378
387
|
var createdDuringEpoch = (sighting, epochStartedAtMs) => sighting.birthtimeMs !== null && sighting.birthtimeMs >= epochStartedAtMs && sighting.birthtimeMs <= sighting.seenAtMs;
|
|
388
|
+
var SESSION_HISTORY_BACKFILL = { limitBytes: SESSION_HISTORY_BACKFILL_LIMIT_BYTES };
|
|
389
|
+
var MS_PER_DAY = 864e5;
|
|
379
390
|
var everythingAlreadyWritten = (sighting) => ({ identity: sighting.identity, offset: sighting.sizeBytes, startedBeforeEpoch: true });
|
|
380
|
-
var
|
|
391
|
+
var earlierConversationStart = (sighting, backfill) => backfill === null ? everythingAlreadyWritten(sighting) : { identity: sighting.identity, offset: Math.max(0, sighting.sizeBytes - backfill.limitBytes), startedBeforeEpoch: true };
|
|
392
|
+
var cutoffAtFirstSight = (sighting, epochStartedAtMs, backfill) => sighting.identity === null || createdDuringEpoch(sighting, epochStartedAtMs) ? { identity: sighting.identity, offset: 0, startedBeforeEpoch: false } : earlierConversationStart(sighting, backfill);
|
|
381
393
|
var replacedOrShrunk = (previous, sighting) => previous.identity !== sighting.identity || sighting.sizeBytes < previous.offset;
|
|
382
|
-
var nextTranscriptCutoff = (previous, sighting, epochStartedAtMs) => {
|
|
383
|
-
if (!previous) return cutoffAtFirstSight(sighting, epochStartedAtMs);
|
|
394
|
+
var nextTranscriptCutoff = (previous, sighting, epochStartedAtMs, backfill = null) => {
|
|
395
|
+
if (!previous) return cutoffAtFirstSight(sighting, epochStartedAtMs, backfill);
|
|
384
396
|
if (sighting.identity === null) return previous;
|
|
385
397
|
if (previous.identity === null) return { ...previous, identity: sighting.identity };
|
|
386
398
|
return replacedOrShrunk(previous, sighting) ? everythingAlreadyWritten(sighting) : previous;
|
|
@@ -401,19 +413,464 @@ var stampedBefore = (record, momentMs) => {
|
|
|
401
413
|
return Number.isFinite(stamped) && stamped < momentMs;
|
|
402
414
|
};
|
|
403
415
|
var isCompactionSummary = (record) => record.isCompactSummary === true;
|
|
404
|
-
var
|
|
416
|
+
var oldestTranscriptLineMs = (epochStartedAtMs, backfill, retentionDays, nowMs) => backfill === null ? epochStartedAtMs : nowMs - retentionDays * MS_PER_DAY;
|
|
417
|
+
var dropsCompactionSummaries = (cutoff, backfill) => cutoff.startedBeforeEpoch && backfill === null;
|
|
418
|
+
var transcriptLinesSince = (lines, oldestLineMs, dropCompactionSummaries) => lines.filter((line) => {
|
|
405
419
|
const record = parsedTranscriptRecord(line);
|
|
406
420
|
if (record === null) return true;
|
|
407
|
-
return !stampedBefore(record,
|
|
421
|
+
return !stampedBefore(record, oldestLineMs) && !(dropCompactionSummaries && isCompactionSummary(record));
|
|
422
|
+
});
|
|
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
|
+
}
|
|
408
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
|
+
};
|
|
409
867
|
|
|
410
868
|
// src/transcript-sync.ts
|
|
411
|
-
import { join as
|
|
412
|
-
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";
|
|
413
870
|
var claudeTranscriptPath = (cwd, sessionId) => {
|
|
414
871
|
const projectId = resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
|
|
415
872
|
const configDir = claudeConfigDir();
|
|
416
|
-
return
|
|
873
|
+
return join4(configDir, "projects", projectId, `${sessionId}.jsonl`);
|
|
417
874
|
};
|
|
418
875
|
var createSyncState = () => ({
|
|
419
876
|
sessionKey: null,
|
|
@@ -425,7 +882,9 @@ var createSyncState = () => ({
|
|
|
425
882
|
keyUploaded: false,
|
|
426
883
|
keyRejected: false,
|
|
427
884
|
keyUnavailable: false,
|
|
428
|
-
|
|
885
|
+
rejectedRecords: 0,
|
|
886
|
+
unsettled: 0,
|
|
887
|
+
retentionDays: null
|
|
429
888
|
});
|
|
430
889
|
var withSessionKey = async (state, uploader) => {
|
|
431
890
|
if (state.sessionKey || state.keyRejected) return state;
|
|
@@ -438,7 +897,8 @@ var withSessionKey = async (state, uploader) => {
|
|
|
438
897
|
wrappedKey: wrapSessionKey(sessionKey, decodePublicKey(recipients.phonePublicKey)),
|
|
439
898
|
providerWrappedKey: recipients.provider ? wrapSessionKey(sessionKey, decodePublicKey(recipients.provider.publicKey)) : state.providerWrappedKey,
|
|
440
899
|
providerKeyId: recipients.provider ? recipients.provider.keyId : state.providerKeyId,
|
|
441
|
-
compressRecords: recipients.phoneReadsCompressedRecords
|
|
900
|
+
compressRecords: recipients.phoneReadsCompressedRecords,
|
|
901
|
+
retentionDays: recipients.retentionDays
|
|
442
902
|
};
|
|
443
903
|
};
|
|
444
904
|
var syncTick = async (lines, state, uploader, sessionId, machineId) => {
|
|
@@ -491,7 +951,9 @@ var syncTick = async (lines, state, uploader, sessionId, machineId) => {
|
|
|
491
951
|
keyUploaded,
|
|
492
952
|
keyRejected,
|
|
493
953
|
keyUnavailable,
|
|
494
|
-
|
|
954
|
+
rejectedRecords: state.rejectedRecords + rejected,
|
|
955
|
+
unsettled: built.records.filter((record) => !seen.has(record.localId)).length,
|
|
956
|
+
retentionDays: keyed.retentionDays
|
|
495
957
|
};
|
|
496
958
|
};
|
|
497
959
|
var MAX_RECORDS_PER_BATCH = 200;
|
|
@@ -513,99 +975,34 @@ var chunkRecords = (records) => {
|
|
|
513
975
|
if (current.length > 0) chunks.push(current);
|
|
514
976
|
return chunks;
|
|
515
977
|
};
|
|
516
|
-
var readLines = (path) => {
|
|
517
|
-
try {
|
|
518
|
-
return existsSync2(path) ? readFileSync3(path, "utf-8").split("\n") : [];
|
|
519
|
-
} catch {
|
|
520
|
-
return [];
|
|
521
|
-
}
|
|
522
|
-
};
|
|
523
978
|
var IDLE_CEILING_MS = 6e4;
|
|
524
|
-
var
|
|
525
|
-
var sightingOf = (info, seenAtMs) => info ? { identity: `${info.dev}:${info.ino}`, sizeBytes: info.size, birthtimeMs: recordedBirthtime(info), seenAtMs } : { identity: null, sizeBytes: 0, birthtimeMs: null, seenAtMs };
|
|
526
|
-
var storeCutoff = (epoch, path, cutoff) => {
|
|
527
|
-
epoch.cutoffs.set(path, cutoff);
|
|
528
|
-
epoch.positions.save(epoch.cutoffs);
|
|
529
|
-
};
|
|
530
|
-
var recordSighting = (epoch, path, sighting) => {
|
|
531
|
-
const previous = epoch.cutoffs.get(path);
|
|
532
|
-
const cutoff = nextTranscriptCutoff(previous, sighting, epoch.startedAtMs);
|
|
533
|
-
if (cutoff !== previous) storeCutoff(epoch, path, cutoff);
|
|
534
|
-
return cutoff;
|
|
535
|
-
};
|
|
536
|
-
var recordSettledPosition = (epoch, path, readFrom, settledThroughOffset) => {
|
|
537
|
-
const current = epoch.cutoffs.get(path);
|
|
538
|
-
const advanced = advancedTranscriptCutoff(current, readFrom, settledThroughOffset);
|
|
539
|
-
if (advanced !== void 0 && advanced !== current) storeCutoff(epoch, path, advanced);
|
|
540
|
-
};
|
|
541
|
-
var forgetUnregisteredTranscripts = (epoch, registeredPaths) => {
|
|
542
|
-
const unregistered = [...epoch.cutoffs.keys()].filter((path) => !registeredPaths.has(path));
|
|
543
|
-
if (unregistered.length === 0) return;
|
|
544
|
-
for (const path of unregistered) epoch.cutoffs.delete(path);
|
|
545
|
-
epoch.positions.save(epoch.cutoffs);
|
|
546
|
-
};
|
|
547
|
-
var statOrNull = (path) => {
|
|
548
|
-
try {
|
|
549
|
-
return statSync2(path);
|
|
550
|
-
} catch {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
var sightTranscript = (epoch, path, seenAtMs) => recordSighting(epoch, path, sightingOf(statOrNull(path), seenAtMs));
|
|
555
|
-
var LINE_FEED = 10;
|
|
556
|
-
var MAXIMUM_TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
|
|
979
|
+
var RECIPIENT_REFRESH_MS = 6e4;
|
|
557
980
|
var CATCH_UP_DELAY_MS = 50;
|
|
558
981
|
var STALLED_BACKOFF_FACTOR = 4;
|
|
559
|
-
var nothingUnread = (cutoff) => ({ lines: [], cutoff, completeThroughOffset: cutoff.offset, reachedEnd: true });
|
|
560
|
-
var readThroughALineEnd = (descriptor, offset, unread) => {
|
|
561
|
-
let window = Math.min(unread, MAXIMUM_TRANSCRIPT_READ_BYTES);
|
|
562
|
-
for (; ; ) {
|
|
563
|
-
const buffer = Buffer.alloc(window);
|
|
564
|
-
const received = buffer.subarray(0, readSync2(descriptor, buffer, 0, window, offset));
|
|
565
|
-
if (window >= unread || received.length < window || received.includes(LINE_FEED)) return received;
|
|
566
|
-
window = Math.min(unread, window * 2);
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
var readLinesSinceCutoff = (epoch, path, seenAtMs) => {
|
|
570
|
-
let descriptor;
|
|
571
|
-
try {
|
|
572
|
-
descriptor = openSync2(path, "r");
|
|
573
|
-
} catch {
|
|
574
|
-
return nothingUnread(sightTranscript(epoch, path, seenAtMs));
|
|
575
|
-
}
|
|
576
|
-
try {
|
|
577
|
-
const info = fstatSync2(descriptor);
|
|
578
|
-
const cutoff = recordSighting(epoch, path, sightingOf(info, seenAtMs));
|
|
579
|
-
const unread = info.size - cutoff.offset;
|
|
580
|
-
if (unread <= 0) return nothingUnread(cutoff);
|
|
581
|
-
const received = readThroughALineEnd(descriptor, cutoff.offset, unread);
|
|
582
|
-
const completeBytes = received.lastIndexOf(LINE_FEED) + 1;
|
|
583
|
-
const reachedEnd = received.length >= unread;
|
|
584
|
-
return {
|
|
585
|
-
lines: (reachedEnd ? received : received.subarray(0, completeBytes)).toString("utf8").split("\n"),
|
|
586
|
-
cutoff,
|
|
587
|
-
completeThroughOffset: cutoff.offset + completeBytes,
|
|
588
|
-
reachedEnd
|
|
589
|
-
};
|
|
590
|
-
} finally {
|
|
591
|
-
closeSync2(descriptor);
|
|
592
|
-
}
|
|
593
|
-
};
|
|
594
|
-
var linesSeenDuringEpoch = (epoch, path, source, seenAtMs) => {
|
|
595
|
-
const read2 = readLinesSinceCutoff(epoch, path, seenAtMs);
|
|
596
|
-
return { ...read2, lines: transcriptLinesSeenLive(normalizeTranscriptLines(read2.lines, source), epoch.startedAtMs, read2.cutoff.startedBeforeEpoch) };
|
|
597
|
-
};
|
|
598
982
|
var nothingLeftToUpload = (state) => state.sessionKey !== null && !state.keyRejected && state.unsettled === 0;
|
|
599
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
|
+
};
|
|
600
988
|
var nextInterval = (previousMs, idle, baseMs, maxMs) => idle ? Math.min(previousMs * 2, maxMs) : baseMs;
|
|
601
989
|
var startTranscriptSync = (opts) => {
|
|
990
|
+
const reportHealth = transcriptHealthReporter({ ...opts, baseUrl: opts.baseUrl ?? getBaseUrl() });
|
|
991
|
+
let recipientFailure = "capture_failed";
|
|
602
992
|
let warnedAboutRecipients = false;
|
|
993
|
+
let verifiedRecipients = null;
|
|
994
|
+
let recipientsVerifiedAt = 0;
|
|
603
995
|
const fetchPinnedRecipients = async () => {
|
|
604
|
-
|
|
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
|
+
});
|
|
605
1001
|
if (!offer) return null;
|
|
606
1002
|
const verdict = verifyRecipients(offer, loadPin(opts.storage?.recipientPinFile));
|
|
607
1003
|
if (verdict.kind === "first-use" && !savePin(verdict.pin, opts.storage?.recipientPinFile)) return null;
|
|
608
1004
|
if (verdict.kind === "changed") {
|
|
1005
|
+
recipientFailure = "phone_key_changed";
|
|
609
1006
|
if (!warnedAboutRecipients) {
|
|
610
1007
|
warnedAboutRecipients = true;
|
|
611
1008
|
process.stderr.write(`${changedRecipientsMessage(verdict, offer)}
|
|
@@ -613,6 +1010,8 @@ var startTranscriptSync = (opts) => {
|
|
|
613
1010
|
}
|
|
614
1011
|
return null;
|
|
615
1012
|
}
|
|
1013
|
+
verifiedRecipients = offer;
|
|
1014
|
+
recipientsVerifiedAt = Date.now();
|
|
616
1015
|
return offer;
|
|
617
1016
|
};
|
|
618
1017
|
const uploader = {
|
|
@@ -620,16 +1019,15 @@ var startTranscriptSync = (opts) => {
|
|
|
620
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: [] })
|
|
621
1020
|
};
|
|
622
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() });
|
|
623
1023
|
const persisted = loadSessionKey(opts.sessionId, opts.storage?.sessionKeyDirectory);
|
|
624
1024
|
let state = persisted ? { ...createSyncState(), ...persisted } : createSyncState();
|
|
625
|
-
|
|
1025
|
+
sweepStaleSessionDisplays(Date.now(), opts.storage?.sessionKeyDirectory);
|
|
626
1026
|
let persistedKey = persisted?.sessionKey ?? null;
|
|
627
1027
|
let persistedUploaded = persisted?.keyUploaded ?? false;
|
|
628
1028
|
const persistSessionKey = () => {
|
|
629
1029
|
if (!state.sessionKey || state.sessionKey === persistedKey && state.keyUploaded === persistedUploaded) return;
|
|
630
|
-
|
|
631
|
-
persistedUploaded = state.keyUploaded;
|
|
632
|
-
saveSessionKey(opts.sessionId, {
|
|
1030
|
+
const saved = saveSessionKey(opts.sessionId, {
|
|
633
1031
|
sessionKey: state.sessionKey,
|
|
634
1032
|
wrappedKey: state.wrappedKey,
|
|
635
1033
|
providerWrappedKey: state.providerWrappedKey,
|
|
@@ -637,6 +1035,9 @@ var startTranscriptSync = (opts) => {
|
|
|
637
1035
|
compressRecords: state.compressRecords,
|
|
638
1036
|
keyUploaded: state.keyUploaded
|
|
639
1037
|
}, opts.storage?.sessionKeyDirectory);
|
|
1038
|
+
if (!saved) throw new Error("Could not persist transcript encryption key");
|
|
1039
|
+
persistedKey = state.sessionKey;
|
|
1040
|
+
persistedUploaded = state.keyUploaded;
|
|
640
1041
|
};
|
|
641
1042
|
let running = true;
|
|
642
1043
|
let inFlight = false;
|
|
@@ -649,26 +1050,42 @@ var startTranscriptSync = (opts) => {
|
|
|
649
1050
|
try {
|
|
650
1051
|
const ending = opts.ending?.() === true;
|
|
651
1052
|
if (state.keyRejected) {
|
|
1053
|
+
await reportHealth("capture_failed");
|
|
652
1054
|
if (ending) opts.onDrained?.();
|
|
653
1055
|
return;
|
|
654
1056
|
}
|
|
655
|
-
|
|
656
|
-
if (!
|
|
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
|
+
}
|
|
657
1071
|
persistSessionKey();
|
|
658
1072
|
const previous = state;
|
|
659
|
-
const modified = existsSync2(path) ? statSync2(path).mtimeMs : null;
|
|
660
1073
|
const source = opts.source ?? "claude";
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
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);
|
|
664
1076
|
stalled = uploadStalled(previous, state);
|
|
665
1077
|
persistSessionKey();
|
|
666
1078
|
const settled = nothingLeftToUpload(state);
|
|
667
|
-
|
|
668
|
-
catchingUp =
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
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");
|
|
672
1089
|
} finally {
|
|
673
1090
|
inFlight = false;
|
|
674
1091
|
}
|
|
@@ -706,15 +1123,32 @@ var startTranscriptSync = (opts) => {
|
|
|
706
1123
|
|
|
707
1124
|
// src/transcript-registration.ts
|
|
708
1125
|
import { homedir as homedir2 } from "os";
|
|
709
|
-
var
|
|
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");
|
|
710
1141
|
var cliRegistry = (apiKey = getApiKey()) => ({ apiKey, baseUrl: getBaseUrl(), directory: defaultRegistryDirectory() });
|
|
711
|
-
var accountFor = (registry) =>
|
|
1142
|
+
var accountFor = (registry) => createHash4("sha256").update(`${registry.baseUrl}\0${registry.apiKey}`).digest("hex");
|
|
712
1143
|
var TTL = 48 * 60 * 60 * 1e3;
|
|
713
|
-
var read = (path) =>
|
|
714
|
-
|
|
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
|
+
};
|
|
1148
|
+
var consent = (record) => record.consentOverride ?? readTranscriptPreference() ?? true;
|
|
715
1149
|
var withoutTrailingSlash = (url) => url.replace(/\/+$/, "");
|
|
716
1150
|
var sameServer = (url, registry) => withoutTrailingSlash(url) === withoutTrailingSlash(registry.baseUrl);
|
|
717
|
-
var fileFor3 = (registry, sessionId) =>
|
|
1151
|
+
var fileFor3 = (registry, sessionId) => join5(registry.directory, `${createHash4("sha256").update(accountFor(registry) + sessionId).digest("hex")}.json`);
|
|
718
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));
|
|
719
1153
|
var registeredTranscriptEnabled = (apiKey, sessionId, registry = cliRegistry(apiKey)) => {
|
|
720
1154
|
try {
|
|
@@ -724,39 +1158,56 @@ var registeredTranscriptEnabled = (apiKey, sessionId, registry = cliRegistry(api
|
|
|
724
1158
|
}
|
|
725
1159
|
};
|
|
726
1160
|
var macOwnsClaudeHooks = (cwd) => [
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1161
|
+
join5(claudeConfigDir(), "settings.json"),
|
|
1162
|
+
join5(cwd, ".claude", "settings.json"),
|
|
1163
|
+
join5(cwd, ".claude", "settings.local.json")
|
|
730
1164
|
].some((path) => {
|
|
731
1165
|
try {
|
|
732
|
-
return hookOwnerOf(JSON.parse(
|
|
1166
|
+
return hookOwnerOf(JSON.parse(readFileSync6(path, "utf8"))) === "app";
|
|
733
1167
|
} catch {
|
|
734
1168
|
return false;
|
|
735
1169
|
}
|
|
736
1170
|
});
|
|
737
|
-
var TRANSCRIPT_ROOT_NAME = { claude: "projects", codex: "sessions" };
|
|
1171
|
+
var TRANSCRIPT_ROOT_NAME = { claude: "projects", codex: "sessions", gemini: "tmp", cursor: "projects", hermes: "hermes-history" };
|
|
738
1172
|
var transcriptRoots = (source) => {
|
|
739
1173
|
const home = homedir2();
|
|
740
|
-
if (source === "codex") return [
|
|
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")];
|
|
741
1178
|
const configured = process.env.CLAUDE_CONFIG_DIR;
|
|
742
|
-
return [
|
|
1179
|
+
return [join5(configured || join5(home, ".claude"), TRANSCRIPT_ROOT_NAME.claude), join5(home, ".claude", TRANSCRIPT_ROOT_NAME.claude)];
|
|
743
1180
|
};
|
|
744
1181
|
var isAgentTranscriptRoot = (root, source) => isAbsolute(root) && resolve2(root) === root && basename(root) === TRANSCRIPT_ROOT_NAME[source];
|
|
745
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
|
+
};
|
|
746
1192
|
var transcriptRootContaining = (candidate, roots) => {
|
|
747
|
-
const target =
|
|
748
|
-
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;
|
|
749
1195
|
};
|
|
750
1196
|
var captureRegistration = (source, value, registeredRoot) => {
|
|
751
1197
|
const input = value;
|
|
752
|
-
if (source !== "claude" && source !== "codex") return null;
|
|
753
|
-
|
|
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;
|
|
754
1201
|
if (typeof input.cwd !== "string" || !isAbsolute(input.cwd)) return null;
|
|
755
|
-
const path = input.transcript_path ?? input.agent_transcript_path ?? (source === "claude" ? claudeTranscriptPath(input.cwd,
|
|
756
|
-
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;
|
|
757
1204
|
const root = transcriptRootContaining(path, acceptedTranscriptRoots(source, registeredRoot));
|
|
758
1205
|
if (root === null) return null;
|
|
759
|
-
|
|
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 };
|
|
760
1211
|
};
|
|
761
1212
|
var registerTranscript = (source, input, startDaemon = () => {
|
|
762
1213
|
void ensureDaemonRunning({ waitTimeoutMs: 0 }).catch(() => {
|
|
@@ -773,10 +1224,15 @@ var registerTranscript = (source, input, startDaemon = () => {
|
|
|
773
1224
|
previous = read(path);
|
|
774
1225
|
} catch {
|
|
775
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
|
+
}
|
|
776
1231
|
if (source === "claude" && !options.siteId && !previous?.expectedSiteId && macOwnsClaudeHooks(registration.cwd)) return;
|
|
777
1232
|
const ending = options.ended || input.hook_event_name === "SessionEnd" && process.env.PUSHARY_WRAPPER_ACTIVE !== "1";
|
|
778
1233
|
const stored = {
|
|
779
1234
|
...registration,
|
|
1235
|
+
path: previous?.account === account ? previous.path : registration.path,
|
|
780
1236
|
account,
|
|
781
1237
|
consentOverride: options.consentOverride ?? transcriptEnvironmentPreference(),
|
|
782
1238
|
// A wrapper refresh must not erase the Mac's account binding.
|
|
@@ -803,7 +1259,8 @@ var sightRegisteredTranscripts = (epoch, registry, listing) => {
|
|
|
803
1259
|
if (listing.complete) forgetUnregisteredTranscripts(epoch, registeredPaths);
|
|
804
1260
|
};
|
|
805
1261
|
var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
|
|
806
|
-
const { intervalMs = 3e3, enabled = () => true, storage
|
|
1262
|
+
const { intervalMs = 3e3, enabled = () => true, storage } = options;
|
|
1263
|
+
const epoch = options.epoch ?? startTranscriptCaptureEpoch(Date.now(), void 0, SESSION_HISTORY_BACKFILL);
|
|
807
1264
|
const registry = { apiKey, baseUrl: options.baseUrl ?? getBaseUrl(), directory: options.registryDirectory ?? defaultRegistryDirectory() };
|
|
808
1265
|
const active = /* @__PURE__ */ new Map();
|
|
809
1266
|
let stopped = false;
|
|
@@ -817,7 +1274,7 @@ var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
|
|
|
817
1274
|
try {
|
|
818
1275
|
let unreadable = 0;
|
|
819
1276
|
const files = readdirSync2(registry.directory).filter((name) => name.endsWith(".json")).flatMap((name) => {
|
|
820
|
-
const path =
|
|
1277
|
+
const path = join5(registry.directory, name);
|
|
821
1278
|
try {
|
|
822
1279
|
const time = statSync3(path).mtimeMs;
|
|
823
1280
|
if (Date.now() - time > TTL) {
|
|
@@ -866,13 +1323,18 @@ var startRegisteredTranscripts = (apiKey, machineId, options = {}) => {
|
|
|
866
1323
|
for (const [id, registration] of wanted) {
|
|
867
1324
|
if (active.has(id)) continue;
|
|
868
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
|
+
});
|
|
869
1331
|
active.set(id, { path: registration.path, stop: startTranscriptSync({
|
|
870
1332
|
apiKey,
|
|
871
1333
|
machineId,
|
|
872
1334
|
...registration,
|
|
873
1335
|
intervalMs,
|
|
874
1336
|
baseUrl: registry.baseUrl,
|
|
875
|
-
storage,
|
|
1337
|
+
storage: keyStorage,
|
|
876
1338
|
epoch,
|
|
877
1339
|
enabled: () => {
|
|
878
1340
|
try {
|