@steipete/oracle 0.10.0 → 0.11.0
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 +55 -10
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +78 -13
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +26 -2
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1257 -485
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +40 -0
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function describeBrowserControlPlan(config = {}) {
|
|
2
|
+
const guidance = [];
|
|
3
|
+
const tabRef = String(config.browserTabRef ?? "").trim();
|
|
4
|
+
const reusesExistingTab = tabRef.length > 0;
|
|
5
|
+
if (config.attachRunning) {
|
|
6
|
+
guidance.push(reusesExistingTab
|
|
7
|
+
? `Oracle reuses the matching ChatGPT tab (${tabRef}) and leaves the existing browser process alone.`
|
|
8
|
+
: "Oracle opens a dedicated tab and leaves the existing browser process alone.");
|
|
9
|
+
if (config.keepBrowser) {
|
|
10
|
+
guidance.push("The browser stays open because Oracle did not launch it.");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
mode: "attach-running",
|
|
14
|
+
launchesChrome: false,
|
|
15
|
+
mayFocusWindow: true,
|
|
16
|
+
summary: reusesExistingTab
|
|
17
|
+
? "attach to an already-running local Chrome tab"
|
|
18
|
+
: "attach to an already-running local Chrome session",
|
|
19
|
+
guidance,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (config.remoteChrome) {
|
|
23
|
+
guidance.push(reusesExistingTab
|
|
24
|
+
? `Oracle reuses the matching ChatGPT tab (${tabRef}) in the configured remote Chrome session.`
|
|
25
|
+
: "Oracle opens a dedicated tab in the configured remote Chrome session.");
|
|
26
|
+
guidance.push("Local Chrome launch, cookie copy, and window hiding flags are skipped.");
|
|
27
|
+
return {
|
|
28
|
+
mode: "remote-chrome",
|
|
29
|
+
launchesChrome: false,
|
|
30
|
+
mayFocusWindow: false,
|
|
31
|
+
summary: reusesExistingTab
|
|
32
|
+
? "reuse an existing remote Chrome tab"
|
|
33
|
+
: "reuse an existing remote Chrome session",
|
|
34
|
+
guidance,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (config.headless) {
|
|
38
|
+
guidance.push("Headless mode avoids visible UI but may be blocked by ChatGPT or Cloudflare.");
|
|
39
|
+
return {
|
|
40
|
+
mode: "headless",
|
|
41
|
+
launchesChrome: true,
|
|
42
|
+
mayFocusWindow: false,
|
|
43
|
+
summary: "launch headless Chrome",
|
|
44
|
+
guidance,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (config.hideWindow) {
|
|
48
|
+
guidance.push("Chrome may briefly focus while launching before Oracle hides it.");
|
|
49
|
+
guidance.push("For the calmest shared-desktop flow, prefer --browser-attach-running or --remote-chrome.");
|
|
50
|
+
return {
|
|
51
|
+
mode: "hidden-window",
|
|
52
|
+
launchesChrome: true,
|
|
53
|
+
mayFocusWindow: true,
|
|
54
|
+
summary: "launch Chrome and hide the window after startup",
|
|
55
|
+
guidance,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
guidance.push(config.manualLogin
|
|
59
|
+
? "Manual-login mode may show the persistent Oracle Chrome profile for sign-in or automation."
|
|
60
|
+
: "A visible automation Chrome window may take focus while Oracle controls ChatGPT.");
|
|
61
|
+
guidance.push("Use --browser-hide-window, --browser-attach-running, or --remote-chrome to reduce desktop disruption.");
|
|
62
|
+
if (config.keepBrowser) {
|
|
63
|
+
guidance.push("Chrome will remain open after the run because --browser-keep-browser is enabled.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
mode: "visible-window",
|
|
67
|
+
launchesChrome: true,
|
|
68
|
+
mayFocusWindow: true,
|
|
69
|
+
summary: "launch visible Chrome",
|
|
70
|
+
guidance,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function formatBrowserControlPlan(plan, label = "browser") {
|
|
74
|
+
const risk = plan.mayFocusWindow
|
|
75
|
+
? "may focus/control the browser UI"
|
|
76
|
+
: "does not use a visible local browser window";
|
|
77
|
+
return [
|
|
78
|
+
`[${label}] Browser control: ${plan.summary}; ${risk}.`,
|
|
79
|
+
...plan.guidance.map((entry) => `[${label}] Browser guidance: ${entry}`),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
@@ -14,7 +14,7 @@ export async function detectChromeBinary() {
|
|
|
14
14
|
if (launcherDetected) {
|
|
15
15
|
return { path: launcherDetected };
|
|
16
16
|
}
|
|
17
|
-
const candidates = platformChromeCandidates();
|
|
17
|
+
const candidates = platformChromeCandidates(process.platform, os.homedir());
|
|
18
18
|
for (const candidate of candidates.absolutePaths) {
|
|
19
19
|
if (await isExecutable(candidate)) {
|
|
20
20
|
return { path: candidate };
|
|
@@ -31,9 +31,9 @@ export async function detectChromeCookieDb({ profile, }) {
|
|
|
31
31
|
if (process.platform === "win32") {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
-
const roots =
|
|
34
|
+
const roots = resolveAttachRunningProfileRoots();
|
|
35
35
|
for (const root of roots) {
|
|
36
|
-
const dir = path.join(root, profileName);
|
|
36
|
+
const dir = path.join(root.root, profileName);
|
|
37
37
|
const direct = path.join(dir, "Cookies");
|
|
38
38
|
if (await isFile(direct))
|
|
39
39
|
return direct;
|
|
@@ -43,8 +43,155 @@ export async function detectChromeCookieDb({ profile, }) {
|
|
|
43
43
|
}
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
46
|
+
export function resolveAttachRunningProfileRoots(platform = process.platform, homeDir = os.homedir()) {
|
|
47
|
+
if (platform === "darwin") {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
family: "chrome",
|
|
51
|
+
root: path.join(homeDir, "Library", "Application Support", "Google", "Chrome"),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
family: "chromium",
|
|
55
|
+
root: path.join(homeDir, "Library", "Application Support", "Chromium"),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
family: "edge",
|
|
59
|
+
root: path.join(homeDir, "Library", "Application Support", "Microsoft Edge"),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
family: "brave",
|
|
63
|
+
root: path.join(homeDir, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
if (platform === "linux") {
|
|
68
|
+
return [
|
|
69
|
+
{ family: "chrome", root: path.join(homeDir, ".config", "google-chrome") },
|
|
70
|
+
{ family: "chromium", root: path.join(homeDir, ".config", "chromium") },
|
|
71
|
+
{ family: "edge", root: path.join(homeDir, ".config", "microsoft-edge") },
|
|
72
|
+
{
|
|
73
|
+
family: "brave",
|
|
74
|
+
root: path.join(homeDir, ".config", "BraveSoftware", "Brave-Browser"),
|
|
75
|
+
},
|
|
76
|
+
{ family: "chromium", root: path.join(homeDir, "snap", "chromium", "common", "chromium") },
|
|
77
|
+
{ family: "chromium", root: path.join(homeDir, "snap", "chromium", "current", "chromium") },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
if (platform === "win32") {
|
|
81
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
family: "chrome",
|
|
85
|
+
root: path.join(localAppData, "Google", "Chrome", "User Data"),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
family: "chromium",
|
|
89
|
+
root: path.join(localAppData, "Chromium", "User Data"),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
family: "edge",
|
|
93
|
+
root: path.join(localAppData, "Microsoft", "Edge", "User Data"),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
family: "brave",
|
|
97
|
+
root: path.join(localAppData, "BraveSoftware", "Brave-Browser", "User Data"),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
export function resolveDevToolsActivePortDiscoveryRoots(platform = process.platform, homeDir = os.homedir()) {
|
|
104
|
+
if (platform === "darwin") {
|
|
105
|
+
return [path.join(homeDir, "Library", "Application Support")];
|
|
106
|
+
}
|
|
107
|
+
if (platform === "linux") {
|
|
108
|
+
return [path.join(homeDir, ".config"), path.join(homeDir, "snap")];
|
|
109
|
+
}
|
|
110
|
+
if (platform === "win32") {
|
|
111
|
+
return [process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local")];
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
export function inferAttachRunningBrowserFamily(chromePath) {
|
|
116
|
+
const normalized = chromePath?.trim().toLowerCase();
|
|
117
|
+
if (!normalized) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (normalized.includes("microsoft edge") || normalized.includes("msedge")) {
|
|
121
|
+
return "edge";
|
|
122
|
+
}
|
|
123
|
+
if (normalized.includes("brave")) {
|
|
124
|
+
return "brave";
|
|
125
|
+
}
|
|
126
|
+
if (normalized.includes("chromium")) {
|
|
127
|
+
return "chromium";
|
|
128
|
+
}
|
|
129
|
+
if (normalized.includes("chrome")) {
|
|
130
|
+
return "chrome";
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
export function parseDevToolsActivePort(raw, options = {}) {
|
|
135
|
+
const host = formatWebSocketHost(options.host ?? "127.0.0.1");
|
|
136
|
+
const [rawPort, rawBrowserPath] = raw.split(/\r?\n/u);
|
|
137
|
+
const port = Number.parseInt(rawPort?.trim() ?? "", 10);
|
|
138
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
|
|
139
|
+
throw new Error("DevToolsActivePort did not contain a valid port.");
|
|
140
|
+
}
|
|
141
|
+
const browserPath = rawBrowserPath?.trim() || "/devtools/browser";
|
|
142
|
+
const normalizedPath = browserPath.startsWith("/") ? browserPath : `/${browserPath}`;
|
|
143
|
+
return {
|
|
144
|
+
port,
|
|
145
|
+
browserWSEndpoint: `ws://${host}:${port}${normalizedPath}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export async function readDevToolsActivePortInfo(profileRoot, options = {}) {
|
|
149
|
+
const candidates = [
|
|
150
|
+
path.join(profileRoot, "DevToolsActivePort"),
|
|
151
|
+
path.join(profileRoot, "Default", "DevToolsActivePort"),
|
|
152
|
+
];
|
|
153
|
+
for (const candidate of candidates) {
|
|
154
|
+
try {
|
|
155
|
+
const raw = await fs.readFile(candidate, "utf8");
|
|
156
|
+
const parsed = parseDevToolsActivePort(raw, options);
|
|
157
|
+
return { ...parsed, path: candidate };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// ignore missing/unreadable candidates
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
export async function discoverDevToolsActivePortCandidates(options = {}) {
|
|
166
|
+
const { host, platform = process.platform, homeDir = os.homedir(), maxDepth = 6 } = options;
|
|
167
|
+
const roots = resolveDevToolsActivePortDiscoveryRoots(platform, homeDir);
|
|
168
|
+
const candidates = [];
|
|
169
|
+
const seenPaths = new Set();
|
|
170
|
+
for (const root of roots) {
|
|
171
|
+
await walkForDevToolsActivePort(root, maxDepth, async (candidatePath, stat) => {
|
|
172
|
+
if (seenPaths.has(candidatePath)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
seenPaths.add(candidatePath);
|
|
176
|
+
try {
|
|
177
|
+
const raw = await fs.readFile(candidatePath, "utf8");
|
|
178
|
+
const parsed = parseDevToolsActivePort(raw, { host });
|
|
179
|
+
candidates.push({
|
|
180
|
+
...parsed,
|
|
181
|
+
path: candidatePath,
|
|
182
|
+
profileRoot: deriveDevToolsProfileRoot(candidatePath),
|
|
183
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// ignore unreadable or malformed DevToolsActivePort files
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return candidates;
|
|
192
|
+
}
|
|
193
|
+
function platformChromeCandidates(platform = process.platform, homeDir = os.homedir()) {
|
|
194
|
+
if (platform === "linux") {
|
|
48
195
|
return {
|
|
49
196
|
binaryNames: [
|
|
50
197
|
"google-chrome",
|
|
@@ -73,7 +220,7 @@ function platformChromeCandidates() {
|
|
|
73
220
|
],
|
|
74
221
|
};
|
|
75
222
|
}
|
|
76
|
-
if (
|
|
223
|
+
if (platform === "darwin") {
|
|
77
224
|
return {
|
|
78
225
|
binaryNames: [],
|
|
79
226
|
absolutePaths: [
|
|
@@ -84,10 +231,10 @@ function platformChromeCandidates() {
|
|
|
84
231
|
],
|
|
85
232
|
};
|
|
86
233
|
}
|
|
87
|
-
if (
|
|
234
|
+
if (platform === "win32") {
|
|
88
235
|
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
89
236
|
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
90
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(
|
|
237
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
|
|
91
238
|
return {
|
|
92
239
|
binaryNames: [],
|
|
93
240
|
absolutePaths: [
|
|
@@ -101,31 +248,6 @@ function platformChromeCandidates() {
|
|
|
101
248
|
}
|
|
102
249
|
return { binaryNames: [], absolutePaths: [] };
|
|
103
250
|
}
|
|
104
|
-
function platformProfileRoots() {
|
|
105
|
-
const home = os.homedir();
|
|
106
|
-
if (process.platform === "darwin") {
|
|
107
|
-
return [
|
|
108
|
-
path.join(home, "Library", "Application Support", "Google", "Chrome"),
|
|
109
|
-
path.join(home, "Library", "Application Support", "Chromium"),
|
|
110
|
-
path.join(home, "Library", "Application Support", "Microsoft Edge"),
|
|
111
|
-
path.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
|
|
112
|
-
];
|
|
113
|
-
}
|
|
114
|
-
if (process.platform === "linux") {
|
|
115
|
-
return [
|
|
116
|
-
path.join(home, ".config", "google-chrome"),
|
|
117
|
-
path.join(home, ".config", "google-chrome-beta"),
|
|
118
|
-
path.join(home, ".config", "google-chrome-unstable"),
|
|
119
|
-
path.join(home, ".config", "chromium"),
|
|
120
|
-
path.join(home, ".config", "microsoft-edge"),
|
|
121
|
-
path.join(home, ".config", "BraveSoftware", "Brave-Browser"),
|
|
122
|
-
// Snap Chromium profiles
|
|
123
|
-
path.join(home, "snap", "chromium", "common", "chromium"),
|
|
124
|
-
path.join(home, "snap", "chromium", "current", "chromium"),
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
251
|
async function isExecutable(candidate) {
|
|
130
252
|
try {
|
|
131
253
|
const stat = await fs.stat(candidate);
|
|
@@ -162,3 +284,54 @@ async function findOnPath(names) {
|
|
|
162
284
|
}
|
|
163
285
|
return null;
|
|
164
286
|
}
|
|
287
|
+
function deriveDevToolsProfileRoot(activePortPath) {
|
|
288
|
+
const parentDir = path.dirname(activePortPath);
|
|
289
|
+
if (path.basename(parentDir).toLowerCase() === "default") {
|
|
290
|
+
return path.dirname(parentDir);
|
|
291
|
+
}
|
|
292
|
+
return parentDir;
|
|
293
|
+
}
|
|
294
|
+
function formatWebSocketHost(host) {
|
|
295
|
+
if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
|
|
296
|
+
return `[${host}]`;
|
|
297
|
+
}
|
|
298
|
+
return host;
|
|
299
|
+
}
|
|
300
|
+
async function walkForDevToolsActivePort(root, maxDepth, onFile) {
|
|
301
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
302
|
+
while (stack.length > 0) {
|
|
303
|
+
const current = stack.pop();
|
|
304
|
+
if (!current) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
let entries;
|
|
308
|
+
try {
|
|
309
|
+
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
const candidatePath = path.join(current.dir, entry.name);
|
|
316
|
+
if (entry.isSymbolicLink()) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (entry.isFile()) {
|
|
320
|
+
if (entry.name !== "DevToolsActivePort") {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const stat = await fs.stat(candidatePath);
|
|
325
|
+
await onFile(candidatePath, stat);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// ignore unreadable candidates
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (entry.isDirectory() && current.depth < maxDepth) {
|
|
333
|
+
stack.push({ dir: candidatePath, depth: current.depth + 1 });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveSessionArtifactsDir } from "./artifacts.js";
|
|
2
5
|
export function buildConversationDebugExpression() {
|
|
3
6
|
return `(() => {
|
|
4
7
|
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
@@ -34,3 +37,49 @@ export async function logDomFailure(Runtime, logger, context) {
|
|
|
34
37
|
// ignore snapshot failures
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
export async function captureBrowserDiagnostics(Runtime, logger, context, options = {}) {
|
|
41
|
+
if (!options.sessionId) {
|
|
42
|
+
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const dir = resolveSessionArtifactsDir(options.sessionId);
|
|
46
|
+
await fs.mkdir(dir, { recursive: true });
|
|
47
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
48
|
+
const baseName = `${context}-${timestamp}`;
|
|
49
|
+
const domPath = path.join(dir, `${baseName}.dom.json`);
|
|
50
|
+
const expression = `(() => {
|
|
51
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
52
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR)).slice(-6).map((node) => ({
|
|
53
|
+
role: node.getAttribute('data-message-author-role') || node.getAttribute('data-turn'),
|
|
54
|
+
text: (node.innerText || node.textContent || '').slice(0, 2000),
|
|
55
|
+
testid: node.getAttribute('data-testid'),
|
|
56
|
+
}));
|
|
57
|
+
return {
|
|
58
|
+
url: location.href,
|
|
59
|
+
title: document.title,
|
|
60
|
+
turns,
|
|
61
|
+
bodyText: (document.body?.innerText || '').slice(0, 5000),
|
|
62
|
+
};
|
|
63
|
+
})()`;
|
|
64
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
65
|
+
await fs.writeFile(domPath, `${JSON.stringify(result?.value ?? null, null, 2)}\n`, "utf8");
|
|
66
|
+
logger(`[browser] Saved DOM diagnostic snapshot to ${domPath}`);
|
|
67
|
+
let screenshotPath;
|
|
68
|
+
if (options.Page?.captureScreenshot) {
|
|
69
|
+
try {
|
|
70
|
+
const screenshot = await options.Page.captureScreenshot({
|
|
71
|
+
format: "png",
|
|
72
|
+
captureBeyondViewport: true,
|
|
73
|
+
});
|
|
74
|
+
if (screenshot?.data) {
|
|
75
|
+
screenshotPath = path.join(dir, `${baseName}.png`);
|
|
76
|
+
await fs.writeFile(screenshotPath, Buffer.from(screenshot.data, "base64"));
|
|
77
|
+
logger(`[browser] Saved screenshot diagnostic snapshot to ${screenshotPath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Screenshots are best-effort; the DOM snapshot above is the primary diagnostic.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { domPath, screenshotPath };
|
|
85
|
+
}
|