@steipete/oracle 0.13.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 +22 -11
- package/dist/bin/oracle-cli.js +71 -71
- package/dist/src/browser/actions/assistantResponse.js +93 -10
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +357 -40
- package/dist/src/browser/actions/navigation.js +294 -65
- package/dist/src/browser/actions/promptComposer.js +50 -4
- package/dist/src/browser/actions/thinkingTime.js +517 -105
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +256 -27
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +2 -0
- package/dist/src/browser/index.js +404 -41
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +16 -3
- package/dist/src/browser/prompt.js +161 -36
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/browserConfig.js +6 -2
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +2 -12
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +132 -0
- package/dist/src/mcp/tools/consult.js +270 -175
- package/dist/src/mcp/types.js +13 -2
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/thinkingTime.js +40 -0
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +47 -58
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/bin/oracle.js +0 -569
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
|
|
1
|
+
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
|
|
2
2
|
export { ensureModelSelection } from "./actions/modelSelection.js";
|
|
3
3
|
export { submitPrompt, clearPromptComposer } from "./actions/promptComposer.js";
|
|
4
4
|
export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, buildUserTurnAttachmentExpressionForTest, } from "./actions/attachments.js";
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatFileSections } from "../oracle/markdown.js";
|
|
2
2
|
export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, maxAttachments = 10, }) {
|
|
3
3
|
if (inlineFiles) {
|
|
4
|
-
const
|
|
5
|
-
sections.forEach((section) => {
|
|
6
|
-
inlineLines.push(formatFileSection(section.displayPath, section.content).trimEnd(), "");
|
|
7
|
-
});
|
|
8
|
-
const inlineBlock = inlineLines.join("\n").trim();
|
|
4
|
+
const inlineBlock = formatFileSections(sections);
|
|
9
5
|
return {
|
|
10
6
|
mode: "inline",
|
|
11
7
|
inlineBlock,
|
|
@@ -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;
|
|
@@ -347,11 +348,20 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
|
|
|
347
348
|
if (!port) {
|
|
348
349
|
const discovered = await findRunningChromeDebugTargetForProfile(userDataDir);
|
|
349
350
|
if (!discovered) {
|
|
351
|
+
if (pid) {
|
|
352
|
+
logger(`No reachable Chrome DevTools target found for ${userDataDir}; clearing stale profile state before launching new Chrome.`);
|
|
353
|
+
await cleanupStaleProfileState(userDataDir, logger, {
|
|
354
|
+
lockRemovalMode: "if_oracle_pid_dead",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
350
357
|
return null;
|
|
351
358
|
}
|
|
352
|
-
const probe = await verifyDevToolsReachable({ port: discovered.port });
|
|
359
|
+
const probe = await (options.probe ?? verifyDevToolsReachable)({ port: discovered.port });
|
|
353
360
|
if (!probe.ok) {
|
|
354
361
|
logger(`Discovered Chrome for ${userDataDir} on port ${discovered.port} but it was unreachable (${probe.error}); launching new Chrome.`);
|
|
362
|
+
await cleanupStaleProfileState(userDataDir, logger, {
|
|
363
|
+
lockRemovalMode: "if_oracle_pid_dead",
|
|
364
|
+
});
|
|
355
365
|
return null;
|
|
356
366
|
}
|
|
357
367
|
await writeDevToolsActivePort(userDataDir, discovered.port);
|
|
@@ -361,7 +371,7 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
|
|
|
361
371
|
logger(`Discovered running Chrome for ${userDataDir}; reusing (DevTools port ${port}, pid ${pid})`);
|
|
362
372
|
return { port, pid, kill: async () => { }, process: undefined };
|
|
363
373
|
}
|
|
364
|
-
const probe = await verifyDevToolsReachable({ port });
|
|
374
|
+
const probe = await (options.probe ?? verifyDevToolsReachable)({ port });
|
|
365
375
|
if (!probe.ok) {
|
|
366
376
|
logger(`Recorded Chrome DevTools port ${port} is stale (${probe.error}); launching new Chrome.`);
|
|
367
377
|
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
|
|
@@ -375,3 +385,6 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
|
|
|
375
385
|
process: undefined,
|
|
376
386
|
};
|
|
377
387
|
}
|
|
388
|
+
export async function maybeReuseProjectSourcesChromeForTest(userDataDir, logger, options = {}) {
|
|
389
|
+
return maybeReuseProjectSourcesChrome(userDataDir, logger, options);
|
|
390
|
+
}
|
|
@@ -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,
|
|
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,38 +33,104 @@ 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
|
}
|
|
38
|
-
function
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
export function isRawUploadFile(filePath) {
|
|
82
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
83
|
+
return MEDIA_EXTENSIONS.has(ext) || ARCHIVE_EXTENSIONS.has(ext);
|
|
84
|
+
}
|
|
85
|
+
function formatSectionsForBundle(sections, options = {}) {
|
|
86
|
+
return formatFileSections(sections, {
|
|
87
|
+
lineNumbers: options.lineNumbers ?? true,
|
|
88
|
+
trailingNewline: true,
|
|
43
89
|
});
|
|
44
|
-
return `${bundleLines
|
|
45
|
-
.join("\n")
|
|
46
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
47
|
-
.trimEnd()}\n`;
|
|
48
90
|
}
|
|
49
|
-
|
|
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) {
|
|
50
111
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
|
|
51
|
-
const tokenEstimateText = formatSectionsForBundle(sections
|
|
112
|
+
const tokenEstimateText = formatSectionsForBundle(sections, {
|
|
113
|
+
lineNumbers: format === "text",
|
|
114
|
+
});
|
|
52
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
|
+
}
|
|
53
120
|
const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
|
|
54
|
-
const buffer = createStoredZip(
|
|
55
|
-
path:
|
|
56
|
-
content:
|
|
57
|
-
})));
|
|
121
|
+
const buffer = createStoredZip(await Promise.all(sources.map(async (source) => ({
|
|
122
|
+
path: source.displayPath,
|
|
123
|
+
content: await fs.readFile(source.absolutePath),
|
|
124
|
+
}))));
|
|
58
125
|
await fs.writeFile(bundlePath, buffer);
|
|
59
126
|
return {
|
|
60
127
|
attachment: {
|
|
61
128
|
path: bundlePath,
|
|
62
129
|
displayPath: bundlePath,
|
|
63
130
|
sizeBytes: buffer.length,
|
|
131
|
+
generatedBundle: true,
|
|
64
132
|
},
|
|
65
|
-
metadata: { originalCount:
|
|
133
|
+
metadata: { originalCount: sources.length, bundlePath, format },
|
|
66
134
|
tokenEstimateText,
|
|
67
135
|
};
|
|
68
136
|
}
|
|
@@ -73,6 +141,7 @@ async function writeBrowserBundle(sections, format) {
|
|
|
73
141
|
path: bundlePath,
|
|
74
142
|
displayPath: bundlePath,
|
|
75
143
|
sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
|
|
144
|
+
generatedBundle: true,
|
|
76
145
|
},
|
|
77
146
|
metadata: { originalCount: sections.length, bundlePath, format },
|
|
78
147
|
tokenEstimateText,
|
|
@@ -82,11 +151,27 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
82
151
|
const cwd = deps.cwd ?? process.cwd();
|
|
83
152
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
84
153
|
const allFilePaths = runOptions.file ?? [];
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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 }) => {
|
|
88
167
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
89
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
|
+
}
|
|
90
175
|
return {
|
|
91
176
|
path: resolvedPath,
|
|
92
177
|
displayPath: path.relative(cwd, resolvedPath) || path.basename(resolvedPath),
|
|
@@ -106,7 +191,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
106
191
|
? "never"
|
|
107
192
|
: (runOptions.browserAttachments ?? "auto");
|
|
108
193
|
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
109
|
-
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
|
+
}
|
|
110
198
|
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
111
199
|
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
112
200
|
const baseComposerSections = [];
|
|
@@ -125,25 +213,47 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
125
213
|
: inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
|
|
126
214
|
? inlinePlan
|
|
127
215
|
: uploadPlan;
|
|
128
|
-
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
|
|
129
238
|
? [...baseComposerSections, selectedPlan.inlineBlock]
|
|
130
239
|
: baseComposerSections)
|
|
131
240
|
.filter(Boolean)
|
|
132
241
|
.join("\n\n")
|
|
133
242
|
.trim();
|
|
134
|
-
const attachments = [...selectedPlan.attachments, ...mediaAttachments];
|
|
135
|
-
const shouldBundle = selectedPlan.shouldBundle;
|
|
136
243
|
let bundleText = null;
|
|
137
244
|
let bundled = null;
|
|
138
245
|
if (shouldBundle) {
|
|
139
|
-
const writtenBundle = await writeBrowserBundle(sections,
|
|
246
|
+
const writtenBundle = await writeBrowserBundle(sections, resolvedBundleFormat === "zip" ? allBundleSources : textBundleSources, resolvedBundleFormat);
|
|
140
247
|
bundleText = writtenBundle.tokenEstimateText;
|
|
141
248
|
attachments.length = 0;
|
|
142
249
|
attachments.push(writtenBundle.attachment);
|
|
143
|
-
|
|
250
|
+
if (resolvedBundleFormat === "text") {
|
|
251
|
+
attachments.push(...rawUploadAttachments);
|
|
252
|
+
}
|
|
144
253
|
bundled = writtenBundle.metadata;
|
|
145
254
|
}
|
|
146
|
-
|
|
255
|
+
assertAttachmentCount(attachments, resolvedBundleFormat);
|
|
256
|
+
const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
|
|
147
257
|
const modelConfig = isKnownModel(runOptions.model)
|
|
148
258
|
? MODEL_CONFIGS[runOptions.model]
|
|
149
259
|
: MODEL_CONFIGS["gpt-5.1"];
|
|
@@ -161,25 +271,34 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
161
271
|
let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0 ? tokenizerMessages : [{ role: "user", content: "" }], TOKENIZER_OPTIONS);
|
|
162
272
|
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
|
|
163
273
|
if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
|
|
164
|
-
const attachmentText = bundleText ??
|
|
165
|
-
sections
|
|
166
|
-
.map((section) => formatFileSection(section.displayPath, section.content).trimEnd())
|
|
167
|
-
.join("\n\n");
|
|
274
|
+
const attachmentText = bundleText ?? formatFileSections(sections, { lineNumbers: false });
|
|
168
275
|
const attachmentTokens = tokenizer([{ role: "user", content: attachmentText }], TOKENIZER_OPTIONS);
|
|
169
276
|
estimatedInputTokens += attachmentTokens;
|
|
170
277
|
}
|
|
171
278
|
let fallback = null;
|
|
172
279
|
if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
|
|
173
280
|
const fallbackComposerText = baseComposerSections.join("\n\n").trim();
|
|
174
|
-
const fallbackAttachments = [...uploadPlan.attachments, ...
|
|
281
|
+
const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
|
|
175
282
|
let fallbackBundled = null;
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
294
|
fallbackAttachments.length = 0;
|
|
179
295
|
fallbackAttachments.push(writtenBundle.attachment);
|
|
180
|
-
|
|
296
|
+
if (fallbackBundleFormat === "text") {
|
|
297
|
+
fallbackAttachments.push(...rawUploadAttachments);
|
|
298
|
+
}
|
|
181
299
|
fallbackBundled = writtenBundle.metadata;
|
|
182
300
|
}
|
|
301
|
+
assertAttachmentCount(fallbackAttachments, fallbackBundleFormat);
|
|
183
302
|
fallback = {
|
|
184
303
|
composerText: fallbackComposerText,
|
|
185
304
|
attachments: fallbackAttachments,
|
|
@@ -194,7 +313,13 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
194
313
|
inlineFileCount,
|
|
195
314
|
tokenEstimateIncludesInlineFiles,
|
|
196
315
|
attachmentsPolicy,
|
|
197
|
-
attachmentMode:
|
|
316
|
+
attachmentMode: shouldBundle
|
|
317
|
+
? "bundle"
|
|
318
|
+
: attachments.length > 0
|
|
319
|
+
? "upload"
|
|
320
|
+
: selectedPlan.mode === "bundle"
|
|
321
|
+
? "inline"
|
|
322
|
+
: selectedPlan.mode,
|
|
198
323
|
fallback,
|
|
199
324
|
bundled,
|
|
200
325
|
};
|
|
@@ -94,7 +94,9 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
94
94
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
95
95
|
if (config?.researchMode === "deep") {
|
|
96
96
|
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
97
|
-
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client
|
|
97
|
+
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
98
|
+
requireScopedTargetOwner: true,
|
|
99
|
+
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
98
100
|
await connection.close().catch(() => undefined);
|
|
99
101
|
return {
|
|
100
102
|
answerText: researchResult.text,
|
|
@@ -239,7 +241,9 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
239
241
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
240
242
|
if (resolved.researchMode === "deep") {
|
|
241
243
|
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
242
|
-
const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client
|
|
244
|
+
const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
245
|
+
requireScopedTargetOwner: true,
|
|
246
|
+
});
|
|
243
247
|
await cleanup();
|
|
244
248
|
return {
|
|
245
249
|
answerText: researchResult.text,
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
1
|
+
/**
|
|
2
|
+
* True when the URL points at a specific ChatGPT conversation (`/c/<id>`) on
|
|
3
|
+
* chatgpt.com or chat.openai.com. Rejects home, project shell, and external
|
|
4
|
+
* URLs — anything else would be unsafe to auto-reopen in a persistent
|
|
5
|
+
* signed-in browser profile.
|
|
6
|
+
*/
|
|
7
|
+
export function isRecoverableChatGptConversationUrl(candidate) {
|
|
8
|
+
const trimmed = candidate?.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
12
|
try {
|
|
13
|
-
const url = new URL(
|
|
13
|
+
const url = new URL(trimmed);
|
|
14
|
+
if (url.protocol !== "https:" || url.port) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
14
17
|
if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
|
|
15
18
|
return false;
|
|
16
19
|
}
|
|
@@ -20,3 +23,12 @@ export function hasRecoverableChatGptConversation(runtime) {
|
|
|
20
23
|
return false;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
export function hasRecoverableChatGptConversation(runtime) {
|
|
27
|
+
if (!runtime) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (runtime.conversationId?.trim()) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return isRecoverableChatGptConversationUrl(runtime.tabUrl);
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { launch } from "chrome-launcher";
|
|
2
|
+
import { defaultManualLoginProfileDir } from "./manualLoginProfile.js";
|
|
3
|
+
import { isRecoverableChatGptConversationUrl } from "./reattachability.js";
|
|
4
|
+
const DEFAULT_HYDRATION_DELAY_MS = 3_000;
|
|
5
|
+
/**
|
|
6
|
+
* Picks the URL to navigate the recovered Chrome tab to.
|
|
7
|
+
*
|
|
8
|
+
* Preference order matches `resolveSessionTabRef`: `harvest.url` (post-harvest,
|
|
9
|
+
* always a ChatGPT conversation URL when present) wins over `runtime.tabUrl`
|
|
10
|
+
* (the URL the original run last navigated to, which can be stale).
|
|
11
|
+
*
|
|
12
|
+
* Both candidates are gated by `isRecoverableChatGptConversationUrl` so a stale
|
|
13
|
+
* home / project shell URL or an unrelated external URL stored in metadata
|
|
14
|
+
* cannot navigate the persistent signed-in profile to the wrong page.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveRecoveryUrl(meta) {
|
|
17
|
+
const harvest = meta?.browser?.harvest ?? {};
|
|
18
|
+
const runtime = meta?.browser?.runtime ?? {};
|
|
19
|
+
for (const candidate of [harvest.url, runtime.tabUrl]) {
|
|
20
|
+
if (isRecoverableChatGptConversationUrl(candidate)) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function resolveProfileDir(meta) {
|
|
27
|
+
const fromMeta = meta?.browser?.config?.manualLoginProfileDir;
|
|
28
|
+
if (typeof fromMeta === "string" && fromMeta.length > 0) {
|
|
29
|
+
return fromMeta;
|
|
30
|
+
}
|
|
31
|
+
return defaultManualLoginProfileDir();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Re-open a previously-harvested ChatGPT conversation by relaunching Chrome
|
|
35
|
+
* with the session's persistent profile and navigating to the saved tab URL.
|
|
36
|
+
*
|
|
37
|
+
* Used as a fallback when `harvestChatGptTab` can find no live tab matching the
|
|
38
|
+
* stored target (common after the original CLI run exits and closes its
|
|
39
|
+
* browser). ChatGPT preserves attachments + history at the conversation URL,
|
|
40
|
+
* so harvesting against the relaunched tab returns the original message + any
|
|
41
|
+
* assistant response that completed after the original run gave up.
|
|
42
|
+
*/
|
|
43
|
+
export async function recoverConversationTab(meta, logger, options = {}) {
|
|
44
|
+
const url = resolveRecoveryUrl(meta);
|
|
45
|
+
if (!url) {
|
|
46
|
+
throw new Error("Cannot recover conversation: session metadata has no recoverable ChatGPT conversation URL " +
|
|
47
|
+
"(expected browser.harvest.url or browser.runtime.tabUrl to be a chatgpt.com/c/<id> URL).");
|
|
48
|
+
}
|
|
49
|
+
const userDataDir = resolveProfileDir(meta);
|
|
50
|
+
logger(`[browser] Recovery: relaunching Chrome with profile ${userDataDir} and navigating to ${url}`);
|
|
51
|
+
const chrome = await launch({
|
|
52
|
+
chromeFlags: [
|
|
53
|
+
"--no-first-run",
|
|
54
|
+
"--no-default-browser-check",
|
|
55
|
+
"--disable-features=AutomationControlled,TranslateUI",
|
|
56
|
+
"--disable-sync",
|
|
57
|
+
"--password-store=basic",
|
|
58
|
+
"--use-mock-keychain",
|
|
59
|
+
"--lang=en-US",
|
|
60
|
+
url,
|
|
61
|
+
],
|
|
62
|
+
userDataDir,
|
|
63
|
+
handleSIGINT: false,
|
|
64
|
+
});
|
|
65
|
+
const host = "127.0.0.1";
|
|
66
|
+
const port = chrome.port;
|
|
67
|
+
const hydrationDelayMs = options.hydrationDelayMs ?? DEFAULT_HYDRATION_DELAY_MS;
|
|
68
|
+
if (hydrationDelayMs > 0) {
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, hydrationDelayMs));
|
|
70
|
+
}
|
|
71
|
+
logger(`[browser] Recovery: Chrome listening on ${host}:${port}; tab loaded.`);
|
|
72
|
+
return { host, port, url, chrome };
|
|
73
|
+
}
|
|
@@ -105,6 +105,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
105
105
|
log(chalk.dim("Chrome automation does not stream output; this may take a minute..."));
|
|
106
106
|
}
|
|
107
107
|
const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
|
|
108
|
+
const executionBrowserConfig = runOptions.browserResumeConversationUrl
|
|
109
|
+
? { ...browserConfig, resumeConversationUrl: runOptions.browserResumeConversationUrl }
|
|
110
|
+
: browserConfig;
|
|
108
111
|
let browserResult;
|
|
109
112
|
try {
|
|
110
113
|
browserResult = await executeBrowser({
|
|
@@ -116,7 +119,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
116
119
|
attachments: promptArtifacts.fallback.attachments,
|
|
117
120
|
}
|
|
118
121
|
: undefined,
|
|
119
|
-
config:
|
|
122
|
+
config: executionBrowserConfig,
|
|
120
123
|
log: automationLogger,
|
|
121
124
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
122
125
|
verbose: runOptions.verbose,
|
|
@@ -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";
|
|
@@ -28,7 +29,10 @@ const BROWSER_MODEL_LABELS = [
|
|
|
28
29
|
["gpt-5.4", "Thinking 5.4"],
|
|
29
30
|
["gpt-5.2", "GPT-5.2"], // Selects "Auto" in ChatGPT UI
|
|
30
31
|
["gpt-5.1", "GPT-5.2"], // Legacy alias → Auto
|
|
31
|
-
["gemini-3-
|
|
32
|
+
["gemini-3.1-flash-lite", "Gemini 3.1 Flash-Lite"],
|
|
33
|
+
["gemini-3.5-flash", "Gemini 3.5 Flash"],
|
|
34
|
+
["gemini-3.1-pro", "Gemini 3.1 Pro"],
|
|
35
|
+
["gemini-3-pro", "Gemini 3.1 Pro"],
|
|
32
36
|
["gemini-3-pro-deep-think", "gemini-3-deep-think"],
|
|
33
37
|
];
|
|
34
38
|
export function normalizeChatGptModelForBrowser(model) {
|
|
@@ -150,7 +154,7 @@ export async function buildBrowserConfig(options) {
|
|
|
150
154
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
151
155
|
remoteChrome,
|
|
152
156
|
browserTabRef: options.browserTab ?? undefined,
|
|
153
|
-
thinkingTime: options.browserThinkingTime,
|
|
157
|
+
thinkingTime: normalizeThinkingTimeLevel(options.browserThinkingTime) ?? undefined,
|
|
154
158
|
researchMode: options.browserResearch === "deep" ? "deep" : "off",
|
|
155
159
|
archiveConversations: options.browserArchive,
|
|
156
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;
|