@steipete/oracle 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +14 -6
- package/dist/docs-site/bridge.html +17 -1
- package/dist/docs-site/browser-mode.html +2 -2
- package/dist/docs-site/configuration.html +12 -2
- package/dist/docs-site/openai-endpoints.html +12 -0
- package/dist/src/browser/actions/assistantResponse.js +40 -31
- package/dist/src/browser/actions/attachments.js +28 -1
- package/dist/src/browser/actions/deepResearch.js +212 -67
- package/dist/src/browser/actions/modelSelection.js +30 -7
- package/dist/src/browser/actions/promptComposer.js +71 -14
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/artifacts.js +191 -6
- package/dist/src/browser/chatgptFiles.js +525 -91
- package/dist/src/browser/constants.js +5 -0
- package/dist/src/browser/index.js +30 -30
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +28 -10
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +13 -13
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { resolveDashPrompt } from "../src/cli/stdin.js";
|
|
|
15
15
|
import chalk from "chalk";
|
|
16
16
|
import { sessionStore, pruneOldSessions } from "../src/sessionStore.js";
|
|
17
17
|
import { DEFAULT_MODEL, MODEL_CONFIGS } from "../src/oracle/config.js";
|
|
18
|
-
import { isKnownModel } from "../src/oracle/modelResolver.js";
|
|
18
|
+
import { isKnownModel, resolveOverriddenApiModel } from "../src/oracle/modelResolver.js";
|
|
19
19
|
import { CHATGPT_URL } from "../src/browser/constants.js";
|
|
20
20
|
import { applyHelpStyling } from "../src/cli/help.js";
|
|
21
21
|
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, parseThinkingTimeOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
@@ -662,6 +662,7 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
662
662
|
previousResponseId: overrides.previousResponseId ?? options.previousResponseId,
|
|
663
663
|
browserResumeConversationUrl: overrides.browserResumeConversationUrl ?? options.browserResumeConversationUrl,
|
|
664
664
|
effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
|
|
665
|
+
modelOverrides: overrides.modelOverrides ?? options.modelOverrides,
|
|
665
666
|
file: overrides.file ?? options.file ?? [],
|
|
666
667
|
maxFileSizeBytes: overrides.maxFileSizeBytes ?? options.maxFileSizeBytes,
|
|
667
668
|
slug: overrides.slug ?? options.slug,
|
|
@@ -900,6 +901,7 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
900
901
|
previousResponseId: stored.previousResponseId,
|
|
901
902
|
browserResumeConversationUrl: stored.browserResumeConversationUrl,
|
|
902
903
|
effectiveModelId: stored.effectiveModelId ?? stored.model,
|
|
904
|
+
modelOverrides: stored.modelOverrides,
|
|
903
905
|
file: stored.file ?? [],
|
|
904
906
|
maxFileSizeBytes: stored.maxFileSizeBytes,
|
|
905
907
|
slug: stored.slug,
|
|
@@ -1172,11 +1174,16 @@ async function runRootCommand(options) {
|
|
|
1172
1174
|
throw new Error("--remote-host does not support --models yet. Use API engine locally instead.");
|
|
1173
1175
|
}
|
|
1174
1176
|
const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1177
|
+
// A user-config apiModel override (known models only) wins over Gemini alias
|
|
1178
|
+
// remapping and the bundled apiModel, so it becomes the on-wire request id.
|
|
1179
|
+
const apiModelOverrides = engine === "api" ? userConfig.modelOverrides : undefined;
|
|
1180
|
+
const overriddenApiModel = resolveOverriddenApiModel(resolvedModel, apiModelOverrides);
|
|
1181
|
+
const effectiveModelId = overriddenApiModel ??
|
|
1182
|
+
(resolvedModel.startsWith("gemini")
|
|
1183
|
+
? resolveGeminiModelId(resolvedModel)
|
|
1184
|
+
: isKnownModel(resolvedModel)
|
|
1185
|
+
? (MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel)
|
|
1186
|
+
: resolvedModel);
|
|
1180
1187
|
const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
|
|
1181
1188
|
const { models: _rawModels, ...optionsWithoutModels } = options;
|
|
1182
1189
|
const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
|
|
@@ -1187,6 +1194,7 @@ async function runRootCommand(options) {
|
|
|
1187
1194
|
}
|
|
1188
1195
|
resolvedOptions.baseUrl = resolvedBaseUrl;
|
|
1189
1196
|
resolvedOptions.effectiveModelId = effectiveModelId;
|
|
1197
|
+
resolvedOptions.modelOverrides = apiModelOverrides;
|
|
1190
1198
|
resolvedOptions.provider = providerMode;
|
|
1191
1199
|
resolvedOptions.writeOutputPath = resolveOutputPath(options.writeOutput, process.cwd());
|
|
1192
1200
|
if (options.status) {
|
|
@@ -251,6 +251,22 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
251
251
|
<li><strong>Host (Windows)</strong>: runs <code>oracle bridge host</code> and holds the signed-in ChatGPT session.</li>
|
|
252
252
|
<li><strong>Client (Linux)</strong>: stores the host connection once and routes browser runs (and MCP browser runs) through the host.</li>
|
|
253
253
|
</ul>
|
|
254
|
+
<h2 id="generated-artifact-transfer"><a class="anchor" href="#generated-artifact-transfer" aria-label="Anchor link">#</a>Generated artifact transfer</h2>
|
|
255
|
+
<p>Bridge runs now keep the Windows browser host and Linux client separated while still returning ChatGPT-generated files, such as ZIP, CSV, PDF, wheels, and source distributions, to a cloud-readable path. The host advertises artifact-transfer support from the token-protected <code>GET /health</code> response. The Linux client uses that capability signal in <code>oracle bridge client --test</code> and <code>oracle bridge doctor</code>; older hosts remain usable for text responses, but generated files require manual copy from the Windows browser until both sides are upgraded.</p>
|
|
256
|
+
<p>The transfer protocol is pull-based and keeps secrets local to the host:</p>
|
|
257
|
+
<ol>
|
|
258
|
+
<li>The browser host saves the ChatGPT file to its local session artifacts directory as before.</li>
|
|
259
|
+
<li>The host emits only a redacted artifact descriptor over the existing NDJSON run stream: artifact id, safe filename, MIME type, byte size, SHA-256, validation status, and coarse source kind. It does not expose cookies, bearer tokens, signed ChatGPT download URLs, or Windows filesystem paths.</li>
|
|
260
|
+
<li>The Linux client fetches <code>GET /runs/<runId>/artifacts/<artifactId></code> with the same bridge bearer token, writes to <code>~/.oracle/sessions/<sessionId>/artifacts/</code>, verifies size and SHA-256, validates ZIP structure when applicable, and only then publishes the final path in session metadata.</li>
|
|
261
|
+
<li>If transfer fails, Oracle keeps the text response and records a warning with manual fallback instructions. Open the ChatGPT browser on the Windows host, use the visible download button/link in the current assistant response, and copy the file to a cloud-readable path yourself.</li>
|
|
262
|
+
</ol>
|
|
263
|
+
<p>Operational notes:</p>
|
|
264
|
+
<ul>
|
|
265
|
+
<li>Run the same patched Oracle version on both Windows host and Linux client before relying on automatic file transfer. Mixed versions remain backward compatible for text-only runs.</li>
|
|
266
|
+
<li><code>oracle bridge doctor</code> reports <code>Artifact transfer: bridge v1</code> when the host supports the protocol, including the advertised maximum artifact size.</li>
|
|
267
|
+
<li>The default bridge transfer size limit is 512 MiB. Larger files stay on the browser host and require manual copy.</li>
|
|
268
|
+
<li>Session inspection prints artifact path, size, SHA-256 prefix, validation status, and transfer status so agents can verify whether the returned path is local to the Linux client.</li>
|
|
269
|
+
</ul>
|
|
254
270
|
<h2 id="1-windows-start-the-host-service-recommended"><a class="anchor" href="#1-windows-start-the-host-service-recommended" aria-label="Anchor link">#</a>1) Windows: start the host service (recommended)</h2>
|
|
255
271
|
<p>Run this on the Windows machine that’s signed into ChatGPT:</p>
|
|
256
272
|
<pre class="shiki github-dark-dimmed" style="background-color:var(--code-bg);color:var(--code-fg)" tabindex="0"><code class="language-powershell"><span class="line"><span style="color:#ADBAC7">oracle bridge host </span><span style="color:#F47067">--</span><span style="color:#ADBAC7">token auto </span><span style="color:#F47067">--</span><span style="color:#ADBAC7">ssh user@your</span><span style="color:#F47067">-</span><span style="color:#ADBAC7">linux</span><span style="color:#F47067">-</span><span style="color:#ADBAC7">host</span></span></code></pre>
|
|
@@ -334,7 +350,7 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
334
350
|
<li>The connection artifact and config file contain secrets; keep them private (Oracle writes them with restrictive permissions on Unix).</li>
|
|
335
351
|
<li>Bridge does <strong>not</strong> extract/decrypt cookies from arbitrary profiles; the Windows machine keeps the authenticated session locally.</li>
|
|
336
352
|
</ul><nav class="page-nav" aria-label="Pager"><a class="page-nav-prev" href="windows-work.html"><small>Previous</small><span>Windows Work</span></a><a class="page-nav-next" href="agents.html"><small>Next</small><span>Coding Agents</span></a></nav></article>
|
|
337
|
-
<nav class="toc" aria-label="On this page"><h2>On this page</h2><a class="toc-l2" href="#concepts">Concepts</a><a class="toc-l2" href="#1-windows-start-the-host-service-recommended">1) Windows: start the host service (recommended)</a><a class="toc-l2" href="#2-linux-configure-the-client-once">2) Linux: configure the client once</a><a class="toc-l2" href="#2b-linux-desktop-local-manual-login-no-bridge">2b) Linux desktop: local manual-login (no bridge)</a><a class="toc-l2" href="#3-codex-cli-mcp-integration">3) Codex CLI (MCP) integration</a><a class="toc-l2" href="#3b-claude-code-mcp-integration">3b) Claude Code (MCP) integration</a><a class="toc-l3" href="#macos-local-browser-let-them-fight">macOS local browser: Let Them Fight</a><a class="toc-l2" href="#4-troubleshooting">4) Troubleshooting</a><a class="toc-l2" href="#security-notes">Security notes</a></nav>
|
|
353
|
+
<nav class="toc" aria-label="On this page"><h2>On this page</h2><a class="toc-l2" href="#concepts">Concepts</a><a class="toc-l2" href="#generated-artifact-transfer">Generated artifact transfer</a><a class="toc-l2" href="#1-windows-start-the-host-service-recommended">1) Windows: start the host service (recommended)</a><a class="toc-l2" href="#2-linux-configure-the-client-once">2) Linux: configure the client once</a><a class="toc-l2" href="#2b-linux-desktop-local-manual-login-no-bridge">2b) Linux desktop: local manual-login (no bridge)</a><a class="toc-l2" href="#3-codex-cli-mcp-integration">3) Codex CLI (MCP) integration</a><a class="toc-l2" href="#3b-claude-code-mcp-integration">3b) Claude Code (MCP) integration</a><a class="toc-l3" href="#macos-local-browser-let-them-fight">macOS local browser: Let Them Fight</a><a class="toc-l2" href="#4-troubleshooting">4) Troubleshooting</a><a class="toc-l2" href="#security-notes">Security notes</a></nav>
|
|
338
354
|
</div>
|
|
339
355
|
</main>
|
|
340
356
|
</div>
|
|
@@ -362,11 +362,11 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
362
362
|
<span class="line"><span style="color:#6CB6FF"> --browser-manual-login</span><span style="color:#F47067"> \</span></span>
|
|
363
363
|
<span class="line"><span style="color:#6CB6FF"> --browser-research</span><span style="color:#96D0FF"> deep</span><span style="color:#F47067"> \</span></span>
|
|
364
364
|
<span class="line"><span style="color:#6CB6FF"> -p</span><span style="color:#96D0FF"> "Research the current browser support for WebGPU in enterprise-managed Chrome and cite sources."</span></span></code></pre>
|
|
365
|
-
<p>Oracle activates ChatGPT Deep Research through the composer <code
|
|
365
|
+
<p>Oracle activates ChatGPT Deep Research through the composer tools menu, recognizing both the <code>Deep research</code> label and current <code>Get a detailed report</code> menu variants. It waits for the research plan to auto-confirm, logs high-level progress, then captures the final report from the Deep Research report surface instead of trusting the assistant tool-call wrapper.</p>
|
|
366
366
|
<p>If ChatGPT initially exposes only <code>Called tool</code> / <code>Used tool</code>, Oracle treats that as an incomplete capture for Deep Research rather than a final answer. Reattach the existing session with <code>oracle session <id> --render</code> so Oracle can recover the lazy-loaded report from the existing Chrome tab; do not rerun the research unless the browser session is unrecoverable.</p>
|
|
367
367
|
<p>Deep Research is browser-only. It does not use connected apps in v1; give it public-web scope, uploaded files, and any domain/source guidance in the prompt. For deep thinking over code or architecture without web search, prefer a normal browser run with a Pro/Thinking model and <code>--browser-thinking-time heavy</code>.</p>
|
|
368
368
|
<p>Completed browser sessions also save durable artifacts under <code>~/.oracle/sessions/<id>/artifacts/</code>. Deep Research writes the extracted report to <code>deep-research-report.md</code>, and every browser run writes <code>transcript.md</code> with the prompt, final answer, conversation URL, and saved artifact references. Use <code>--write-output <path></code> when you also need a copy of just the final answer at a specific path.</p>
|
|
369
|
-
<p>When ChatGPT generates downloadable files in the assistant response (for example a ZIP, wheel, source distribution, CSV, or PDF), Oracle saves those files beside the transcript before any archive attempt. The downloader is intentionally narrow: it only follows ChatGPT-owned file/download URLs from the assistant response and uses <code>sandbox:/mnt/data/...</code> links as source metadata and filename hints, not as arbitrary fetch targets. External links in the response are left in the transcript but are not downloaded.</p>
|
|
369
|
+
<p>When ChatGPT generates downloadable files in the assistant response (for example a ZIP, wheel, source distribution, CSV, or PDF), Oracle saves those files beside the transcript before any archive attempt. The downloader is intentionally narrow: it only follows ChatGPT-owned file/download URLs from the assistant response and uses <code>sandbox:/mnt/data/...</code> links as source metadata and filename hints, not as arbitrary fetch targets. External links in the response are left in the transcript but are not downloaded. In bridge mode, a patched Windows host advertises artifact-transfer capability through <code>/health</code>; the Linux client then pulls each saved file over the authenticated bridge endpoint, stores it under the Linux session <code>artifacts/</code> directory, and verifies safe filename, byte size, SHA-256, and ZIP structure where applicable. If either side is older or transfer validation fails, the text response still completes and Oracle prints a manual-copy fallback instead of leaking host paths or signed download URLs.</p>
|
|
370
370
|
<h3 id="conversation-archiving"><a class="anchor" href="#conversation-archiving" aria-label="Anchor link">#</a>Conversation archiving</h3>
|
|
371
371
|
<p>Browser mode keeps the local session as the source of truth, so Oracle can optionally archive the ChatGPT conversation after a successful run. The default <code>--browser-archive auto</code> archives only successful non-project, non-Deep-Research, non-multi-turn one-shot chats after <code>transcript.md</code>, generated artifacts, the final answer, and the conversation URL are saved locally.</p>
|
|
372
372
|
<p>Oracle does not auto-archive failed, incomplete, running, project, Deep Research, or multi-turn sessions. Use <code>--browser-archive never</code> to disable archiving, or <code>--browser-archive always</code> when you explicitly want a successful browser conversation archived even outside the default one-shot policy. Archived chats are still visible and manageable from ChatGPT's own archive UI.</p>
|
|
@@ -307,6 +307,15 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
307
307
|
<span class="line"><span style="color:#96D0FF"> sessionRetentionHours</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">72</span><span style="color:#ADBAC7">, </span><span style="color:#768390">// prune cached sessions older than 72h before each run (0 disables)</span></span>
|
|
308
308
|
<span class="line"><span style="color:#96D0FF"> promptSuffix</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"// signed-off by me"</span><span style="color:#ADBAC7">, </span><span style="color:#768390">// appended to every prompt</span></span>
|
|
309
309
|
<span class="line"><span style="color:#96D0FF"> apiBaseUrl</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"https://api.openai.com/v1"</span><span style="color:#ADBAC7">, </span><span style="color:#768390">// override for LiteLLM / custom gateways</span></span>
|
|
310
|
+
<span class="line"><span style="color:#768390"> // API-only, user-config-only overrides for known model keys.</span></span>
|
|
311
|
+
<span class="line"><span style="color:#96D0FF"> modelOverrides</span><span style="color:#ADBAC7">: {</span></span>
|
|
312
|
+
<span class="line"><span style="color:#96D0FF"> "gpt-5.5"</span><span style="color:#ADBAC7">: {</span></span>
|
|
313
|
+
<span class="line"><span style="color:#96D0FF"> apiModel</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"gateway-model"</span><span style="color:#ADBAC7">, </span><span style="color:#768390">// on-wire id exposed by the gateway</span></span>
|
|
314
|
+
<span class="line"><span style="color:#96D0FF"> reasoning</span><span style="color:#ADBAC7">: { </span><span style="color:#96D0FF">effort</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"xhigh"</span><span style="color:#ADBAC7"> }, </span><span style="color:#768390">// or null to clear the bundled effort</span></span>
|
|
315
|
+
<span class="line"><span style="color:#96D0FF"> inputLimit</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">1050000</span><span style="color:#ADBAC7">,</span></span>
|
|
316
|
+
<span class="line"><span style="color:#96D0FF"> pricing</span><span style="color:#ADBAC7">: { </span><span style="color:#96D0FF">inputPerToken</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">0.000005</span><span style="color:#ADBAC7">, </span><span style="color:#96D0FF">outputPerToken</span><span style="color:#ADBAC7">: </span><span style="color:#6CB6FF">0.00003</span><span style="color:#ADBAC7"> },</span></span>
|
|
317
|
+
<span class="line"><span style="color:#ADBAC7"> },</span></span>
|
|
318
|
+
<span class="line"><span style="color:#ADBAC7"> },</span></span>
|
|
310
319
|
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
|
|
311
320
|
<h2 id="project-configs"><a class="anchor" href="#project-configs" aria-label="Anchor link">#</a>Project configs</h2>
|
|
312
321
|
<p>Put a project-level config at <code>.oracle/config.json</code> inside any project folder:</p>
|
|
@@ -320,17 +329,18 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
320
329
|
<span class="line"><span style="color:#ADBAC7"> },</span></span>
|
|
321
330
|
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
|
|
322
331
|
<p>Oracle discovers every <code>.oracle/config.json</code> from the current directory upward until your home directory, then applies them from parent to child after the user config. Nested objects are merged, while scalars and arrays replace earlier values. This lets a parent folder set broad defaults and override a specific ChatGPT Project URL in a package subdirectory.</p>
|
|
323
|
-
<p>Project configs intentionally support only workflow defaults. They cannot set provider routing or secret/executable fields such as <code>apiBaseUrl</code>, <code>azure</code>, <code>browser.remoteHost</code>, <code>browser.remoteToken</code>, <code>browser.chromePath</code>, or <code>browser.chromeCookiePath</code>. Keep tokens and machine-local executable/profile paths in <code>~/.oracle/config.json</code>, environment variables, or explicit CLI flags.</p>
|
|
332
|
+
<p>Project configs intentionally support only workflow defaults. They cannot set provider routing or secret/executable fields such as <code>apiBaseUrl</code>, <code>modelOverrides</code>, <code>azure</code>, <code>browser.remoteHost</code>, <code>browser.remoteToken</code>, <code>browser.chromePath</code>, or <code>browser.chromeCookiePath</code>. Keep tokens and machine-local executable/profile paths in <code>~/.oracle/config.json</code>, environment variables, or explicit CLI flags.</p>
|
|
324
333
|
<h2 id="precedence"><a class="anchor" href="#precedence" aria-label="Anchor link">#</a>Precedence</h2>
|
|
325
334
|
<p>CLI flags and explicit override environment variables → effective config (project <code>.oracle/config.json</code> files over <code>~/.oracle/config.json</code>) → auto-detected environment → built-in defaults.</p>
|
|
326
335
|
<ul>
|
|
327
336
|
<li>The effective config starts with <code>~/.oracle/config.json</code>, then layers project <code>.oracle/config.json</code> files from parent to child. <code>engine</code>, <code>model</code>, <code>search</code>, <code>filesReport</code>, <code>heartbeatSeconds</code>, <code>maxFileSizeBytes</code>, and <code>apiBaseUrl</code> in the effective config override auto-detected values unless explicitly set on the CLI or through a supported override environment variable.</li>
|
|
328
337
|
<li>Project <code>.oracle/config.json</code> files can override safe workflow defaults such as <code>engine</code>, <code>model</code>, <code>search</code>, <code>filesReport</code>, <code>heartbeatSeconds</code>, <code>maxFileSizeBytes</code>, <code>promptSuffix</code>, and allowed <code>browser.*</code> workflow settings.</li>
|
|
329
|
-
<li>Provider routing and machine-local fields (<code>apiBaseUrl</code>, <code>azure</code>, remote browser host/token defaults, Chrome binary/profile paths, cookie DB paths, and session retention cleanup) are ignored in project configs and are read only from the user config, environment variables, or explicit CLI flags.</li>
|
|
338
|
+
<li>Provider routing and machine-local fields (<code>apiBaseUrl</code>, <code>modelOverrides</code>, <code>azure</code>, remote browser host/token defaults, Chrome binary/profile paths, cookie DB paths, and session retention cleanup) are ignored in project configs and are read only from the user config, environment variables, or explicit CLI flags.</li>
|
|
330
339
|
<li><code>ORACLE_ENGINE=api|browser</code> is a global override for engine selection (useful for MCP/Codex setups); it wins over <code>config.json</code>.</li>
|
|
331
340
|
<li>If <code>azure.endpoint</code> (or <code>--azure-endpoint</code>) is set, Oracle reads <code>AZURE_OPENAI_API_KEY</code> first and falls back to <code>OPENAI_API_KEY</code> for GPT models.</li>
|
|
332
341
|
<li>Remote browser defaults follow the same order: <code>--remote-host/--remote-token</code> win, then <code>browser.remoteHost</code> / <code>browser.remoteToken</code> in the config, then <code>ORACLE_REMOTE_HOST</code> / <code>ORACLE_REMOTE_TOKEN</code> if still unset.</li>
|
|
333
342
|
<li><code>OPENAI_API_KEY</code> only influences engine selection when neither the CLI nor <code>config.json</code> specify an engine (API when present, otherwise browser).</li>
|
|
343
|
+
<li><code>modelOverrides</code> applies only to API runs and existing built-in model keys. It can replace the on-wire <code>apiModel</code>, reasoning effort, input limit, and per-token pricing; unspecified fields and the bundled tokenizer remain unchanged. Invalid override values are ignored. Project configs cannot set this field.</li>
|
|
334
344
|
<li><code>ORACLE_NOTIFY*</code> env vars still layer on top of the config’s <code>notify</code> block.</li>
|
|
335
345
|
<li><code>sessionRetentionHours</code> controls the default value for <code>--retain-hours</code>. When unset, <code>ORACLE_RETAIN_HOURS</code> (if present) becomes the fallback, and the CLI flag still wins over both.</li>
|
|
336
346
|
<li><code>ORACLE_MAX_FILE_SIZE_BYTES</code> overrides <code>maxFileSizeBytes</code> when set. Oracle validates it as a positive integer number of bytes before reading any <code>--file</code> inputs.</li>
|
|
@@ -292,6 +292,18 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
292
292
|
<pre class="shiki github-dark-dimmed" style="background-color:var(--code-bg);color:var(--code-fg)" tabindex="0"><code class="language-json"><span class="line"><span style="color:#ADBAC7">{</span></span>
|
|
293
293
|
<span class="line"><span style="color:#8DDB8C"> "apiBaseUrl"</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"http://localhost:4000"</span></span>
|
|
294
294
|
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
|
|
295
|
+
<p>If a gateway exposes a different on-wire model id, add an API-only override to the user config at <code>~/.oracle/config.json</code>:</p>
|
|
296
|
+
<pre class="shiki github-dark-dimmed" style="background-color:var(--code-bg);color:var(--code-fg)" tabindex="0"><code class="language-json5"><span class="line"><span style="color:#ADBAC7">{</span></span>
|
|
297
|
+
<span class="line"><span style="color:#96D0FF"> apiBaseUrl</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"http://localhost:4000"</span><span style="color:#ADBAC7">,</span></span>
|
|
298
|
+
<span class="line"><span style="color:#96D0FF"> model</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"gpt-5.5"</span><span style="color:#ADBAC7">,</span></span>
|
|
299
|
+
<span class="line"><span style="color:#96D0FF"> modelOverrides</span><span style="color:#ADBAC7">: {</span></span>
|
|
300
|
+
<span class="line"><span style="color:#96D0FF"> "gpt-5.5"</span><span style="color:#ADBAC7">: {</span></span>
|
|
301
|
+
<span class="line"><span style="color:#96D0FF"> apiModel</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"gateway-model"</span><span style="color:#ADBAC7">,</span></span>
|
|
302
|
+
<span class="line"><span style="color:#96D0FF"> reasoning</span><span style="color:#ADBAC7">: { </span><span style="color:#96D0FF">effort</span><span style="color:#ADBAC7">: </span><span style="color:#96D0FF">"high"</span><span style="color:#ADBAC7"> },</span></span>
|
|
303
|
+
<span class="line"><span style="color:#ADBAC7"> },</span></span>
|
|
304
|
+
<span class="line"><span style="color:#ADBAC7"> },</span></span>
|
|
305
|
+
<span class="line"><span style="color:#ADBAC7">}</span></span></code></pre>
|
|
306
|
+
<p>Overrides accept existing built-in model keys only and are ignored in project <code>.oracle/config.json</code> files. See <a href="configuration.html">Local configuration</a> for the full field list and precedence.</p>
|
|
295
307
|
<h2 id="model-aliases"><a class="anchor" href="#model-aliases" aria-label="Anchor link">#</a>Model aliases</h2>
|
|
296
308
|
<p>Oracle keeps a stable CLI-facing model set, but some names are aliases for the concrete API model ids it sends:</p>
|
|
297
309
|
<ul>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR,
|
|
1
|
+
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
|
|
2
2
|
import { delay } from "../utils.js";
|
|
3
3
|
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
|
|
4
4
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
5
|
const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
|
|
6
|
+
const STOP_CONTROL_SELECTOR = STOP_BUTTON_SELECTORS.join(", ");
|
|
6
7
|
const THINKING_STATUS_LABELS = [
|
|
7
8
|
"thinking",
|
|
8
9
|
"pro thinking",
|
|
@@ -44,8 +45,8 @@ function isAnswerNowPlaceholderText(normalized) {
|
|
|
44
45
|
}
|
|
45
46
|
function buildActiveThinkingStatusPredicateJs(fnName) {
|
|
46
47
|
const labelsLiteral = JSON.stringify(THINKING_STATUS_LABELS);
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
return `${buildStopButtonVisibilityPredicateJs("isStopControlVisible")}
|
|
49
|
+
const ${fnName} = (snapshot) => {
|
|
49
50
|
const normalized = String(snapshot?.text ?? '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
50
51
|
if (!normalized) return false;
|
|
51
52
|
const labels = ${labelsLiteral};
|
|
@@ -53,7 +54,7 @@ function buildActiveThinkingStatusPredicateJs(fnName) {
|
|
|
53
54
|
labels.includes(normalized) ||
|
|
54
55
|
(normalized.startsWith('thought for ') && normalized.length <= 40) ||
|
|
55
56
|
(normalized.startsWith('pro thinking') && normalized.length <= 40);
|
|
56
|
-
return matches &&
|
|
57
|
+
return matches && isStopControlVisible();
|
|
57
58
|
};`;
|
|
58
59
|
}
|
|
59
60
|
export function matchesThinkingStatusLabelForTest(text) {
|
|
@@ -166,16 +167,19 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
166
167
|
isStopButtonVisible(Runtime),
|
|
167
168
|
isCompletionVisible(Runtime),
|
|
168
169
|
]);
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
// Completion controls can appear briefly while Pro is still replacing its thinking UI.
|
|
171
|
+
// Confirm every capture from that transition with the stability-based watchdog; a
|
|
172
|
+
// partial first paragraph can be arbitrarily long.
|
|
173
|
+
const candidateText = String(candidate?.text ?? "").trim();
|
|
174
|
+
if (stopVisible || completionVisible) {
|
|
175
|
+
logger(stopVisible
|
|
176
|
+
? "Assistant still generating; waiting for completion"
|
|
177
|
+
: "Completion controls surfaced; confirming stable assistant response");
|
|
171
178
|
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
172
|
-
if (completed) {
|
|
179
|
+
if (completed && String(completed.text ?? "").trim().length >= candidateText.length) {
|
|
173
180
|
return completed;
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
|
-
else if (completionVisible) {
|
|
177
|
-
// No-op: completion UI surfaced and stop button is gone.
|
|
178
|
-
}
|
|
179
183
|
}
|
|
180
184
|
return candidate;
|
|
181
185
|
}
|
|
@@ -390,7 +394,7 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expecte
|
|
|
390
394
|
async function isStopButtonVisible(Runtime) {
|
|
391
395
|
try {
|
|
392
396
|
const { result } = await Runtime.evaluate({
|
|
393
|
-
expression:
|
|
397
|
+
expression: buildStopButtonVisibilityExpression(),
|
|
394
398
|
returnByValue: true,
|
|
395
399
|
});
|
|
396
400
|
return Boolean(result?.value);
|
|
@@ -399,6 +403,30 @@ async function isStopButtonVisible(Runtime) {
|
|
|
399
403
|
return false;
|
|
400
404
|
}
|
|
401
405
|
}
|
|
406
|
+
function buildStopButtonVisibilityExpression() {
|
|
407
|
+
return `(() => {
|
|
408
|
+
${buildStopButtonVisibilityPredicateJs("isStopControlVisible")}
|
|
409
|
+
return isStopControlVisible();
|
|
410
|
+
})()`;
|
|
411
|
+
}
|
|
412
|
+
function buildStopButtonVisibilityPredicateJs(fnName) {
|
|
413
|
+
const selectorLiteral = JSON.stringify(STOP_CONTROL_SELECTOR);
|
|
414
|
+
return `const ${fnName} = () => {
|
|
415
|
+
const isVisible = (node) => {
|
|
416
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
417
|
+
const rect = node.getBoundingClientRect();
|
|
418
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
419
|
+
const style = window.getComputedStyle(node);
|
|
420
|
+
return !(
|
|
421
|
+
style.display === 'none' ||
|
|
422
|
+
style.visibility === 'hidden' ||
|
|
423
|
+
(style.opacity !== '' && Number(style.opacity) === 0)
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
return Array.from(document.querySelectorAll(${selectorLiteral})).some((node) => isVisible(node));
|
|
427
|
+
};`;
|
|
428
|
+
}
|
|
429
|
+
export const buildStopButtonVisibilityExpressionForTest = buildStopButtonVisibilityExpression;
|
|
402
430
|
async function isCompletionVisible(Runtime) {
|
|
403
431
|
try {
|
|
404
432
|
const { result } = await Runtime.evaluate({
|
|
@@ -540,7 +568,7 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
540
568
|
return `(() => {
|
|
541
569
|
${buildClickDispatcher()}
|
|
542
570
|
const SELECTORS = ${selectorsLiteral};
|
|
543
|
-
const STOP_SELECTOR =
|
|
571
|
+
const STOP_SELECTOR = ${JSON.stringify(STOP_CONTROL_SELECTOR)};
|
|
544
572
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
545
573
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
546
574
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
@@ -598,7 +626,6 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
598
626
|
const captureViaObserver = () =>
|
|
599
627
|
new Promise((resolve, reject) => {
|
|
600
628
|
const deadline = Date.now() + ${timeoutMs};
|
|
601
|
-
let stopInterval = null;
|
|
602
629
|
let timeoutId = null;
|
|
603
630
|
let cleanedUp = false;
|
|
604
631
|
let observer = null;
|
|
@@ -607,10 +634,6 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
607
634
|
const cleanup = () => {
|
|
608
635
|
if (cleanedUp) return;
|
|
609
636
|
cleanedUp = true;
|
|
610
|
-
if (stopInterval) {
|
|
611
|
-
clearInterval(stopInterval);
|
|
612
|
-
stopInterval = null;
|
|
613
|
-
}
|
|
614
637
|
if (timeoutId) {
|
|
615
638
|
clearTimeout(timeoutId);
|
|
616
639
|
timeoutId = null;
|
|
@@ -662,20 +685,6 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
662
685
|
observer = new MutationObserver(observerCallback);
|
|
663
686
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
664
687
|
|
|
665
|
-
stopInterval = setInterval(() => {
|
|
666
|
-
if (cleanedUp) return;
|
|
667
|
-
const stop = document.querySelector(STOP_SELECTOR);
|
|
668
|
-
if (!stop) {
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const isStopButton =
|
|
672
|
-
stop.getAttribute('data-testid') === 'stop-button' || stop.getAttribute('aria-label')?.toLowerCase()?.includes('stop');
|
|
673
|
-
if (isStopButton) {
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
dispatchClickSequence(stop);
|
|
677
|
-
}, 500);
|
|
678
|
-
|
|
679
688
|
timeoutId = setTimeout(() => {
|
|
680
689
|
cleanup();
|
|
681
690
|
reject(new Error('Response timeout'));
|
|
@@ -1173,7 +1173,14 @@ export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
|
1173
1173
|
export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNames = [], logger) {
|
|
1174
1174
|
const deadline = Date.now() + timeoutMs;
|
|
1175
1175
|
const expectedNormalized = expectedNames.map((name) => name.toLowerCase());
|
|
1176
|
+
const expectedInputBasenames = expectedNormalized
|
|
1177
|
+
.map((name) => name.split("/").pop()?.split("\\").pop() ?? name)
|
|
1178
|
+
.map((name) => name.toLowerCase().replace(/\s+/g, " ").trim())
|
|
1179
|
+
.filter(Boolean);
|
|
1180
|
+
const expectedInputSignature = expectedInputBasenames.slice().sort().join("\0");
|
|
1176
1181
|
let inputMatchSince = null;
|
|
1182
|
+
let inputOnlyReadySince = null;
|
|
1183
|
+
let inputOnlySignature = "";
|
|
1177
1184
|
let sawInputMatch = false;
|
|
1178
1185
|
let attachmentMatchSince = null;
|
|
1179
1186
|
let lastVerboseLog = 0;
|
|
@@ -1466,6 +1473,26 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1466
1473
|
else {
|
|
1467
1474
|
attachmentMatchSince = null;
|
|
1468
1475
|
}
|
|
1476
|
+
const inputOnlySignatureNow = inputNames.slice().sort().join("\0");
|
|
1477
|
+
const inputOnlyNamesSatisfied = expectedInputBasenames.length > 0 && inputOnlySignatureNow === expectedInputSignature;
|
|
1478
|
+
const inputOnlyReady = inputOnlyNamesSatisfied &&
|
|
1479
|
+
value.state === "ready" &&
|
|
1480
|
+
value.uploading === false &&
|
|
1481
|
+
!value.filesAttached &&
|
|
1482
|
+
fileCount === 0;
|
|
1483
|
+
if (inputOnlyReady) {
|
|
1484
|
+
if (inputOnlyReadySince === null || inputOnlySignature !== inputOnlySignatureNow) {
|
|
1485
|
+
inputOnlyReadySince = Date.now();
|
|
1486
|
+
inputOnlySignature = inputOnlySignatureNow;
|
|
1487
|
+
}
|
|
1488
|
+
if (Date.now() - inputOnlyReadySince > 1500) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
inputOnlyReadySince = null;
|
|
1494
|
+
inputOnlySignature = "";
|
|
1495
|
+
}
|
|
1469
1496
|
// Fallback: if the file input has the expected names, allow progress once that condition is stable.
|
|
1470
1497
|
// Some ChatGPT surfaces only render the filename after sending the message.
|
|
1471
1498
|
const inputMissing = expectedNormalized.filter((expected) => {
|
|
@@ -1478,7 +1505,7 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1478
1505
|
// Don't include 'disabled' - a disabled button likely means upload is still in progress.
|
|
1479
1506
|
const inputStateOk = value.state === "ready" || value.state === "missing";
|
|
1480
1507
|
const inputSeenNow = inputMissing.length === 0 || fileCountSatisfied;
|
|
1481
|
-
const inputEvidenceOk = Boolean(value.filesAttached) ||
|
|
1508
|
+
const inputEvidenceOk = Boolean(value.filesAttached) || fileCountSatisfied;
|
|
1482
1509
|
const stableThresholdMs = value.uploading ? 3000 : 1500;
|
|
1483
1510
|
if (inputSeenNow && inputStateOk && inputEvidenceOk) {
|
|
1484
1511
|
if (inputMatchSince === null) {
|