@steipete/oracle 0.14.0 → 0.14.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 +2 -2
- package/dist/bin/oracle-cli.js +8 -8
- package/dist/src/browser/actions/assistantResponse.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +344 -29
- package/dist/src/browser/actions/thinkingTime.js +239 -46
- package/dist/src/browser/chatgptImages.js +126 -24
- package/dist/src/browser/index.js +63 -8
- package/dist/src/browser/projectSourcesRunner.js +2 -1
- package/dist/src/browser/prompt.js +151 -22
- package/dist/src/cli/browserConfig.js +2 -1
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/options.js +8 -0
- package/dist/src/mcp/tools/chatgptImage.js +8 -3
- package/dist/src/mcp/tools/consult.js +9 -8
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/oracle/thinkingTime.js +40 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +32 -34
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/bin/oracle.js +0 -569
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -593
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -452
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -375
- package/dist/docs-site/gemini.html +0 -383
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -596
- package/dist/docs-site/mcp.html +0 -391
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/vendor/oracle-notifier/README.md +0 -26
|
@@ -9,6 +9,33 @@ import { resolveSessionArtifactsDir } from "./artifacts.js";
|
|
|
9
9
|
import { saveAssistantDownloadButtonArtifacts } from "./chatgptFiles.js";
|
|
10
10
|
const GENERATED_IMAGE_WAIT_MIN_MS = 15_000;
|
|
11
11
|
const GENERATED_IMAGE_WAIT_MAX_MS = 15 * 60_000;
|
|
12
|
+
const CHATGPT_GENERATED_IMAGE_BASE_URL = "https://chatgpt.com/";
|
|
13
|
+
function isAllowedChatGptHost(hostname) {
|
|
14
|
+
const value = hostname.toLowerCase();
|
|
15
|
+
return value === "chatgpt.com" || value === "chat.openai.com";
|
|
16
|
+
}
|
|
17
|
+
function normalizeGeneratedImageUrl(value) {
|
|
18
|
+
const raw = String(value ?? "").trim();
|
|
19
|
+
if (!raw)
|
|
20
|
+
return undefined;
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(raw, CHATGPT_GENERATED_IMAGE_BASE_URL);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (url.protocol !== "https:" || url.port || !isAllowedChatGptHost(url.hostname)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (url.pathname !== "/backend-api/estuary/content") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (!(url.searchParams.get("id") ?? "").startsWith("file_")) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return url.href;
|
|
38
|
+
}
|
|
12
39
|
function extractFileId(url) {
|
|
13
40
|
try {
|
|
14
41
|
return new URL(url).searchParams.get("id") ?? undefined;
|
|
@@ -41,8 +68,12 @@ function buildAssistantImageExpression(minTurnIndex) {
|
|
|
41
68
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
42
69
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
43
70
|
const isGeneratedImage = (img) => {
|
|
44
|
-
const url = img?.src || '';
|
|
45
|
-
|
|
71
|
+
const url = new URL(img?.src || '', location.origin || 'https://chatgpt.com');
|
|
72
|
+
const host = url.hostname.toLowerCase();
|
|
73
|
+
if (url.protocol !== 'https:' || url.port) return false;
|
|
74
|
+
if (host !== 'chatgpt.com' && host !== 'chat.openai.com') return false;
|
|
75
|
+
if (url.pathname !== '/backend-api/estuary/content') return false;
|
|
76
|
+
if (!String(url.searchParams.get('id') || '').startsWith('file_')) return false;
|
|
46
77
|
const alt = String(img.alt || '').toLowerCase();
|
|
47
78
|
if (alt.includes('generated image')) return true;
|
|
48
79
|
let node = img;
|
|
@@ -104,13 +135,16 @@ export async function readAssistantGeneratedImages(Runtime, minTurnIndex) {
|
|
|
104
135
|
});
|
|
105
136
|
const raw = Array.isArray(result?.value) ? result.value : [];
|
|
106
137
|
const normalized = raw
|
|
107
|
-
.map((item) =>
|
|
108
|
-
url
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
.map((item) => {
|
|
139
|
+
const url = normalizeGeneratedImageUrl(typeof item?.url === "string" ? item.url : "");
|
|
140
|
+
return {
|
|
141
|
+
url: url ?? "",
|
|
142
|
+
alt: typeof item?.alt === "string" ? item.alt : undefined,
|
|
143
|
+
width: typeof item?.width === "number" ? item.width : undefined,
|
|
144
|
+
height: typeof item?.height === "number" ? item.height : undefined,
|
|
145
|
+
fileId: url ? extractFileId(url) : undefined,
|
|
146
|
+
};
|
|
147
|
+
})
|
|
114
148
|
.filter((item) => item.url.length > 0);
|
|
115
149
|
return dedupeImages(normalized);
|
|
116
150
|
}
|
|
@@ -209,8 +243,51 @@ async function buildCookieHeader(Network) {
|
|
|
209
243
|
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
210
244
|
.join("; ");
|
|
211
245
|
}
|
|
246
|
+
async function fetchGeneratedImageInBrowserContext(Runtime, url) {
|
|
247
|
+
const expression = `
|
|
248
|
+
(async () => {
|
|
249
|
+
const url = ${JSON.stringify(url)};
|
|
250
|
+
const response = await fetch(url, { credentials: 'include', redirect: 'follow' });
|
|
251
|
+
const contentType = response.headers.get('content-type') || '';
|
|
252
|
+
const buffer = await response.arrayBuffer();
|
|
253
|
+
const bytes = new Uint8Array(buffer);
|
|
254
|
+
let binary = '';
|
|
255
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
256
|
+
binary += String.fromCharCode(...bytes.slice(index, index + 0x8000));
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
ok: response.ok,
|
|
260
|
+
status: response.status,
|
|
261
|
+
statusText: response.statusText,
|
|
262
|
+
contentType,
|
|
263
|
+
finalUrl: response.url,
|
|
264
|
+
b64: btoa(binary),
|
|
265
|
+
};
|
|
266
|
+
})()
|
|
267
|
+
`;
|
|
268
|
+
const { result, exceptionDetails } = await Runtime.evaluate({
|
|
269
|
+
expression,
|
|
270
|
+
awaitPromise: true,
|
|
271
|
+
returnByValue: true,
|
|
272
|
+
timeout: 120_000,
|
|
273
|
+
});
|
|
274
|
+
if (exceptionDetails) {
|
|
275
|
+
throw new Error("browser-context fetch threw an exception");
|
|
276
|
+
}
|
|
277
|
+
const value = result?.value;
|
|
278
|
+
if (!value?.ok || typeof value.b64 !== "string") {
|
|
279
|
+
const status = typeof value?.status === "number" ? value.status : "unknown";
|
|
280
|
+
const statusText = typeof value?.statusText === "string" ? value.statusText : "";
|
|
281
|
+
throw new Error(`browser-context fetch failed: ${status} ${statusText}`.trim());
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
buffer: Buffer.from(value.b64, "base64"),
|
|
285
|
+
contentType: typeof value.contentType === "string" ? value.contentType : null,
|
|
286
|
+
finalUrl: typeof value.finalUrl === "string" && value.finalUrl ? value.finalUrl : url,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
212
289
|
export async function saveChatGptGeneratedImages(params) {
|
|
213
|
-
const { Network, images, outputPath, logger } = params;
|
|
290
|
+
const { Network, Runtime, images, outputPath, logger } = params;
|
|
214
291
|
if (!images.length)
|
|
215
292
|
return { saved: false, imageCount: 0, savedImages: [], errors: [] };
|
|
216
293
|
const cookieHeader = await buildCookieHeader(Network);
|
|
@@ -228,20 +305,41 @@ export async function saveChatGptGeneratedImages(params) {
|
|
|
228
305
|
for (let index = 0; index < images.length; index += 1) {
|
|
229
306
|
const image = images[index];
|
|
230
307
|
try {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
308
|
+
const imageUrl = normalizeGeneratedImageUrl(image.url);
|
|
309
|
+
if (!imageUrl) {
|
|
310
|
+
throw new Error("rejected non-ChatGPT generated image URL");
|
|
311
|
+
}
|
|
312
|
+
let contentType = null;
|
|
313
|
+
let finalUrl = imageUrl;
|
|
314
|
+
let buffer;
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(imageUrl, {
|
|
317
|
+
headers: {
|
|
318
|
+
cookie: cookieHeader,
|
|
319
|
+
"user-agent": "Mozilla/5.0",
|
|
320
|
+
},
|
|
321
|
+
redirect: "follow",
|
|
322
|
+
});
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
throw new Error(`download failed: ${response.status} ${response.statusText}`);
|
|
325
|
+
}
|
|
326
|
+
contentType = response.headers.get("content-type");
|
|
327
|
+
finalUrl = response.url;
|
|
328
|
+
buffer = Buffer.from(await response.arrayBuffer());
|
|
329
|
+
}
|
|
330
|
+
catch (downloadError) {
|
|
331
|
+
if (!Runtime) {
|
|
332
|
+
throw downloadError;
|
|
333
|
+
}
|
|
334
|
+
const message = downloadError instanceof Error ? downloadError.message : String(downloadError);
|
|
335
|
+
logger?.(`[browser] ChatGPT generated image download failed via Node fetch; retrying in browser context (${image.fileId ?? imageUrl}: ${message}).`);
|
|
336
|
+
const browserFetch = await fetchGeneratedImageInBrowserContext(Runtime, imageUrl);
|
|
337
|
+
contentType = browserFetch.contentType;
|
|
338
|
+
finalUrl = browserFetch.finalUrl;
|
|
339
|
+
buffer = browserFetch.buffer;
|
|
240
340
|
}
|
|
241
|
-
const contentType = response.headers.get("content-type");
|
|
242
341
|
const extension = contentTypeToExtension(contentType);
|
|
243
342
|
const targetPath = resolveSiblingImagePath(path.resolve(outputPath), index, extension);
|
|
244
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
245
343
|
await fs.writeFile(targetPath, buffer);
|
|
246
344
|
savedImages.push({
|
|
247
345
|
kind: "image",
|
|
@@ -249,9 +347,9 @@ export async function saveChatGptGeneratedImages(params) {
|
|
|
249
347
|
label: index === 0 ? "Generated image" : `Generated image ${index + 1}`,
|
|
250
348
|
mimeType: contentType ?? undefined,
|
|
251
349
|
sizeBytes: buffer.length,
|
|
252
|
-
sourceUrl:
|
|
253
|
-
url:
|
|
254
|
-
finalUrl
|
|
350
|
+
sourceUrl: imageUrl,
|
|
351
|
+
url: imageUrl,
|
|
352
|
+
finalUrl,
|
|
255
353
|
alt: image.alt,
|
|
256
354
|
width: image.width,
|
|
257
355
|
height: image.height,
|
|
@@ -336,6 +434,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
336
434
|
let generatedImages = await readAssistantGeneratedImagesWithFallback(params.Runtime, params.minTurnIndex ?? undefined);
|
|
337
435
|
let latestAnswerText = params.answerText;
|
|
338
436
|
if (explicitTargetPath && generatedImages.length === 0) {
|
|
437
|
+
await params.checkBlockingUiWarning?.();
|
|
339
438
|
const targetPath = path.resolve(explicitTargetPath);
|
|
340
439
|
const buttonImages = await saveGeneratedImageButtonArtifacts({
|
|
341
440
|
Browser: params.Browser,
|
|
@@ -352,6 +451,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
352
451
|
const deadline = Date.now() + resolveGeneratedImageWaitTimeoutMs(params.waitTimeoutMs);
|
|
353
452
|
while (Date.now() < deadline) {
|
|
354
453
|
await delay(1500);
|
|
454
|
+
await params.checkBlockingUiWarning?.();
|
|
355
455
|
generatedImages = await readAssistantGeneratedImagesWithFallback(params.Runtime, params.minTurnIndex ?? undefined);
|
|
356
456
|
if (generatedImages.length > 0) {
|
|
357
457
|
break;
|
|
@@ -363,6 +463,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
363
463
|
}
|
|
364
464
|
}
|
|
365
465
|
if (generatedImages.length === 0) {
|
|
466
|
+
await params.checkBlockingUiWarning?.();
|
|
366
467
|
const delayedButtonImages = await saveGeneratedImageButtonArtifacts({
|
|
367
468
|
Browser: params.Browser,
|
|
368
469
|
Client: params.Client,
|
|
@@ -396,6 +497,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
396
497
|
}
|
|
397
498
|
const saved = await saveChatGptGeneratedImages({
|
|
398
499
|
Network: params.Network,
|
|
500
|
+
Runtime: params.Runtime,
|
|
399
501
|
images: generatedImages,
|
|
400
502
|
outputPath: targetPath,
|
|
401
503
|
logger: params.logger,
|
|
@@ -240,20 +240,39 @@ function formatChatGptUiWarningType(type) {
|
|
|
240
240
|
return "authentication/challenge";
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
async function
|
|
243
|
+
async function createChatGptUiWarningError(params) {
|
|
244
244
|
const [uiWarning] = await collectChatGptUiWarnings(params.Runtime);
|
|
245
|
-
if (!uiWarning)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
245
|
+
if (!uiWarning)
|
|
246
|
+
return null;
|
|
248
247
|
params.logger(`[browser] ChatGPT UI warning detected (${uiWarning.type}): ${uiWarning.message}`);
|
|
249
|
-
return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for
|
|
250
|
-
stage:
|
|
248
|
+
return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for ${params.waitTarget}: ${uiWarning.message}`, {
|
|
249
|
+
stage: params.stage,
|
|
251
250
|
code: "chatgpt-ui-warning",
|
|
252
251
|
uiWarning,
|
|
253
252
|
runtime: params.runtime,
|
|
254
253
|
diagnostics: params.diagnostics,
|
|
255
254
|
}, params.cause);
|
|
256
255
|
}
|
|
256
|
+
async function throwChatGptUiWarningIfPresent(params) {
|
|
257
|
+
const error = await createChatGptUiWarningError(params);
|
|
258
|
+
if (error)
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
async function createAssistantTimeoutError(params) {
|
|
262
|
+
const warningError = await createChatGptUiWarningError({
|
|
263
|
+
Runtime: params.Runtime,
|
|
264
|
+
logger: params.logger,
|
|
265
|
+
runtime: params.runtime,
|
|
266
|
+
stage: "assistant-timeout",
|
|
267
|
+
waitTarget: "the assistant",
|
|
268
|
+
diagnostics: params.diagnostics,
|
|
269
|
+
cause: params.cause,
|
|
270
|
+
});
|
|
271
|
+
if (!warningError) {
|
|
272
|
+
return new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime: params.runtime, diagnostics: params.diagnostics }, params.cause);
|
|
273
|
+
}
|
|
274
|
+
return warningError;
|
|
275
|
+
}
|
|
257
276
|
function listIgnoredRemoteChromeFlags(config) {
|
|
258
277
|
return [
|
|
259
278
|
config.headless ? "--browser-headless" : null,
|
|
@@ -334,7 +353,8 @@ function isImageOnlyUiChromeText(text) {
|
|
|
334
353
|
return (normalized.length === 0 ||
|
|
335
354
|
normalized === "edit" ||
|
|
336
355
|
normalized === "stopped thinking" ||
|
|
337
|
-
normalized === "stopped thinking edit"
|
|
356
|
+
normalized === "stopped thinking edit" ||
|
|
357
|
+
/^thought for \d+(?:\.\d+)?\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\s+edit$/.test(normalized));
|
|
338
358
|
}
|
|
339
359
|
function normalizeBrowserFollowUpPrompts(values) {
|
|
340
360
|
return (values ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -690,9 +710,10 @@ export async function runBrowserMode(options) {
|
|
|
690
710
|
}
|
|
691
711
|
else {
|
|
692
712
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
713
|
+
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
693
714
|
const connection = await connectWithNewTab(chrome.port, logger, config.url, chromeHost, {
|
|
694
715
|
fallbackToDefault: !strictTabIsolation,
|
|
695
|
-
retries:
|
|
716
|
+
retries: devtoolsRetries,
|
|
696
717
|
retryDelayMs: 500,
|
|
697
718
|
});
|
|
698
719
|
client = connection.client;
|
|
@@ -1504,6 +1525,23 @@ export async function runBrowserMode(options) {
|
|
|
1504
1525
|
outputPath: options.outputPath,
|
|
1505
1526
|
answerText,
|
|
1506
1527
|
waitTimeoutMs: options.config?.timeoutMs,
|
|
1528
|
+
checkBlockingUiWarning: () => throwChatGptUiWarningIfPresent({
|
|
1529
|
+
Runtime,
|
|
1530
|
+
logger,
|
|
1531
|
+
stage: "image-artifact-wait",
|
|
1532
|
+
waitTarget: "generated image artifacts",
|
|
1533
|
+
runtime: {
|
|
1534
|
+
chromePid: chrome.pid,
|
|
1535
|
+
chromePort: chrome.port,
|
|
1536
|
+
chromeHost,
|
|
1537
|
+
userDataDir,
|
|
1538
|
+
chromeTargetId: lastTargetId,
|
|
1539
|
+
tabUrl: lastUrl,
|
|
1540
|
+
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1541
|
+
promptSubmitted,
|
|
1542
|
+
controllerPid: process.pid,
|
|
1543
|
+
},
|
|
1544
|
+
}),
|
|
1507
1545
|
});
|
|
1508
1546
|
answerText = imageArtifacts.answerText || answerText;
|
|
1509
1547
|
if (imageArtifacts.markdownSuffix) {
|
|
@@ -2616,6 +2654,23 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2616
2654
|
outputPath: options.outputPath,
|
|
2617
2655
|
answerText,
|
|
2618
2656
|
waitTimeoutMs: options.config?.timeoutMs,
|
|
2657
|
+
checkBlockingUiWarning: () => throwChatGptUiWarningIfPresent({
|
|
2658
|
+
Runtime,
|
|
2659
|
+
logger,
|
|
2660
|
+
stage: "image-artifact-wait",
|
|
2661
|
+
waitTarget: "generated image artifacts",
|
|
2662
|
+
runtime: {
|
|
2663
|
+
chromePort: port,
|
|
2664
|
+
chromeHost: host,
|
|
2665
|
+
chromeBrowserWSEndpoint: browserWSEndpoint,
|
|
2666
|
+
chromeProfileRoot,
|
|
2667
|
+
chromeTargetId: remoteTargetId ?? undefined,
|
|
2668
|
+
tabUrl: lastUrl,
|
|
2669
|
+
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2670
|
+
promptSubmitted,
|
|
2671
|
+
controllerPid: process.pid,
|
|
2672
|
+
},
|
|
2673
|
+
}),
|
|
2619
2674
|
});
|
|
2620
2675
|
answerText = imageArtifacts.answerText || answerText;
|
|
2621
2676
|
if (imageArtifacts.markdownSuffix) {
|
|
@@ -97,9 +97,10 @@ export async function runBrowserProjectSources(request) {
|
|
|
97
97
|
preserveUserDataDir: manualLogin,
|
|
98
98
|
});
|
|
99
99
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
100
|
+
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
100
101
|
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
101
102
|
fallbackToDefault: !strictTabIsolation,
|
|
102
|
-
retries:
|
|
103
|
+
retries: devtoolsRetries,
|
|
103
104
|
retryDelayMs: 500,
|
|
104
105
|
});
|
|
105
106
|
client = connection.client;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSections, } from "../oracle.js";
|
|
4
|
+
import { readFiles, createFileSections, FileValidationError, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSections, } from "../oracle.js";
|
|
5
5
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
6
6
|
import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
|
|
7
7
|
import { buildAttachmentPlan } from "./policies.js";
|
|
8
8
|
import { createStoredZip } from "./zipBundle.js";
|
|
9
9
|
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
10
|
+
const MAX_BROWSER_ATTACHMENTS = 10;
|
|
11
|
+
const MAX_BROWSER_ZIP_BUNDLE_BYTES = 128 * 1024 * 1024;
|
|
10
12
|
const MEDIA_EXTENSIONS = new Set([
|
|
11
13
|
".mp4",
|
|
12
14
|
".mov",
|
|
@@ -31,27 +33,95 @@ const MEDIA_EXTENSIONS = new Set([
|
|
|
31
33
|
".heif",
|
|
32
34
|
".pdf",
|
|
33
35
|
]);
|
|
36
|
+
const ARCHIVE_EXTENSIONS = new Set([
|
|
37
|
+
".7z",
|
|
38
|
+
".aab",
|
|
39
|
+
".apk",
|
|
40
|
+
".br",
|
|
41
|
+
".bz2",
|
|
42
|
+
".cab",
|
|
43
|
+
".crx",
|
|
44
|
+
".deb",
|
|
45
|
+
".dmg",
|
|
46
|
+
".doc",
|
|
47
|
+
".docx",
|
|
48
|
+
".ear",
|
|
49
|
+
".epub",
|
|
50
|
+
".gz",
|
|
51
|
+
".ipa",
|
|
52
|
+
".iso",
|
|
53
|
+
".jar",
|
|
54
|
+
".lz",
|
|
55
|
+
".lz4",
|
|
56
|
+
".msi",
|
|
57
|
+
".odp",
|
|
58
|
+
".ods",
|
|
59
|
+
".odt",
|
|
60
|
+
".pkg",
|
|
61
|
+
".ppt",
|
|
62
|
+
".pptx",
|
|
63
|
+
".rar",
|
|
64
|
+
".rpm",
|
|
65
|
+
".tar",
|
|
66
|
+
".tgz",
|
|
67
|
+
".war",
|
|
68
|
+
".whl",
|
|
69
|
+
".xls",
|
|
70
|
+
".xlsx",
|
|
71
|
+
".xz",
|
|
72
|
+
".xpi",
|
|
73
|
+
".zip",
|
|
74
|
+
".zipx",
|
|
75
|
+
".zst",
|
|
76
|
+
]);
|
|
34
77
|
export function isMediaFile(filePath) {
|
|
35
78
|
const ext = path.extname(filePath).toLowerCase();
|
|
36
79
|
return MEDIA_EXTENSIONS.has(ext);
|
|
37
80
|
}
|
|
81
|
+
export function isRawUploadFile(filePath) {
|
|
82
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
83
|
+
return MEDIA_EXTENSIONS.has(ext) || ARCHIVE_EXTENSIONS.has(ext);
|
|
84
|
+
}
|
|
38
85
|
function formatSectionsForBundle(sections, options = {}) {
|
|
39
86
|
return formatFileSections(sections, {
|
|
40
87
|
lineNumbers: options.lineNumbers ?? true,
|
|
41
88
|
trailingNewline: true,
|
|
42
89
|
});
|
|
43
90
|
}
|
|
44
|
-
|
|
91
|
+
function resolveBrowserBundleFormat(format, sources) {
|
|
92
|
+
if (format !== "auto") {
|
|
93
|
+
return format;
|
|
94
|
+
}
|
|
95
|
+
return sources.hasRawUploadFiles ? "zip" : "text";
|
|
96
|
+
}
|
|
97
|
+
function shouldWriteBrowserBundle(format, { attachmentCount, bundleRequested, textSourceCount, textPlanShouldBundle, }) {
|
|
98
|
+
if (format === "zip") {
|
|
99
|
+
return (textPlanShouldBundle ||
|
|
100
|
+
(bundleRequested && attachmentCount > 0) ||
|
|
101
|
+
attachmentCount > MAX_BROWSER_ATTACHMENTS);
|
|
102
|
+
}
|
|
103
|
+
return textSourceCount > 0 && (textPlanShouldBundle || attachmentCount > MAX_BROWSER_ATTACHMENTS);
|
|
104
|
+
}
|
|
105
|
+
function assertAttachmentCount(attachments, format) {
|
|
106
|
+
if (attachments.length <= MAX_BROWSER_ATTACHMENTS)
|
|
107
|
+
return;
|
|
108
|
+
throw new Error(`Browser upload has ${attachments.length} attachments after applying bundle format "${format}". Use --browser-bundle-format auto or zip to stay within the ${MAX_BROWSER_ATTACHMENTS}-attachment limit.`);
|
|
109
|
+
}
|
|
110
|
+
async function writeBrowserBundle(sections, sources, format) {
|
|
45
111
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
|
|
46
112
|
const tokenEstimateText = formatSectionsForBundle(sections, {
|
|
47
113
|
lineNumbers: format === "text",
|
|
48
114
|
});
|
|
49
115
|
if (format === "zip") {
|
|
116
|
+
const totalSourceBytes = sources.reduce((total, source) => total + source.sizeBytes, 0);
|
|
117
|
+
if (totalSourceBytes > MAX_BROWSER_ZIP_BUNDLE_BYTES) {
|
|
118
|
+
throw new Error(`Browser ZIP bundle inputs exceed the ${MAX_BROWSER_ZIP_BUNDLE_BYTES}-byte in-memory limit.`);
|
|
119
|
+
}
|
|
50
120
|
const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
|
|
51
|
-
const buffer = createStoredZip(
|
|
52
|
-
path:
|
|
53
|
-
content:
|
|
54
|
-
})));
|
|
121
|
+
const buffer = createStoredZip(await Promise.all(sources.map(async (source) => ({
|
|
122
|
+
path: source.displayPath,
|
|
123
|
+
content: await fs.readFile(source.absolutePath),
|
|
124
|
+
}))));
|
|
55
125
|
await fs.writeFile(bundlePath, buffer);
|
|
56
126
|
return {
|
|
57
127
|
attachment: {
|
|
@@ -60,7 +130,7 @@ async function writeBrowserBundle(sections, format) {
|
|
|
60
130
|
sizeBytes: buffer.length,
|
|
61
131
|
generatedBundle: true,
|
|
62
132
|
},
|
|
63
|
-
metadata: { originalCount:
|
|
133
|
+
metadata: { originalCount: sources.length, bundlePath, format },
|
|
64
134
|
tokenEstimateText,
|
|
65
135
|
};
|
|
66
136
|
}
|
|
@@ -81,11 +151,27 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
81
151
|
const cwd = deps.cwd ?? process.cwd();
|
|
82
152
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
83
153
|
const allFilePaths = runOptions.file ?? [];
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
154
|
+
const discoveredFiles = allFilePaths.length > 0
|
|
155
|
+
? await readFilesFn(allFilePaths, {
|
|
156
|
+
cwd,
|
|
157
|
+
maxFileSizeBytes: 0,
|
|
158
|
+
readContents: false,
|
|
159
|
+
})
|
|
160
|
+
: [];
|
|
161
|
+
const textFilePaths = discoveredFiles
|
|
162
|
+
.filter((file) => !isRawUploadFile(file.path))
|
|
163
|
+
.map((file) => file.path);
|
|
164
|
+
const rawUploadFiles = discoveredFiles.filter((file) => isRawUploadFile(file.path));
|
|
165
|
+
const maxFileSizeBytes = runOptions.maxFileSizeBytes;
|
|
166
|
+
const rawUploadAttachments = await Promise.all(rawUploadFiles.map(async ({ path: filePath }) => {
|
|
87
167
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
88
168
|
const stats = await fs.stat(resolvedPath);
|
|
169
|
+
if (maxFileSizeBytes && stats.size > maxFileSizeBytes) {
|
|
170
|
+
throw new FileValidationError(`The following file exceeds the ${maxFileSizeBytes}-byte limit:\n- ${path.relative(cwd, resolvedPath) || resolvedPath} (${stats.size} bytes)`, {
|
|
171
|
+
files: [resolvedPath],
|
|
172
|
+
limitBytes: maxFileSizeBytes,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
89
175
|
return {
|
|
90
176
|
path: resolvedPath,
|
|
91
177
|
displayPath: path.relative(cwd, resolvedPath) || path.basename(resolvedPath),
|
|
@@ -105,7 +191,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
105
191
|
? "never"
|
|
106
192
|
: (runOptions.browserAttachments ?? "auto");
|
|
107
193
|
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
108
|
-
const bundleFormat = runOptions.browserBundleFormat ?? "
|
|
194
|
+
const bundleFormat = runOptions.browserBundleFormat ?? "auto";
|
|
195
|
+
if (attachmentsPolicy === "never" && rawUploadAttachments.length > 0) {
|
|
196
|
+
throw new FileValidationError("Raw or binary files cannot be pasted inline when browser attachments are disabled. Use --browser-attachments auto or always.", { files: rawUploadAttachments.map((attachment) => attachment.displayPath) });
|
|
197
|
+
}
|
|
109
198
|
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
110
199
|
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
111
200
|
const baseComposerSections = [];
|
|
@@ -124,25 +213,47 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
124
213
|
: inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
|
|
125
214
|
? inlinePlan
|
|
126
215
|
: uploadPlan;
|
|
127
|
-
const
|
|
216
|
+
const textBundleSources = sections.map((section) => ({
|
|
217
|
+
absolutePath: section.absolutePath,
|
|
218
|
+
displayPath: section.displayPath,
|
|
219
|
+
sizeBytes: Buffer.byteLength(section.content, "utf8"),
|
|
220
|
+
}));
|
|
221
|
+
const rawUploadBundleSources = rawUploadAttachments.map((attachment) => ({
|
|
222
|
+
absolutePath: attachment.path,
|
|
223
|
+
displayPath: attachment.displayPath,
|
|
224
|
+
sizeBytes: attachment.sizeBytes ?? 0,
|
|
225
|
+
}));
|
|
226
|
+
const allBundleSources = [...textBundleSources, ...rawUploadBundleSources];
|
|
227
|
+
const attachments = [...selectedPlan.attachments, ...rawUploadAttachments];
|
|
228
|
+
const resolvedBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
|
|
229
|
+
hasRawUploadFiles: rawUploadAttachments.length > 0,
|
|
230
|
+
});
|
|
231
|
+
const shouldBundle = shouldWriteBrowserBundle(resolvedBundleFormat, {
|
|
232
|
+
attachmentCount: attachments.length,
|
|
233
|
+
bundleRequested,
|
|
234
|
+
textSourceCount: textBundleSources.length,
|
|
235
|
+
textPlanShouldBundle: selectedPlan.shouldBundle,
|
|
236
|
+
});
|
|
237
|
+
const composerText = (!shouldBundle && selectedPlan.inlineBlock
|
|
128
238
|
? [...baseComposerSections, selectedPlan.inlineBlock]
|
|
129
239
|
: baseComposerSections)
|
|
130
240
|
.filter(Boolean)
|
|
131
241
|
.join("\n\n")
|
|
132
242
|
.trim();
|
|
133
|
-
const attachments = [...selectedPlan.attachments, ...mediaAttachments];
|
|
134
|
-
const shouldBundle = selectedPlan.shouldBundle;
|
|
135
243
|
let bundleText = null;
|
|
136
244
|
let bundled = null;
|
|
137
245
|
if (shouldBundle) {
|
|
138
|
-
const writtenBundle = await writeBrowserBundle(sections,
|
|
246
|
+
const writtenBundle = await writeBrowserBundle(sections, resolvedBundleFormat === "zip" ? allBundleSources : textBundleSources, resolvedBundleFormat);
|
|
139
247
|
bundleText = writtenBundle.tokenEstimateText;
|
|
140
248
|
attachments.length = 0;
|
|
141
249
|
attachments.push(writtenBundle.attachment);
|
|
142
|
-
|
|
250
|
+
if (resolvedBundleFormat === "text") {
|
|
251
|
+
attachments.push(...rawUploadAttachments);
|
|
252
|
+
}
|
|
143
253
|
bundled = writtenBundle.metadata;
|
|
144
254
|
}
|
|
145
|
-
|
|
255
|
+
assertAttachmentCount(attachments, resolvedBundleFormat);
|
|
256
|
+
const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
|
|
146
257
|
const modelConfig = isKnownModel(runOptions.model)
|
|
147
258
|
? MODEL_CONFIGS[runOptions.model]
|
|
148
259
|
: MODEL_CONFIGS["gpt-5.1"];
|
|
@@ -167,15 +278,27 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
167
278
|
let fallback = null;
|
|
168
279
|
if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
|
|
169
280
|
const fallbackComposerText = baseComposerSections.join("\n\n").trim();
|
|
170
|
-
const fallbackAttachments = [...uploadPlan.attachments, ...
|
|
281
|
+
const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
|
|
171
282
|
let fallbackBundled = null;
|
|
172
|
-
|
|
173
|
-
|
|
283
|
+
const fallbackBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
|
|
284
|
+
hasRawUploadFiles: rawUploadAttachments.length > 0,
|
|
285
|
+
});
|
|
286
|
+
const fallbackShouldBundle = shouldWriteBrowserBundle(fallbackBundleFormat, {
|
|
287
|
+
attachmentCount: fallbackAttachments.length,
|
|
288
|
+
bundleRequested,
|
|
289
|
+
textSourceCount: textBundleSources.length,
|
|
290
|
+
textPlanShouldBundle: uploadPlan.shouldBundle,
|
|
291
|
+
});
|
|
292
|
+
if (fallbackShouldBundle) {
|
|
293
|
+
const writtenBundle = await writeBrowserBundle(sections, fallbackBundleFormat === "zip" ? allBundleSources : textBundleSources, fallbackBundleFormat);
|
|
174
294
|
fallbackAttachments.length = 0;
|
|
175
295
|
fallbackAttachments.push(writtenBundle.attachment);
|
|
176
|
-
|
|
296
|
+
if (fallbackBundleFormat === "text") {
|
|
297
|
+
fallbackAttachments.push(...rawUploadAttachments);
|
|
298
|
+
}
|
|
177
299
|
fallbackBundled = writtenBundle.metadata;
|
|
178
300
|
}
|
|
301
|
+
assertAttachmentCount(fallbackAttachments, fallbackBundleFormat);
|
|
179
302
|
fallback = {
|
|
180
303
|
composerText: fallbackComposerText,
|
|
181
304
|
attachments: fallbackAttachments,
|
|
@@ -190,7 +313,13 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
190
313
|
inlineFileCount,
|
|
191
314
|
tokenEstimateIncludesInlineFiles,
|
|
192
315
|
attachmentsPolicy,
|
|
193
|
-
attachmentMode:
|
|
316
|
+
attachmentMode: shouldBundle
|
|
317
|
+
? "bundle"
|
|
318
|
+
: attachments.length > 0
|
|
319
|
+
? "upload"
|
|
320
|
+
: selectedPlan.mode === "bundle"
|
|
321
|
+
? "inline"
|
|
322
|
+
: selectedPlan.mode,
|
|
194
323
|
fallback,
|
|
195
324
|
bundled,
|
|
196
325
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
|
|
3
4
|
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
|
|
4
5
|
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
5
6
|
import { parseDuration } from "../duration.js";
|
|
@@ -153,7 +154,7 @@ export async function buildBrowserConfig(options) {
|
|
|
153
154
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
154
155
|
remoteChrome,
|
|
155
156
|
browserTabRef: options.browserTab ?? undefined,
|
|
156
|
-
thinkingTime: options.browserThinkingTime,
|
|
157
|
+
thinkingTime: normalizeThinkingTimeLevel(options.browserThinkingTime) ?? undefined,
|
|
157
158
|
researchMode: options.browserResearch === "deep" ? "deep" : "off",
|
|
158
159
|
archiveConversations: options.browserArchive,
|
|
159
160
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CHATGPT_URL } from "../browser/constants.js";
|
|
2
2
|
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
3
|
+
import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
|
|
3
4
|
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
4
5
|
const browser = config.browser;
|
|
5
6
|
if (!browser)
|
|
@@ -89,7 +90,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
89
90
|
options.browserModelStrategy = browser.modelStrategy;
|
|
90
91
|
}
|
|
91
92
|
if (isUnset("browserThinkingTime") && browser.thinkingTime !== undefined) {
|
|
92
|
-
options.browserThinkingTime = browser.thinkingTime;
|
|
93
|
+
options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
|
|
93
94
|
}
|
|
94
95
|
if (isUnset("browserResearch") && browser.researchMode !== undefined) {
|
|
95
96
|
options.browserResearch = browser.researchMode;
|