pushary 1.9.76 → 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 +6 -0
- package/dist/{api-YDCHRRA4.js → api-IGMCPM35.js} +10 -10
- package/dist/bin/pushary-bell-hook.js +25 -25
- package/dist/bin/pushary-bell.js +10 -10
- package/dist/bin/pushary-claude.js +26 -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 +30 -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 +122 -66
- package/dist/bin/pushary-disconnect.js +25 -25
- package/dist/bin/pushary-display-hook.js +16 -16
- package/dist/bin/pushary-doctor.js +41 -41
- package/dist/bin/pushary-elicitation-hook.js +25 -25
- package/dist/bin/pushary-gemini-bridge.js +15 -15
- package/dist/bin/pushary-gemini-hook.js +27 -27
- package/dist/bin/pushary-hook.js +29 -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 +24 -24
- package/dist/bin/pushary-opencode-hook.js +26 -26
- package/dist/bin/pushary-permission-denied-hook.js +29 -29
- package/dist/bin/pushary-permission-hook.js +29 -29
- package/dist/bin/pushary-post-hook.js +24 -24
- package/dist/bin/pushary-prompt-hook.js +24 -24
- package/dist/bin/pushary-session-end-hook.js +26 -26
- package/dist/bin/pushary-session-start-hook.js +34 -34
- package/dist/bin/pushary-setup.js +46 -46
- package/dist/bin/pushary-stats.js +5 -5
- package/dist/bin/pushary-status.js +31 -31
- package/dist/bin/pushary-stop-hook.js +24 -24
- package/dist/bin/pushary-stopfailure-hook.js +24 -24
- package/dist/bin/pushary-suggestions.js +15 -15
- package/dist/bin/pushary-transcript-register.js +46 -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-2QFYEB3P.js → chunk-2IOQHB3C.js} +5 -5
- package/dist/{chunk-R3HXGHRQ.js → chunk-2OWG667E.js} +1 -1
- package/dist/{chunk-NJTAUEW6.js → chunk-2YRNRYZW.js} +1 -1
- package/dist/{chunk-C2AR7QXU.js → chunk-3UYMJZXV.js} +2 -2
- package/dist/{chunk-B2PFCS72.js → chunk-4ZXFOT44.js} +2 -2
- package/dist/{chunk-47TIRGS5.js → chunk-6WOG2SCC.js} +1 -0
- package/dist/{chunk-GJVV2EUC.js → chunk-6XOEYBWJ.js} +3 -3
- package/dist/{chunk-KWGOKO4E.js → chunk-7AQEWD2J.js} +3 -3
- package/dist/{chunk-FAGQAR6I.js → chunk-7CU7OGWP.js} +263 -5
- package/dist/{chunk-GII565PB.js → chunk-7HYNAA6B.js} +1 -1
- package/dist/{chunk-MTRZQR2R.js → chunk-A3BF7SGM.js} +3 -3
- package/dist/{chunk-T2EJ3H6Y.js → chunk-ABAWD6QM.js} +3 -3
- package/dist/{chunk-TFYFQM5C.js → chunk-BUBC4R5J.js} +2 -2
- package/dist/{chunk-5M5F4IWR.js → chunk-CIMFZIGP.js} +1 -1
- package/dist/{chunk-E7U4YM4A.js → chunk-CKUFXQUJ.js} +3 -3
- package/dist/{chunk-DZF2M3LM.js → chunk-CS5PJXL7.js} +6 -6
- package/dist/{chunk-57MQNSM5.js → chunk-CU65AFT4.js} +1 -1
- package/dist/{chunk-CMNB4IZO.js → chunk-D5NLXF72.js} +3 -3
- package/dist/{chunk-7LLC4R66.js → chunk-DYDA77FS.js} +4 -4
- package/dist/{chunk-5FV472Q2.js → chunk-EFHCTIFT.js} +1 -1
- package/dist/{chunk-7ZOWQBHQ.js → chunk-ESHMWZXX.js} +5 -5
- package/dist/{chunk-YLB4AGZJ.js → chunk-EV47UGDK.js} +1 -1
- package/dist/{chunk-CHIOUBPB.js → chunk-FBLVK2WR.js} +13 -13
- package/dist/{chunk-XLGD5LDG.js → chunk-FXQCSU4L.js} +3 -3
- package/dist/{chunk-4LX5QUFV.js → chunk-GI3PX2JU.js} +1 -1
- package/dist/{chunk-MMVIKXCP.js → chunk-GL34UITI.js} +3 -3
- package/dist/{chunk-VELPOV2F.js → chunk-GOJO2OLP.js} +4 -4
- package/dist/{chunk-JNV5LOZH.js → chunk-GV7FB5L3.js} +4 -4
- package/dist/{chunk-3YU6NIIL.js → chunk-H4GOJIOK.js} +4 -4
- package/dist/{chunk-NFUV25JZ.js → chunk-HGVUTO67.js} +3 -3
- package/dist/{chunk-M7JFGDGP.js → chunk-IHT33GZN.js} +1 -1
- package/dist/{chunk-Y4OBAK4I.js → chunk-INBG473K.js} +4 -2
- package/dist/{chunk-QUGP76DQ.js → chunk-J2NPFRDI.js} +6 -6
- package/dist/{chunk-DB3NOEDW.js → chunk-JJPBWBAF.js} +6 -6
- package/dist/{chunk-S7C5NY4R.js → chunk-JLYVYHD2.js} +1 -1
- package/dist/{chunk-5NW7VM35.js → chunk-K2MW33DG.js} +2 -2
- package/dist/{chunk-NVLKH5FS.js → chunk-KUI2KGRZ.js} +9 -9
- package/dist/{chunk-BUR2LYQI.js → chunk-LD5LSJ2B.js} +2 -2
- package/dist/{chunk-UEY4SHY2.js → chunk-LIEPMKTX.js} +1 -1
- package/dist/{chunk-YDPGBBV7.js → chunk-MQBQRXTN.js} +5 -5
- package/dist/{chunk-M5VJ3HXB.js → chunk-MRTGCXG4.js} +6 -6
- package/dist/{chunk-IRLHN5UP.js → chunk-NHIEDRZR.js} +2 -2
- package/dist/{chunk-E2VJ7B7D.js → chunk-NNV2XS3Y.js} +6 -6
- package/dist/{chunk-NQA56AMZ.js → chunk-OJ4EEE2B.js} +6 -6
- package/dist/{chunk-VZSSL6US.js → chunk-OXA2P5NJ.js} +10 -10
- package/dist/{chunk-CUD6SYDG.js → chunk-P4IFREOO.js} +1 -1
- package/dist/{chunk-W5AUTVVL.js → chunk-P7CGVHH5.js} +5 -5
- package/dist/{chunk-7SFP6Y5U.js → chunk-PAPUUZOP.js} +603 -162
- package/dist/{chunk-F7R7M5MG.js → chunk-PGZFXNRJ.js} +1 -1
- package/dist/{chunk-WLHHDDVA.js → chunk-PKHNZTHY.js} +3 -3
- package/dist/{chunk-PPD64MZW.js → chunk-PUF3BL6C.js} +1 -1
- package/dist/{chunk-JZ5KV2FK.js → chunk-PZWC3JXW.js} +5 -5
- package/dist/{chunk-7KYQ24FI.js → chunk-PZXQUV3P.js} +4 -4
- package/dist/{chunk-SWRNJ3BP.js → chunk-QBCV27CM.js} +3 -3
- package/dist/{chunk-Y2CYSVWU.js → chunk-QGOY3HNV.js} +6 -6
- package/dist/{chunk-IRN6JLSM.js → chunk-QS2IKPQW.js} +4 -4
- package/dist/{chunk-2BK2EQOB.js → chunk-QVP6F6BX.js} +7 -7
- package/dist/{chunk-XJ5DYPTM.js → chunk-RYDT6Y32.js} +4 -4
- package/dist/{chunk-5KPMDLXB.js → chunk-S2UHLCF5.js} +3 -3
- package/dist/{chunk-UMBVIFP5.js → chunk-SNHAXVCJ.js} +1 -1
- package/dist/{chunk-MAWC4PJV.js → chunk-SUOW4HJB.js} +5 -5
- package/dist/{chunk-KGWLDNNT.js → chunk-TK3XMPG2.js} +4 -4
- package/dist/{chunk-WK2L3XVQ.js → chunk-UBQ3TG3B.js} +1 -1
- package/dist/{chunk-USZJJWZL.js → chunk-UU2CQFSJ.js} +2 -2
- package/dist/{chunk-WRCM4XGY.js → chunk-UUX6Q3CB.js} +2 -2
- package/dist/{chunk-ITLWEGLE.js → chunk-WFSE7NRM.js} +15 -5
- package/dist/{chunk-ZY25SNFQ.js → chunk-XYNQA6YC.js} +1 -1
- package/dist/{chunk-74ONBRPF.js → chunk-Y5YJLBML.js} +4 -4
- package/dist/{chunk-USMUW3LI.js → chunk-ZZFYV26G.js} +2 -2
- package/dist/{codex-app-cards-switch-3OUTABVU.js → codex-app-cards-switch-LTZCEFYB.js} +11 -11
- package/dist/{codex-desktop-launch-JH3E2CIA.js → codex-desktop-launch-DBJBV66U.js} +1 -1
- package/dist/{codex-session-launch-GR363OQN.js → codex-session-launch-6RTU73OP.js} +18 -18
- package/dist/{config-R43UFHX7.js → config-YREUMA6Z.js} +3 -3
- package/dist/{global-upgrade-73GP4MZM.js → global-upgrade-EYLSJXXN.js} +10 -10
- package/dist/{identity-CA7CZGDL.js → identity-WIM6NVLP.js} +4 -4
- package/dist/{live-capabilities-RCGMQIIZ.js → live-capabilities-SU3BEP2Z.js} +5 -5
- package/dist/{paths-H7VNNB46.js → paths-XV3FXWOM.js} +3 -3
- package/dist/{reapply-3UIA3CUO.js → reapply-Q7QC3MWL.js} +27 -27
- package/dist/src/index.js +28 -28
- package/dist/{upgrade-lock-ZTLX5PDF.js → upgrade-lock-A2ALHHUG.js} +4 -4
- package/dist/{usage-C53NVWC2.js → usage-UNEATRXG.js} +3 -3
- package/package.json +1 -1
|
@@ -3,16 +3,16 @@ import {
|
|
|
3
3
|
loadPin,
|
|
4
4
|
savePin,
|
|
5
5
|
verifyRecipients
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EV47UGDK.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-
|
|
12
|
+
} from "./chunk-JJPBWBAF.js";
|
|
13
13
|
import {
|
|
14
14
|
hookOwnerOf
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-TK3XMPG2.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-
|
|
26
|
+
} from "./chunk-WFSE7NRM.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-
|
|
33
|
+
} from "./chunk-CIMFZIGP.js";
|
|
34
34
|
import {
|
|
35
35
|
readTranscriptPreference
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-DYDA77FS.js";
|
|
37
37
|
import {
|
|
38
38
|
claudeConfigDir,
|
|
39
39
|
codexHome,
|
|
40
40
|
pusharyDir
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-PZXQUV3P.js";
|
|
42
42
|
import {
|
|
43
|
-
writeJsonAtomic
|
|
44
|
-
|
|
43
|
+
writeJsonAtomic,
|
|
44
|
+
writeJsonAtomicDurable
|
|
45
|
+
} from "./chunk-ABAWD6QM.js";
|
|
45
46
|
import {
|
|
46
47
|
getApiKey,
|
|
47
48
|
getBaseUrl
|
|
48
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-IHT33GZN.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-
|
|
56
|
+
} from "./chunk-INBG473K.js";
|
|
54
57
|
|
|
55
58
|
// src/transcript-registration.ts
|
|
56
|
-
import { createHash } from "crypto";
|
|
57
|
-
import { readFileSync as
|
|
58
|
-
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";
|
|
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
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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(".
|
|
257
|
+
if (!name.endsWith(".display")) continue;
|
|
258
258
|
const path = join2(dir, name);
|
|
259
259
|
try {
|
|
260
|
-
if (now - statSync(path).mtimeMs <=
|
|
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
|
|
292
|
-
if (
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
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
|
+
}
|
|
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
|
|
681
|
-
|
|
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
|
-
|
|
688
|
-
catchingUp =
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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(
|
|
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 [
|
|
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 [
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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 {
|