@steipete/oracle 0.5.4 → 0.6.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 +3 -3
- package/dist/bin/oracle-cli.js +8 -2
- package/dist/src/browser/actions/attachments.js +19 -19
- package/dist/src/browser/actions/modelSelection.js +22 -0
- package/dist/src/browser/actions/promptComposer.js +82 -5
- package/dist/src/browser/actions/thinkingTime.js +190 -0
- package/dist/src/browser/config.js +1 -0
- package/dist/src/browser/index.js +84 -24
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/prompt.js +63 -19
- package/dist/src/browser/sessionRunner.js +10 -6
- package/dist/src/cli/browserConfig.js +6 -2
- package/dist/src/cli/sessionRunner.js +2 -3
- package/dist/src/cli/tui/index.js +1 -0
- package/dist/src/config.js +2 -3
- package/dist/src/oracle/config.js +38 -3
- package/dist/src/oracle/errors.js +3 -3
- package/dist/src/oracle/gemini.js +4 -1
- package/dist/src/oracle/run.js +4 -7
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/sessionManager.js +10 -8
- package/dist/src/sessionStore.js +2 -2
- package/package.json +3 -7
- package/dist/.DS_Store +0 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.1 Pro (default), GPT-5.1 Codex (API-only), GPT-5.1, Gemini 3 Pro, Claude Sonnet 4.5, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; API remains the most reliable path, and `--copy` is an easy manual fallback.
|
|
14
|
+
Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.1 Pro (default alias to GPT-5.2 Pro on the API), GPT-5.1 Codex (API-only), GPT-5.1, GPT-5.2, Gemini 3 Pro, Claude Sonnet 4.5, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; API remains the most reliable path, and `--copy` is an easy manual fallback.
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
@@ -95,7 +95,7 @@ npx -y @steipete/oracle oracle-mcp
|
|
|
95
95
|
| `-p, --prompt <text>` | Required prompt. |
|
|
96
96
|
| `-f, --file <paths...>` | Attach files/dirs (globs + `!` excludes). |
|
|
97
97
|
| `-e, --engine <api\|browser>` | Choose API or browser (browser is experimental). |
|
|
98
|
-
| `-m, --model <name>` | Built-ins (`gpt-5.1-pro` default, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gemini-3-pro`, `claude-4.5-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
|
|
98
|
+
| `-m, --model <name>` | Built-ins (`gpt-5.1-pro` default, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.2`, `gpt-5.2-instant`, `gpt-5.2-pro`, `gemini-3-pro`, `claude-4.5-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
|
|
99
99
|
| `--models <list>` | Comma-separated API models (mix built-ins and OpenRouter ids) for multi-model runs. |
|
|
100
100
|
| `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
|
|
101
101
|
| `--chatgpt-url <url>` | Target a ChatGPT workspace/folder (browser). |
|
|
@@ -131,7 +131,7 @@ Advanced flags
|
|
|
131
131
|
|
|
132
132
|
| Area | Flags |
|
|
133
133
|
| --- | --- |
|
|
134
|
-
| Browser | `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
134
|
+
| Browser | `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
135
135
|
| Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |
|
|
136
136
|
|
|
137
137
|
Remote browser example
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -110,7 +110,7 @@ program
|
|
|
110
110
|
.addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
|
|
111
111
|
.addOption(new Option('--copy').hideHelp().default(false))
|
|
112
112
|
.option('-s, --slug <words>', 'Custom session slug (3-5 words).')
|
|
113
|
-
.option('-m, --model <model>', 'Model to target (gpt-5.1-pro default;
|
|
113
|
+
.option('-m, --model <model>', 'Model to target (gpt-5.1-pro default; aliases to gpt-5.2-pro on API. Also gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).', normalizeModelOption)
|
|
114
114
|
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.1-pro,gemini-3-pro").')
|
|
115
115
|
.argParser(collectModelList)
|
|
116
116
|
.default([]))
|
|
@@ -172,11 +172,15 @@ program
|
|
|
172
172
|
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
173
173
|
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
174
174
|
.addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
|
|
175
|
+
.addOption(new Option('--browser-extended-thinking', 'Select Extended thinking time for GPT-5.2 Thinking model.').hideHelp())
|
|
175
176
|
.addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
|
|
177
|
+
.addOption(new Option('--browser-attachments <mode>', 'How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.')
|
|
178
|
+
.choices(['auto', 'never', 'always'])
|
|
179
|
+
.default('auto'))
|
|
176
180
|
.addOption(new Option('--remote-chrome <host:port>', 'Connect to remote Chrome DevTools Protocol (e.g., 192.168.1.10:9222 or [2001:db8::1]:9222 for IPv6).'))
|
|
177
181
|
.addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
|
|
178
182
|
.addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
|
|
179
|
-
.addOption(new Option('--browser-inline-files', '
|
|
183
|
+
.addOption(new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false))
|
|
180
184
|
.addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
|
|
181
185
|
.option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
|
|
182
186
|
.option('--force', 'Force start a new session even if an identical prompt is already running.', false)
|
|
@@ -317,6 +321,7 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
317
321
|
sessionId: overrides.sessionId ?? options.sessionId,
|
|
318
322
|
verbose: overrides.verbose ?? options.verbose,
|
|
319
323
|
heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
|
|
324
|
+
browserAttachments: overrides.browserAttachments ?? options.browserAttachments ?? 'auto',
|
|
320
325
|
browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
|
|
321
326
|
browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
|
|
322
327
|
background: overrides.background ?? undefined,
|
|
@@ -359,6 +364,7 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
359
364
|
sessionId: metadata.id,
|
|
360
365
|
verbose: stored.verbose,
|
|
361
366
|
heartbeatIntervalMs: stored.heartbeatIntervalMs,
|
|
367
|
+
browserAttachments: stored.browserAttachments,
|
|
362
368
|
browserInlineFiles: stored.browserInlineFiles,
|
|
363
369
|
browserBundleFiles: stored.browserBundleFiles,
|
|
364
370
|
background: stored.background,
|
|
@@ -135,9 +135,27 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
135
135
|
await runtime.evaluate({ expression: `(function(){${dispatchEvents} return true;})()`, returnByValue: true });
|
|
136
136
|
};
|
|
137
137
|
await tryFileInput();
|
|
138
|
+
// Snapshot the attachment state immediately after setting files so we can detect silent failures.
|
|
139
|
+
const snapshotExpr = `(() => {
|
|
140
|
+
const chips = Array.from(document.querySelectorAll('[data-testid*="attachment"],[data-testid*="chip"],[data-testid*="upload"],[aria-label="Remove file"]'))
|
|
141
|
+
.map((node) => (node?.textContent || '').trim())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]')).map((el) => ({
|
|
144
|
+
files: Array.from(el.files || []).map((f) => f?.name ?? ''),
|
|
145
|
+
}));
|
|
146
|
+
return { chips, inputs };
|
|
147
|
+
})()`;
|
|
148
|
+
const snapshot = await runtime
|
|
149
|
+
.evaluate({ expression: snapshotExpr, returnByValue: true })
|
|
150
|
+
.then((res) => res?.result?.value)
|
|
151
|
+
.catch(() => undefined);
|
|
152
|
+
if (snapshot) {
|
|
153
|
+
logger?.(`Attachment snapshot after setFileInputFiles: chips=${JSON.stringify(snapshot.chips || [])} inputs=${JSON.stringify(snapshot.inputs || [])}`);
|
|
154
|
+
}
|
|
155
|
+
const inputHasFile = snapshot?.inputs?.some((entry) => (entry.files || []).some((name) => name?.toLowerCase?.().includes(expectedName.toLowerCase()))) ?? false;
|
|
138
156
|
if (await waitForAttachmentAnchored(runtime, expectedName, 20_000)) {
|
|
139
157
|
await waitForAttachmentVisible(runtime, expectedName, 20_000, logger);
|
|
140
|
-
logger('Attachment queued (file input)');
|
|
158
|
+
logger(inputHasFile ? 'Attachment queued (file input, confirmed present)' : 'Attachment queued (file input)');
|
|
141
159
|
return;
|
|
142
160
|
}
|
|
143
161
|
await logDomFailure(runtime, logger, 'file-upload-missing');
|
|
@@ -189,10 +207,6 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
189
207
|
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
190
208
|
);
|
|
191
209
|
attachedNames.push(...cardTexts.filter(Boolean));
|
|
192
|
-
const filesPills = Array.from(document.querySelectorAll('button,div'))
|
|
193
|
-
.map((node) => (node?.textContent || '').toLowerCase())
|
|
194
|
-
.filter((text) => /\bfiles\b/.test(text));
|
|
195
|
-
attachedNames.push(...filesPills);
|
|
196
210
|
const filesAttached = attachedNames.length > 0;
|
|
197
211
|
return { state: button ? (disabled ? 'disabled' : 'ready') : 'missing', uploading, filesAttached, attachedNames };
|
|
198
212
|
})()`;
|
|
@@ -274,13 +288,6 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
274
288
|
return { found: true, userTurns: userTurns.length, source: 'attachment-cards' };
|
|
275
289
|
}
|
|
276
290
|
|
|
277
|
-
const filesPills = Array.from(document.querySelectorAll('button,div')).map((node) =>
|
|
278
|
-
(node?.textContent || '').toLowerCase(),
|
|
279
|
-
);
|
|
280
|
-
if (filesPills.some((text) => /\bfiles\b/.test(text))) {
|
|
281
|
-
return { found: true, userTurns: userTurns.length, source: 'files-pill' };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
291
|
const attrMatch = Array.from(document.querySelectorAll('[aria-label], [title], [data-testid]')).some(matchNode);
|
|
285
292
|
if (attrMatch) {
|
|
286
293
|
return { found: true, userTurns: userTurns.length, source: 'attrs' };
|
|
@@ -328,13 +335,6 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
328
335
|
return { found: true, text: cards.find((t) => t.includes(normalized)) };
|
|
329
336
|
}
|
|
330
337
|
|
|
331
|
-
const filesPills = Array.from(document.querySelectorAll('button,div')).map((node) =>
|
|
332
|
-
(node?.textContent || '').toLowerCase(),
|
|
333
|
-
);
|
|
334
|
-
if (filesPills.some((text) => /\bfiles\b/.test(text))) {
|
|
335
|
-
return { found: true, text: filesPills.find((t) => /\bfiles\b/.test(t)) };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
338
|
// As a last resort, treat file inputs that hold the target name as anchored. Some UIs delay chip rendering.
|
|
339
339
|
const inputHit = Array.from(document.querySelectorAll('input[type="file"]')).some((el) =>
|
|
340
340
|
Array.from(el.files || []).some((file) => file?.name?.toLowerCase?.().includes(normalized)),
|
|
@@ -248,6 +248,23 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
248
248
|
testIdTokens.add('gpt5-0');
|
|
249
249
|
testIdTokens.add('gpt50');
|
|
250
250
|
}
|
|
251
|
+
// Numeric variations (5.2 ↔ 52 ↔ gpt-5-2)
|
|
252
|
+
if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
|
|
253
|
+
push('5.2', labelTokens);
|
|
254
|
+
push('gpt-5.2', labelTokens);
|
|
255
|
+
push('gpt5.2', labelTokens);
|
|
256
|
+
push('gpt-5-2', labelTokens);
|
|
257
|
+
push('gpt5-2', labelTokens);
|
|
258
|
+
push('gpt52', labelTokens);
|
|
259
|
+
push('chatgpt 5.2', labelTokens);
|
|
260
|
+
if (base.includes('thinking'))
|
|
261
|
+
push('thinking', labelTokens);
|
|
262
|
+
if (base.includes('instant'))
|
|
263
|
+
push('instant', labelTokens);
|
|
264
|
+
testIdTokens.add('gpt-5-2');
|
|
265
|
+
testIdTokens.add('gpt5-2');
|
|
266
|
+
testIdTokens.add('gpt52');
|
|
267
|
+
}
|
|
251
268
|
// Pro / research variants
|
|
252
269
|
if (base.includes('pro')) {
|
|
253
270
|
push('proresearch', labelTokens);
|
|
@@ -263,6 +280,11 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
263
280
|
testIdTokens.add('gpt-5-0-pro');
|
|
264
281
|
testIdTokens.add('gpt50pro');
|
|
265
282
|
}
|
|
283
|
+
if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
|
|
284
|
+
testIdTokens.add('gpt-5.2-pro');
|
|
285
|
+
testIdTokens.add('gpt-5-2-pro');
|
|
286
|
+
testIdTokens.add('gpt52pro');
|
|
287
|
+
}
|
|
266
288
|
testIdTokens.add('pro');
|
|
267
289
|
testIdTokens.add('proresearch');
|
|
268
290
|
}
|
|
@@ -2,6 +2,7 @@ import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEN
|
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure } from '../domDebug.js';
|
|
4
4
|
import { buildClickDispatcher } from './domEvents.js';
|
|
5
|
+
import { BrowserAutomationError } from '../../oracle/errors.js';
|
|
5
6
|
const ENTER_KEY_EVENT = {
|
|
6
7
|
key: 'Enter',
|
|
7
8
|
code: 'Enter',
|
|
@@ -70,9 +71,11 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
70
71
|
})()`,
|
|
71
72
|
returnByValue: true,
|
|
72
73
|
});
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const editorTextRaw = verification.result?.value?.editorText ?? '';
|
|
75
|
+
const fallbackValueRaw = verification.result?.value?.fallbackValue ?? '';
|
|
76
|
+
const editorTextTrimmed = editorTextRaw?.trim?.() ?? '';
|
|
77
|
+
const fallbackValueTrimmed = fallbackValueRaw?.trim?.() ?? '';
|
|
78
|
+
if (!editorTextTrimmed && !fallbackValueTrimmed) {
|
|
76
79
|
await runtime.evaluate({
|
|
77
80
|
expression: `(() => {
|
|
78
81
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
@@ -90,6 +93,30 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
90
93
|
})()`,
|
|
91
94
|
});
|
|
92
95
|
}
|
|
96
|
+
const promptLength = prompt.length;
|
|
97
|
+
const postVerification = await runtime.evaluate({
|
|
98
|
+
expression: `(() => {
|
|
99
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
100
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
101
|
+
return {
|
|
102
|
+
editorText: editor?.innerText ?? '',
|
|
103
|
+
fallbackValue: fallback?.value ?? '',
|
|
104
|
+
};
|
|
105
|
+
})()`,
|
|
106
|
+
returnByValue: true,
|
|
107
|
+
});
|
|
108
|
+
const observedEditor = postVerification.result?.value?.editorText ?? '';
|
|
109
|
+
const observedFallback = postVerification.result?.value?.fallbackValue ?? '';
|
|
110
|
+
const observedLength = Math.max(observedEditor.length, observedFallback.length);
|
|
111
|
+
if (promptLength >= 50_000 && observedLength > 0 && observedLength < promptLength - 2_000) {
|
|
112
|
+
await logDomFailure(runtime, logger, 'prompt-too-large');
|
|
113
|
+
throw new BrowserAutomationError('Prompt appears truncated in the composer (likely too large).', {
|
|
114
|
+
stage: 'submit-prompt',
|
|
115
|
+
code: 'prompt-too-large',
|
|
116
|
+
promptLength,
|
|
117
|
+
observedLength,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
93
120
|
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames);
|
|
94
121
|
if (!clicked) {
|
|
95
122
|
await input.dispatchKeyEvent({
|
|
@@ -110,6 +137,35 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
110
137
|
await verifyPromptCommitted(runtime, prompt, 30_000, logger);
|
|
111
138
|
await clickAnswerNowIfPresent(runtime, logger);
|
|
112
139
|
}
|
|
140
|
+
export async function clearPromptComposer(Runtime, logger) {
|
|
141
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
142
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
143
|
+
const result = await Runtime.evaluate({
|
|
144
|
+
expression: `(() => {
|
|
145
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
146
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
147
|
+
let cleared = false;
|
|
148
|
+
if (fallback) {
|
|
149
|
+
fallback.value = '';
|
|
150
|
+
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
151
|
+
fallback.dispatchEvent(new Event('change', { bubbles: true }));
|
|
152
|
+
cleared = true;
|
|
153
|
+
}
|
|
154
|
+
if (editor) {
|
|
155
|
+
editor.textContent = '';
|
|
156
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
157
|
+
cleared = true;
|
|
158
|
+
}
|
|
159
|
+
return { cleared };
|
|
160
|
+
})()`,
|
|
161
|
+
returnByValue: true,
|
|
162
|
+
});
|
|
163
|
+
if (!result.result?.value?.cleared) {
|
|
164
|
+
await logDomFailure(Runtime, logger, 'clear-composer');
|
|
165
|
+
throw new Error('Failed to clear prompt composer');
|
|
166
|
+
}
|
|
167
|
+
await delay(250);
|
|
168
|
+
}
|
|
113
169
|
async function waitForDomReady(Runtime, logger) {
|
|
114
170
|
const deadline = Date.now() + 10_000;
|
|
115
171
|
while (Date.now() < deadline) {
|
|
@@ -231,25 +287,46 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger) {
|
|
|
231
287
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
232
288
|
const normalize = (value) => value?.toLowerCase?.().replace(/\\s+/g, ' ').trim() ?? '';
|
|
233
289
|
const normalizedPrompt = normalize(${encodedPrompt});
|
|
290
|
+
const normalizedPromptPrefix = normalizedPrompt.slice(0, 120);
|
|
234
291
|
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
235
292
|
const articles = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
236
|
-
const
|
|
293
|
+
const normalizedTurns = articles.map((node) => normalize(node?.innerText));
|
|
294
|
+
const userMatched = normalizedTurns.some((text) => text.includes(normalizedPrompt));
|
|
295
|
+
const prefixMatched =
|
|
296
|
+
normalizedPromptPrefix.length > 30 &&
|
|
297
|
+
normalizedTurns.some((text) => text.includes(normalizedPromptPrefix));
|
|
298
|
+
const lastTurn = normalizedTurns[normalizedTurns.length - 1] ?? '';
|
|
237
299
|
return {
|
|
238
300
|
userMatched,
|
|
301
|
+
prefixMatched,
|
|
239
302
|
fallbackValue: fallback?.value ?? '',
|
|
240
303
|
editorValue: editor?.innerText ?? '',
|
|
304
|
+
lastTurn,
|
|
305
|
+
turnsCount: normalizedTurns.length,
|
|
241
306
|
};
|
|
242
307
|
})()`;
|
|
243
308
|
while (Date.now() < deadline) {
|
|
244
309
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
245
310
|
const info = result.value;
|
|
246
|
-
if (info?.userMatched) {
|
|
311
|
+
if (info?.userMatched || info?.prefixMatched) {
|
|
247
312
|
return;
|
|
248
313
|
}
|
|
249
314
|
await delay(100);
|
|
250
315
|
}
|
|
251
316
|
if (logger) {
|
|
317
|
+
logger(`Prompt commit check failed; latest state: ${await Runtime.evaluate({
|
|
318
|
+
expression: script,
|
|
319
|
+
returnByValue: true,
|
|
320
|
+
}).then((res) => JSON.stringify(res?.result?.value)).catch(() => 'unavailable')}`);
|
|
252
321
|
await logDomFailure(Runtime, logger, 'prompt-commit');
|
|
253
322
|
}
|
|
323
|
+
if (prompt.trim().length >= 50_000) {
|
|
324
|
+
throw new BrowserAutomationError('Prompt did not appear in conversation before timeout (likely too large).', {
|
|
325
|
+
stage: 'submit-prompt',
|
|
326
|
+
code: 'prompt-too-large',
|
|
327
|
+
promptLength: prompt.trim().length,
|
|
328
|
+
timeoutMs,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
254
331
|
throw new Error('Prompt did not appear in conversation before timeout (send may have failed)');
|
|
255
332
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
|
|
2
|
+
import { logDomFailure } from '../domDebug.js';
|
|
3
|
+
import { buildClickDispatcher } from './domEvents.js';
|
|
4
|
+
export async function ensureExtendedThinking(Runtime, logger) {
|
|
5
|
+
const result = await evaluateThinkingTimeSelection(Runtime);
|
|
6
|
+
switch (result?.status) {
|
|
7
|
+
case 'already-extended':
|
|
8
|
+
logger(`Thinking time: ${result.label ?? 'Extended'} (already selected)`);
|
|
9
|
+
return;
|
|
10
|
+
case 'switched':
|
|
11
|
+
logger(`Thinking time: ${result.label ?? 'Extended'}`);
|
|
12
|
+
return;
|
|
13
|
+
case 'chip-not-found': {
|
|
14
|
+
await logDomFailure(Runtime, logger, 'thinking-chip');
|
|
15
|
+
throw new Error('Unable to find the Thinking chip button in the composer area.');
|
|
16
|
+
}
|
|
17
|
+
case 'menu-not-found': {
|
|
18
|
+
await logDomFailure(Runtime, logger, 'thinking-time-menu');
|
|
19
|
+
throw new Error('Unable to find the Thinking time dropdown menu.');
|
|
20
|
+
}
|
|
21
|
+
case 'extended-not-found': {
|
|
22
|
+
await logDomFailure(Runtime, logger, 'extended-option');
|
|
23
|
+
throw new Error('Unable to find the Extended option in the Thinking time menu.');
|
|
24
|
+
}
|
|
25
|
+
default: {
|
|
26
|
+
await logDomFailure(Runtime, logger, 'thinking-time-unknown');
|
|
27
|
+
throw new Error('Unknown error selecting Extended thinking time.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort selection of the "Extended" thinking-time option in ChatGPT's composer pill menu.
|
|
33
|
+
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureExtendedThinkingIfAvailable(Runtime, logger) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await evaluateThinkingTimeSelection(Runtime);
|
|
38
|
+
switch (result?.status) {
|
|
39
|
+
case 'already-extended':
|
|
40
|
+
logger(`Thinking time: ${result.label ?? 'Extended'} (already selected)`);
|
|
41
|
+
return true;
|
|
42
|
+
case 'switched':
|
|
43
|
+
logger(`Thinking time: ${result.label ?? 'Extended'}`);
|
|
44
|
+
return true;
|
|
45
|
+
case 'chip-not-found':
|
|
46
|
+
case 'menu-not-found':
|
|
47
|
+
case 'extended-not-found':
|
|
48
|
+
if (logger.verbose) {
|
|
49
|
+
logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
default:
|
|
53
|
+
if (logger.verbose) {
|
|
54
|
+
logger('Thinking time: unknown outcome; continuing with default.');
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
if (logger.verbose) {
|
|
62
|
+
logger(`Thinking time selection failed (${message}); continuing with default.`);
|
|
63
|
+
await logDomFailure(Runtime, logger, 'thinking-time');
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function evaluateThinkingTimeSelection(Runtime) {
|
|
69
|
+
const outcome = await Runtime.evaluate({
|
|
70
|
+
expression: buildThinkingTimeExpression(),
|
|
71
|
+
awaitPromise: true,
|
|
72
|
+
returnByValue: true,
|
|
73
|
+
});
|
|
74
|
+
return outcome.result?.value;
|
|
75
|
+
}
|
|
76
|
+
function buildThinkingTimeExpression() {
|
|
77
|
+
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
78
|
+
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
79
|
+
return `(async () => {
|
|
80
|
+
${buildClickDispatcher()}
|
|
81
|
+
|
|
82
|
+
const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
|
|
83
|
+
const MENU_ITEM_SELECTOR = ${menuItemLiteral};
|
|
84
|
+
|
|
85
|
+
const CHIP_SELECTORS = [
|
|
86
|
+
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
|
|
87
|
+
'button.__composer-pill[aria-haspopup="menu"]',
|
|
88
|
+
'.__composer-pill-composite button[aria-haspopup="menu"]',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const INITIAL_WAIT_MS = 150;
|
|
92
|
+
const MAX_WAIT_MS = 10000;
|
|
93
|
+
|
|
94
|
+
const normalize = (value) => (value || '')
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
97
|
+
.replace(/\\s+/g, ' ')
|
|
98
|
+
.trim();
|
|
99
|
+
|
|
100
|
+
const findThinkingChip = () => {
|
|
101
|
+
for (const selector of CHIP_SELECTORS) {
|
|
102
|
+
const buttons = document.querySelectorAll(selector);
|
|
103
|
+
for (const btn of buttons) {
|
|
104
|
+
const aria = normalize(btn.getAttribute?.('aria-label') ?? '');
|
|
105
|
+
const text = normalize(btn.textContent ?? '');
|
|
106
|
+
if (aria.includes('thinking') || text.includes('thinking')) {
|
|
107
|
+
return btn;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const chip = findThinkingChip();
|
|
115
|
+
if (!chip) {
|
|
116
|
+
return { status: 'chip-not-found' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dispatchClickSequence(chip);
|
|
120
|
+
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const start = performance.now();
|
|
123
|
+
|
|
124
|
+
const findMenu = () => {
|
|
125
|
+
const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="group"]');
|
|
126
|
+
for (const menu of menus) {
|
|
127
|
+
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
128
|
+
if (normalize(label?.textContent ?? '').includes('thinking time')) {
|
|
129
|
+
return menu;
|
|
130
|
+
}
|
|
131
|
+
const text = normalize(menu.textContent ?? '');
|
|
132
|
+
if (text.includes('standard') && text.includes('extended')) {
|
|
133
|
+
return menu;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const findExtendedOption = (menu) => {
|
|
140
|
+
const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
|
|
141
|
+
for (const item of items) {
|
|
142
|
+
const text = normalize(item.textContent ?? '');
|
|
143
|
+
if (text.includes('extended')) {
|
|
144
|
+
return item;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const optionIsSelected = (node) => {
|
|
151
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
152
|
+
const ariaChecked = node.getAttribute('aria-checked');
|
|
153
|
+
const dataState = (node.getAttribute('data-state') || '').toLowerCase();
|
|
154
|
+
if (ariaChecked === 'true') return true;
|
|
155
|
+
if (dataState === 'checked' || dataState === 'selected' || dataState === 'on') return true;
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const attempt = () => {
|
|
160
|
+
const menu = findMenu();
|
|
161
|
+
if (!menu) {
|
|
162
|
+
if (performance.now() - start > MAX_WAIT_MS) {
|
|
163
|
+
resolve({ status: 'menu-not-found' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
setTimeout(attempt, 100);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const extendedOption = findExtendedOption(menu);
|
|
171
|
+
if (!extendedOption) {
|
|
172
|
+
resolve({ status: 'extended-not-found' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const alreadySelected =
|
|
177
|
+
optionIsSelected(extendedOption) ||
|
|
178
|
+
optionIsSelected(extendedOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
|
|
179
|
+
const label = extendedOption.textContent?.trim?.() || null;
|
|
180
|
+
dispatchClickSequence(extendedOption);
|
|
181
|
+
resolve({ status: alreadySelected ? 'already-extended' : 'switched', label });
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
setTimeout(attempt, INITIAL_WAIT_MS);
|
|
185
|
+
});
|
|
186
|
+
})()`;
|
|
187
|
+
}
|
|
188
|
+
export function buildThinkingTimeExpressionForTest() {
|
|
189
|
+
return buildThinkingTimeExpression();
|
|
190
|
+
}
|
|
@@ -24,6 +24,7 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
24
24
|
remoteChrome: null,
|
|
25
25
|
manualLogin: false,
|
|
26
26
|
manualLoginProfileDir: null,
|
|
27
|
+
extendedThinking: false,
|
|
27
28
|
};
|
|
28
29
|
export function resolveBrowserConfig(config) {
|
|
29
30
|
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
@@ -5,8 +5,9 @@ import net from 'node:net';
|
|
|
5
5
|
import { resolveBrowserConfig } from './config.js';
|
|
6
6
|
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChrome, connectToRemoteChrome, closeRemoteChromeTarget, } from './chromeLifecycle.js';
|
|
7
7
|
import { syncCookies } from './cookies.js';
|
|
8
|
-
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
|
|
8
|
+
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
|
|
9
9
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
10
|
+
import { ensureExtendedThinking } from './actions/thinkingTime.js';
|
|
10
11
|
import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
11
12
|
import { formatElapsed } from '../oracle/format.js';
|
|
12
13
|
import { CHATGPT_URL } from './constants.js';
|
|
@@ -19,6 +20,7 @@ export async function runBrowserMode(options) {
|
|
|
19
20
|
throw new Error('Prompt text is required when using browser mode.');
|
|
20
21
|
}
|
|
21
22
|
const attachments = options.attachments ?? [];
|
|
23
|
+
const fallbackSubmission = options.fallbackSubmission;
|
|
22
24
|
let config = resolveBrowserConfig(options.config);
|
|
23
25
|
const logger = options.log ?? ((_message) => { });
|
|
24
26
|
if (logger.verbose === undefined) {
|
|
@@ -254,20 +256,49 @@ export async function runBrowserMode(options) {
|
|
|
254
256
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
255
257
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
256
258
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
if (config.extendedThinking) {
|
|
260
|
+
await raceWithDisconnect(withRetries(() => ensureExtendedThinking(Runtime, logger), {
|
|
261
|
+
retries: 2,
|
|
262
|
+
delayMs: 300,
|
|
263
|
+
onRetry: (attempt, error) => {
|
|
264
|
+
if (options.verbose) {
|
|
265
|
+
logger(`[retry] Extended thinking attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
const submitOnce = async (prompt, submissionAttachments) => {
|
|
271
|
+
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
272
|
+
if (submissionAttachments.length > 0) {
|
|
273
|
+
if (!DOM) {
|
|
274
|
+
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
275
|
+
}
|
|
276
|
+
for (const attachment of submissionAttachments) {
|
|
277
|
+
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
278
|
+
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
279
|
+
}
|
|
280
|
+
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
281
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
282
|
+
logger('All attachments uploaded');
|
|
261
283
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, prompt, logger);
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
await raceWithDisconnect(submitOnce(promptText, attachments));
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const isPromptTooLarge = error instanceof BrowserAutomationError &&
|
|
291
|
+
error.details?.code === 'prompt-too-large';
|
|
292
|
+
if (fallbackSubmission && isPromptTooLarge) {
|
|
293
|
+
logger('[browser] Inline prompt too large; retrying with file uploads.');
|
|
294
|
+
await raceWithDisconnect(clearPromptComposer(Runtime, logger));
|
|
295
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
296
|
+
await raceWithDisconnect(submitOnce(fallbackSubmission.prompt, fallbackSubmission.attachments));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
throw error;
|
|
265
300
|
}
|
|
266
|
-
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
267
|
-
await raceWithDisconnect(waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger));
|
|
268
|
-
logger('All attachments uploaded');
|
|
269
301
|
}
|
|
270
|
-
await raceWithDisconnect(submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, promptText, logger));
|
|
271
302
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
272
303
|
const answer = await raceWithDisconnect(waitForAssistantResponse(Runtime, config.timeoutMs, logger));
|
|
273
304
|
answerText = answer.text;
|
|
@@ -636,21 +667,50 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
636
667
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
637
668
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
638
669
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
670
|
+
if (config.extendedThinking) {
|
|
671
|
+
await withRetries(() => ensureExtendedThinking(Runtime, logger), {
|
|
672
|
+
retries: 2,
|
|
673
|
+
delayMs: 300,
|
|
674
|
+
onRetry: (attempt, error) => {
|
|
675
|
+
if (options.verbose) {
|
|
676
|
+
logger(`[retry] Extended thinking attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const submitOnce = async (prompt, submissionAttachments) => {
|
|
682
|
+
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
683
|
+
if (submissionAttachments.length > 0) {
|
|
684
|
+
if (!DOM) {
|
|
685
|
+
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
686
|
+
}
|
|
687
|
+
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
688
|
+
for (const attachment of submissionAttachments) {
|
|
689
|
+
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
690
|
+
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
691
|
+
}
|
|
692
|
+
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
693
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
694
|
+
logger('All attachments uploaded');
|
|
695
|
+
}
|
|
696
|
+
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, prompt, logger);
|
|
697
|
+
};
|
|
698
|
+
try {
|
|
699
|
+
await submitOnce(promptText, attachments);
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
const isPromptTooLarge = error instanceof BrowserAutomationError &&
|
|
703
|
+
error.details?.code === 'prompt-too-large';
|
|
704
|
+
if (options.fallbackSubmission && isPromptTooLarge) {
|
|
705
|
+
logger('[browser] Inline prompt too large; retrying with file uploads.');
|
|
706
|
+
await clearPromptComposer(Runtime, logger);
|
|
707
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
708
|
+
await submitOnce(options.fallbackSubmission.prompt, options.fallbackSubmission.attachments);
|
|
643
709
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
647
|
-
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
710
|
+
else {
|
|
711
|
+
throw error;
|
|
648
712
|
}
|
|
649
|
-
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
650
|
-
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
651
|
-
logger('All attachments uploaded');
|
|
652
713
|
}
|
|
653
|
-
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, promptText, logger);
|
|
654
714
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
655
715
|
const answer = await waitForAssistantResponse(Runtime, config.timeoutMs, logger);
|
|
656
716
|
answerText = answer.text;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady } from './actions/navigation.js';
|
|
2
2
|
export { ensureModelSelection } from './actions/modelSelection.js';
|
|
3
|
-
export { submitPrompt } from './actions/promptComposer.js';
|
|
3
|
+
export { submitPrompt, clearPromptComposer } from './actions/promptComposer.js';
|
|
4
4
|
export { uploadAttachmentFile, waitForAttachmentCompletion } from './actions/attachments.js';
|
|
5
5
|
export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildConversationDebugExpressionForTest, } from './actions/assistantResponse.js';
|
|
@@ -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
|
+
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
8
9
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
9
10
|
const cwd = deps.cwd ?? process.cwd();
|
|
10
11
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
@@ -14,22 +15,33 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
14
15
|
const systemPrompt = runOptions.system?.trim() || '';
|
|
15
16
|
const sections = createFileSections(files, cwd);
|
|
16
17
|
const markdown = buildPromptMarkdown(systemPrompt, userPrompt, sections);
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const attachmentsPolicy = runOptions.browserInlineFiles
|
|
19
|
+
? 'never'
|
|
20
|
+
: runOptions.browserAttachments ?? 'auto';
|
|
21
|
+
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
22
|
+
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
23
|
+
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
24
|
+
const baseComposerSections = [];
|
|
19
25
|
if (systemPrompt)
|
|
20
|
-
|
|
26
|
+
baseComposerSections.push(systemPrompt);
|
|
21
27
|
if (userPrompt)
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
28
|
+
baseComposerSections.push(userPrompt);
|
|
29
|
+
const inlineComposerText = [...baseComposerSections, inlinePlan.inlineBlock].filter(Boolean).join('\n\n').trim();
|
|
30
|
+
const selectedPlan = attachmentsPolicy === 'always'
|
|
31
|
+
? uploadPlan
|
|
32
|
+
: attachmentsPolicy === 'never'
|
|
33
|
+
? inlinePlan
|
|
34
|
+
: inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
|
|
35
|
+
? inlinePlan
|
|
36
|
+
: uploadPlan;
|
|
37
|
+
const composerText = (selectedPlan.inlineBlock
|
|
38
|
+
? [...baseComposerSections, selectedPlan.inlineBlock]
|
|
39
|
+
: baseComposerSections)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n\n')
|
|
42
|
+
.trim();
|
|
43
|
+
const attachments = selectedPlan.attachments.slice();
|
|
44
|
+
const shouldBundle = selectedPlan.shouldBundle;
|
|
33
45
|
let bundleText = null;
|
|
34
46
|
if (shouldBundle) {
|
|
35
47
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
@@ -48,11 +60,11 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
48
60
|
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
49
61
|
});
|
|
50
62
|
}
|
|
51
|
-
const inlineFileCount =
|
|
63
|
+
const inlineFileCount = selectedPlan.inlineFileCount;
|
|
52
64
|
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
53
|
-
const tokenizer = modelConfig.tokenizer;
|
|
54
|
-
const tokenizerUserContent = inlineFileCount > 0 &&
|
|
55
|
-
? [userPrompt,
|
|
65
|
+
const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
|
|
66
|
+
const tokenizerUserContent = inlineFileCount > 0 && selectedPlan.inlineBlock
|
|
67
|
+
? [userPrompt, selectedPlan.inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
|
|
56
68
|
: userPrompt;
|
|
57
69
|
const tokenizerMessages = [
|
|
58
70
|
systemPrompt ? { role: 'system', content: systemPrompt } : null,
|
|
@@ -61,7 +73,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
61
73
|
let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0
|
|
62
74
|
? tokenizerMessages
|
|
63
75
|
: [{ role: 'user', content: '' }], TOKENIZER_OPTIONS);
|
|
64
|
-
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(
|
|
76
|
+
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
|
|
65
77
|
if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
|
|
66
78
|
const attachmentText = bundleText ??
|
|
67
79
|
sections
|
|
@@ -70,6 +82,35 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
70
82
|
const attachmentTokens = tokenizer([{ role: 'user', content: attachmentText }], TOKENIZER_OPTIONS);
|
|
71
83
|
estimatedInputTokens += attachmentTokens;
|
|
72
84
|
}
|
|
85
|
+
let fallback = null;
|
|
86
|
+
if (attachmentsPolicy === 'auto' && selectedPlan.mode === 'inline' && sections.length > 0) {
|
|
87
|
+
const fallbackComposerText = baseComposerSections.join('\n\n').trim();
|
|
88
|
+
const fallbackAttachments = uploadPlan.attachments.slice();
|
|
89
|
+
let fallbackBundled = null;
|
|
90
|
+
if (uploadPlan.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
|
+
const fallbackBundleText = `${bundleLines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
99
|
+
await fs.writeFile(bundlePath, fallbackBundleText, 'utf8');
|
|
100
|
+
fallbackAttachments.length = 0;
|
|
101
|
+
fallbackAttachments.push({
|
|
102
|
+
path: bundlePath,
|
|
103
|
+
displayPath: bundlePath,
|
|
104
|
+
sizeBytes: Buffer.byteLength(fallbackBundleText, 'utf8'),
|
|
105
|
+
});
|
|
106
|
+
fallbackBundled = { originalCount: sections.length, bundlePath };
|
|
107
|
+
}
|
|
108
|
+
fallback = {
|
|
109
|
+
composerText: fallbackComposerText,
|
|
110
|
+
attachments: fallbackAttachments,
|
|
111
|
+
bundled: fallbackBundled,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
73
114
|
return {
|
|
74
115
|
markdown,
|
|
75
116
|
composerText,
|
|
@@ -77,6 +118,9 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
77
118
|
attachments,
|
|
78
119
|
inlineFileCount,
|
|
79
120
|
tokenEstimateIncludesInlineFiles,
|
|
121
|
+
attachmentsPolicy,
|
|
122
|
+
attachmentMode: selectedPlan.mode,
|
|
123
|
+
fallback,
|
|
80
124
|
bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
|
|
81
125
|
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
82
126
|
: null,
|
|
@@ -25,8 +25,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
25
25
|
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
else if (runOptions.file && runOptions.file.length > 0 &&
|
|
29
|
-
log(chalk.dim('[verbose] Browser
|
|
28
|
+
else if (runOptions.file && runOptions.file.length > 0 && promptArtifacts.attachmentMode === 'inline') {
|
|
29
|
+
log(chalk.dim('[verbose] Browser will paste file contents inline (no uploads).'));
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
if (promptArtifacts.bundled) {
|
|
@@ -34,11 +34,12 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
34
34
|
}
|
|
35
35
|
const headerLine = `Launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
|
|
36
36
|
const automationLogger = ((message) => {
|
|
37
|
-
if (
|
|
37
|
+
if (typeof message !== 'string')
|
|
38
38
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const shouldAlwaysPrint = message.startsWith('[browser] ') && /fallback|retry/i.test(message);
|
|
40
|
+
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
41
|
+
return;
|
|
42
|
+
log(message);
|
|
42
43
|
});
|
|
43
44
|
automationLogger.verbose = Boolean(runOptions.verbose);
|
|
44
45
|
automationLogger.sessionLog = runOptions.verbose ? log : (() => { });
|
|
@@ -53,6 +54,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
53
54
|
browserResult = await executeBrowser({
|
|
54
55
|
prompt: promptArtifacts.composerText,
|
|
55
56
|
attachments: promptArtifacts.attachments,
|
|
57
|
+
fallbackSubmission: promptArtifacts.fallback
|
|
58
|
+
? { prompt: promptArtifacts.fallback.composerText, attachments: promptArtifacts.fallback.attachments }
|
|
59
|
+
: undefined,
|
|
56
60
|
config: browserConfig,
|
|
57
61
|
log: automationLogger,
|
|
58
62
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import { CHATGPT_URL, DEFAULT_MODEL_TARGET, normalizeChatgptUrl, parseDuration } from '../browserMode.js';
|
|
4
|
+
import { getOracleHomeDir } from '../oracleHome.js';
|
|
5
5
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
6
6
|
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 30_000;
|
|
7
7
|
const DEFAULT_CHROME_PROFILE = 'Default';
|
|
@@ -9,6 +9,9 @@ const BROWSER_MODEL_LABELS = {
|
|
|
9
9
|
'gpt-5-pro': 'GPT-5 Pro',
|
|
10
10
|
'gpt-5.1-pro': 'GPT-5.1 Pro',
|
|
11
11
|
'gpt-5.1': 'GPT-5.1',
|
|
12
|
+
'gpt-5.2': 'GPT-5.2 Thinking',
|
|
13
|
+
'gpt-5.2-instant': 'GPT-5.2 Instant',
|
|
14
|
+
'gpt-5.2-pro': 'GPT-5.2 Pro',
|
|
12
15
|
'gemini-3-pro': 'Gemini 3 Pro',
|
|
13
16
|
};
|
|
14
17
|
export async function buildBrowserConfig(options) {
|
|
@@ -53,6 +56,7 @@ export async function buildBrowserConfig(options) {
|
|
|
53
56
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
54
57
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
55
58
|
remoteChrome,
|
|
59
|
+
extendedThinking: options.browserExtendedThinking ? true : undefined,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
function selectBrowserPort(options) {
|
|
@@ -155,7 +159,7 @@ async function resolveInlineCookies({ inlineArg, inlineFileArg, envPayload, envF
|
|
|
155
159
|
return { cookies: parsed, source };
|
|
156
160
|
}
|
|
157
161
|
// fallback: ~/.oracle/cookies.{json,base64}
|
|
158
|
-
const oracleHome =
|
|
162
|
+
const oracleHome = getOracleHomeDir();
|
|
159
163
|
const candidates = ['cookies.json', 'cookies.base64'];
|
|
160
164
|
for (const file of candidates) {
|
|
161
165
|
const fullPath = path.join(oracleHome, file);
|
|
@@ -19,7 +19,6 @@ import { formatElapsed } from '../oracle/format.js';
|
|
|
19
19
|
import { sanitizeOscProgress } from './oscUtils.js';
|
|
20
20
|
import { readFiles } from '../oracle/files.js';
|
|
21
21
|
import { formatUSD } from '../oracle/format.js';
|
|
22
|
-
import { SESSIONS_DIR } from '../sessionManager.js';
|
|
23
22
|
import { cwd as getCwd } from 'node:process';
|
|
24
23
|
const isTty = process.stdout.isTTY;
|
|
25
24
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
@@ -386,7 +385,7 @@ async function writeAssistantOutput(targetPath, content, log) {
|
|
|
386
385
|
return;
|
|
387
386
|
}
|
|
388
387
|
const normalizedTarget = path.resolve(targetPath);
|
|
389
|
-
const normalizedSessionsDir = path.resolve(
|
|
388
|
+
const normalizedSessionsDir = path.resolve(sessionStore.sessionsDir());
|
|
390
389
|
if (normalizedTarget === normalizedSessionsDir ||
|
|
391
390
|
normalizedTarget.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
392
391
|
log(dim(`write-output skipped: refusing to write inside session storage (${normalizedSessionsDir}).`));
|
|
@@ -442,7 +441,7 @@ function buildFallbackPath(original) {
|
|
|
442
441
|
const dir = getCwd();
|
|
443
442
|
const candidate = ext ? `${stem}.fallback${ext}` : `${stem}.fallback`;
|
|
444
443
|
const fallback = path.join(dir, candidate);
|
|
445
|
-
const normalizedSessionsDir = path.resolve(
|
|
444
|
+
const normalizedSessionsDir = path.resolve(sessionStore.sessionsDir());
|
|
446
445
|
const normalizedFallback = path.resolve(fallback);
|
|
447
446
|
if (normalizedFallback === normalizedSessionsDir ||
|
|
448
447
|
normalizedFallback.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
@@ -388,6 +388,7 @@ async function askOracleFlow(version, userConfig) {
|
|
|
388
388
|
sessionId: undefined,
|
|
389
389
|
verbose: false,
|
|
390
390
|
heartbeatIntervalMs: undefined,
|
|
391
|
+
browserAttachments: 'auto',
|
|
391
392
|
browserInlineFiles: false,
|
|
392
393
|
browserBundleFiles: false,
|
|
393
394
|
background: undefined,
|
package/dist/src/config.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import JSON5 from 'json5';
|
|
4
|
+
import { getOracleHomeDir } from './oracleHome.js';
|
|
5
5
|
function resolveConfigPath() {
|
|
6
|
-
|
|
7
|
-
return path.join(oracleHome, 'config.json');
|
|
6
|
+
return path.join(getOracleHomeDir(), 'config.json');
|
|
8
7
|
}
|
|
9
8
|
export async function loadUserConfig() {
|
|
10
9
|
const CONFIG_PATH = resolveConfigPath();
|
|
@@ -3,17 +3,18 @@ import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro
|
|
|
3
3
|
import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
|
|
4
4
|
import { stringifyTokenizerInput } from './tokenStringifier.js';
|
|
5
5
|
export const DEFAULT_MODEL = 'gpt-5.1-pro';
|
|
6
|
-
export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
|
|
6
|
+
export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'gpt-5.2-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
|
|
7
7
|
const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
|
|
8
8
|
export const MODEL_CONFIGS = {
|
|
9
9
|
'gpt-5.1-pro': {
|
|
10
10
|
model: 'gpt-5.1-pro',
|
|
11
|
+
apiModel: 'gpt-5.2-pro',
|
|
11
12
|
provider: 'openai',
|
|
12
13
|
tokenizer: countTokensGpt5Pro,
|
|
13
14
|
inputLimit: 196000,
|
|
14
15
|
pricing: {
|
|
15
|
-
inputPerToken:
|
|
16
|
-
outputPerToken:
|
|
16
|
+
inputPerToken: 21 / 1_000_000,
|
|
17
|
+
outputPerToken: 168 / 1_000_000,
|
|
17
18
|
},
|
|
18
19
|
reasoning: null,
|
|
19
20
|
},
|
|
@@ -50,6 +51,40 @@ export const MODEL_CONFIGS = {
|
|
|
50
51
|
},
|
|
51
52
|
reasoning: { effort: 'high' },
|
|
52
53
|
},
|
|
54
|
+
'gpt-5.2': {
|
|
55
|
+
model: 'gpt-5.2',
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
tokenizer: countTokensGpt5,
|
|
58
|
+
inputLimit: 196000,
|
|
59
|
+
pricing: {
|
|
60
|
+
inputPerToken: 1.75 / 1_000_000,
|
|
61
|
+
outputPerToken: 14 / 1_000_000,
|
|
62
|
+
},
|
|
63
|
+
reasoning: { effort: 'xhigh' },
|
|
64
|
+
},
|
|
65
|
+
'gpt-5.2-instant': {
|
|
66
|
+
model: 'gpt-5.2-instant',
|
|
67
|
+
apiModel: 'gpt-5.2-chat-latest',
|
|
68
|
+
provider: 'openai',
|
|
69
|
+
tokenizer: countTokensGpt5,
|
|
70
|
+
inputLimit: 196000,
|
|
71
|
+
pricing: {
|
|
72
|
+
inputPerToken: 1.75 / 1_000_000,
|
|
73
|
+
outputPerToken: 14 / 1_000_000,
|
|
74
|
+
},
|
|
75
|
+
reasoning: null,
|
|
76
|
+
},
|
|
77
|
+
'gpt-5.2-pro': {
|
|
78
|
+
model: 'gpt-5.2-pro',
|
|
79
|
+
provider: 'openai',
|
|
80
|
+
tokenizer: countTokensGpt5Pro,
|
|
81
|
+
inputLimit: 196000,
|
|
82
|
+
pricing: {
|
|
83
|
+
inputPerToken: 21 / 1_000_000,
|
|
84
|
+
outputPerToken: 168 / 1_000_000,
|
|
85
|
+
},
|
|
86
|
+
reasoning: { effort: 'xhigh' },
|
|
87
|
+
},
|
|
53
88
|
'gemini-3-pro': {
|
|
54
89
|
model: 'gemini-3-pro',
|
|
55
90
|
provider: 'google',
|
|
@@ -95,13 +95,13 @@ export function toTransportError(error, model) {
|
|
|
95
95
|
const apiMessage = apiError.error?.message ||
|
|
96
96
|
apiError.message ||
|
|
97
97
|
(apiError.status ? `${apiError.status} OpenAI API error` : 'OpenAI API error');
|
|
98
|
-
//
|
|
99
|
-
if (model === 'gpt-5.
|
|
98
|
+
// Friendly guidance when a pro-tier model isn't available on this base URL / API key.
|
|
99
|
+
if (model === 'gpt-5.2-pro' &&
|
|
100
100
|
(code === 'model_not_found' ||
|
|
101
101
|
messageText.includes('does not exist') ||
|
|
102
102
|
messageText.includes('unknown model') ||
|
|
103
103
|
messageText.includes('model_not_found'))) {
|
|
104
|
-
return new OracleTransportError('model-unavailable', 'gpt-5.
|
|
104
|
+
return new OracleTransportError('model-unavailable', 'gpt-5.2-pro is not available on this API base/key. Try gpt-5-pro or gpt-5.2, or switch to the browser engine.', apiError);
|
|
105
105
|
}
|
|
106
106
|
if (apiError.status === 404 || apiError.status === 405) {
|
|
107
107
|
return new OracleTransportError('unsupported-endpoint', 'HTTP 404/405 from the Responses API; this base URL or gateway likely does not expose /v1/responses. Set OPENAI_BASE_URL to api.openai.com/v1, update your Azure API version/deployment, or use the browser engine.', apiError);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai';
|
|
2
2
|
const MODEL_ID_MAP = {
|
|
3
3
|
'gemini-3-pro': 'gemini-3-pro-preview',
|
|
4
|
-
'gpt-5.1-pro': 'gpt-5.1-pro',
|
|
4
|
+
'gpt-5.1-pro': 'gpt-5.1-pro',
|
|
5
5
|
'gpt-5-pro': 'gpt-5-pro',
|
|
6
6
|
'gpt-5.1': 'gpt-5.1',
|
|
7
7
|
'gpt-5.1-codex': 'gpt-5.1-codex',
|
|
8
|
+
'gpt-5.2': 'gpt-5.2',
|
|
9
|
+
'gpt-5.2-instant': 'gpt-5.2-instant',
|
|
10
|
+
'gpt-5.2-pro': 'gpt-5.2-pro',
|
|
8
11
|
'claude-4.5-sonnet': 'claude-4.5-sonnet',
|
|
9
12
|
'claude-4.1-opus': 'claude-4.1-opus',
|
|
10
13
|
'grok-4.1': 'grok-4.1',
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -186,17 +186,14 @@ export async function runOracle(options, deps = {}) {
|
|
|
186
186
|
: DEFAULT_TIMEOUT_NON_PRO_MS / 1000
|
|
187
187
|
: options.timeoutSeconds;
|
|
188
188
|
const timeoutMs = timeoutSeconds * 1000;
|
|
189
|
-
const apiModelFromConfig = modelConfig.apiModel ?? modelConfig.model;
|
|
190
|
-
const modelDowngraded = apiModelFromConfig === 'gpt-5.1-pro';
|
|
191
|
-
const resolvedApiModelId = modelDowngraded ? 'gpt-5-pro' : apiModelFromConfig;
|
|
192
189
|
// Track the concrete model id we dispatch to (especially for Gemini preview aliases)
|
|
193
190
|
const effectiveModelId = options.effectiveModelId ??
|
|
194
191
|
(options.model.startsWith('gemini')
|
|
195
192
|
? resolveGeminiModelId(options.model)
|
|
196
|
-
:
|
|
193
|
+
: (modelConfig.apiModel ?? modelConfig.model));
|
|
197
194
|
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
198
195
|
const requestBody = buildRequestBody({
|
|
199
|
-
modelConfig
|
|
196
|
+
modelConfig,
|
|
200
197
|
systemPrompt,
|
|
201
198
|
userPrompt: promptWithFiles,
|
|
202
199
|
searchEnabled,
|
|
@@ -222,8 +219,8 @@ export async function runOracle(options, deps = {}) {
|
|
|
222
219
|
if (baseUrl) {
|
|
223
220
|
log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
|
|
224
221
|
}
|
|
225
|
-
if (
|
|
226
|
-
log(dim(
|
|
222
|
+
if (effectiveModelId !== modelConfig.model) {
|
|
223
|
+
log(dim(`Resolved model: ${modelConfig.model} → ${effectiveModelId}`));
|
|
227
224
|
}
|
|
228
225
|
if (options.background && !supportsBackground) {
|
|
229
226
|
log(dim('Background runs are not supported for this model; streaming in foreground instead.'));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
let oracleHomeDirOverride = null;
|
|
4
|
+
/**
|
|
5
|
+
* Test-only hook: avoid mutating process.env (shared across Vitest worker threads).
|
|
6
|
+
* This override is scoped to the current Node worker.
|
|
7
|
+
*/
|
|
8
|
+
export function setOracleHomeDirOverrideForTest(dir) {
|
|
9
|
+
oracleHomeDirOverride = dir;
|
|
10
|
+
}
|
|
11
|
+
export function getOracleHomeDir() {
|
|
12
|
+
return oracleHomeDirOverride ?? process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
|
|
13
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import fs from 'node:fs/promises';
|
|
4
3
|
import { createWriteStream } from 'node:fs';
|
|
5
4
|
import { DEFAULT_MODEL } from './oracle.js';
|
|
6
5
|
import { safeModelSlug } from './oracle/modelResolver.js';
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { getOracleHomeDir } from './oracleHome.js';
|
|
7
|
+
export function getSessionsDir() {
|
|
8
|
+
return path.join(getOracleHomeDir(), 'sessions');
|
|
9
|
+
}
|
|
9
10
|
const METADATA_FILENAME = 'meta.json';
|
|
10
11
|
const LEGACY_SESSION_FILENAME = 'session.json';
|
|
11
12
|
const LEGACY_REQUEST_FILENAME = 'request.json';
|
|
@@ -22,7 +23,7 @@ async function ensureDir(dirPath) {
|
|
|
22
23
|
await fs.mkdir(dirPath, { recursive: true });
|
|
23
24
|
}
|
|
24
25
|
export async function ensureSessionStorage() {
|
|
25
|
-
await ensureDir(
|
|
26
|
+
await ensureDir(getSessionsDir());
|
|
26
27
|
}
|
|
27
28
|
function slugify(text, maxWords = MAX_SLUG_WORDS) {
|
|
28
29
|
const normalized = text?.toLowerCase() ?? '';
|
|
@@ -50,7 +51,7 @@ export function createSessionId(prompt, customSlug) {
|
|
|
50
51
|
return slugify(prompt);
|
|
51
52
|
}
|
|
52
53
|
function sessionDir(id) {
|
|
53
|
-
return path.join(
|
|
54
|
+
return path.join(getSessionsDir(), id);
|
|
54
55
|
}
|
|
55
56
|
function metaPath(id) {
|
|
56
57
|
return path.join(sessionDir(id), METADATA_FILENAME);
|
|
@@ -191,6 +192,7 @@ export async function initializeSession(options, cwd, notifications) {
|
|
|
191
192
|
browserConfig,
|
|
192
193
|
verbose: options.verbose,
|
|
193
194
|
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
195
|
+
browserAttachments: options.browserAttachments,
|
|
194
196
|
browserInlineFiles: options.browserInlineFiles,
|
|
195
197
|
browserBundleFiles: options.browserBundleFiles,
|
|
196
198
|
background: options.background,
|
|
@@ -287,7 +289,7 @@ export function createSessionLogWriter(sessionId, model) {
|
|
|
287
289
|
}
|
|
288
290
|
export async function listSessionsMetadata() {
|
|
289
291
|
await ensureSessionStorage();
|
|
290
|
-
const entries = await fs.readdir(
|
|
292
|
+
const entries = await fs.readdir(getSessionsDir()).catch(() => []);
|
|
291
293
|
const metas = [];
|
|
292
294
|
for (const entry of entries) {
|
|
293
295
|
let meta = await readSessionMetadata(entry);
|
|
@@ -379,7 +381,7 @@ export async function readSessionRequest(sessionId) {
|
|
|
379
381
|
}
|
|
380
382
|
export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
|
|
381
383
|
await ensureSessionStorage();
|
|
382
|
-
const entries = await fs.readdir(
|
|
384
|
+
const entries = await fs.readdir(getSessionsDir()).catch(() => []);
|
|
383
385
|
if (!entries.length) {
|
|
384
386
|
return { deleted: 0, remaining: 0 };
|
|
385
387
|
}
|
|
@@ -415,7 +417,7 @@ export async function deleteSessionsOlderThan({ hours = 24, includeAll = false,
|
|
|
415
417
|
export async function wait(ms) {
|
|
416
418
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
417
419
|
}
|
|
418
|
-
export {
|
|
420
|
+
export { MAX_STATUS_LIMIT };
|
|
419
421
|
export { ZOMBIE_MAX_AGE_MS };
|
|
420
422
|
export async function getSessionPaths(sessionId) {
|
|
421
423
|
const dir = sessionDir(sessionId);
|
package/dist/src/sessionStore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ensureSessionStorage, initializeSession, readSessionMetadata, updateSessionMetadata, createSessionLogWriter, readSessionLog, readModelLog, readSessionRequest, listSessionsMetadata, filterSessionsByRange, deleteSessionsOlderThan, updateModelRunMetadata, getSessionPaths,
|
|
1
|
+
import { ensureSessionStorage, initializeSession, readSessionMetadata, updateSessionMetadata, createSessionLogWriter, readSessionLog, readModelLog, readSessionRequest, listSessionsMetadata, filterSessionsByRange, deleteSessionsOlderThan, updateModelRunMetadata, getSessionPaths, getSessionsDir, } from './sessionManager.js';
|
|
2
2
|
class FileSessionStore {
|
|
3
3
|
ensureStorage() {
|
|
4
4
|
return ensureSessionStorage();
|
|
@@ -40,7 +40,7 @@ class FileSessionStore {
|
|
|
40
40
|
return getSessionPaths(sessionId);
|
|
41
41
|
}
|
|
42
42
|
sessionsDir() {
|
|
43
|
-
return
|
|
43
|
+
return getSessionsDir();
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
export const sessionStore = new FileSessionStore();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI wrapper around OpenAI Responses API with GPT-5.1
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -30,11 +30,7 @@
|
|
|
30
30
|
"name": "node",
|
|
31
31
|
"version": ">=20"
|
|
32
32
|
}
|
|
33
|
-
]
|
|
34
|
-
"packageManager": {
|
|
35
|
-
"name": "pnpm",
|
|
36
|
-
"version": ">=8"
|
|
37
|
-
}
|
|
33
|
+
]
|
|
38
34
|
},
|
|
39
35
|
"keywords": [],
|
|
40
36
|
"author": "",
|
package/dist/.DS_Store
DELETED
|
Binary file
|