codex-snapshots 0.1.0 → 0.1.1
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/README.md +101 -6
- package/bin/codex-snapshot.mjs +1 -6326
- package/deploy/aliyun/README.md +311 -0
- package/deploy/aliyun/backup-share-data.sh +109 -0
- package/deploy/aliyun/check-ecs-status.sh +149 -0
- package/deploy/aliyun/codex-snapshot-share.env.example +29 -0
- package/deploy/aliyun/codex-snapshot-share.service +26 -0
- package/deploy/aliyun/configure-github-pages-api.sh +141 -0
- package/deploy/aliyun/configure-local-publisher.sh +197 -0
- package/deploy/aliyun/deploy-to-ecs.sh +669 -0
- package/deploy/aliyun/deploy.env.example +52 -0
- package/deploy/aliyun/doctor.mjs +398 -0
- package/deploy/aliyun/install-share-api.sh +252 -0
- package/deploy/aliyun/install-system-deps.sh +84 -0
- package/deploy/aliyun/nginx-codex-snapshots.bootstrap.conf +34 -0
- package/deploy/aliyun/nginx-codex-snapshots.conf +52 -0
- package/deploy/aliyun/preflight.mjs +321 -0
- package/deploy/aliyun/restore-share-data.sh +141 -0
- package/deploy/aliyun/verify-public-share.mjs +404 -0
- package/dist/cli/codex-snapshot.mjs +2654 -0
- package/dist/core/privacy.js +81 -0
- package/dist/core/snapshot.js +1 -0
- package/dist/renderers/markdown.mjs +81 -0
- package/dist/renderers/transcript.js +195 -0
- package/dist/server/http.js +10 -0
- package/dist/server/local-security.js +66 -0
- package/dist/server/local-viewer-app.mjs +1670 -0
- package/dist/server/local-viewer.mjs +210 -0
- package/dist/server/share-api.mjs +1149 -0
- package/dist/server/share-store.js +136 -0
- package/dist/shared/sanitize.js +126 -0
- package/dist/shared/transcript.js +1 -0
- package/dist/sources/index.mjs +2 -0
- package/dist/sources/local-history.mjs +2221 -0
- package/package.json +42 -14
- package/scripts/build-site.mjs +71 -0
- package/scripts/launch-agent.mjs +19 -227
- package/scripts/serve-site.mjs +2 -2
- package/scripts/test-aliyun-deploy-config.sh +230 -0
- package/scripts/test-share-api.mjs +967 -0
- package/scripts/test-site-config.mjs +100 -0
- package/scripts/test-static-site.mjs +403 -0
- package/scripts/write-site-config.mjs +161 -0
- package/server/share-api.mjs +1 -771
- package/site/assets/config.js +3 -0
- package/site/assets/share.js +43 -106
- package/site/assets/site.css +3 -605
- package/site/assets/site.js +15 -92
- package/site/favicon.svg +7 -0
- package/site/index.html +3 -83
- package/site/share/index.html +3 -8
|
@@ -0,0 +1,2221 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { addImageRisk, addRisks, detectRisks, redactText, severityRank } from "../core/privacy.js";
|
|
10
|
+
import { renderMarkdownHtml } from "../renderers/markdown.mjs";
|
|
11
|
+
import { stripAppDirectives as stripCodexAppDirectives } from "../shared/sanitize.js";
|
|
12
|
+
const MAX_TEXT_CHARS = 20000;
|
|
13
|
+
const MAX_SUMMARY_LINES = 140;
|
|
14
|
+
const TOOL_OUTPUT_PREVIEW_CHARS = 24000;
|
|
15
|
+
const MAX_INLINE_IMAGE_CHARS = 5_000_000;
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
function formatBytes(bytes) {
|
|
18
|
+
if (!Number.isFinite(bytes)) {
|
|
19
|
+
return "0 B";
|
|
20
|
+
}
|
|
21
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
22
|
+
let value = bytes;
|
|
23
|
+
let unit = 0;
|
|
24
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
25
|
+
value /= 1024;
|
|
26
|
+
unit += 1;
|
|
27
|
+
}
|
|
28
|
+
return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
|
|
29
|
+
}
|
|
30
|
+
export async function listSessions({ codexHome, claudeHome, traeHome, traeAppHome, traeRecordingsDir, limit, cwd, includeArchived, source = "codex", completeOnly = false }) {
|
|
31
|
+
if (source === "all") {
|
|
32
|
+
const [codexSessions, claudeSessions, traeSessions] = await Promise.all([
|
|
33
|
+
listCodexSessions({ codexHome, limit, cwd, includeArchived }),
|
|
34
|
+
listClaudeSessions({ claudeHome, limit, cwd }),
|
|
35
|
+
listTraeSessions({ traeHome, traeAppHome, traeRecordingsDir, limit, cwd }),
|
|
36
|
+
]);
|
|
37
|
+
const sessions = [...codexSessions, ...claudeSessions, ...traeSessions]
|
|
38
|
+
.filter((summary) => !completeOnly || isCompleteSessionSummary(summary))
|
|
39
|
+
.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
|
|
40
|
+
return Number.isFinite(limit) ? sessions.slice(0, limit) : sessions;
|
|
41
|
+
}
|
|
42
|
+
if (source === "claude") {
|
|
43
|
+
return filterSessionCompleteness(await listClaudeSessions({ claudeHome, limit, cwd }), completeOnly);
|
|
44
|
+
}
|
|
45
|
+
if (source === "trae") {
|
|
46
|
+
return filterSessionCompleteness(await listTraeSessions({ traeHome, traeAppHome, traeRecordingsDir, limit, cwd }), completeOnly);
|
|
47
|
+
}
|
|
48
|
+
return filterSessionCompleteness(await listCodexSessions({ codexHome, limit, cwd, includeArchived }), completeOnly);
|
|
49
|
+
}
|
|
50
|
+
function filterSessionCompleteness(sessions, completeOnly) {
|
|
51
|
+
return completeOnly ? sessions.filter((summary) => isCompleteSessionSummary(summary)) : sessions;
|
|
52
|
+
}
|
|
53
|
+
function isCompleteSessionSummary(summary) {
|
|
54
|
+
if (summary.engine === "claude") {
|
|
55
|
+
return summary.sourceKind === "transcript";
|
|
56
|
+
}
|
|
57
|
+
if (summary.engine === "trae") {
|
|
58
|
+
return summary.sourceKind === "recorded";
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
async function listCodexSessions({ codexHome, limit, cwd, includeArchived }) {
|
|
63
|
+
const titleIndex = await readTitleIndex(codexHome);
|
|
64
|
+
const files = await discoverSessionFiles(codexHome, includeArchived);
|
|
65
|
+
const cwdFilter = cwd ? path.resolve(cwd) : "";
|
|
66
|
+
const summaries = [];
|
|
67
|
+
const unlimited = !Number.isFinite(limit);
|
|
68
|
+
const scanLimit = unlimited ? files.length : Math.max(limit * 4, limit);
|
|
69
|
+
for (const fileInfo of files.slice(0, scanLimit)) {
|
|
70
|
+
const summary = await scanSessionSummary(fileInfo.filePath, fileInfo, titleIndex);
|
|
71
|
+
if (cwdFilter && summary.cwd && !path.resolve(summary.cwd).startsWith(cwdFilter)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
summaries.push(summary);
|
|
75
|
+
if (!unlimited && summaries.length >= limit) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return summaries;
|
|
80
|
+
}
|
|
81
|
+
async function discoverSessionFiles(codexHome, includeArchived = true) {
|
|
82
|
+
const roots = [path.join(codexHome, "sessions")];
|
|
83
|
+
if (includeArchived) {
|
|
84
|
+
roots.push(path.join(codexHome, "archived_sessions"));
|
|
85
|
+
}
|
|
86
|
+
const files = [];
|
|
87
|
+
for (const root of roots) {
|
|
88
|
+
await collectJsonlFiles(root, files);
|
|
89
|
+
}
|
|
90
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
async function collectJsonlFiles(dir, files) {
|
|
94
|
+
let entries = [];
|
|
95
|
+
try {
|
|
96
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await Promise.all(entries.map(async (entry) => {
|
|
102
|
+
const entryPath = path.join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
await collectJsonlFiles(entryPath, files);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const info = await stat(entryPath);
|
|
111
|
+
files.push({
|
|
112
|
+
filePath: entryPath,
|
|
113
|
+
size: info.size,
|
|
114
|
+
mtimeMs: info.mtimeMs,
|
|
115
|
+
mtime: info.mtime.toISOString(),
|
|
116
|
+
});
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
async function readTitleIndex(codexHome) {
|
|
120
|
+
const indexPath = path.join(codexHome, "session_index.jsonl");
|
|
121
|
+
const map = new Map();
|
|
122
|
+
let raw = "";
|
|
123
|
+
try {
|
|
124
|
+
raw = await readFile(indexPath, "utf8");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return map;
|
|
128
|
+
}
|
|
129
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
130
|
+
if (!line.trim()) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const row = JSON.parse(line);
|
|
135
|
+
if (row.id && row.thread_name) {
|
|
136
|
+
map.set(row.id, row.thread_name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore malformed index rows.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return map;
|
|
144
|
+
}
|
|
145
|
+
async function scanSessionSummary(filePath, fileInfo, titleIndex) {
|
|
146
|
+
const fallbackId = sessionIdFromPath(filePath);
|
|
147
|
+
const summary = {
|
|
148
|
+
id: fallbackId,
|
|
149
|
+
title: "",
|
|
150
|
+
cwd: "",
|
|
151
|
+
filePath,
|
|
152
|
+
size: fileInfo.size,
|
|
153
|
+
mtime: fileInfo.mtime,
|
|
154
|
+
createdAt: "",
|
|
155
|
+
modelProvider: "",
|
|
156
|
+
source: "",
|
|
157
|
+
messageCount: 0,
|
|
158
|
+
toolCallCount: 0,
|
|
159
|
+
riskCount: 0,
|
|
160
|
+
};
|
|
161
|
+
let firstUser = "";
|
|
162
|
+
let lineCount = 0;
|
|
163
|
+
for await (const row of readJsonl(filePath)) {
|
|
164
|
+
lineCount += 1;
|
|
165
|
+
if (row.type === "session_meta" && row.payload) {
|
|
166
|
+
summary.id = row.payload.id || summary.id;
|
|
167
|
+
summary.cwd = row.payload.cwd || "";
|
|
168
|
+
summary.createdAt = row.payload.timestamp || "";
|
|
169
|
+
summary.modelProvider = row.payload.model_provider || "";
|
|
170
|
+
summary.source = row.payload.originator || row.payload.source || "";
|
|
171
|
+
}
|
|
172
|
+
if (row.type === "response_item" && row.payload) {
|
|
173
|
+
if (row.payload.type === "message" && (row.payload.role === "user" || row.payload.role === "assistant")) {
|
|
174
|
+
const message = extractMessageParts(row.payload);
|
|
175
|
+
const text = stripCodexAppDirectives(message.text);
|
|
176
|
+
if (!isBootstrapUserMessage(row.payload.role, text) && (text || message.images.length)) {
|
|
177
|
+
summary.messageCount += 1;
|
|
178
|
+
if (!firstUser && row.payload.role === "user") {
|
|
179
|
+
firstUser = text ? truncateForTitle(text) : "[image]";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (isToolPayload(row.payload)) {
|
|
184
|
+
summary.toolCallCount += 1;
|
|
185
|
+
}
|
|
186
|
+
const text = extractMessageText(row.payload) || row.payload.arguments || row.payload.output || "";
|
|
187
|
+
if (text) {
|
|
188
|
+
if (!isBootstrapUserMessage(row.payload.role, text)) {
|
|
189
|
+
summary.riskCount += detectRisks(text).length;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (summary.id && summary.cwd && firstUser && lineCount >= 8) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (lineCount >= MAX_SUMMARY_LINES) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
summary.title = titleIndex.get(summary.id) || firstUser || summary.id;
|
|
201
|
+
summary.engine = "codex";
|
|
202
|
+
summary.engineLabel = "Codex";
|
|
203
|
+
summary.projectKind = projectKindForCodexCwd(summary.cwd);
|
|
204
|
+
summary.ref = `codex:${summary.id}`;
|
|
205
|
+
summary.displayCwd = redactText(summary.cwd || "");
|
|
206
|
+
summary.displayFilePath = redactText(summary.filePath || "");
|
|
207
|
+
return summary;
|
|
208
|
+
}
|
|
209
|
+
function projectKindForCodexCwd(cwd) {
|
|
210
|
+
if (!cwd) {
|
|
211
|
+
return "none";
|
|
212
|
+
}
|
|
213
|
+
return isCodexStandaloneConversationCwd(cwd) ? "conversation" : "project";
|
|
214
|
+
}
|
|
215
|
+
function isCodexStandaloneConversationCwd(cwd) {
|
|
216
|
+
const parts = String(cwd || "").trim().replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
|
|
217
|
+
const codexIndex = parts.findIndex((part, index) => part === "Codex" && parts[index - 1] === "Documents");
|
|
218
|
+
if (codexIndex < 0 || codexIndex + 3 !== parts.length) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(parts[codexIndex + 1]) && Boolean(parts[codexIndex + 2]);
|
|
222
|
+
}
|
|
223
|
+
export async function loadSnapshot(ref, { codexHome, claudeHome, traeHome, traeAppHome, traeRecordingsDir, includeTools, includeToolOutput, redact }) {
|
|
224
|
+
const target = splitSnapshotRef(ref);
|
|
225
|
+
if (target.engine === "claude") {
|
|
226
|
+
return loadClaudeSnapshot(target.ref, {
|
|
227
|
+
claudeHome,
|
|
228
|
+
includeTools,
|
|
229
|
+
includeToolOutput,
|
|
230
|
+
redact,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (target.engine === "trae") {
|
|
234
|
+
return loadTraeSnapshot(target.ref, {
|
|
235
|
+
traeHome,
|
|
236
|
+
traeAppHome,
|
|
237
|
+
traeRecordingsDir,
|
|
238
|
+
includeTools,
|
|
239
|
+
includeToolOutput,
|
|
240
|
+
redact,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return loadCodexSnapshot(target.ref, {
|
|
244
|
+
codexHome,
|
|
245
|
+
includeTools,
|
|
246
|
+
includeToolOutput,
|
|
247
|
+
redact,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async function loadCodexSnapshot(ref, { codexHome, includeTools, includeToolOutput, redact }) {
|
|
251
|
+
const titleIndex = await readTitleIndex(codexHome);
|
|
252
|
+
const filePath = await resolveSessionRef(ref, codexHome);
|
|
253
|
+
const fileInfo = await stat(filePath);
|
|
254
|
+
const summary = await scanSessionSummary(filePath, {
|
|
255
|
+
filePath,
|
|
256
|
+
size: fileInfo.size,
|
|
257
|
+
mtimeMs: fileInfo.mtimeMs,
|
|
258
|
+
mtime: fileInfo.mtime.toISOString(),
|
|
259
|
+
}, titleIndex);
|
|
260
|
+
const risks = new Map();
|
|
261
|
+
const turns = [];
|
|
262
|
+
let turnNumber = 0;
|
|
263
|
+
let goalObjective = "";
|
|
264
|
+
for await (const row of readJsonl(filePath)) {
|
|
265
|
+
if (row.type !== "response_item" || !row.payload) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const item = row.payload;
|
|
269
|
+
if (item.type === "message") {
|
|
270
|
+
if (item.role !== "user" && item.role !== "assistant") {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const message = extractMessageParts(item);
|
|
274
|
+
const rawMessageText = message.text;
|
|
275
|
+
const internalGoalObjective = extractInternalGoalObjective(rawMessageText);
|
|
276
|
+
if (internalGoalObjective) {
|
|
277
|
+
goalObjective = internalGoalObjective;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (isBootstrapUserMessage(item.role, rawMessageText)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const rawText = stripCodexAppDirectives(rawMessageText);
|
|
284
|
+
if (!rawText.trim() && !message.images.length) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
turnNumber += 1;
|
|
288
|
+
addRisks(risks, rawText, turnNumber);
|
|
289
|
+
addImageRisk(risks, message.images.length, turnNumber);
|
|
290
|
+
const text = redact ? redactText(rawText) : rawText;
|
|
291
|
+
turns.push({
|
|
292
|
+
kind: "message",
|
|
293
|
+
role: item.role,
|
|
294
|
+
turn: turnNumber,
|
|
295
|
+
text,
|
|
296
|
+
html: renderMarkdownHtml(text),
|
|
297
|
+
images: message.images,
|
|
298
|
+
timestamp: row.timestamp || "",
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (includeTools && isToolPayload(item)) {
|
|
303
|
+
const rawText = renderToolText(item, includeToolOutput);
|
|
304
|
+
if (!rawText.trim()) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
addRisks(risks, rawText, turnNumber || 1);
|
|
308
|
+
turns.push({
|
|
309
|
+
kind: "tool",
|
|
310
|
+
role: "tool",
|
|
311
|
+
turn: turnNumber || 1,
|
|
312
|
+
name: toolName(item),
|
|
313
|
+
text: redact ? redactText(rawText) : rawText,
|
|
314
|
+
timestamp: row.timestamp || "",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
...summary,
|
|
320
|
+
engine: "codex",
|
|
321
|
+
engineLabel: "Codex",
|
|
322
|
+
ref: `codex:${summary.id}`,
|
|
323
|
+
goalObjective: redact ? redactText(goalObjective) : goalObjective,
|
|
324
|
+
displayCwd: redact ? redactText(summary.cwd || "") : summary.cwd,
|
|
325
|
+
displayFilePath: redact ? redactText(summary.filePath || "") : summary.filePath,
|
|
326
|
+
generatedAt: new Date().toISOString(),
|
|
327
|
+
redacted: redact,
|
|
328
|
+
includeTools,
|
|
329
|
+
includeToolOutput,
|
|
330
|
+
notices: [],
|
|
331
|
+
risks: [...risks.values()].sort((a, b) => severityRank(b.severity) - severityRank(a.severity)),
|
|
332
|
+
turns,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function splitSnapshotRef(ref) {
|
|
336
|
+
if (ref.startsWith("claude:")) {
|
|
337
|
+
return { engine: "claude", ref: ref.slice("claude:".length) };
|
|
338
|
+
}
|
|
339
|
+
if (ref.startsWith("trae:")) {
|
|
340
|
+
return { engine: "trae", ref: ref.slice("trae:".length) };
|
|
341
|
+
}
|
|
342
|
+
if (ref.startsWith("codex:")) {
|
|
343
|
+
return { engine: "codex", ref: ref.slice("codex:".length) };
|
|
344
|
+
}
|
|
345
|
+
return { engine: "codex", ref };
|
|
346
|
+
}
|
|
347
|
+
async function listClaudeSessions({ claudeHome, limit, cwd }) {
|
|
348
|
+
const files = await discoverClaudeSessionFiles(claudeHome);
|
|
349
|
+
const cwdFilter = cwd ? path.resolve(cwd) : "";
|
|
350
|
+
const summaries = [];
|
|
351
|
+
const fileSessionIds = new Set();
|
|
352
|
+
for (const fileInfo of files) {
|
|
353
|
+
const summary = await scanClaudeFileSessionSummary(fileInfo.filePath, fileInfo, claudeHome);
|
|
354
|
+
fileSessionIds.add(summary.id);
|
|
355
|
+
if (cwdFilter && summary.cwd && !path.resolve(summary.cwd).startsWith(cwdFilter)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
summaries.push(summary);
|
|
359
|
+
}
|
|
360
|
+
for (const historyGroup of await readClaudeHistoryGroups(claudeHome, fileSessionIds)) {
|
|
361
|
+
const { entries: _entries, ...summary } = historyGroup;
|
|
362
|
+
if (cwdFilter && summary.cwd && !path.resolve(summary.cwd).startsWith(cwdFilter)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
summaries.push(summary);
|
|
366
|
+
}
|
|
367
|
+
summaries.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
|
|
368
|
+
return Number.isFinite(limit) ? summaries.slice(0, limit) : summaries;
|
|
369
|
+
}
|
|
370
|
+
async function discoverClaudeSessionFiles(claudeHome) {
|
|
371
|
+
const roots = [path.join(claudeHome, "projects"), path.join(claudeHome, "sessions")];
|
|
372
|
+
const files = [];
|
|
373
|
+
for (const root of roots) {
|
|
374
|
+
await collectJsonlFiles(root, files);
|
|
375
|
+
}
|
|
376
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
377
|
+
return files;
|
|
378
|
+
}
|
|
379
|
+
async function scanClaudeFileSessionSummary(filePath, fileInfo, claudeHome) {
|
|
380
|
+
const summary = createClaudeSummary({
|
|
381
|
+
id: sessionIdFromPath(filePath),
|
|
382
|
+
filePath,
|
|
383
|
+
size: fileInfo.size,
|
|
384
|
+
mtime: fileInfo.mtime,
|
|
385
|
+
sourceKind: "transcript",
|
|
386
|
+
});
|
|
387
|
+
summary.cwd = cwdFromClaudeProjectPath(filePath, claudeHome);
|
|
388
|
+
let firstUser = "";
|
|
389
|
+
let lineCount = 0;
|
|
390
|
+
for await (const row of readJsonl(filePath)) {
|
|
391
|
+
lineCount += 1;
|
|
392
|
+
if (row.sessionId) {
|
|
393
|
+
summary.id = row.sessionId;
|
|
394
|
+
}
|
|
395
|
+
if (row.cwd) {
|
|
396
|
+
summary.cwd = row.cwd;
|
|
397
|
+
}
|
|
398
|
+
const timestamp = normalizeClaudeTimestamp(row.timestamp);
|
|
399
|
+
if (timestamp && !summary.createdAt) {
|
|
400
|
+
summary.createdAt = timestamp;
|
|
401
|
+
}
|
|
402
|
+
const role = claudeRole(row);
|
|
403
|
+
if (!role) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const message = extractClaudeMessageParts(row.message || row);
|
|
407
|
+
const rawText = stripCodexAppDirectives(message.text);
|
|
408
|
+
summary.toolCallCount += message.toolCalls.length + message.toolResults.length;
|
|
409
|
+
if (rawText || message.images.length) {
|
|
410
|
+
summary.messageCount += 1;
|
|
411
|
+
if (!firstUser && role === "user" && !isClaudeCommand(rawText)) {
|
|
412
|
+
firstUser = rawText ? truncateForTitle(rawText) : "[image]";
|
|
413
|
+
}
|
|
414
|
+
summary.riskCount += detectRisks(rawText).length;
|
|
415
|
+
if (message.images.length) {
|
|
416
|
+
summary.riskCount += 1;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const tool of message.toolCalls) {
|
|
420
|
+
summary.riskCount += detectRisks(tool.text).length;
|
|
421
|
+
}
|
|
422
|
+
if (summary.id && summary.cwd && firstUser && lineCount >= 12) {
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (lineCount >= MAX_SUMMARY_LINES) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
summary.title = firstUser || summary.id;
|
|
430
|
+
return finishClaudeSummary(summary);
|
|
431
|
+
}
|
|
432
|
+
async function readClaudeHistoryGroups(claudeHome, excludeIds = new Set()) {
|
|
433
|
+
const historyPath = path.join(claudeHome, "history.jsonl");
|
|
434
|
+
let info;
|
|
435
|
+
try {
|
|
436
|
+
info = await stat(historyPath);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
const groups = new Map();
|
|
442
|
+
let fallbackIndex = 0;
|
|
443
|
+
for await (const row of readJsonl(historyPath)) {
|
|
444
|
+
const id = row.sessionId || `history-${fallbackIndex += 1}`;
|
|
445
|
+
if (excludeIds.has(id)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const timestamp = normalizeClaudeTimestamp(row.timestamp) || info.mtime.toISOString();
|
|
449
|
+
if (!groups.has(id)) {
|
|
450
|
+
groups.set(id, createClaudeSummary({
|
|
451
|
+
id,
|
|
452
|
+
filePath: historyPath,
|
|
453
|
+
size: info.size,
|
|
454
|
+
mtime: timestamp,
|
|
455
|
+
sourceKind: "history",
|
|
456
|
+
entries: [],
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
const group = groups.get(id);
|
|
460
|
+
group.entries.push(row);
|
|
461
|
+
group.cwd = row.project || group.cwd;
|
|
462
|
+
group.createdAt = group.createdAt || timestamp;
|
|
463
|
+
if (new Date(timestamp).getTime() > new Date(group.mtime).getTime()) {
|
|
464
|
+
group.mtime = timestamp;
|
|
465
|
+
}
|
|
466
|
+
const display = String(row.display || "").trim();
|
|
467
|
+
if (!display) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
group.messageCount += 1;
|
|
471
|
+
group.riskCount += detectRisks(display).length;
|
|
472
|
+
if (!group.title && !isClaudeCommand(display)) {
|
|
473
|
+
group.title = truncateForTitle(display);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return [...groups.values()].map((group) => finishClaudeSummary({
|
|
477
|
+
...group,
|
|
478
|
+
title: group.title || group.id,
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
function createClaudeSummary({ id, filePath, size, mtime, sourceKind, entries }) {
|
|
482
|
+
return {
|
|
483
|
+
id,
|
|
484
|
+
title: "",
|
|
485
|
+
cwd: "",
|
|
486
|
+
filePath,
|
|
487
|
+
size,
|
|
488
|
+
mtime,
|
|
489
|
+
createdAt: "",
|
|
490
|
+
modelProvider: "anthropic",
|
|
491
|
+
source: "claude-code",
|
|
492
|
+
sourceKind,
|
|
493
|
+
messageCount: 0,
|
|
494
|
+
toolCallCount: 0,
|
|
495
|
+
riskCount: 0,
|
|
496
|
+
entries,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function finishClaudeSummary(summary) {
|
|
500
|
+
summary.engine = "claude";
|
|
501
|
+
summary.engineLabel = "Claude Code";
|
|
502
|
+
summary.ref = `claude:${summary.id}`;
|
|
503
|
+
summary.historyOnly = summary.sourceKind === "history";
|
|
504
|
+
summary.sourceDetail = summary.historyOnly ? "history only" : "full transcript";
|
|
505
|
+
summary.displayCwd = redactText(summary.cwd || "");
|
|
506
|
+
summary.displayFilePath = redactText(summary.filePath || "");
|
|
507
|
+
return summary;
|
|
508
|
+
}
|
|
509
|
+
async function loadClaudeSnapshot(ref, { claudeHome, includeTools, includeToolOutput, redact }) {
|
|
510
|
+
const resolved = await resolveClaudeSessionRef(ref, claudeHome);
|
|
511
|
+
if (resolved.kind === "history") {
|
|
512
|
+
return loadClaudeHistorySnapshot(resolved.group, { includeTools, includeToolOutput, redact });
|
|
513
|
+
}
|
|
514
|
+
return loadClaudeFileSnapshot(resolved.filePath, { claudeHome, includeTools, includeToolOutput, redact });
|
|
515
|
+
}
|
|
516
|
+
async function loadClaudeFileSnapshot(filePath, { claudeHome, includeTools, includeToolOutput, redact }) {
|
|
517
|
+
const fileInfo = await stat(filePath);
|
|
518
|
+
const summary = await scanClaudeFileSessionSummary(filePath, {
|
|
519
|
+
filePath,
|
|
520
|
+
size: fileInfo.size,
|
|
521
|
+
mtimeMs: fileInfo.mtimeMs,
|
|
522
|
+
mtime: fileInfo.mtime.toISOString(),
|
|
523
|
+
}, claudeHome);
|
|
524
|
+
const risks = new Map();
|
|
525
|
+
const turns = [];
|
|
526
|
+
let turnNumber = 0;
|
|
527
|
+
for await (const row of readJsonl(filePath)) {
|
|
528
|
+
const role = claudeRole(row);
|
|
529
|
+
if (!role) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const message = extractClaudeMessageParts(row.message || row);
|
|
533
|
+
const rawText = stripCodexAppDirectives(message.text);
|
|
534
|
+
if (rawText.trim() || message.images.length) {
|
|
535
|
+
turnNumber += 1;
|
|
536
|
+
addRisks(risks, rawText, turnNumber);
|
|
537
|
+
addImageRisk(risks, message.images.length, turnNumber);
|
|
538
|
+
const text = redact ? redactText(rawText) : rawText;
|
|
539
|
+
turns.push({
|
|
540
|
+
kind: "message",
|
|
541
|
+
role,
|
|
542
|
+
turn: turnNumber,
|
|
543
|
+
text,
|
|
544
|
+
html: renderMarkdownHtml(text),
|
|
545
|
+
images: message.images,
|
|
546
|
+
timestamp: normalizeClaudeTimestamp(row.timestamp),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (includeTools) {
|
|
550
|
+
for (const tool of message.toolCalls) {
|
|
551
|
+
addRisks(risks, tool.text, turnNumber || 1);
|
|
552
|
+
turns.push({
|
|
553
|
+
kind: "tool",
|
|
554
|
+
role: "tool",
|
|
555
|
+
turn: turnNumber || 1,
|
|
556
|
+
name: tool.name,
|
|
557
|
+
text: redact ? redactText(tool.text) : tool.text,
|
|
558
|
+
timestamp: normalizeClaudeTimestamp(row.timestamp),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
for (const tool of message.toolResults) {
|
|
562
|
+
const text = includeToolOutput ? tool.text : "Tool output hidden. Re-run with Output enabled to include it.";
|
|
563
|
+
addRisks(risks, text, turnNumber || 1);
|
|
564
|
+
turns.push({
|
|
565
|
+
kind: "tool",
|
|
566
|
+
role: "tool",
|
|
567
|
+
turn: turnNumber || 1,
|
|
568
|
+
name: tool.name,
|
|
569
|
+
text: redact ? redactText(text) : text,
|
|
570
|
+
timestamp: normalizeClaudeTimestamp(row.timestamp),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
...summary,
|
|
577
|
+
displayCwd: redact ? redactText(summary.cwd || "") : summary.cwd,
|
|
578
|
+
displayFilePath: redact ? redactText(summary.filePath || "") : summary.filePath,
|
|
579
|
+
generatedAt: new Date().toISOString(),
|
|
580
|
+
redacted: redact,
|
|
581
|
+
includeTools,
|
|
582
|
+
includeToolOutput,
|
|
583
|
+
notices: [],
|
|
584
|
+
risks: [...risks.values()].sort((a, b) => severityRank(b.severity) - severityRank(a.severity)),
|
|
585
|
+
turns,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function loadClaudeHistorySnapshot(group, { includeTools, includeToolOutput, redact }) {
|
|
589
|
+
const risks = new Map();
|
|
590
|
+
const turns = [];
|
|
591
|
+
let turnNumber = 0;
|
|
592
|
+
for (const row of group.entries || []) {
|
|
593
|
+
const rawText = stripCodexAppDirectives(row.display);
|
|
594
|
+
if (!rawText) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
turnNumber += 1;
|
|
598
|
+
addRisks(risks, rawText, turnNumber);
|
|
599
|
+
const text = redact ? redactText(rawText) : rawText;
|
|
600
|
+
turns.push({
|
|
601
|
+
kind: "message",
|
|
602
|
+
role: "user",
|
|
603
|
+
turn: turnNumber,
|
|
604
|
+
text,
|
|
605
|
+
html: renderMarkdownHtml(text),
|
|
606
|
+
images: [],
|
|
607
|
+
timestamp: normalizeClaudeTimestamp(row.timestamp),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
const { entries: _entries, ...summary } = group;
|
|
611
|
+
return {
|
|
612
|
+
...summary,
|
|
613
|
+
displayCwd: redact ? redactText(summary.cwd || "") : summary.cwd,
|
|
614
|
+
displayFilePath: redact ? redactText(summary.filePath || "") : summary.filePath,
|
|
615
|
+
generatedAt: new Date().toISOString(),
|
|
616
|
+
redacted: redact,
|
|
617
|
+
includeTools,
|
|
618
|
+
includeToolOutput,
|
|
619
|
+
notices: [{
|
|
620
|
+
severity: "medium",
|
|
621
|
+
label: "History only",
|
|
622
|
+
text: "No Claude Code transcript file was found under ~/.claude/projects or ~/.claude/sessions for this session, so this preview is built from ~/.claude/history.jsonl and contains user prompts only.",
|
|
623
|
+
}],
|
|
624
|
+
risks: [...risks.values()].sort((a, b) => severityRank(b.severity) - severityRank(a.severity)),
|
|
625
|
+
turns,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
async function resolveClaudeSessionRef(ref, claudeHome) {
|
|
629
|
+
const maybePath = path.resolve(ref);
|
|
630
|
+
if (ref.endsWith(".jsonl")) {
|
|
631
|
+
assertInsideClaudeHome(maybePath, claudeHome);
|
|
632
|
+
return { kind: "file", filePath: maybePath };
|
|
633
|
+
}
|
|
634
|
+
const files = await discoverClaudeSessionFiles(claudeHome);
|
|
635
|
+
const exact = files.find((file) => sessionIdFromPath(file.filePath) === ref || path.basename(file.filePath, ".jsonl") === ref);
|
|
636
|
+
if (exact) {
|
|
637
|
+
return { kind: "file", filePath: exact.filePath };
|
|
638
|
+
}
|
|
639
|
+
for (const file of files) {
|
|
640
|
+
const summary = await scanClaudeFileSessionSummary(file.filePath, file, claudeHome);
|
|
641
|
+
if (summary.id === ref || summary.id.startsWith(ref)) {
|
|
642
|
+
return { kind: "file", filePath: file.filePath };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const groups = await readClaudeHistoryGroups(claudeHome);
|
|
646
|
+
const group = groups.find((item) => item.id === ref || item.id.startsWith(ref));
|
|
647
|
+
if (group) {
|
|
648
|
+
return { kind: "history", group };
|
|
649
|
+
}
|
|
650
|
+
throw new Error(`Claude Code session not found: ${ref}`);
|
|
651
|
+
}
|
|
652
|
+
function claudeRole(row) {
|
|
653
|
+
const role = row.message?.role || row.role || row.type;
|
|
654
|
+
return role === "user" || role === "assistant" ? role : "";
|
|
655
|
+
}
|
|
656
|
+
function extractClaudeMessageParts(message) {
|
|
657
|
+
const parts = [];
|
|
658
|
+
const images = [];
|
|
659
|
+
const toolCalls = [];
|
|
660
|
+
const toolResults = [];
|
|
661
|
+
const content = message?.content;
|
|
662
|
+
if (typeof content === "string") {
|
|
663
|
+
parts.push(content);
|
|
664
|
+
}
|
|
665
|
+
else if (Array.isArray(content)) {
|
|
666
|
+
for (const item of content) {
|
|
667
|
+
if (typeof item === "string") {
|
|
668
|
+
parts.push(item);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (typeof item?.text === "string" && (item.type === "text" || !item.type)) {
|
|
672
|
+
parts.push(item.text);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const image = extractClaudeImageAttachment(item, images.length + 1);
|
|
676
|
+
if (image) {
|
|
677
|
+
images.push(image);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (item?.type === "tool_use") {
|
|
681
|
+
toolCalls.push({
|
|
682
|
+
name: item.name || "tool_use",
|
|
683
|
+
text: renderClaudeToolCall(item),
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (item?.type === "tool_result") {
|
|
688
|
+
toolResults.push({
|
|
689
|
+
name: item.tool_use_id || "tool_result",
|
|
690
|
+
text: trimLongText(stringifyClaudeContent(item.content), TOOL_OUTPUT_PREVIEW_CHARS),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
text: trimLongText(parts.join("\n\n").trim(), MAX_TEXT_CHARS),
|
|
697
|
+
images,
|
|
698
|
+
toolCalls,
|
|
699
|
+
toolResults,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function renderClaudeToolCall(item) {
|
|
703
|
+
return `Tool call: ${item.name || "unknown"}\n${trimLongText(stringifyClaudeContent(item.input || {}), TOOL_OUTPUT_PREVIEW_CHARS)}`;
|
|
704
|
+
}
|
|
705
|
+
function stringifyClaudeContent(value) {
|
|
706
|
+
if (typeof value === "string") {
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
if (Array.isArray(value)) {
|
|
710
|
+
return value.map((item) => {
|
|
711
|
+
if (typeof item === "string") {
|
|
712
|
+
return item;
|
|
713
|
+
}
|
|
714
|
+
if (typeof item?.text === "string") {
|
|
715
|
+
return item.text;
|
|
716
|
+
}
|
|
717
|
+
return JSON.stringify(item, null, 2);
|
|
718
|
+
}).join("\n\n");
|
|
719
|
+
}
|
|
720
|
+
if (value && typeof value === "object") {
|
|
721
|
+
return JSON.stringify(value, null, 2);
|
|
722
|
+
}
|
|
723
|
+
return String(value || "");
|
|
724
|
+
}
|
|
725
|
+
function extractClaudeImageAttachment(item, index) {
|
|
726
|
+
if (item?.type !== "image") {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const source = item.source || {};
|
|
730
|
+
const src = source.type === "base64" && source.data
|
|
731
|
+
? `data:${source.media_type || "image/png"};base64,${source.data}`
|
|
732
|
+
: source.type === "url"
|
|
733
|
+
? source.url || ""
|
|
734
|
+
: "";
|
|
735
|
+
const safe = isSafeImageSource(src);
|
|
736
|
+
const srcLength = src.length;
|
|
737
|
+
const tooLarge = srcLength > MAX_INLINE_IMAGE_CHARS;
|
|
738
|
+
return {
|
|
739
|
+
alt: `Image attachment ${index}`,
|
|
740
|
+
detail: "",
|
|
741
|
+
mimeType: source.media_type || imageMimeType(src),
|
|
742
|
+
size: imageSourceSize(src),
|
|
743
|
+
src: safe && !tooLarge ? src : "",
|
|
744
|
+
unavailableReason: !safe ? "Unsupported image source" : tooLarge ? `Image is larger than ${formatBytes(MAX_INLINE_IMAGE_CHARS)}` : "",
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function normalizeClaudeTimestamp(value) {
|
|
748
|
+
if (!value) {
|
|
749
|
+
return "";
|
|
750
|
+
}
|
|
751
|
+
if (typeof value === "number") {
|
|
752
|
+
return new Date(value).toISOString();
|
|
753
|
+
}
|
|
754
|
+
const date = new Date(value);
|
|
755
|
+
return Number.isNaN(date.valueOf()) ? "" : date.toISOString();
|
|
756
|
+
}
|
|
757
|
+
function cwdFromClaudeProjectPath(filePath, claudeHome) {
|
|
758
|
+
const projectsRoot = path.join(claudeHome, "projects");
|
|
759
|
+
const relative = path.relative(projectsRoot, path.dirname(filePath));
|
|
760
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
761
|
+
return "";
|
|
762
|
+
}
|
|
763
|
+
const projectDir = relative.split(path.sep)[0] || "";
|
|
764
|
+
if (!projectDir.startsWith("-")) {
|
|
765
|
+
return "";
|
|
766
|
+
}
|
|
767
|
+
return projectDir.replace(/-/g, "/");
|
|
768
|
+
}
|
|
769
|
+
function isClaudeCommand(text) {
|
|
770
|
+
return String(text || "").trim().startsWith("/");
|
|
771
|
+
}
|
|
772
|
+
async function listTraeSessions({ traeHome, traeAppHome, traeRecordingsDir, limit, cwd }) {
|
|
773
|
+
const [recordedSessions, memorySessions, inputHistorySessions] = await Promise.all([
|
|
774
|
+
readTraeRecordedSummaries(traeRecordingsDir),
|
|
775
|
+
readTraeMemorySummaries(traeHome),
|
|
776
|
+
readTraeInputHistorySummaries(traeAppHome),
|
|
777
|
+
]);
|
|
778
|
+
const cwdFilter = cwd ? path.resolve(cwd) : "";
|
|
779
|
+
const sessions = [...recordedSessions, ...memorySessions, ...inputHistorySessions]
|
|
780
|
+
.filter((summary) => !cwdFilter || !summary.cwd || path.resolve(summary.cwd).startsWith(cwdFilter))
|
|
781
|
+
.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
|
|
782
|
+
return Number.isFinite(limit) ? sessions.slice(0, limit) : sessions;
|
|
783
|
+
}
|
|
784
|
+
async function readTraeRecordedSummaries(traeRecordingsDir) {
|
|
785
|
+
const files = [];
|
|
786
|
+
await collectJsonlFiles(traeRecordingsDir, files);
|
|
787
|
+
const summaries = [];
|
|
788
|
+
for (const fileInfo of files) {
|
|
789
|
+
const records = await readTraeCaptureRecords(fileInfo.filePath);
|
|
790
|
+
for (const group of groupTraeRecordedRecords(fileInfo, records)) {
|
|
791
|
+
const summary = await scanTraeRecordedSummaryFromRecords(fileInfo, group.records, group.id);
|
|
792
|
+
if (summary) {
|
|
793
|
+
summaries.push(summary);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return summaries;
|
|
798
|
+
}
|
|
799
|
+
async function scanTraeRecordedSummary(fileInfo) {
|
|
800
|
+
const records = await readTraeCaptureRecords(fileInfo.filePath);
|
|
801
|
+
return scanTraeRecordedSummaryFromRecords(fileInfo, records, path.basename(fileInfo.filePath, ".jsonl"));
|
|
802
|
+
}
|
|
803
|
+
function groupTraeRecordedRecords(fileInfo, records) {
|
|
804
|
+
if (!records.length) {
|
|
805
|
+
return [];
|
|
806
|
+
}
|
|
807
|
+
const fallbackId = path.basename(fileInfo.filePath, ".jsonl");
|
|
808
|
+
const groups = new Map();
|
|
809
|
+
for (const record of records) {
|
|
810
|
+
const key = record.domThreadId || record.captureSessionId || record.actualSessionId || record.pageSession || fallbackId;
|
|
811
|
+
if (!groups.has(key)) {
|
|
812
|
+
groups.set(key, []);
|
|
813
|
+
}
|
|
814
|
+
groups.get(key).push(record);
|
|
815
|
+
}
|
|
816
|
+
return [...groups.entries()].map(([id, groupRecords]) => ({
|
|
817
|
+
id: safeCaptureId(id || fallbackId),
|
|
818
|
+
records: groupRecords,
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
async function scanTraeRecordedSummaryFromRecords(fileInfo, records, captureId) {
|
|
822
|
+
if (!records.length) {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
const { turns } = buildTraeRecordedTurns(records, { redact: false });
|
|
826
|
+
if (!turns.length) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
const firstUser = turns.find((turn) => turn.role === "user" && turn.text.trim());
|
|
830
|
+
const firstAssistant = turns.find((turn) => turn.role === "assistant" && turn.text.trim());
|
|
831
|
+
const firstRecord = records[0] || {};
|
|
832
|
+
const lastRecord = records[records.length - 1] || {};
|
|
833
|
+
const cwd = records.map(extractTraeCwdFromRecord).find(Boolean) || "";
|
|
834
|
+
const title = firstUser?.text || firstAssistant?.text || firstRecord.pageTitle || "Trae local capture";
|
|
835
|
+
const createdAt = firstRecord.capturedAt || "";
|
|
836
|
+
const lastTimestamp = lastRecord.capturedAt || fileInfo.mtime;
|
|
837
|
+
const summary = createTraeSummary({
|
|
838
|
+
id: `recorded-${captureId || path.basename(fileInfo.filePath, ".jsonl")}`,
|
|
839
|
+
filePath: fileInfo.filePath,
|
|
840
|
+
filePaths: [fileInfo.filePath],
|
|
841
|
+
size: fileInfo.size,
|
|
842
|
+
mtime: normalizeRecordedTimestamp(lastTimestamp) || fileInfo.mtime,
|
|
843
|
+
cwd,
|
|
844
|
+
sourceKind: "recorded",
|
|
845
|
+
});
|
|
846
|
+
summary.title = truncateForTitle(title);
|
|
847
|
+
summary.createdAt = normalizeRecordedTimestamp(createdAt);
|
|
848
|
+
summary.messageCount = turns.length;
|
|
849
|
+
summary.toolCallCount = records.length;
|
|
850
|
+
summary.riskCount = turns.reduce((total, turn) => total + detectRisks(turn.text).length, 0);
|
|
851
|
+
summary.recordGroupId = captureId || "";
|
|
852
|
+
const actualSessionIds = uniqueStrings(records.map((record) => record.actualSessionId).filter(Boolean));
|
|
853
|
+
summary.actualSessionIds = summary.recordGroupId.startsWith("dom-thread")
|
|
854
|
+
? actualSessionIds.filter((id) => safeCaptureId(id) === summary.recordGroupId)
|
|
855
|
+
: actualSessionIds;
|
|
856
|
+
summary.captureSessionIds = uniqueStrings(records.map((record) => record.domThreadId || record.captureSessionId).filter(Boolean));
|
|
857
|
+
return finishTraeSummary(summary);
|
|
858
|
+
}
|
|
859
|
+
async function readTraeCaptureRecords(filePath) {
|
|
860
|
+
const records = [];
|
|
861
|
+
for await (const row of readJsonl(filePath)) {
|
|
862
|
+
if (row && typeof row === "object" && String(row.schema || "").startsWith("trae-local-recorder-event")) {
|
|
863
|
+
records.push(row);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
records.sort((a, b) => {
|
|
867
|
+
const seqA = Number(a.sequence || 0);
|
|
868
|
+
const seqB = Number(b.sequence || 0);
|
|
869
|
+
if (seqA !== seqB) {
|
|
870
|
+
return seqA - seqB;
|
|
871
|
+
}
|
|
872
|
+
return new Date(a.capturedAt || 0).getTime() - new Date(b.capturedAt || 0).getTime();
|
|
873
|
+
});
|
|
874
|
+
return records;
|
|
875
|
+
}
|
|
876
|
+
async function loadTraeRecordedSnapshot(summary, { includeTools, includeToolOutput, redact }) {
|
|
877
|
+
const allRecords = await readTraeCaptureRecords(summary.filePath);
|
|
878
|
+
const records = summary.recordGroupId
|
|
879
|
+
? allRecords.filter((record) => {
|
|
880
|
+
const key = safeCaptureId(record.domThreadId || record.captureSessionId || record.actualSessionId || record.pageSession || "");
|
|
881
|
+
return key === summary.recordGroupId;
|
|
882
|
+
})
|
|
883
|
+
: allRecords;
|
|
884
|
+
const { risks, turns } = buildTraeRecordedTurns(records, { redact });
|
|
885
|
+
const notices = [{
|
|
886
|
+
severity: "medium",
|
|
887
|
+
label: "Local recorder",
|
|
888
|
+
text: "This transcript was reconstructed from opt-in local Trae DOM, fetch, WebSocket, EventSource, and stream capture events. Raw capture events are preserved in the local JSONL file for re-parsing.",
|
|
889
|
+
}];
|
|
890
|
+
if (!turns.length && records.length) {
|
|
891
|
+
notices.push({
|
|
892
|
+
severity: "medium",
|
|
893
|
+
label: "No extracted turns",
|
|
894
|
+
text: "Capture events were recorded, but no user or assistant message fields matched the current parser heuristics yet.",
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
...summary,
|
|
899
|
+
displayCwd: redact ? redactText(summary.cwd || "") : summary.cwd,
|
|
900
|
+
displayFilePath: redact ? redactText(summary.filePath || "") : summary.filePath,
|
|
901
|
+
generatedAt: new Date().toISOString(),
|
|
902
|
+
redacted: redact,
|
|
903
|
+
includeTools,
|
|
904
|
+
includeToolOutput,
|
|
905
|
+
notices,
|
|
906
|
+
risks,
|
|
907
|
+
turns,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function buildTraeRecordedTurns(records, { redact }) {
|
|
911
|
+
const turns = [];
|
|
912
|
+
const seen = new Set();
|
|
913
|
+
const pendingDeltas = new Map();
|
|
914
|
+
const replaceableTurns = new Map();
|
|
915
|
+
let turnNumber = 0;
|
|
916
|
+
function flushDelta(key) {
|
|
917
|
+
const pending = pendingDeltas.get(key);
|
|
918
|
+
if (!pending) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
pendingDeltas.delete(key);
|
|
922
|
+
pushTurn(pending.role, pending.text, pending.timestamp);
|
|
923
|
+
}
|
|
924
|
+
function flushAllDeltas() {
|
|
925
|
+
for (const key of [...pendingDeltas.keys()]) {
|
|
926
|
+
flushDelta(key);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function pushTurn(role, rawText, timestamp, options = {}) {
|
|
930
|
+
const cleaned = cleanCapturedMessageText(rawText);
|
|
931
|
+
if (!cleaned || isNoiseCapturedMessage(cleaned)) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (options.replaceKey && replaceableTurns.has(options.replaceKey)) {
|
|
935
|
+
const existing = replaceableTurns.get(options.replaceKey);
|
|
936
|
+
existing.rawText = cleaned;
|
|
937
|
+
existing.text = redact ? redactText(cleaned) : cleaned;
|
|
938
|
+
existing.html = renderMarkdownHtml(existing.text);
|
|
939
|
+
existing.timestamp = normalizeRecordedTimestamp(timestamp) || existing.timestamp;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const dedupeKey = stableHash(`${role}\0${normalizeDedupeText(cleaned)}`);
|
|
943
|
+
const last = turns[turns.length - 1];
|
|
944
|
+
if (seen.has(dedupeKey) || (last && last.role === role && normalizeDedupeText(last.rawText || last.text) === normalizeDedupeText(cleaned))) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
seen.add(dedupeKey);
|
|
948
|
+
turnNumber += 1;
|
|
949
|
+
const text = redact ? redactText(cleaned) : cleaned;
|
|
950
|
+
const turn = {
|
|
951
|
+
kind: "message",
|
|
952
|
+
role,
|
|
953
|
+
turn: turnNumber,
|
|
954
|
+
rawText: cleaned,
|
|
955
|
+
text,
|
|
956
|
+
html: renderMarkdownHtml(text),
|
|
957
|
+
images: [],
|
|
958
|
+
timestamp: normalizeRecordedTimestamp(timestamp),
|
|
959
|
+
};
|
|
960
|
+
turns.push(turn);
|
|
961
|
+
if (options.replaceKey) {
|
|
962
|
+
replaceableTurns.set(options.replaceKey, turn);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const record of expandTraeFetchChunkRecords(records)) {
|
|
966
|
+
const candidates = extractTraeCaptureCandidates(record);
|
|
967
|
+
for (const candidate of candidates) {
|
|
968
|
+
if (candidate.isDelta) {
|
|
969
|
+
const key = `${candidate.role}:${candidate.sourceKey || "stream"}`;
|
|
970
|
+
const pending = pendingDeltas.get(key) || {
|
|
971
|
+
role: candidate.role,
|
|
972
|
+
text: "",
|
|
973
|
+
timestamp: candidate.timestamp,
|
|
974
|
+
};
|
|
975
|
+
pending.text += candidate.text;
|
|
976
|
+
pending.timestamp = candidate.timestamp || pending.timestamp;
|
|
977
|
+
pendingDeltas.set(key, pending);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (candidate.role === "user") {
|
|
981
|
+
flushAllDeltas();
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
flushDelta(`${candidate.role}:${candidate.sourceKey || "stream"}`);
|
|
985
|
+
}
|
|
986
|
+
pushTurn(candidate.role, candidate.text, candidate.timestamp, {
|
|
987
|
+
replaceKey: candidate.replaceKey,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
flushAllDeltas();
|
|
992
|
+
const risks = new Map();
|
|
993
|
+
const finalTurns = turns.map((turn, index) => {
|
|
994
|
+
const nextTurn = {
|
|
995
|
+
...turn,
|
|
996
|
+
turn: index + 1,
|
|
997
|
+
};
|
|
998
|
+
addRisks(risks, nextTurn.rawText || nextTurn.text, nextTurn.turn);
|
|
999
|
+
const { rawText: _rawText, ...publicTurn } = nextTurn;
|
|
1000
|
+
return publicTurn;
|
|
1001
|
+
});
|
|
1002
|
+
return {
|
|
1003
|
+
risks: [...risks.values()].sort((a, b) => severityRank(b.severity) - severityRank(a.severity)),
|
|
1004
|
+
turns: finalTurns,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function expandTraeFetchChunkRecords(records) {
|
|
1008
|
+
const expanded = [];
|
|
1009
|
+
const buffers = new Map();
|
|
1010
|
+
for (const record of records) {
|
|
1011
|
+
const key = record.requestId || record.url || record.pageSession || "fetch";
|
|
1012
|
+
if (record.kind === "fetch-response-chunk") {
|
|
1013
|
+
const existing = buffers.get(key) || { ...record, body: "", kind: "fetch-response" };
|
|
1014
|
+
existing.body += String(record.chunk || "");
|
|
1015
|
+
existing.capturedAt = record.capturedAt || existing.capturedAt;
|
|
1016
|
+
buffers.set(key, existing);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (record.kind === "fetch-response-end") {
|
|
1020
|
+
const existing = buffers.get(key);
|
|
1021
|
+
if (existing) {
|
|
1022
|
+
expanded.push({ ...existing, capturedAt: record.capturedAt || existing.capturedAt });
|
|
1023
|
+
buffers.delete(key);
|
|
1024
|
+
}
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (record.kind === "fetch-response" && buffers.has(key)) {
|
|
1028
|
+
buffers.delete(key);
|
|
1029
|
+
}
|
|
1030
|
+
expanded.push(record);
|
|
1031
|
+
}
|
|
1032
|
+
for (const record of buffers.values()) {
|
|
1033
|
+
expanded.push(record);
|
|
1034
|
+
}
|
|
1035
|
+
return expanded;
|
|
1036
|
+
}
|
|
1037
|
+
function extractTraeCaptureCandidates(record) {
|
|
1038
|
+
const sourceKey = record.requestId || record.wsId || record.eventSourceId || record.url || record.pageSession || "";
|
|
1039
|
+
const defaultRole = defaultRoleForCaptureKind(record.kind);
|
|
1040
|
+
const bodyText = String(record.body ?? record.chunk ?? "");
|
|
1041
|
+
if (!bodyText.trim()) {
|
|
1042
|
+
return [];
|
|
1043
|
+
}
|
|
1044
|
+
const payloads = parseCapturePayloads(bodyText);
|
|
1045
|
+
const candidates = [];
|
|
1046
|
+
if (record.kind === "dom-message") {
|
|
1047
|
+
for (const payload of payloads) {
|
|
1048
|
+
const role = normalizeCaptureRole(payload?.role);
|
|
1049
|
+
const text = stringifyCapturedContent(payload?.text ?? payload?.content ?? payload?.message ?? payload);
|
|
1050
|
+
if (!role || !text) {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
candidates.push({
|
|
1054
|
+
role,
|
|
1055
|
+
text,
|
|
1056
|
+
isDelta: false,
|
|
1057
|
+
sourceKey,
|
|
1058
|
+
replaceKey: payload?.messageId ? `dom:${payload.messageId}` : "",
|
|
1059
|
+
timestamp: payload?.timestamp || record.capturedAt,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
return candidates;
|
|
1063
|
+
}
|
|
1064
|
+
if (!isLikelyTraeChatNetworkRecord(record, payloads)) {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
for (const payload of payloads) {
|
|
1068
|
+
collectCaptureMessageCandidates(payload, {
|
|
1069
|
+
defaultRole,
|
|
1070
|
+
sourceKey,
|
|
1071
|
+
timestamp: record.capturedAt,
|
|
1072
|
+
depth: 0,
|
|
1073
|
+
}, candidates);
|
|
1074
|
+
}
|
|
1075
|
+
return candidates;
|
|
1076
|
+
}
|
|
1077
|
+
function isLikelyTraeChatNetworkRecord(record, payloads) {
|
|
1078
|
+
const url = String(record.url || record.responseUrl || "").toLowerCase();
|
|
1079
|
+
if (/ide-market|extensions\/vscode|\/gallery\/extensionquery|\/release\/note|\/asr\/get\/a/.test(url)) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
if (record.source === "dom") {
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
return payloads.some((payload) => hasExplicitChatMessageShape(payload, 0));
|
|
1086
|
+
}
|
|
1087
|
+
function hasExplicitChatMessageShape(value, depth) {
|
|
1088
|
+
if (!value || depth > 8) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
if (Array.isArray(value)) {
|
|
1092
|
+
return value.some((item) => hasExplicitChatMessageShape(item, depth + 1));
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof value !== "object") {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
const role = normalizeCaptureRole(value.role || value.sender || value.speaker || value.from || value.author?.role || value.author);
|
|
1098
|
+
if (role === "user" || role === "assistant") {
|
|
1099
|
+
return Boolean(stringifyCapturedContent(value.content ?? value.text ?? value.message ?? value.parts ?? value.delta ?? value));
|
|
1100
|
+
}
|
|
1101
|
+
if (Array.isArray(value.messages) || Array.isArray(value.choices)) {
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
return Object.values(value).some((child) => hasExplicitChatMessageShape(child, depth + 1));
|
|
1105
|
+
}
|
|
1106
|
+
function defaultRoleForCaptureKind(kind) {
|
|
1107
|
+
if (kind === "fetch-request" || kind === "ws-send") {
|
|
1108
|
+
return "user";
|
|
1109
|
+
}
|
|
1110
|
+
if (kind === "fetch-response" || kind === "ws-message" || kind === "eventsource-message") {
|
|
1111
|
+
return "assistant";
|
|
1112
|
+
}
|
|
1113
|
+
return "";
|
|
1114
|
+
}
|
|
1115
|
+
function parseCapturePayloads(text) {
|
|
1116
|
+
const trimmed = String(text || "").trim();
|
|
1117
|
+
if (!trimmed) {
|
|
1118
|
+
return [];
|
|
1119
|
+
}
|
|
1120
|
+
const direct = parseMaybeJson(trimmed);
|
|
1121
|
+
if (direct.ok) {
|
|
1122
|
+
return [direct.value];
|
|
1123
|
+
}
|
|
1124
|
+
const payloads = [];
|
|
1125
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
1126
|
+
const item = line.trim();
|
|
1127
|
+
if (!item || item === "data: [DONE]" || item === "[DONE]") {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const data = item.startsWith("data:") ? item.slice(5).trim() : item;
|
|
1131
|
+
const parsed = parseMaybeJson(data);
|
|
1132
|
+
if (parsed.ok) {
|
|
1133
|
+
payloads.push(parsed.value);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return payloads.length ? payloads : [trimmed];
|
|
1137
|
+
}
|
|
1138
|
+
function parseMaybeJson(text) {
|
|
1139
|
+
try {
|
|
1140
|
+
return { ok: true, value: JSON.parse(text) };
|
|
1141
|
+
}
|
|
1142
|
+
catch {
|
|
1143
|
+
return { ok: false, value: null };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function collectCaptureMessageCandidates(value, context, candidates) {
|
|
1147
|
+
if (context.depth > 10 || value == null) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof value === "string") {
|
|
1151
|
+
const parsed = parseMaybeJson(value.trim());
|
|
1152
|
+
if (parsed.ok && parsed.value && typeof parsed.value === "object") {
|
|
1153
|
+
collectCaptureMessageCandidates(parsed.value, { ...context, depth: context.depth + 1 }, candidates);
|
|
1154
|
+
}
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(value)) {
|
|
1158
|
+
for (const item of value) {
|
|
1159
|
+
collectCaptureMessageCandidates(item, { ...context, depth: context.depth + 1 }, candidates);
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (typeof value !== "object") {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const role = normalizeCaptureRole(value.role || value.sender || value.speaker || value.from || value.author?.role || value.author);
|
|
1167
|
+
if (role === "tool" || role === "system") {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
collectOpenAiStyleCandidates(value, context, candidates);
|
|
1171
|
+
collectAnthropicStyleCandidates(value, context, candidates);
|
|
1172
|
+
for (const key of Object.keys(value)) {
|
|
1173
|
+
const child = value[key];
|
|
1174
|
+
const lowerKey = key.toLowerCase();
|
|
1175
|
+
if (lowerKey === "choices" || lowerKey === "delta") {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
const keyRole = roleForCaptureContentKey(lowerKey, context.defaultRole);
|
|
1179
|
+
const candidateRole = role || keyRole;
|
|
1180
|
+
if (candidateRole) {
|
|
1181
|
+
const text = stringifyCapturedContent(child);
|
|
1182
|
+
if (text && shouldUseCaptureContentKey(lowerKey, role, keyRole)) {
|
|
1183
|
+
candidates.push({
|
|
1184
|
+
role: candidateRole,
|
|
1185
|
+
text,
|
|
1186
|
+
isDelta: isDeltaCaptureObject(value, lowerKey),
|
|
1187
|
+
sourceKey: context.sourceKey,
|
|
1188
|
+
timestamp: context.timestamp,
|
|
1189
|
+
});
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (child && typeof child === "object") {
|
|
1194
|
+
collectCaptureMessageCandidates(child, { ...context, depth: context.depth + 1 }, candidates);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function collectOpenAiStyleCandidates(value, context, candidates) {
|
|
1199
|
+
if (!Array.isArray(value.choices)) {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
for (const choice of value.choices) {
|
|
1203
|
+
if (!choice || typeof choice !== "object") {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const deltaText = stringifyCapturedContent(choice.delta?.content ?? choice.delta?.text);
|
|
1207
|
+
if (deltaText) {
|
|
1208
|
+
candidates.push({
|
|
1209
|
+
role: "assistant",
|
|
1210
|
+
text: deltaText,
|
|
1211
|
+
isDelta: true,
|
|
1212
|
+
sourceKey: context.sourceKey,
|
|
1213
|
+
timestamp: context.timestamp,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
const message = choice.message;
|
|
1217
|
+
if (message) {
|
|
1218
|
+
collectCaptureMessageCandidates(message, { ...context, defaultRole: "assistant", depth: context.depth + 1 }, candidates);
|
|
1219
|
+
}
|
|
1220
|
+
if (typeof choice.text === "string" && choice.text.trim()) {
|
|
1221
|
+
candidates.push({
|
|
1222
|
+
role: "assistant",
|
|
1223
|
+
text: choice.text,
|
|
1224
|
+
isDelta: true,
|
|
1225
|
+
sourceKey: context.sourceKey,
|
|
1226
|
+
timestamp: context.timestamp,
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function collectAnthropicStyleCandidates(value, context, candidates) {
|
|
1232
|
+
const type = String(value.type || value.event || "").toLowerCase();
|
|
1233
|
+
const deltaText = stringifyCapturedContent(value.delta?.text ?? value.delta?.content ?? value.completion);
|
|
1234
|
+
if (deltaText && (type.includes("delta") || Object.hasOwn(value, "delta") || Object.hasOwn(value, "completion"))) {
|
|
1235
|
+
candidates.push({
|
|
1236
|
+
role: "assistant",
|
|
1237
|
+
text: deltaText,
|
|
1238
|
+
isDelta: true,
|
|
1239
|
+
sourceKey: context.sourceKey,
|
|
1240
|
+
timestamp: context.timestamp,
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
if (Array.isArray(value.content) && normalizeCaptureRole(value.role) === "assistant") {
|
|
1244
|
+
const text = stringifyCapturedContent(value.content);
|
|
1245
|
+
if (text) {
|
|
1246
|
+
candidates.push({
|
|
1247
|
+
role: "assistant",
|
|
1248
|
+
text,
|
|
1249
|
+
isDelta: false,
|
|
1250
|
+
sourceKey: context.sourceKey,
|
|
1251
|
+
timestamp: context.timestamp,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
function normalizeCaptureRole(value) {
|
|
1257
|
+
const text = String(value || "").toLowerCase();
|
|
1258
|
+
if (!text) {
|
|
1259
|
+
return "";
|
|
1260
|
+
}
|
|
1261
|
+
if (/(user|human|customer|client|me)/.test(text)) {
|
|
1262
|
+
return "user";
|
|
1263
|
+
}
|
|
1264
|
+
if (/(assistant|agent|bot|ai|model|claude|gpt|trae)/.test(text)) {
|
|
1265
|
+
return "assistant";
|
|
1266
|
+
}
|
|
1267
|
+
if (/(tool|function)/.test(text)) {
|
|
1268
|
+
return "tool";
|
|
1269
|
+
}
|
|
1270
|
+
if (/(system|developer)/.test(text)) {
|
|
1271
|
+
return "system";
|
|
1272
|
+
}
|
|
1273
|
+
return "";
|
|
1274
|
+
}
|
|
1275
|
+
function roleForCaptureContentKey(lowerKey, defaultRole) {
|
|
1276
|
+
if (["inputtext", "input", "prompt", "query", "question", "userinput", "utterance"].includes(lowerKey)) {
|
|
1277
|
+
return "user";
|
|
1278
|
+
}
|
|
1279
|
+
if (["answer", "response", "reply", "output", "completion", "assistantmessage", "assistantresponse", "resulttext", "markdown"].includes(lowerKey)) {
|
|
1280
|
+
return "assistant";
|
|
1281
|
+
}
|
|
1282
|
+
if (["content", "text", "message", "value"].includes(lowerKey)) {
|
|
1283
|
+
return defaultRole === "user" || defaultRole === "assistant" ? defaultRole : "";
|
|
1284
|
+
}
|
|
1285
|
+
return "";
|
|
1286
|
+
}
|
|
1287
|
+
function shouldUseCaptureContentKey(lowerKey, explicitRole, keyRole) {
|
|
1288
|
+
if (explicitRole && ["content", "text", "message", "value"].includes(lowerKey)) {
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
return Boolean(keyRole);
|
|
1292
|
+
}
|
|
1293
|
+
function isDeltaCaptureObject(value, lowerKey) {
|
|
1294
|
+
const type = String(value.type || value.event || "").toLowerCase();
|
|
1295
|
+
return lowerKey.includes("delta") || type.includes("delta") || Object.hasOwn(value, "delta");
|
|
1296
|
+
}
|
|
1297
|
+
function stringifyCapturedContent(value) {
|
|
1298
|
+
if (value == null) {
|
|
1299
|
+
return "";
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof value === "string") {
|
|
1302
|
+
return value;
|
|
1303
|
+
}
|
|
1304
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1305
|
+
return String(value);
|
|
1306
|
+
}
|
|
1307
|
+
if (Array.isArray(value)) {
|
|
1308
|
+
return value.map((item) => stringifyCapturedContent(item)).filter(Boolean).join("\n");
|
|
1309
|
+
}
|
|
1310
|
+
if (typeof value !== "object") {
|
|
1311
|
+
return "";
|
|
1312
|
+
}
|
|
1313
|
+
if (typeof value.text === "string") {
|
|
1314
|
+
return value.text;
|
|
1315
|
+
}
|
|
1316
|
+
if (typeof value.content === "string") {
|
|
1317
|
+
return value.content;
|
|
1318
|
+
}
|
|
1319
|
+
if (typeof value.markdown === "string") {
|
|
1320
|
+
return value.markdown;
|
|
1321
|
+
}
|
|
1322
|
+
if (typeof value.value === "string") {
|
|
1323
|
+
return value.value;
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof value.message === "string") {
|
|
1326
|
+
return value.message;
|
|
1327
|
+
}
|
|
1328
|
+
if (Array.isArray(value.parts)) {
|
|
1329
|
+
return stringifyCapturedContent(value.parts);
|
|
1330
|
+
}
|
|
1331
|
+
return "";
|
|
1332
|
+
}
|
|
1333
|
+
function cleanCapturedMessageText(text) {
|
|
1334
|
+
const cleaned = String(text || "")
|
|
1335
|
+
.replace(/\u0000/g, "")
|
|
1336
|
+
.replace(/\u00a0/g, " ")
|
|
1337
|
+
.replace(/\r\n/g, "\n")
|
|
1338
|
+
.trim();
|
|
1339
|
+
return repairTraeFlattenedCodeBlocks(stripCodexAppDirectives(cleaned));
|
|
1340
|
+
}
|
|
1341
|
+
const TRAE_FLATTENED_CODE_LANGUAGES = new Map([
|
|
1342
|
+
["bash", "bash"],
|
|
1343
|
+
["css", "css"],
|
|
1344
|
+
["html", "html"],
|
|
1345
|
+
["javascript", "js"],
|
|
1346
|
+
["js", "js"],
|
|
1347
|
+
["json", "json"],
|
|
1348
|
+
["jsx", "jsx"],
|
|
1349
|
+
["plaintext", "text"],
|
|
1350
|
+
["plain text", "text"],
|
|
1351
|
+
["text", "text"],
|
|
1352
|
+
["tsx", "tsx"],
|
|
1353
|
+
["ts", "ts"],
|
|
1354
|
+
["typescript", "ts"],
|
|
1355
|
+
["xml", "xml"],
|
|
1356
|
+
["yaml", "yaml"],
|
|
1357
|
+
["yml", "yaml"],
|
|
1358
|
+
]);
|
|
1359
|
+
function normalizeTraeFlattenedCodeLanguage(line) {
|
|
1360
|
+
const key = String(line || "").trim().toLowerCase();
|
|
1361
|
+
return TRAE_FLATTENED_CODE_LANGUAGES.get(key) || "";
|
|
1362
|
+
}
|
|
1363
|
+
function isTraeFlattenedLineNumber(line) {
|
|
1364
|
+
return /^\d{1,4}$/.test(String(line || "").trim());
|
|
1365
|
+
}
|
|
1366
|
+
function looksLikeTraeCodeLine(line) {
|
|
1367
|
+
const value = String(line || "").trim();
|
|
1368
|
+
if (!value) {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
if (/^(\/\/|\/\*|\*|#|<!--)/.test(value)) {
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
if (/^[}\])>;,{]|.*[{}\[\]();=<>|].*$/.test(value)) {
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
if (/^(const|let|var|return|if|else|for|while|switch|case|break|continue|await|async|function|class|type|interface|export|import|from|use[A-Z]|set[A-Z]|on[A-Z])\b/.test(value)) {
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
if (/^[A-Za-z_$][\w$]*(\.|:|\?|\(|<)/.test(value)) {
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
if (/^<\/?[A-Za-z][\w.-]*/.test(value)) {
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
function isTraeCodeBlockBoundary(line, nextLine, codeLines, language) {
|
|
1389
|
+
const value = String(line || "").trim();
|
|
1390
|
+
if (!value) {
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1393
|
+
if (normalizeTraeFlattenedCodeLanguage(value) && isTraeFlattenedLineNumber(nextLine)) {
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
if (/^[一二三四五六七八九十]+、/.test(value)) {
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
if (/^第\s*\d/.test(value)) {
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
if (/^\d+\.\s+/.test(value) && /[\u4e00-\u9fff]/.test(value)) {
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
if (/^[A-Za-z_$][\w$]*:/.test(value) && /[\u4e00-\u9fff]/.test(value)) {
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
if (/^[^::]{1,32}:/.test(value) && /[\u4e00-\u9fff]/.test(value) && !/[{}()[\];<>]/.test(value)) {
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
if (/^(要点|支付成功时|组件卸载时|Hook 返回|职责分离|支付与升级解耦|健壮的轮询取消|等级读取兜底|遵循|如果你希望|这部分|返回最新值|用 Promise|没有 uid|否则|命中后|首轮|升级条件|令牌模式)/.test(value)) {
|
|
1412
|
+
return true;
|
|
1413
|
+
}
|
|
1414
|
+
const previous = String(codeLines[codeLines.length - 1] || "").trim();
|
|
1415
|
+
const plainTextBlock = language === "text";
|
|
1416
|
+
if (!plainTextBlock && /[\u4e00-\u9fff]/.test(value) && !previous.endsWith("//") && !looksLikeTraeCodeLine(value)) {
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
function repairTraeFlattenedCodeBlocks(text) {
|
|
1422
|
+
const lines = String(text || "").split("\n");
|
|
1423
|
+
const output = [];
|
|
1424
|
+
let changed = false;
|
|
1425
|
+
let inFence = false;
|
|
1426
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1427
|
+
const line = lines[index];
|
|
1428
|
+
if (/^```/.test(line.trim())) {
|
|
1429
|
+
inFence = !inFence;
|
|
1430
|
+
output.push(line);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (inFence) {
|
|
1434
|
+
output.push(line);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const language = normalizeTraeFlattenedCodeLanguage(line);
|
|
1438
|
+
if (!language || !isTraeFlattenedLineNumber(lines[index + 1])) {
|
|
1439
|
+
output.push(line);
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
let cursor = index + 1;
|
|
1443
|
+
while (cursor < lines.length && isTraeFlattenedLineNumber(lines[cursor])) {
|
|
1444
|
+
cursor += 1;
|
|
1445
|
+
}
|
|
1446
|
+
const code = [];
|
|
1447
|
+
while (cursor < lines.length) {
|
|
1448
|
+
const candidate = lines[cursor];
|
|
1449
|
+
if (isTraeCodeBlockBoundary(candidate, lines[cursor + 1], code, language)) {
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
code.push(candidate.replace(/\s+$/g, ""));
|
|
1453
|
+
cursor += 1;
|
|
1454
|
+
}
|
|
1455
|
+
if (!code.length) {
|
|
1456
|
+
output.push(line);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
while (code.length && !code[code.length - 1].trim()) {
|
|
1460
|
+
code.pop();
|
|
1461
|
+
}
|
|
1462
|
+
output.push(`\`\`\`${language}`, ...repairTraeFlattenedCodeLines(code, language), "```");
|
|
1463
|
+
changed = true;
|
|
1464
|
+
index = cursor - 1;
|
|
1465
|
+
}
|
|
1466
|
+
return changed ? output.join("\n") : String(text || "");
|
|
1467
|
+
}
|
|
1468
|
+
function repairTraeFlattenedCodeLines(lines, language) {
|
|
1469
|
+
if (!/^(ts|tsx|js|jsx)$/.test(language)) {
|
|
1470
|
+
return lines;
|
|
1471
|
+
}
|
|
1472
|
+
const repaired = [];
|
|
1473
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1474
|
+
let current = String(lines[index] || "").replace(/\s+$/g, "");
|
|
1475
|
+
while (index + 1 < lines.length && shouldJoinTraeCodeLine(current, lines[index + 1])) {
|
|
1476
|
+
current = joinTraeCodeLines(current, lines[index + 1]);
|
|
1477
|
+
index += 1;
|
|
1478
|
+
}
|
|
1479
|
+
repaired.push(current);
|
|
1480
|
+
}
|
|
1481
|
+
return repaired;
|
|
1482
|
+
}
|
|
1483
|
+
function shouldJoinTraeCodeLine(currentLine, nextLine) {
|
|
1484
|
+
const current = String(currentLine || "").trimEnd();
|
|
1485
|
+
const next = String(nextLine || "").trimStart();
|
|
1486
|
+
if (!current || !next) {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
if (/^[一二三四五六七八九十]+、/.test(next) || /^第\s*\d/.test(next)) {
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
if (/^(\/\/|\/\*)/.test(next)) {
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
if (current.endsWith("//")) {
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
if (/(\.|=|:|\?|,|<|\+|-|\*|\/|&&|\|\||!==|===|!=|==|\bextends|\bimplements|\bawait|\basync|\breturn|\bfrom)\s*$/.test(current)) {
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
if (/^(=>|\)|\]|\}|[A-Za-z_$][\w$.]*(?:[;),}]|$)|\(|<)/.test(next) && hasOpenTraeExpression(current)) {
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
if (/^(=>|\(|<|\+\+|--)/.test(next)) {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
if (/^(export\s+)?(interface|type|class|function|const|let|var|return|if|for|while|switch|use[A-Z]|set[A-Z]|on[A-Z])$/.test(current)) {
|
|
1508
|
+
return true;
|
|
1509
|
+
}
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
function hasOpenTraeExpression(line) {
|
|
1513
|
+
const value = String(line || "");
|
|
1514
|
+
const openParen = (value.match(/\(/g) || []).length - (value.match(/\)/g) || []).length;
|
|
1515
|
+
const openBracket = (value.match(/\[/g) || []).length - (value.match(/\]/g) || []).length;
|
|
1516
|
+
const openAngle = (value.match(/</g) || []).length - (value.match(/>/g) || []).length;
|
|
1517
|
+
return openParen > 0 || openBracket > 0 || openAngle > 0;
|
|
1518
|
+
}
|
|
1519
|
+
function joinTraeCodeLines(currentLine, nextLine) {
|
|
1520
|
+
const current = String(currentLine || "").trimEnd();
|
|
1521
|
+
const next = String(nextLine || "").trimStart();
|
|
1522
|
+
if (!current) {
|
|
1523
|
+
return next;
|
|
1524
|
+
}
|
|
1525
|
+
if (!next) {
|
|
1526
|
+
return current;
|
|
1527
|
+
}
|
|
1528
|
+
if (current.endsWith(".") || /^(\)|\]|\}|,|;)/.test(next) || /^(\(|<|\[)/.test(next)) {
|
|
1529
|
+
return current + next;
|
|
1530
|
+
}
|
|
1531
|
+
return `${current} ${next}`;
|
|
1532
|
+
}
|
|
1533
|
+
function isNoiseCapturedMessage(text) {
|
|
1534
|
+
const value = String(text || "").trim();
|
|
1535
|
+
if (!value || value === "[DONE]") {
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
if (/^https?:\/\//i.test(value) || /^data:[^,]+,/i.test(value)) {
|
|
1539
|
+
return true;
|
|
1540
|
+
}
|
|
1541
|
+
if (/^[A-Za-z0-9_-]{40,}$/.test(value)) {
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
function normalizeDedupeText(text) {
|
|
1547
|
+
return String(text || "").replace(/\s+/g, " ").trim();
|
|
1548
|
+
}
|
|
1549
|
+
function stableHash(value) {
|
|
1550
|
+
return createHash("sha256").update(String(value)).digest("hex").slice(0, 20);
|
|
1551
|
+
}
|
|
1552
|
+
function uniqueStrings(values) {
|
|
1553
|
+
return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))];
|
|
1554
|
+
}
|
|
1555
|
+
function normalizeRecordedTimestamp(value) {
|
|
1556
|
+
if (!value) {
|
|
1557
|
+
return "";
|
|
1558
|
+
}
|
|
1559
|
+
const date = new Date(value);
|
|
1560
|
+
return Number.isNaN(date.valueOf()) ? "" : date.toISOString();
|
|
1561
|
+
}
|
|
1562
|
+
function extractTraeCwdFromRecord(record) {
|
|
1563
|
+
const values = [];
|
|
1564
|
+
collectNamedStringValues(record, new Set([
|
|
1565
|
+
"cwd",
|
|
1566
|
+
"projectpath",
|
|
1567
|
+
"workspacepath",
|
|
1568
|
+
"workspacefolder",
|
|
1569
|
+
"folderpath",
|
|
1570
|
+
"rootpath",
|
|
1571
|
+
]), values, 0);
|
|
1572
|
+
for (const value of values) {
|
|
1573
|
+
const decoded = decodeFileUrlPath(value);
|
|
1574
|
+
if (decoded.startsWith("/") || decoded.startsWith("~")) {
|
|
1575
|
+
return decoded;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return "";
|
|
1579
|
+
}
|
|
1580
|
+
function collectNamedStringValues(value, keys, results, depth) {
|
|
1581
|
+
if (!value || depth > 8 || typeof value !== "object") {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (Array.isArray(value)) {
|
|
1585
|
+
for (const item of value) {
|
|
1586
|
+
collectNamedStringValues(item, keys, results, depth + 1);
|
|
1587
|
+
}
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1591
|
+
if (typeof child === "string" && keys.has(key.toLowerCase())) {
|
|
1592
|
+
results.push(child);
|
|
1593
|
+
}
|
|
1594
|
+
else if (child && typeof child === "object") {
|
|
1595
|
+
collectNamedStringValues(child, keys, results, depth + 1);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
async function readTraeMemorySummaries(traeHome) {
|
|
1600
|
+
const files = [];
|
|
1601
|
+
await collectJsonlFiles(path.join(traeHome, "memory", "projects"), files);
|
|
1602
|
+
const groups = new Map();
|
|
1603
|
+
for (const fileInfo of files) {
|
|
1604
|
+
const id = traeMemorySessionIdFromPath(fileInfo.filePath);
|
|
1605
|
+
const key = `${cwdFromTraeMemoryPath(fileInfo.filePath, traeHome)}::${id}`;
|
|
1606
|
+
if (!groups.has(key)) {
|
|
1607
|
+
groups.set(key, []);
|
|
1608
|
+
}
|
|
1609
|
+
groups.get(key).push(fileInfo);
|
|
1610
|
+
}
|
|
1611
|
+
const summaries = [];
|
|
1612
|
+
for (const groupedFiles of groups.values()) {
|
|
1613
|
+
summaries.push(await scanTraeMemorySummary(groupedFiles, traeHome));
|
|
1614
|
+
}
|
|
1615
|
+
return summaries;
|
|
1616
|
+
}
|
|
1617
|
+
async function scanTraeMemorySummary(files, traeHome) {
|
|
1618
|
+
const sortedFiles = files.slice().sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1619
|
+
const latestFile = sortedFiles[sortedFiles.length - 1];
|
|
1620
|
+
const summary = createTraeSummary({
|
|
1621
|
+
id: traeMemorySessionIdFromPath(latestFile.filePath),
|
|
1622
|
+
filePath: latestFile.filePath,
|
|
1623
|
+
filePaths: sortedFiles.map((file) => file.filePath),
|
|
1624
|
+
size: sortedFiles.reduce((total, file) => total + file.size, 0),
|
|
1625
|
+
mtime: latestFile.mtime,
|
|
1626
|
+
cwd: cwdFromTraeMemoryPath(latestFile.filePath, traeHome),
|
|
1627
|
+
sourceKind: "memory",
|
|
1628
|
+
});
|
|
1629
|
+
for (const fileInfo of sortedFiles) {
|
|
1630
|
+
for await (const row of readJsonl(fileInfo.filePath)) {
|
|
1631
|
+
const text = renderTraeMemoryText(row);
|
|
1632
|
+
if (!text.trim()) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
summary.messageCount += 1;
|
|
1636
|
+
summary.riskCount += detectRisks(text).length;
|
|
1637
|
+
const timestamp = normalizeTraeTimestamp(row.message_summary_time);
|
|
1638
|
+
summary.createdAt = summary.createdAt || timestamp;
|
|
1639
|
+
if (timestamp && new Date(timestamp).getTime() > new Date(summary.mtime).getTime()) {
|
|
1640
|
+
summary.mtime = timestamp;
|
|
1641
|
+
}
|
|
1642
|
+
if (!summary.title && row.intent) {
|
|
1643
|
+
summary.title = truncateForTitle(String(row.intent));
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
summary.title = summary.title || summary.id;
|
|
1648
|
+
return finishTraeSummary(summary);
|
|
1649
|
+
}
|
|
1650
|
+
async function readTraeInputHistorySummaries(traeAppHome) {
|
|
1651
|
+
const workspaces = await discoverTraeWorkspaceStores(traeAppHome);
|
|
1652
|
+
const summaries = [];
|
|
1653
|
+
for (const workspace of workspaces) {
|
|
1654
|
+
const entries = await readTraeInputHistoryEntries(workspace.dbPath);
|
|
1655
|
+
if (!entries.length) {
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const latestPrompt = entries.slice().reverse().find((entry) => String(entry.inputText || "").trim());
|
|
1659
|
+
const summary = createTraeSummary({
|
|
1660
|
+
id: `input-history-${workspace.workspaceId}`,
|
|
1661
|
+
filePath: workspace.dbPath,
|
|
1662
|
+
filePaths: [workspace.dbPath],
|
|
1663
|
+
size: workspace.size,
|
|
1664
|
+
mtime: workspace.mtime,
|
|
1665
|
+
cwd: workspace.cwd,
|
|
1666
|
+
sourceKind: "input-history",
|
|
1667
|
+
});
|
|
1668
|
+
summary.workspaceId = workspace.workspaceId;
|
|
1669
|
+
summary.title = latestPrompt ? truncateForTitle(String(latestPrompt.inputText || "")) : "Input history";
|
|
1670
|
+
summary.messageCount = entries.length;
|
|
1671
|
+
summary.riskCount = entries.reduce((total, entry) => total + detectRisks(traeInputEntryText(entry)).length, 0);
|
|
1672
|
+
summaries.push(finishTraeSummary(summary));
|
|
1673
|
+
}
|
|
1674
|
+
return summaries;
|
|
1675
|
+
}
|
|
1676
|
+
async function discoverTraeWorkspaceStores(traeAppHome) {
|
|
1677
|
+
const root = path.join(traeAppHome, "User", "workspaceStorage");
|
|
1678
|
+
let entries = [];
|
|
1679
|
+
try {
|
|
1680
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
1681
|
+
}
|
|
1682
|
+
catch {
|
|
1683
|
+
return [];
|
|
1684
|
+
}
|
|
1685
|
+
const workspaces = [];
|
|
1686
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1687
|
+
if (!entry.isDirectory()) {
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const workspaceDir = path.join(root, entry.name);
|
|
1691
|
+
const dbPath = path.join(workspaceDir, "state.vscdb");
|
|
1692
|
+
let info;
|
|
1693
|
+
try {
|
|
1694
|
+
info = await stat(dbPath);
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
workspaces.push({
|
|
1700
|
+
workspaceId: entry.name,
|
|
1701
|
+
dbPath,
|
|
1702
|
+
cwd: await readTraeWorkspaceCwd(path.join(workspaceDir, "workspace.json")),
|
|
1703
|
+
size: info.size,
|
|
1704
|
+
mtime: info.mtime.toISOString(),
|
|
1705
|
+
});
|
|
1706
|
+
}));
|
|
1707
|
+
return workspaces;
|
|
1708
|
+
}
|
|
1709
|
+
async function readTraeWorkspaceCwd(workspacePath) {
|
|
1710
|
+
let raw = "";
|
|
1711
|
+
try {
|
|
1712
|
+
raw = await readFile(workspacePath, "utf8");
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
return "";
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
const workspace = JSON.parse(raw);
|
|
1719
|
+
return decodeFileUrlPath(workspace.folder || workspace.workspace || "");
|
|
1720
|
+
}
|
|
1721
|
+
catch {
|
|
1722
|
+
return "";
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function decodeFileUrlPath(value) {
|
|
1726
|
+
if (!value) {
|
|
1727
|
+
return "";
|
|
1728
|
+
}
|
|
1729
|
+
if (String(value).startsWith("file://")) {
|
|
1730
|
+
try {
|
|
1731
|
+
return fileURLToPath(value);
|
|
1732
|
+
}
|
|
1733
|
+
catch {
|
|
1734
|
+
return String(value);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return String(value);
|
|
1738
|
+
}
|
|
1739
|
+
async function readTraeInputHistoryEntries(dbPath) {
|
|
1740
|
+
const raw = await readSqliteItem(dbPath, "icube-ai-agent-storage-input-history");
|
|
1741
|
+
if (!raw.trim()) {
|
|
1742
|
+
return [];
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
const parsed = JSON.parse(raw);
|
|
1746
|
+
return Array.isArray(parsed)
|
|
1747
|
+
? parsed.filter((entry) => String(entry?.inputText || "").trim())
|
|
1748
|
+
: [];
|
|
1749
|
+
}
|
|
1750
|
+
catch {
|
|
1751
|
+
return [];
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
async function readSqliteItem(dbPath, key) {
|
|
1755
|
+
try {
|
|
1756
|
+
const { stdout } = await execFileAsync("sqlite3", [
|
|
1757
|
+
dbPath,
|
|
1758
|
+
`select cast(value as text) from ItemTable where key=${sqliteString(key)};`,
|
|
1759
|
+
], { maxBuffer: 32 * 1024 * 1024 });
|
|
1760
|
+
return stdout.trim();
|
|
1761
|
+
}
|
|
1762
|
+
catch {
|
|
1763
|
+
return "";
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
function sqliteString(value) {
|
|
1767
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1768
|
+
}
|
|
1769
|
+
function createTraeSummary({ id, filePath, filePaths, size, mtime, cwd, sourceKind }) {
|
|
1770
|
+
return {
|
|
1771
|
+
id,
|
|
1772
|
+
title: "",
|
|
1773
|
+
cwd: cwd || "",
|
|
1774
|
+
filePath,
|
|
1775
|
+
filePaths,
|
|
1776
|
+
size,
|
|
1777
|
+
mtime,
|
|
1778
|
+
createdAt: "",
|
|
1779
|
+
modelProvider: "trae",
|
|
1780
|
+
source: "trae",
|
|
1781
|
+
sourceKind,
|
|
1782
|
+
messageCount: 0,
|
|
1783
|
+
toolCallCount: 0,
|
|
1784
|
+
riskCount: 0,
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function finishTraeSummary(summary) {
|
|
1788
|
+
summary.engine = "trae";
|
|
1789
|
+
summary.engineLabel = "Trae";
|
|
1790
|
+
summary.ref = `trae:${summary.id}`;
|
|
1791
|
+
summary.historyOnly = summary.sourceKind === "input-history";
|
|
1792
|
+
summary.sourceDetail = summary.sourceKind === "input-history"
|
|
1793
|
+
? "input history only"
|
|
1794
|
+
: summary.sourceKind === "recorded"
|
|
1795
|
+
? "local recorder"
|
|
1796
|
+
: "memory summary";
|
|
1797
|
+
summary.displayCwd = redactText(summary.cwd || "");
|
|
1798
|
+
summary.displayFilePath = redactText(summary.filePath || "");
|
|
1799
|
+
return summary;
|
|
1800
|
+
}
|
|
1801
|
+
function safeCaptureId(value) {
|
|
1802
|
+
const clean = String(value || "")
|
|
1803
|
+
.trim()
|
|
1804
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
1805
|
+
.replace(/^-+|-+$/g, "");
|
|
1806
|
+
if (!clean) {
|
|
1807
|
+
return `trae-${Date.now().toString(36)}`;
|
|
1808
|
+
}
|
|
1809
|
+
return clean.length > 96 ? `${clean.slice(0, 72)}-${stableHash(clean)}` : clean;
|
|
1810
|
+
}
|
|
1811
|
+
async function loadTraeSnapshot(ref, { traeHome, traeAppHome, traeRecordingsDir, includeTools, includeToolOutput, redact }) {
|
|
1812
|
+
const resolved = await resolveTraeSessionRef(ref, traeHome, traeAppHome, traeRecordingsDir);
|
|
1813
|
+
if (resolved.kind === "recorded") {
|
|
1814
|
+
return loadTraeRecordedSnapshot(resolved.summary, { includeTools, includeToolOutput, redact });
|
|
1815
|
+
}
|
|
1816
|
+
if (resolved.kind === "input-history") {
|
|
1817
|
+
return loadTraeInputHistorySnapshot(resolved.summary, { includeTools, includeToolOutput, redact });
|
|
1818
|
+
}
|
|
1819
|
+
return loadTraeMemorySnapshot(resolved.summary, { includeTools, includeToolOutput, redact });
|
|
1820
|
+
}
|
|
1821
|
+
async function resolveTraeSessionRef(ref, traeHome, traeAppHome, traeRecordingsDir) {
|
|
1822
|
+
const maybePath = path.resolve(ref);
|
|
1823
|
+
if (ref.endsWith(".jsonl")) {
|
|
1824
|
+
if (isInsideHome(maybePath, traeRecordingsDir)) {
|
|
1825
|
+
const info = await stat(maybePath);
|
|
1826
|
+
const summary = await scanTraeRecordedSummary({
|
|
1827
|
+
filePath: maybePath,
|
|
1828
|
+
size: info.size,
|
|
1829
|
+
mtimeMs: info.mtimeMs,
|
|
1830
|
+
mtime: info.mtime.toISOString(),
|
|
1831
|
+
});
|
|
1832
|
+
return { kind: "recorded", summary };
|
|
1833
|
+
}
|
|
1834
|
+
assertInsideTraeHome(maybePath, traeHome);
|
|
1835
|
+
const info = await stat(maybePath);
|
|
1836
|
+
const summary = await scanTraeMemorySummary([{
|
|
1837
|
+
filePath: maybePath,
|
|
1838
|
+
size: info.size,
|
|
1839
|
+
mtimeMs: info.mtimeMs,
|
|
1840
|
+
mtime: info.mtime.toISOString(),
|
|
1841
|
+
}], traeHome);
|
|
1842
|
+
return { kind: "memory", summary };
|
|
1843
|
+
}
|
|
1844
|
+
const [recordedSummaries, memorySummaries, inputHistorySummaries] = await Promise.all([
|
|
1845
|
+
readTraeRecordedSummaries(traeRecordingsDir),
|
|
1846
|
+
readTraeMemorySummaries(traeHome),
|
|
1847
|
+
readTraeInputHistorySummaries(traeAppHome),
|
|
1848
|
+
]);
|
|
1849
|
+
const recordedSummary = recordedSummaries.find((summary) => summary.id === ref || summary.id.startsWith(ref));
|
|
1850
|
+
if (recordedSummary) {
|
|
1851
|
+
return { kind: "recorded", summary: recordedSummary };
|
|
1852
|
+
}
|
|
1853
|
+
const memorySummary = memorySummaries.find((summary) => summary.id === ref || summary.id.startsWith(ref));
|
|
1854
|
+
if (memorySummary) {
|
|
1855
|
+
return { kind: "memory", summary: memorySummary };
|
|
1856
|
+
}
|
|
1857
|
+
const inputSummary = inputHistorySummaries.find((summary) => summary.id === ref || summary.id.startsWith(ref));
|
|
1858
|
+
if (inputSummary) {
|
|
1859
|
+
return { kind: "input-history", summary: inputSummary };
|
|
1860
|
+
}
|
|
1861
|
+
throw new Error(`Trae session not found: ${ref}`);
|
|
1862
|
+
}
|
|
1863
|
+
async function loadTraeMemorySnapshot(summary, { includeTools, includeToolOutput, redact }) {
|
|
1864
|
+
const risks = new Map();
|
|
1865
|
+
const turns = [];
|
|
1866
|
+
let turnNumber = 0;
|
|
1867
|
+
for (const filePath of summary.filePaths || [summary.filePath]) {
|
|
1868
|
+
for await (const row of readJsonl(filePath)) {
|
|
1869
|
+
const rawText = stripCodexAppDirectives(renderTraeMemoryText(row));
|
|
1870
|
+
if (!rawText.trim()) {
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
turnNumber += 1;
|
|
1874
|
+
addRisks(risks, rawText, turnNumber);
|
|
1875
|
+
const text = redact ? redactText(rawText) : rawText;
|
|
1876
|
+
turns.push({
|
|
1877
|
+
kind: "message",
|
|
1878
|
+
role: "assistant",
|
|
1879
|
+
turn: turnNumber,
|
|
1880
|
+
text,
|
|
1881
|
+
html: renderMarkdownHtml(text),
|
|
1882
|
+
images: [],
|
|
1883
|
+
timestamp: normalizeTraeTimestamp(row.message_summary_time),
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
...summary,
|
|
1889
|
+
displayCwd: redact ? redactText(summary.cwd || "") : summary.cwd,
|
|
1890
|
+
displayFilePath: redact ? redactText(summary.filePath || "") : summary.filePath,
|
|
1891
|
+
generatedAt: new Date().toISOString(),
|
|
1892
|
+
redacted: redact,
|
|
1893
|
+
includeTools,
|
|
1894
|
+
includeToolOutput,
|
|
1895
|
+
notices: [{
|
|
1896
|
+
severity: "medium",
|
|
1897
|
+
label: "Memory summary",
|
|
1898
|
+
text: "Trae local storage exposed session memory summaries here, not the full raw user/assistant transcript.",
|
|
1899
|
+
}],
|
|
1900
|
+
risks: [...risks.values()].sort((a, b) => severityRank(b.severity) - severityRank(a.severity)),
|
|
1901
|
+
turns,
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
async function loadTraeInputHistorySnapshot(summary, { includeTools, includeToolOutput, redact }) {
|
|
1905
|
+
const entries = await readTraeInputHistoryEntries(summary.filePath);
|
|
1906
|
+
const risks = new Map();
|
|
1907
|
+
const turns = [];
|
|
1908
|
+
let turnNumber = 0;
|
|
1909
|
+
for (const entry of entries) {
|
|
1910
|
+
const rawText = stripCodexAppDirectives(traeInputEntryText(entry));
|
|
1911
|
+
if (!rawText.trim()) {
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
turnNumber += 1;
|
|
1915
|
+
addRisks(risks, rawText, turnNumber);
|
|
1916
|
+
if (Array.isArray(entry.multiMedia) && entry.multiMedia.length) {
|
|
1917
|
+
addImageRisk(risks, entry.multiMedia.length, turnNumber);
|
|
1918
|
+
}
|
|
1919
|
+
const text = redact ? redactText(rawText) : rawText;
|
|
1920
|
+
turns.push({
|
|
1921
|
+
kind: "message",
|
|
1922
|
+
role: "user",
|
|
1923
|
+
turn: turnNumber,
|
|
1924
|
+
text,
|
|
1925
|
+
html: renderMarkdownHtml(text),
|
|
1926
|
+
images: [],
|
|
1927
|
+
timestamp: "",
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
return {
|
|
1931
|
+
...summary,
|
|
1932
|
+
displayCwd: redact ? redactText(summary.cwd || "") : summary.cwd,
|
|
1933
|
+
displayFilePath: redact ? redactText(summary.filePath || "") : summary.filePath,
|
|
1934
|
+
generatedAt: new Date().toISOString(),
|
|
1935
|
+
redacted: redact,
|
|
1936
|
+
includeTools,
|
|
1937
|
+
includeToolOutput,
|
|
1938
|
+
notices: [{
|
|
1939
|
+
severity: "medium",
|
|
1940
|
+
label: "Input history only",
|
|
1941
|
+
text: "No full Trae transcript was found in local storage for this item, so this preview is built from Trae input history and contains user prompts only.",
|
|
1942
|
+
}],
|
|
1943
|
+
risks: [...risks.values()].sort((a, b) => severityRank(b.severity) - severityRank(a.severity)),
|
|
1944
|
+
turns,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function renderTraeMemoryText(row) {
|
|
1948
|
+
const blocks = [];
|
|
1949
|
+
if (row.intent) {
|
|
1950
|
+
blocks.push(`### Intent\n${String(row.intent).trim()}`);
|
|
1951
|
+
}
|
|
1952
|
+
if (Array.isArray(row.actions) && row.actions.length) {
|
|
1953
|
+
blocks.push(`### Actions\n${row.actions.map((item) => `- ${String(item).trim()}`).join("\n")}`);
|
|
1954
|
+
}
|
|
1955
|
+
if (row.outcome) {
|
|
1956
|
+
blocks.push(`### Outcome\n${String(row.outcome).trim()}`);
|
|
1957
|
+
}
|
|
1958
|
+
if (Array.isArray(row.learned) && row.learned.length) {
|
|
1959
|
+
blocks.push(`### Learned\n${row.learned.map((item) => `- ${String(item).trim()}`).join("\n")}`);
|
|
1960
|
+
}
|
|
1961
|
+
const meta = [
|
|
1962
|
+
row.message_summary_time ? `time: ${row.message_summary_time}` : "",
|
|
1963
|
+
row.message_id ? `message: ${row.message_id}` : "",
|
|
1964
|
+
].filter(Boolean).join(" | ");
|
|
1965
|
+
if (meta) {
|
|
1966
|
+
blocks.push(`_${meta}_`);
|
|
1967
|
+
}
|
|
1968
|
+
if (!blocks.length && row && typeof row === "object") {
|
|
1969
|
+
return trimLongText(JSON.stringify(row, null, 2), MAX_TEXT_CHARS);
|
|
1970
|
+
}
|
|
1971
|
+
return trimLongText(blocks.join("\n\n"), MAX_TEXT_CHARS);
|
|
1972
|
+
}
|
|
1973
|
+
function traeInputEntryText(entry) {
|
|
1974
|
+
const text = String(entry?.inputText || "").trim();
|
|
1975
|
+
const mediaCount = Array.isArray(entry?.multiMedia) ? entry.multiMedia.length : 0;
|
|
1976
|
+
return mediaCount ? `${text}\n\n[media attachments: ${mediaCount}]` : text;
|
|
1977
|
+
}
|
|
1978
|
+
function traeMemorySessionIdFromPath(filePath) {
|
|
1979
|
+
return path.basename(filePath, ".jsonl").replace(/^session_memory_/, "");
|
|
1980
|
+
}
|
|
1981
|
+
function cwdFromTraeMemoryPath(filePath, traeHome) {
|
|
1982
|
+
const root = path.join(traeHome, "memory", "projects");
|
|
1983
|
+
const relative = path.relative(root, path.dirname(filePath));
|
|
1984
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
1985
|
+
return "";
|
|
1986
|
+
}
|
|
1987
|
+
return decodeTraeProjectPath(relative.split(path.sep)[0] || "");
|
|
1988
|
+
}
|
|
1989
|
+
function decodeTraeProjectPath(value) {
|
|
1990
|
+
const text = String(value || "");
|
|
1991
|
+
if (!text.startsWith("-")) {
|
|
1992
|
+
return text;
|
|
1993
|
+
}
|
|
1994
|
+
const parts = text.slice(1).split("-").filter(Boolean);
|
|
1995
|
+
if (parts.length >= 4) {
|
|
1996
|
+
return `/${parts[0]}/${parts[1]}/${parts[2]}/${parts.slice(3).join("-")}`;
|
|
1997
|
+
}
|
|
1998
|
+
return `/${parts.join("/")}`;
|
|
1999
|
+
}
|
|
2000
|
+
function normalizeTraeTimestamp(value) {
|
|
2001
|
+
if (!value) {
|
|
2002
|
+
return "";
|
|
2003
|
+
}
|
|
2004
|
+
const text = String(value).trim();
|
|
2005
|
+
const withTimezone = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(text)
|
|
2006
|
+
? `${text.replace(" ", "T")}+08:00`
|
|
2007
|
+
: text;
|
|
2008
|
+
const date = new Date(withTimezone);
|
|
2009
|
+
return Number.isNaN(date.valueOf()) ? "" : date.toISOString();
|
|
2010
|
+
}
|
|
2011
|
+
async function resolveSessionRef(ref, codexHome) {
|
|
2012
|
+
const maybePath = path.resolve(ref);
|
|
2013
|
+
if (ref.endsWith(".jsonl")) {
|
|
2014
|
+
assertInsideCodexHome(maybePath, codexHome);
|
|
2015
|
+
return maybePath;
|
|
2016
|
+
}
|
|
2017
|
+
const files = await discoverSessionFiles(codexHome, true);
|
|
2018
|
+
const exact = files.find((file) => sessionIdFromPath(file.filePath) === ref);
|
|
2019
|
+
if (exact) {
|
|
2020
|
+
return exact.filePath;
|
|
2021
|
+
}
|
|
2022
|
+
for (const file of files) {
|
|
2023
|
+
const summary = await scanSessionSummary(file.filePath, file, new Map());
|
|
2024
|
+
if (summary.id === ref || summary.id.startsWith(ref)) {
|
|
2025
|
+
return file.filePath;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
throw new Error(`session not found: ${ref}`);
|
|
2029
|
+
}
|
|
2030
|
+
function assertInsideCodexHome(filePath, codexHome) {
|
|
2031
|
+
assertInsideHome(filePath, codexHome, "Codex");
|
|
2032
|
+
}
|
|
2033
|
+
function assertInsideClaudeHome(filePath, claudeHome) {
|
|
2034
|
+
assertInsideHome(filePath, claudeHome, "Claude Code");
|
|
2035
|
+
}
|
|
2036
|
+
function assertInsideTraeHome(filePath, traeHome) {
|
|
2037
|
+
assertInsideHome(filePath, traeHome, "Trae");
|
|
2038
|
+
}
|
|
2039
|
+
function isInsideHome(filePath, home) {
|
|
2040
|
+
const relative = path.relative(path.resolve(home), filePath);
|
|
2041
|
+
return Boolean(relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
2042
|
+
}
|
|
2043
|
+
function assertInsideHome(filePath, home, label) {
|
|
2044
|
+
const relative = path.relative(path.resolve(home), filePath);
|
|
2045
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
2046
|
+
throw new Error(`JSONL paths must live inside the ${label} home directory`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
async function* readJsonl(filePath) {
|
|
2050
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
2051
|
+
const reader = createInterface({ input: stream, crlfDelay: Infinity });
|
|
2052
|
+
try {
|
|
2053
|
+
for await (const line of reader) {
|
|
2054
|
+
if (!line.trim()) {
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
try {
|
|
2058
|
+
yield JSON.parse(line);
|
|
2059
|
+
}
|
|
2060
|
+
catch {
|
|
2061
|
+
yield { type: "parse_error", payload: { lineLength: line.length } };
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
finally {
|
|
2066
|
+
reader.close();
|
|
2067
|
+
stream.destroy();
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
function extractMessageText(item) {
|
|
2071
|
+
return extractMessageParts(item).text;
|
|
2072
|
+
}
|
|
2073
|
+
function extractMessageParts(item) {
|
|
2074
|
+
const parts = [];
|
|
2075
|
+
const images = [];
|
|
2076
|
+
for (const content of item.content || []) {
|
|
2077
|
+
if (typeof content.text === "string") {
|
|
2078
|
+
const text = stripImageMarkers(content.text);
|
|
2079
|
+
if (text) {
|
|
2080
|
+
parts.push(text);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
const image = extractImageAttachment(content, images.length + 1);
|
|
2084
|
+
if (image) {
|
|
2085
|
+
images.push(image);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
text: trimLongText(parts.join("\n\n"), MAX_TEXT_CHARS),
|
|
2090
|
+
images,
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function stripImageMarkers(text) {
|
|
2094
|
+
return String(text || "")
|
|
2095
|
+
.split(/\r?\n/)
|
|
2096
|
+
.filter((line) => !/^\s*<\/?image>\s*$/i.test(line))
|
|
2097
|
+
.join("\n")
|
|
2098
|
+
.trim();
|
|
2099
|
+
}
|
|
2100
|
+
function extractImageAttachment(content, index) {
|
|
2101
|
+
const src = typeof content.image_url === "string"
|
|
2102
|
+
? content.image_url.trim()
|
|
2103
|
+
: typeof content.imageUrl === "string"
|
|
2104
|
+
? content.imageUrl.trim()
|
|
2105
|
+
: typeof content.url === "string"
|
|
2106
|
+
? content.url.trim()
|
|
2107
|
+
: "";
|
|
2108
|
+
if (!src && content.type !== "input_image") {
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
const safe = isSafeImageSource(src);
|
|
2112
|
+
const srcLength = src.length;
|
|
2113
|
+
const tooLarge = srcLength > MAX_INLINE_IMAGE_CHARS;
|
|
2114
|
+
return {
|
|
2115
|
+
alt: `Image attachment ${index}`,
|
|
2116
|
+
detail: typeof content.detail === "string" ? content.detail : "",
|
|
2117
|
+
mimeType: imageMimeType(src),
|
|
2118
|
+
size: imageSourceSize(src),
|
|
2119
|
+
src: safe && !tooLarge ? src : "",
|
|
2120
|
+
unavailableReason: !safe ? "Unsupported image source" : tooLarge ? `Image is larger than ${formatBytes(MAX_INLINE_IMAGE_CHARS)}` : "",
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function isSafeImageSource(src) {
|
|
2124
|
+
if (!src) {
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
return /^data:image\/(?:png|jpe?g|gif|webp);base64,[A-Za-z0-9+/=\s]+$/i.test(src) || /^https?:\/\//i.test(src);
|
|
2128
|
+
}
|
|
2129
|
+
function imageMimeType(src) {
|
|
2130
|
+
const match = src.match(/^data:(image\/[^;,]+)[;,]/i);
|
|
2131
|
+
if (match) {
|
|
2132
|
+
return match[1].toLowerCase();
|
|
2133
|
+
}
|
|
2134
|
+
if (/^https?:\/\//i.test(src)) {
|
|
2135
|
+
const clean = src.split(/[?#]/)[0] || "";
|
|
2136
|
+
const ext = path.extname(clean).toLowerCase();
|
|
2137
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
2138
|
+
return "image/jpeg";
|
|
2139
|
+
}
|
|
2140
|
+
if (ext === ".png") {
|
|
2141
|
+
return "image/png";
|
|
2142
|
+
}
|
|
2143
|
+
if (ext === ".gif") {
|
|
2144
|
+
return "image/gif";
|
|
2145
|
+
}
|
|
2146
|
+
if (ext === ".webp") {
|
|
2147
|
+
return "image/webp";
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return "image";
|
|
2151
|
+
}
|
|
2152
|
+
function imageSourceSize(src) {
|
|
2153
|
+
const comma = src.indexOf(",");
|
|
2154
|
+
if (!src.startsWith("data:") || comma === -1) {
|
|
2155
|
+
return "";
|
|
2156
|
+
}
|
|
2157
|
+
const base64 = src.slice(comma + 1).replace(/\s/g, "");
|
|
2158
|
+
const padding = (base64.match(/=+$/)?.[0].length) || 0;
|
|
2159
|
+
const bytes = Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
|
|
2160
|
+
return formatBytes(bytes);
|
|
2161
|
+
}
|
|
2162
|
+
function isBootstrapUserMessage(role, text) {
|
|
2163
|
+
return role === "user" && (text.startsWith("# AGENTS.md instructions for ") ||
|
|
2164
|
+
text.includes("<environment_context>") ||
|
|
2165
|
+
isInternalCodexContextMessage(text));
|
|
2166
|
+
}
|
|
2167
|
+
function isInternalCodexContextMessage(text) {
|
|
2168
|
+
const value = String(text || "").trim();
|
|
2169
|
+
return /^<goal_context>\s*[\s\S]*<\/goal_context>\s*$/i.test(value);
|
|
2170
|
+
}
|
|
2171
|
+
function extractInternalGoalObjective(text) {
|
|
2172
|
+
const value = String(text || "").trim();
|
|
2173
|
+
if (!isInternalCodexContextMessage(value)) {
|
|
2174
|
+
return "";
|
|
2175
|
+
}
|
|
2176
|
+
const match = value.match(/<objective>\s*([\s\S]*?)\s*<\/objective>/i);
|
|
2177
|
+
return match ? trimLongText(match[1].trim(), MAX_TEXT_CHARS) : "";
|
|
2178
|
+
}
|
|
2179
|
+
function isToolPayload(item) {
|
|
2180
|
+
return item.type === "function_call" || item.type === "function_call_output" || item.type === "web_search_call";
|
|
2181
|
+
}
|
|
2182
|
+
function renderToolText(item, includeToolOutput) {
|
|
2183
|
+
if (item.type === "function_call") {
|
|
2184
|
+
return `Tool call: ${item.name || "unknown"}\n${trimLongText(item.arguments || "", TOOL_OUTPUT_PREVIEW_CHARS)}`;
|
|
2185
|
+
}
|
|
2186
|
+
if (item.type === "function_call_output") {
|
|
2187
|
+
if (!includeToolOutput) {
|
|
2188
|
+
return "Tool output hidden. Re-run with --include-tool-output to include it.";
|
|
2189
|
+
}
|
|
2190
|
+
return trimLongText(item.output || "", TOOL_OUTPUT_PREVIEW_CHARS);
|
|
2191
|
+
}
|
|
2192
|
+
if (item.type === "web_search_call") {
|
|
2193
|
+
return `Web search: ${item.action?.query || item.action?.url || item.status || "completed"}`;
|
|
2194
|
+
}
|
|
2195
|
+
return "";
|
|
2196
|
+
}
|
|
2197
|
+
function toolName(item) {
|
|
2198
|
+
if (item.type === "function_call") {
|
|
2199
|
+
return item.name || "function_call";
|
|
2200
|
+
}
|
|
2201
|
+
if (item.type === "function_call_output") {
|
|
2202
|
+
return "function_output";
|
|
2203
|
+
}
|
|
2204
|
+
return "web_search";
|
|
2205
|
+
}
|
|
2206
|
+
function trimLongText(text, maxChars) {
|
|
2207
|
+
if (!text || text.length <= maxChars) {
|
|
2208
|
+
return text || "";
|
|
2209
|
+
}
|
|
2210
|
+
return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} chars]`;
|
|
2211
|
+
}
|
|
2212
|
+
function sessionIdFromPath(filePath) {
|
|
2213
|
+
const base = path.basename(filePath, ".jsonl");
|
|
2214
|
+
const match = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
|
|
2215
|
+
return match ? match[1] : base.replace(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-/, "");
|
|
2216
|
+
}
|
|
2217
|
+
function truncateForTitle(text) {
|
|
2218
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
2219
|
+
return singleLine.length > 80 ? `${singleLine.slice(0, 77)}...` : singleLine;
|
|
2220
|
+
}
|
|
2221
|
+
export { detectRisks, redactText } from "../core/privacy.js";
|