@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.
Files changed (47) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/modelSelection.js +53 -15
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +75 -18
  6. package/dist/src/browser/actions/thinkingTime.js +23 -8
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +41 -7
  9. package/dist/src/browser/manualLoginProfile.js +54 -0
  10. package/dist/src/browser/projectSourcesRunner.js +16 -5
  11. package/dist/src/browser/prompt.js +56 -37
  12. package/dist/src/browser/sessionRunner.js +72 -1
  13. package/dist/src/browser/utils.js +1 -47
  14. package/dist/src/browser/zipBundle.js +152 -0
  15. package/dist/src/cli/browserConfig.js +13 -11
  16. package/dist/src/cli/browserDefaults.js +2 -1
  17. package/dist/src/cli/docsCheck.js +186 -0
  18. package/dist/src/cli/engine.js +11 -4
  19. package/dist/src/cli/options.js +12 -6
  20. package/dist/src/cli/perfTrace.js +242 -0
  21. package/dist/src/cli/promptRequirement.js +2 -0
  22. package/dist/src/cli/providerDoctor.js +85 -0
  23. package/dist/src/cli/runOptions.js +46 -16
  24. package/dist/src/cli/sessionDisplay.js +39 -4
  25. package/dist/src/cli/sessionLifecycle.js +38 -0
  26. package/dist/src/cli/sessionRunner.js +228 -3
  27. package/dist/src/cli/sessionTable.js +2 -1
  28. package/dist/src/duration.js +47 -0
  29. package/dist/src/mcp/tools/consult.js +19 -3
  30. package/dist/src/mcp/types.js +1 -0
  31. package/dist/src/mcp/utils.js +4 -1
  32. package/dist/src/oracle/baseUrl.js +17 -0
  33. package/dist/src/oracle/client.js +1 -22
  34. package/dist/src/oracle/config.js +17 -4
  35. package/dist/src/oracle/gemini.js +2 -22
  36. package/dist/src/oracle/geminiModels.js +21 -0
  37. package/dist/src/oracle/modelResolver.js +7 -1
  38. package/dist/src/oracle/multiModelRunner.js +20 -2
  39. package/dist/src/oracle/providerFailures.js +204 -0
  40. package/dist/src/oracle/providerRoutePlan.js +281 -0
  41. package/dist/src/oracle/providerRouting.js +92 -0
  42. package/dist/src/oracle/run.js +157 -54
  43. package/dist/src/oracle.js +1 -0
  44. package/dist/src/remote/client.js +8 -0
  45. package/dist/src/remote/server.js +26 -0
  46. package/dist/src/sessionManager.js +5 -1
  47. 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
- : path.join(os.homedir(), ".oracle", "browser-profile");
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 deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
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
- throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
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, { cwd });
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 bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
92
- const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
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 = { originalCount: sections.length, bundlePath };
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 bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
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 = { originalCount: sections.length, bundlePath };
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 function parseDuration(input, fallback) {
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, normalizeChatgptUrl, parseDuration, } from "../browserMode.js";
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", "GPT-5.5 Pro"],
17
+ ["gpt-5.5-pro", "Pro"],
16
18
  ["gpt-5.5", "Thinking 5.5"],
17
- ["gpt-5.4-pro", "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", "GPT-5.5 Pro"],
21
- ["gpt-5.1-pro", "GPT-5.5 Pro"],
22
- ["gpt-5-pro", "GPT-5.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" || normalized === "gpt-5.1-pro" || normalized === "gpt-5.2-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 { normalizeChatgptUrl, CHATGPT_URL } from "../browserMode.js";
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)