@steipete/oracle 0.11.1 → 0.12.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 +440 -98
- package/dist/src/browser/actions/modelSelection.js +53 -15
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +41 -7
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/sessionRunner.js +72 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -11
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +5 -1
- package/package.json +3 -1
|
@@ -9,6 +9,7 @@ import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLe
|
|
|
9
9
|
import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
10
10
|
import { CHATGPT_URL } from "./constants.js";
|
|
11
11
|
import { delay } from "./utils.js";
|
|
12
|
+
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
12
13
|
import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
|
|
13
14
|
import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
|
|
14
15
|
import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
|
|
@@ -45,13 +46,18 @@ export async function runBrowserProjectSources(request) {
|
|
|
45
46
|
const manualLogin = Boolean(config.manualLogin);
|
|
46
47
|
const manualProfileDir = config.manualLoginProfileDir
|
|
47
48
|
? path.resolve(config.manualLoginProfileDir)
|
|
48
|
-
:
|
|
49
|
+
: defaultManualLoginProfileDir();
|
|
49
50
|
const userDataDir = manualLogin
|
|
50
51
|
? manualProfileDir
|
|
51
52
|
: await mkdtemp(path.join(os.tmpdir(), "oracle-project-sources-"));
|
|
53
|
+
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
52
54
|
if (manualLogin) {
|
|
53
55
|
await mkdir(userDataDir, { recursive: true });
|
|
54
56
|
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
57
|
+
await assertManualLoginProfileReadyForRun({
|
|
58
|
+
userDataDir,
|
|
59
|
+
keepBrowser: effectiveKeepBrowser,
|
|
60
|
+
});
|
|
55
61
|
}
|
|
56
62
|
else {
|
|
57
63
|
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
@@ -73,7 +79,6 @@ export async function runBrowserProjectSources(request) {
|
|
|
73
79
|
let removeDialogHandler = null;
|
|
74
80
|
let connectionClosedUnexpectedly = false;
|
|
75
81
|
let completed = false;
|
|
76
|
-
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
77
82
|
try {
|
|
78
83
|
const acquired = manualLogin
|
|
79
84
|
? await acquireManualLoginChromeForProjectSources(userDataDir, config, logger)
|
|
@@ -140,6 +145,8 @@ export async function runBrowserProjectSources(request) {
|
|
|
140
145
|
appliedCookies,
|
|
141
146
|
manualLogin,
|
|
142
147
|
timeoutMs: config.timeoutMs,
|
|
148
|
+
profileDir: userDataDir,
|
|
149
|
+
keepBrowser: effectiveKeepBrowser,
|
|
143
150
|
}));
|
|
144
151
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, projectUrl, logger));
|
|
145
152
|
await raceWithDisconnect(openProjectSourcesTab(Runtime, Input, config.inputTimeoutMs, logger));
|
|
@@ -261,12 +268,13 @@ async function applyProjectSourcesCookies({ config, network, manualLogin, logger
|
|
|
261
268
|
: "No Chrome cookies found; continuing without session reuse");
|
|
262
269
|
return cookieCount;
|
|
263
270
|
}
|
|
264
|
-
async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
|
|
271
|
+
async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, profileDir, keepBrowser, }) {
|
|
265
272
|
if (!manualLogin) {
|
|
266
273
|
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
267
274
|
return;
|
|
268
275
|
}
|
|
269
|
-
const
|
|
276
|
+
const waitMs = resolveManualLoginWaitMs(timeoutMs, Boolean(keepBrowser));
|
|
277
|
+
const deadline = Date.now() + waitMs;
|
|
270
278
|
let lastNotice = 0;
|
|
271
279
|
while (Date.now() < deadline) {
|
|
272
280
|
try {
|
|
@@ -288,7 +296,10 @@ async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, man
|
|
|
288
296
|
await delay(1000);
|
|
289
297
|
}
|
|
290
298
|
}
|
|
291
|
-
|
|
299
|
+
const setupCommand = formatManualLoginSetupCommand(profileDir ?? defaultManualLoginProfileDir());
|
|
300
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session. " +
|
|
301
|
+
`Browser mode is using Oracle's private Chrome profile at ${profileDir ?? "(default profile)"}, not your normal Chrome profile. ` +
|
|
302
|
+
`Run first-time setup, sign in there, then retry: ${setupCommand}`);
|
|
292
303
|
}
|
|
293
304
|
async function acquireManualLoginChromeForProjectSources(userDataDir, config, logger) {
|
|
294
305
|
const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
@@ -5,6 +5,7 @@ import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, format
|
|
|
5
5
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
6
6
|
import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
|
|
7
7
|
import { buildAttachmentPlan } from "./policies.js";
|
|
8
|
+
import { createStoredZip } from "./zipBundle.js";
|
|
8
9
|
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
9
10
|
const MEDIA_EXTENSIONS = new Set([
|
|
10
11
|
".mp4",
|
|
@@ -34,6 +35,49 @@ export function isMediaFile(filePath) {
|
|
|
34
35
|
const ext = path.extname(filePath).toLowerCase();
|
|
35
36
|
return MEDIA_EXTENSIONS.has(ext);
|
|
36
37
|
}
|
|
38
|
+
function formatSectionsForBundle(sections) {
|
|
39
|
+
const bundleLines = [];
|
|
40
|
+
sections.forEach((section) => {
|
|
41
|
+
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
42
|
+
bundleLines.push("");
|
|
43
|
+
});
|
|
44
|
+
return `${bundleLines
|
|
45
|
+
.join("\n")
|
|
46
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
47
|
+
.trimEnd()}\n`;
|
|
48
|
+
}
|
|
49
|
+
async function writeBrowserBundle(sections, format) {
|
|
50
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
|
|
51
|
+
const tokenEstimateText = formatSectionsForBundle(sections);
|
|
52
|
+
if (format === "zip") {
|
|
53
|
+
const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
|
|
54
|
+
const buffer = createStoredZip(sections.map((section) => ({
|
|
55
|
+
path: section.displayPath,
|
|
56
|
+
content: section.content,
|
|
57
|
+
})));
|
|
58
|
+
await fs.writeFile(bundlePath, buffer);
|
|
59
|
+
return {
|
|
60
|
+
attachment: {
|
|
61
|
+
path: bundlePath,
|
|
62
|
+
displayPath: bundlePath,
|
|
63
|
+
sizeBytes: buffer.length,
|
|
64
|
+
},
|
|
65
|
+
metadata: { originalCount: sections.length, bundlePath, format },
|
|
66
|
+
tokenEstimateText,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
|
|
70
|
+
await fs.writeFile(bundlePath, tokenEstimateText, "utf8");
|
|
71
|
+
return {
|
|
72
|
+
attachment: {
|
|
73
|
+
path: bundlePath,
|
|
74
|
+
displayPath: bundlePath,
|
|
75
|
+
sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
|
|
76
|
+
},
|
|
77
|
+
metadata: { originalCount: sections.length, bundlePath, format },
|
|
78
|
+
tokenEstimateText,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
37
81
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
38
82
|
const cwd = deps.cwd ?? process.cwd();
|
|
39
83
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
@@ -49,7 +93,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
49
93
|
sizeBytes: stats.size,
|
|
50
94
|
};
|
|
51
95
|
}));
|
|
52
|
-
const files = await readFilesFn(textFilePaths, {
|
|
96
|
+
const files = await readFilesFn(textFilePaths, {
|
|
97
|
+
cwd,
|
|
98
|
+
maxFileSizeBytes: runOptions.maxFileSizeBytes,
|
|
99
|
+
});
|
|
53
100
|
const basePrompt = (runOptions.prompt ?? "").trim();
|
|
54
101
|
const userPrompt = basePrompt;
|
|
55
102
|
const systemPrompt = runOptions.system?.trim() || "";
|
|
@@ -59,6 +106,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
59
106
|
? "never"
|
|
60
107
|
: (runOptions.browserAttachments ?? "auto");
|
|
61
108
|
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
109
|
+
const bundleFormat = runOptions.browserBundleFormat ?? "text";
|
|
62
110
|
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
63
111
|
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
64
112
|
const baseComposerSections = [];
|
|
@@ -88,26 +136,12 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
88
136
|
let bundleText = null;
|
|
89
137
|
let bundled = null;
|
|
90
138
|
if (shouldBundle) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const bundleLines = [];
|
|
94
|
-
sections.forEach((section) => {
|
|
95
|
-
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
96
|
-
bundleLines.push("");
|
|
97
|
-
});
|
|
98
|
-
bundleText = `${bundleLines
|
|
99
|
-
.join("\n")
|
|
100
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
101
|
-
.trimEnd()}\n`;
|
|
102
|
-
await fs.writeFile(bundlePath, bundleText, "utf8");
|
|
139
|
+
const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
|
|
140
|
+
bundleText = writtenBundle.tokenEstimateText;
|
|
103
141
|
attachments.length = 0;
|
|
104
|
-
attachments.push(
|
|
105
|
-
path: bundlePath,
|
|
106
|
-
displayPath: bundlePath,
|
|
107
|
-
sizeBytes: Buffer.byteLength(bundleText, "utf8"),
|
|
108
|
-
});
|
|
142
|
+
attachments.push(writtenBundle.attachment);
|
|
109
143
|
attachments.push(...mediaAttachments);
|
|
110
|
-
bundled =
|
|
144
|
+
bundled = writtenBundle.metadata;
|
|
111
145
|
}
|
|
112
146
|
const inlineFileCount = selectedPlan.inlineFileCount;
|
|
113
147
|
const modelConfig = isKnownModel(runOptions.model)
|
|
@@ -140,26 +174,11 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
140
174
|
const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
|
|
141
175
|
let fallbackBundled = null;
|
|
142
176
|
if (uploadPlan.shouldBundle) {
|
|
143
|
-
const
|
|
144
|
-
const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
|
|
145
|
-
const bundleLines = [];
|
|
146
|
-
sections.forEach((section) => {
|
|
147
|
-
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
148
|
-
bundleLines.push("");
|
|
149
|
-
});
|
|
150
|
-
const fallbackBundleText = `${bundleLines
|
|
151
|
-
.join("\n")
|
|
152
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
153
|
-
.trimEnd()}\n`;
|
|
154
|
-
await fs.writeFile(bundlePath, fallbackBundleText, "utf8");
|
|
177
|
+
const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
|
|
155
178
|
fallbackAttachments.length = 0;
|
|
156
|
-
fallbackAttachments.push(
|
|
157
|
-
path: bundlePath,
|
|
158
|
-
displayPath: bundlePath,
|
|
159
|
-
sizeBytes: Buffer.byteLength(fallbackBundleText, "utf8"),
|
|
160
|
-
});
|
|
179
|
+
fallbackAttachments.push(writtenBundle.attachment);
|
|
161
180
|
fallbackAttachments.push(...mediaAttachments);
|
|
162
|
-
fallbackBundled =
|
|
181
|
+
fallbackBundled = writtenBundle.metadata;
|
|
163
182
|
}
|
|
164
183
|
fallback = {
|
|
165
184
|
composerText: fallbackComposerText,
|
|
@@ -5,6 +5,61 @@ import { runBrowserMode } from "../browserMode.js";
|
|
|
5
5
|
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
6
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
7
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
8
|
+
const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
|
|
9
|
+
const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
|
|
10
|
+
function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
11
|
+
if (!browserConfig.desiredModel) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
requestedModel: browserConfig.desiredModel,
|
|
16
|
+
resolvedLabel: null,
|
|
17
|
+
strategy: browserConfig.modelStrategy,
|
|
18
|
+
status: "unavailable",
|
|
19
|
+
verified: false,
|
|
20
|
+
source: "config",
|
|
21
|
+
capturedAt: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function formatModelSelectionEvidence(evidence) {
|
|
25
|
+
const requested = evidence.requestedModel ?? "(none)";
|
|
26
|
+
const resolved = evidence.resolvedLabel ?? "(unavailable)";
|
|
27
|
+
const strategy = evidence.strategy ?? "(default)";
|
|
28
|
+
const verified = evidence.verified ? "yes" : "no";
|
|
29
|
+
return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
|
|
30
|
+
}
|
|
31
|
+
function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
|
|
32
|
+
const candidates = [
|
|
33
|
+
runOptions.model,
|
|
34
|
+
browserConfig.desiredModel,
|
|
35
|
+
evidence?.requestedModel,
|
|
36
|
+
evidence?.resolvedLabel,
|
|
37
|
+
];
|
|
38
|
+
return candidates.some((value) => typeof value === "string" && /\bpro\b/i.test(value));
|
|
39
|
+
}
|
|
40
|
+
export function buildBrowserRunWarningsForTest(args) {
|
|
41
|
+
return buildBrowserRunWarnings(args);
|
|
42
|
+
}
|
|
43
|
+
function buildBrowserRunWarnings(args) {
|
|
44
|
+
if (!isRequestedProBrowserRun(args.runOptions, args.browserConfig, args.modelSelection) ||
|
|
45
|
+
args.inputTokens < LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD ||
|
|
46
|
+
args.elapsedMs >= LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
code: "browser-pro-fast-large-run",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `Large browser Pro run completed quickly (${(args.elapsedMs / 1000).toFixed(0)}s for ~${args.inputTokens.toLocaleString()} input tokens); verify the stored model selection evidence before claiming Pro Extended output.`,
|
|
54
|
+
details: {
|
|
55
|
+
inputTokens: args.inputTokens,
|
|
56
|
+
elapsedMs: args.elapsedMs,
|
|
57
|
+
requestedModel: args.modelSelection?.requestedModel ?? args.browserConfig.desiredModel,
|
|
58
|
+
resolvedLabel: args.modelSelection?.resolvedLabel ?? null,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
8
63
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
9
64
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
10
65
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
@@ -37,7 +92,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
37
92
|
if (typeof message !== "string")
|
|
38
93
|
return;
|
|
39
94
|
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
40
|
-
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance/i.test(message);
|
|
95
|
+
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
41
96
|
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
42
97
|
return;
|
|
43
98
|
log(message);
|
|
@@ -84,6 +139,20 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
84
139
|
const message = error instanceof Error ? error.message : "Browser automation failed.";
|
|
85
140
|
throw new BrowserAutomationError(message, { stage: "execute-browser" }, error);
|
|
86
141
|
}
|
|
142
|
+
const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
|
|
143
|
+
if (modelSelection) {
|
|
144
|
+
log(formatModelSelectionEvidence(modelSelection));
|
|
145
|
+
}
|
|
146
|
+
const warnings = buildBrowserRunWarnings({
|
|
147
|
+
runOptions,
|
|
148
|
+
browserConfig,
|
|
149
|
+
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
150
|
+
elapsedMs: browserResult.tookMs,
|
|
151
|
+
modelSelection,
|
|
152
|
+
});
|
|
153
|
+
for (const warning of warnings) {
|
|
154
|
+
log(chalk.yellow(`[browser] ${warning.message}`));
|
|
155
|
+
}
|
|
87
156
|
if (!runOptions.silent) {
|
|
88
157
|
log(chalk.bold("Answer:"));
|
|
89
158
|
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim("(no text output)"));
|
|
@@ -148,6 +217,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
148
217
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
149
218
|
},
|
|
150
219
|
archive: browserResult.archive,
|
|
220
|
+
modelSelection,
|
|
221
|
+
warnings,
|
|
151
222
|
answerText,
|
|
152
223
|
artifacts: savedArtifacts,
|
|
153
224
|
};
|
|
@@ -1,50 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
if (!input) {
|
|
3
|
-
return fallback;
|
|
4
|
-
}
|
|
5
|
-
const trimmed = input.trim();
|
|
6
|
-
if (!trimmed) {
|
|
7
|
-
return fallback;
|
|
8
|
-
}
|
|
9
|
-
const lowercase = trimmed.toLowerCase();
|
|
10
|
-
if (/^[0-9]+$/.test(lowercase)) {
|
|
11
|
-
return Number(lowercase);
|
|
12
|
-
}
|
|
13
|
-
const normalized = lowercase.replace(/\s+/g, "");
|
|
14
|
-
const singleMatch = /^([0-9]+)(ms|s|m|h)$/i.exec(normalized);
|
|
15
|
-
if (singleMatch && singleMatch[0].length === normalized.length) {
|
|
16
|
-
const value = Number(singleMatch[1]);
|
|
17
|
-
return convertUnit(value, singleMatch[2]);
|
|
18
|
-
}
|
|
19
|
-
const multiDuration = /([0-9]+)(ms|h|m|s)/g;
|
|
20
|
-
let total = 0;
|
|
21
|
-
let lastIndex = 0;
|
|
22
|
-
let match = multiDuration.exec(normalized);
|
|
23
|
-
while (match !== null) {
|
|
24
|
-
total += convertUnit(Number(match[1]), match[2]);
|
|
25
|
-
lastIndex = multiDuration.lastIndex;
|
|
26
|
-
match = multiDuration.exec(normalized);
|
|
27
|
-
}
|
|
28
|
-
if (total > 0 && lastIndex === normalized.length) {
|
|
29
|
-
return total;
|
|
30
|
-
}
|
|
31
|
-
return fallback;
|
|
32
|
-
}
|
|
33
|
-
function convertUnit(value, unitRaw) {
|
|
34
|
-
const unit = unitRaw?.toLowerCase();
|
|
35
|
-
switch (unit) {
|
|
36
|
-
case "ms":
|
|
37
|
-
return value;
|
|
38
|
-
case "s":
|
|
39
|
-
return value * 1000;
|
|
40
|
-
case "m":
|
|
41
|
-
return value * 60_000;
|
|
42
|
-
case "h":
|
|
43
|
-
return value * 3_600_000;
|
|
44
|
-
default:
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
export { parseDuration } from "../duration.js";
|
|
48
2
|
export function delay(ms) {
|
|
49
3
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
4
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const ZIP_UTF8_FLAG = 0x0800;
|
|
2
|
+
const ZIP_STORE_METHOD = 0;
|
|
3
|
+
const ZIP_VERSION_NEEDED = 20;
|
|
4
|
+
const ZIP_DOS_TIME = 0x0000;
|
|
5
|
+
const ZIP_DOS_DATE = 0x0021;
|
|
6
|
+
const CRC32_TABLE = (() => {
|
|
7
|
+
const table = new Uint32Array(256);
|
|
8
|
+
for (let i = 0; i < table.length; i += 1) {
|
|
9
|
+
let value = i;
|
|
10
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
11
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
12
|
+
}
|
|
13
|
+
table[i] = value >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return table;
|
|
16
|
+
})();
|
|
17
|
+
function crc32(buffer) {
|
|
18
|
+
let crc = 0xffffffff;
|
|
19
|
+
for (const byte of buffer) {
|
|
20
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
21
|
+
}
|
|
22
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
function normalizeZipPath(inputPath, fallback) {
|
|
25
|
+
const normalized = inputPath
|
|
26
|
+
.replace(/\\/g, "/")
|
|
27
|
+
.replace(/^[a-zA-Z]:\//, "")
|
|
28
|
+
.replace(/^\/+/, "")
|
|
29
|
+
.split("/")
|
|
30
|
+
.filter((segment) => segment && segment !== "." && segment !== "..")
|
|
31
|
+
.join("/");
|
|
32
|
+
return normalized || fallback;
|
|
33
|
+
}
|
|
34
|
+
function uniqueZipPath(inputPath, index, seen) {
|
|
35
|
+
const normalized = normalizeZipPath(inputPath, `file-${index + 1}.txt`);
|
|
36
|
+
const extIndex = normalized.lastIndexOf(".");
|
|
37
|
+
const base = extIndex > 0 ? normalized.slice(0, extIndex) : normalized;
|
|
38
|
+
const ext = extIndex > 0 ? normalized.slice(extIndex) : "";
|
|
39
|
+
let candidate = normalized;
|
|
40
|
+
let suffix = 2;
|
|
41
|
+
while (seen.has(candidate)) {
|
|
42
|
+
candidate = `${base}-${suffix}${ext}`;
|
|
43
|
+
suffix += 1;
|
|
44
|
+
}
|
|
45
|
+
seen.add(candidate);
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
function assertZip32(value, label) {
|
|
49
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
50
|
+
throw new Error(`${label} exceeds ZIP32 limits.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function assertZip16(value, label) {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff) {
|
|
55
|
+
throw new Error(`${label} exceeds ZIP16 limits.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function localFileHeader(entry) {
|
|
59
|
+
const header = Buffer.alloc(30);
|
|
60
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
61
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
|
|
62
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 6);
|
|
63
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 8);
|
|
64
|
+
header.writeUInt16LE(ZIP_DOS_TIME, 10);
|
|
65
|
+
header.writeUInt16LE(ZIP_DOS_DATE, 12);
|
|
66
|
+
header.writeUInt32LE(entry.crc32, 14);
|
|
67
|
+
header.writeUInt32LE(entry.content.length, 18);
|
|
68
|
+
header.writeUInt32LE(entry.content.length, 22);
|
|
69
|
+
header.writeUInt16LE(entry.name.length, 26);
|
|
70
|
+
header.writeUInt16LE(0, 28);
|
|
71
|
+
return header;
|
|
72
|
+
}
|
|
73
|
+
function centralDirectoryHeader(entry) {
|
|
74
|
+
const header = Buffer.alloc(46);
|
|
75
|
+
header.writeUInt32LE(0x02014b50, 0);
|
|
76
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
|
|
77
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 6);
|
|
78
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 8);
|
|
79
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 10);
|
|
80
|
+
header.writeUInt16LE(ZIP_DOS_TIME, 12);
|
|
81
|
+
header.writeUInt16LE(ZIP_DOS_DATE, 14);
|
|
82
|
+
header.writeUInt32LE(entry.crc32, 16);
|
|
83
|
+
header.writeUInt32LE(entry.content.length, 20);
|
|
84
|
+
header.writeUInt32LE(entry.content.length, 24);
|
|
85
|
+
header.writeUInt16LE(entry.name.length, 28);
|
|
86
|
+
header.writeUInt16LE(0, 30);
|
|
87
|
+
header.writeUInt16LE(0, 32);
|
|
88
|
+
header.writeUInt16LE(0, 34);
|
|
89
|
+
header.writeUInt16LE(0, 36);
|
|
90
|
+
header.writeUInt32LE(0, 38);
|
|
91
|
+
header.writeUInt32LE(entry.localHeaderOffset, 42);
|
|
92
|
+
return header;
|
|
93
|
+
}
|
|
94
|
+
function endOfCentralDirectory(entryCount, centralSize, centralOffset) {
|
|
95
|
+
const footer = Buffer.alloc(22);
|
|
96
|
+
footer.writeUInt32LE(0x06054b50, 0);
|
|
97
|
+
footer.writeUInt16LE(0, 4);
|
|
98
|
+
footer.writeUInt16LE(0, 6);
|
|
99
|
+
footer.writeUInt16LE(entryCount, 8);
|
|
100
|
+
footer.writeUInt16LE(entryCount, 10);
|
|
101
|
+
footer.writeUInt32LE(centralSize, 12);
|
|
102
|
+
footer.writeUInt32LE(centralOffset, 16);
|
|
103
|
+
footer.writeUInt16LE(0, 20);
|
|
104
|
+
return footer;
|
|
105
|
+
}
|
|
106
|
+
export function createStoredZip(entries) {
|
|
107
|
+
if (entries.length > 0xffff) {
|
|
108
|
+
throw new Error("Too many files for a ZIP32 browser bundle.");
|
|
109
|
+
}
|
|
110
|
+
assertZip16(entries.length, "ZIP entry count");
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const prepared = [];
|
|
113
|
+
const localParts = [];
|
|
114
|
+
let offset = 0;
|
|
115
|
+
entries.forEach((entry, index) => {
|
|
116
|
+
const name = Buffer.from(uniqueZipPath(entry.path, index, seen), "utf8");
|
|
117
|
+
const content = Buffer.isBuffer(entry.content)
|
|
118
|
+
? entry.content
|
|
119
|
+
: Buffer.from(entry.content, "utf8");
|
|
120
|
+
assertZip16(name.length, "ZIP file name");
|
|
121
|
+
assertZip32(content.length, "ZIP entry size");
|
|
122
|
+
assertZip32(offset, "ZIP local header offset");
|
|
123
|
+
const preparedEntry = {
|
|
124
|
+
name,
|
|
125
|
+
content,
|
|
126
|
+
crc32: crc32(content),
|
|
127
|
+
localHeaderOffset: offset,
|
|
128
|
+
};
|
|
129
|
+
prepared.push(preparedEntry);
|
|
130
|
+
const header = localFileHeader(preparedEntry);
|
|
131
|
+
localParts.push(header, name, content);
|
|
132
|
+
offset += header.length + name.length + content.length;
|
|
133
|
+
assertZip32(offset, "ZIP local data size");
|
|
134
|
+
});
|
|
135
|
+
const centralOffset = offset;
|
|
136
|
+
const centralParts = [];
|
|
137
|
+
for (const entry of prepared) {
|
|
138
|
+
const header = centralDirectoryHeader(entry);
|
|
139
|
+
centralParts.push(header, entry.name);
|
|
140
|
+
offset += header.length + entry.name.length;
|
|
141
|
+
assertZip32(offset, "ZIP central directory size");
|
|
142
|
+
}
|
|
143
|
+
const centralSize = offset - centralOffset;
|
|
144
|
+
assertZip32(centralOffset, "ZIP central directory offset");
|
|
145
|
+
assertZip32(centralSize, "ZIP central directory size");
|
|
146
|
+
const footer = endOfCentralDirectory(prepared.length, centralSize, centralOffset);
|
|
147
|
+
return Buffer.concat([...localParts, ...centralParts, footer]);
|
|
148
|
+
}
|
|
149
|
+
export const __test__ = {
|
|
150
|
+
crc32,
|
|
151
|
+
normalizeZipPath,
|
|
152
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET
|
|
3
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
|
|
4
|
+
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
5
|
+
import { parseDuration } from "../duration.js";
|
|
4
6
|
import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
|
|
5
7
|
import { getOracleHomeDir } from "../oracleHome.js";
|
|
6
8
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
@@ -12,14 +14,14 @@ const DEFAULT_CHROME_PROFILE = "Default";
|
|
|
12
14
|
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
13
15
|
const BROWSER_MODEL_LABELS = [
|
|
14
16
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
15
|
-
["gpt-5.5-pro", "
|
|
17
|
+
["gpt-5.5-pro", "Pro"],
|
|
16
18
|
["gpt-5.5", "Thinking 5.5"],
|
|
17
|
-
["gpt-5.4-pro", "
|
|
19
|
+
["gpt-5.4-pro", "Pro"],
|
|
18
20
|
["gpt-5.2-thinking", "GPT-5.2 Thinking"],
|
|
19
21
|
["gpt-5.2-instant", "GPT-5.2 Instant"],
|
|
20
|
-
["gpt-5.2-pro", "
|
|
21
|
-
["gpt-5.1-pro", "
|
|
22
|
-
["gpt-5-pro", "
|
|
22
|
+
["gpt-5.2-pro", "Pro"],
|
|
23
|
+
["gpt-5.1-pro", "Pro"],
|
|
24
|
+
["gpt-5-pro", "Pro"],
|
|
23
25
|
// Base models last (least specific)
|
|
24
26
|
["gpt-5.4", "Thinking 5.4"],
|
|
25
27
|
["gpt-5.2", "GPT-5.2"], // Selects "Auto" in ChatGPT UI
|
|
@@ -32,14 +34,14 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
32
34
|
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
33
35
|
return model;
|
|
34
36
|
}
|
|
35
|
-
if (normalized === "gpt-5.5-pro" ||
|
|
36
|
-
normalized === "gpt-5.5" ||
|
|
37
|
-
normalized === "gpt-5.4-pro" ||
|
|
38
|
-
normalized === "gpt-5.4") {
|
|
37
|
+
if (normalized === "gpt-5.5-pro" || normalized === "gpt-5.5" || normalized === "gpt-5.4") {
|
|
39
38
|
return normalized;
|
|
40
39
|
}
|
|
41
40
|
// Pro variants: resolve to the latest Pro model in ChatGPT.
|
|
42
|
-
if (normalized === "gpt-5-pro" ||
|
|
41
|
+
if (normalized === "gpt-5-pro" ||
|
|
42
|
+
normalized === "gpt-5.1-pro" ||
|
|
43
|
+
normalized === "gpt-5.2-pro" ||
|
|
44
|
+
normalized === "gpt-5.4-pro") {
|
|
43
45
|
return "gpt-5.5-pro";
|
|
44
46
|
}
|
|
45
47
|
// Explicit model variants: keep as-is (they have their own browser labels)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CHATGPT_URL } from "../browser/constants.js";
|
|
2
|
+
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
2
3
|
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
3
4
|
const browser = config.browser;
|
|
4
5
|
if (!browser)
|