@steipete/oracle 0.8.6 → 0.9.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 +76 -4
- package/dist/bin/oracle-cli.js +188 -7
- package/dist/src/browser/actions/modelSelection.js +60 -8
- package/dist/src/browser/actions/navigation.js +2 -1
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +73 -19
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +245 -0
- package/dist/src/browser/providers/index.js +2 -0
- package/dist/src/cli/browserConfig.js +12 -6
- package/dist/src/cli/detach.js +5 -2
- package/dist/src/cli/fileSize.js +11 -0
- package/dist/src/cli/help.js +3 -3
- package/dist/src/cli/markdownBundle.js +5 -1
- package/dist/src/cli/options.js +40 -3
- package/dist/src/cli/runOptions.js +11 -3
- package/dist/src/cli/sessionDisplay.js +91 -2
- package/dist/src/cli/sessionLineage.js +56 -0
- package/dist/src/cli/sessionRunner.js +20 -2
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/cli/tui/index.js +2 -0
- package/dist/src/gemini-web/browserSessionManager.js +76 -0
- package/dist/src/gemini-web/client.js +16 -5
- package/dist/src/gemini-web/executionClients.js +1 -0
- package/dist/src/gemini-web/executionMode.js +18 -0
- package/dist/src/gemini-web/executor.js +273 -120
- package/dist/src/mcp/tools/consult.js +34 -21
- package/dist/src/oracle/client.js +42 -13
- package/dist/src/oracle/config.js +43 -7
- package/dist/src/oracle/errors.js +2 -2
- package/dist/src/oracle/files.js +20 -5
- package/dist/src/oracle/gemini.js +3 -0
- package/dist/src/oracle/request.js +7 -2
- package/dist/src/oracle/run.js +22 -12
- package/dist/src/sessionManager.js +4 -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 +18 -18
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md
CHANGED
|
@@ -11,7 +11,39 @@
|
|
|
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.
|
|
14
|
+
Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.4 Pro (default), GPT-5.4, GPT-5.1 Pro, GPT-5.1 Codex (API-only), GPT-5.1, GPT-5.2, Gemini 3.1 Pro (API-only), 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; use `--browser-model-strategy current` to keep the active ChatGPT model (or `ignore` to skip the picker). API remains the most reliable path, and `--copy` is an easy manual fallback.
|
|
15
|
+
|
|
16
|
+
## Setting up (macOS Browser Mode)
|
|
17
|
+
|
|
18
|
+
Browser mode lets you use GPT-5.2 Pro without any API keys — it automates your Chrome browser directly.
|
|
19
|
+
|
|
20
|
+
### First-time login
|
|
21
|
+
|
|
22
|
+
Run this once to create an automation profile and log into ChatGPT. The browser will stay open so you can complete the login:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
oracle --engine browser --browser-manual-login \
|
|
26
|
+
--browser-keep-browser --browser-input-timeout 120000 \
|
|
27
|
+
-p "HI"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Subsequent runs
|
|
31
|
+
|
|
32
|
+
Once logged in, the automation profile is saved. Use this for all future runs:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
oracle --engine browser --browser-manual-login \
|
|
36
|
+
--browser-auto-reattach-delay 5s \
|
|
37
|
+
--browser-auto-reattach-interval 3s \
|
|
38
|
+
--browser-auto-reattach-timeout 60s \
|
|
39
|
+
-p "your prompt"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **Why these flags?**
|
|
43
|
+
> - `--browser-manual-login` — Skips macOS Keychain cookie access (avoids repeated permission popups)
|
|
44
|
+
> - `--browser-auto-reattach-*` — Reconnects when ChatGPT redirects mid-page-load (fixes "Inspected target navigated or closed" error)
|
|
45
|
+
> - `--browser-keep-browser` — Keeps browser open for first-time login (not needed after)
|
|
46
|
+
> - `--browser-input-timeout 120000` — Gives you 2 minutes to log in on first run
|
|
15
47
|
|
|
16
48
|
## Quick start
|
|
17
49
|
|
|
@@ -30,6 +62,12 @@ npx -y @steipete/oracle -p "Write a concise architecture note for the storage ad
|
|
|
30
62
|
# Multi-model API run
|
|
31
63
|
npx -y @steipete/oracle -p "Cross-check the data layer assumptions" --models gpt-5.1-pro,gemini-3-pro --file "src/**/*.ts"
|
|
32
64
|
|
|
65
|
+
# Follow up from an existing OpenAI/Azure session id
|
|
66
|
+
npx -y @steipete/oracle --engine api --model gpt-5.2-pro --followup release-readiness-audit --followup-model gpt-5.2-pro -p "Re-evaluate with this new context" --file "src/**/*.ts"
|
|
67
|
+
|
|
68
|
+
# Follow up directly from an OpenAI Responses API id
|
|
69
|
+
npx -y @steipete/oracle --engine api --model gpt-5.2-pro --followup resp_abc1234567890 -p "Continue from this response" --file docs/notes.md
|
|
70
|
+
|
|
33
71
|
# Preview without spending tokens
|
|
34
72
|
npx -y @steipete/oracle --dry-run summary -p "Check release notes" --file docs/release-notes.md
|
|
35
73
|
|
|
@@ -53,7 +91,7 @@ Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser i
|
|
|
53
91
|
## Integration
|
|
54
92
|
|
|
55
93
|
**CLI**
|
|
56
|
-
- API mode expects API keys in your environment: `OPENAI_API_KEY` (GPT-5.x), `GEMINI_API_KEY` (Gemini 3 Pro), `ANTHROPIC_API_KEY` (Claude Sonnet 4.5 / Opus 4.1).
|
|
94
|
+
- API mode expects API keys in your environment: `OPENAI_API_KEY` (GPT-5.x), `GEMINI_API_KEY` (Gemini 3.1 Pro / Gemini 3 Pro), `ANTHROPIC_API_KEY` (Claude Sonnet 4.5 / Opus 4.1).
|
|
57
95
|
- Gemini browser mode uses Chrome cookies instead of an API key—just be logged into `gemini.google.com` in Chrome (no Python/venv required).
|
|
58
96
|
- If your Gemini account can’t access “Pro”, Oracle auto-falls back to a supported model for web runs (and logs the fallback in verbose mode).
|
|
59
97
|
- Prefer API mode or `--copy` + manual paste; browser automation is experimental.
|
|
@@ -96,11 +134,43 @@ npx -y @steipete/oracle oracle-mcp
|
|
|
96
134
|
- Multi-model API runs with aggregated cost/usage, including OpenRouter IDs alongside first-party models.
|
|
97
135
|
- Render/copy bundles for manual paste into ChatGPT when automation is blocked.
|
|
98
136
|
- GPT‑5 Pro API runs detach by default; reattach via `oracle session <id>` / `oracle status` or block with `--wait`.
|
|
137
|
+
- OpenAI/Azure follow-up API runs can continue from `--followup <sessionId|responseId>`; for multi-model parents, add `--followup-model <model>`.
|
|
99
138
|
- Azure endpoints supported via `--azure-endpoint/--azure-deployment/--azure-api-version` or `AZURE_OPENAI_*` envs.
|
|
100
139
|
- File safety: globs/excludes, size guards, `--files-report`.
|
|
101
140
|
- Sessions you can replay (`oracle status`, `oracle session <id> --render`).
|
|
102
141
|
- Session logs and bundles live in `~/.oracle/sessions` (override with `ORACLE_HOME_DIR`).
|
|
103
142
|
|
|
143
|
+
## Follow-up and lineage
|
|
144
|
+
|
|
145
|
+
Use `--followup` to continue an existing OpenAI/Azure Responses API run with additional context/files:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
oracle \
|
|
149
|
+
--engine api \
|
|
150
|
+
--model gpt-5.2-pro \
|
|
151
|
+
--followup <existing-session-id-or-resp_id> \
|
|
152
|
+
--followup-model gpt-5.2-pro \
|
|
153
|
+
--slug "my-followup-run" \
|
|
154
|
+
--wait \
|
|
155
|
+
-p "Follow-up: re-evaluate the previous recommendation with the attached files." \
|
|
156
|
+
--file "server/src/strategy/plan.ts" \
|
|
157
|
+
--file "server/src/strategy/executor.ts"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
When the parent session used `--models`, `--followup-model` picks which model's response id to chain from.
|
|
161
|
+
Custom `--base-url` providers plus Gemini/Claude API runs are excluded here because they do not preserve `previous_response_id` in Oracle.
|
|
162
|
+
|
|
163
|
+
`oracle status` shows parent/child lineage in tree form:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
Recent Sessions
|
|
167
|
+
Status Model Mode Timestamp Chars Cost Slug
|
|
168
|
+
completed gpt-5.2-pro api 03/01/2026 09:00 AM 1800 $2.110 architecture-review-parent
|
|
169
|
+
completed gpt-5.2-pro api 03/01/2026 09:14 AM 2200 $2.980 ├─ architecture-review-followup
|
|
170
|
+
running gpt-5.2-pro api 03/01/2026 09:22 AM 1400 - │ └─ architecture-review-implementation-pass
|
|
171
|
+
pending gpt-5.2-pro api 03/01/2026 09:25 AM 900 - └─ architecture-review-risk-check
|
|
172
|
+
```
|
|
173
|
+
|
|
104
174
|
## Browser auto-reattach (long Pro runs)
|
|
105
175
|
|
|
106
176
|
When browser runs time out (common with long GPT‑5.x Pro responses), Oracle can keep polling the existing ChatGPT tab and capture the final answer without manual `oracle session <id>` commands.
|
|
@@ -126,8 +196,10 @@ oracle --engine browser \
|
|
|
126
196
|
| `-p, --prompt <text>` | Required prompt. |
|
|
127
197
|
| `-f, --file <paths...>` | Attach files/dirs (globs + `!` excludes). |
|
|
128
198
|
| `-e, --engine <api\|browser>` | Choose API or browser (browser is experimental). |
|
|
129
|
-
| `-m, --model <name>` | Built-ins (`gpt-5.
|
|
199
|
+
| `-m, --model <name>` | Built-ins (`gpt-5.4-pro` default, `gpt-5.4`, `gpt-5.1-pro`, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.2`, `gpt-5.2-instant`, `gpt-5.2-pro`, `gemini-3.1-pro` API-only, `gemini-3-pro`, `claude-4.5-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
|
|
130
200
|
| `--models <list>` | Comma-separated API models (mix built-ins and OpenRouter ids) for multi-model runs. |
|
|
201
|
+
| `--followup <sessionId\|responseId>` | Continue an OpenAI/Azure Responses API run from a stored oracle session or `resp_...` response id. |
|
|
202
|
+
| `--followup-model <model>` | For multi-model OpenAI/Azure parent sessions, choose which model response to continue from. |
|
|
131
203
|
| `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
|
|
132
204
|
| `--chatgpt-url <url>` | Target a ChatGPT workspace/folder (browser). |
|
|
133
205
|
| `--browser-model-strategy <select\|current\|ignore>` | Control ChatGPT model selection in browser mode (current keeps the active model; ignore skips the picker). |
|
|
@@ -161,7 +233,7 @@ oracle --engine browser \
|
|
|
161
233
|
Put defaults in `~/.oracle/config.json` (JSON5). Example:
|
|
162
234
|
```json5
|
|
163
235
|
{
|
|
164
|
-
model: "gpt-5.
|
|
236
|
+
model: "gpt-5.4-pro",
|
|
165
237
|
engine: "api",
|
|
166
238
|
filesReport: true,
|
|
167
239
|
browser: {
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -47,6 +47,7 @@ import { loadUserConfig } from '../src/config.js';
|
|
|
47
47
|
import { applyBrowserDefaultsFromConfig } from '../src/cli/browserDefaults.js';
|
|
48
48
|
import { shouldBlockDuplicatePrompt } from '../src/cli/duplicatePromptGuard.js';
|
|
49
49
|
import { resolveRemoteServiceConfig } from '../src/remote/remoteServiceConfig.js';
|
|
50
|
+
import { resolveConfiguredMaxFileSizeBytes } from '../src/cli/fileSize.js';
|
|
50
51
|
const VERSION = getCliVersion();
|
|
51
52
|
const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
|
|
52
53
|
const LEGACY_FLAG_ALIASES = new Map([
|
|
@@ -98,12 +99,14 @@ program.hook('preAction', (thisCommand) => {
|
|
|
98
99
|
});
|
|
99
100
|
program
|
|
100
101
|
.name('oracle')
|
|
101
|
-
.description('One-shot GPT-5.
|
|
102
|
+
.description('One-shot GPT-5.4 Pro / GPT-5.4 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
|
|
102
103
|
.version(VERSION)
|
|
103
104
|
.argument('[prompt]', 'Prompt text (shorthand for --prompt).')
|
|
104
105
|
.option('-p, --prompt <text>', 'User prompt to send to the model.')
|
|
105
106
|
.addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
|
|
106
|
-
.option('
|
|
107
|
+
.option('--followup <sessionId|responseId>', 'Continue an OpenAI/Azure Responses API run from a stored response id (resp_...) or from a stored oracle session id.')
|
|
108
|
+
.option('--followup-model <model>', 'When following up a multi-model session, choose which model response to continue from.')
|
|
109
|
+
.option('-f, --file <paths...>', 'Files/directories or glob patterns to attach (prefix with !pattern to exclude). Oversized files are rejected automatically (default cap: 1 MB; configurable via ORACLE_MAX_FILE_SIZE_BYTES or config.maxFileSizeBytes).', collectPaths, [])
|
|
107
110
|
.addOption(new Option('--include <paths...>', 'Alias for --file.')
|
|
108
111
|
.argParser(collectPaths)
|
|
109
112
|
.default([])
|
|
@@ -123,8 +126,8 @@ program
|
|
|
123
126
|
.addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
|
|
124
127
|
.addOption(new Option('--copy').hideHelp().default(false))
|
|
125
128
|
.option('-s, --slug <words>', 'Custom session slug (3-5 words).')
|
|
126
|
-
.option('-m, --model <model>', 'Model to target (gpt-5.
|
|
127
|
-
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.
|
|
129
|
+
.option('-m, --model <model>', 'Model to target (gpt-5.4-pro default). Also gpt-5.4, gpt-5.1-pro, 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.1-pro API-only, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).', normalizeModelOption)
|
|
130
|
+
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.4-pro,gemini-3-pro").')
|
|
128
131
|
.argParser(collectModelList)
|
|
129
132
|
.default([]))
|
|
130
133
|
.addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
|
|
@@ -135,7 +138,7 @@ program
|
|
|
135
138
|
.addOption(new Option('--no-notify', 'Disable desktop notifications.').default(undefined))
|
|
136
139
|
.addOption(new Option('--notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
|
|
137
140
|
.addOption(new Option('--no-notify-sound', 'Disable notification sounds.').default(undefined))
|
|
138
|
-
.addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.
|
|
141
|
+
.addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.4-pro, 120s otherwise).')
|
|
139
142
|
.argParser(parseTimeoutOption)
|
|
140
143
|
.default('auto'))
|
|
141
144
|
.addOption(new Option('--background', 'Use Responses API background mode (create + retrieve) for API runs.').default(undefined))
|
|
@@ -413,8 +416,10 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
413
416
|
prompt: options.prompt,
|
|
414
417
|
model: options.model,
|
|
415
418
|
models: overrides.models ?? options.models,
|
|
419
|
+
previousResponseId: overrides.previousResponseId ?? options.previousResponseId,
|
|
416
420
|
effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
|
|
417
421
|
file: overrides.file ?? options.file ?? [],
|
|
422
|
+
maxFileSizeBytes: overrides.maxFileSizeBytes ?? options.maxFileSizeBytes,
|
|
418
423
|
slug: overrides.slug ?? options.slug,
|
|
419
424
|
filesReport: overrides.filesReport ?? options.filesReport,
|
|
420
425
|
maxInput: overrides.maxInput ?? options.maxInput,
|
|
@@ -454,14 +459,141 @@ function resolveHeartbeatIntervalMs(seconds) {
|
|
|
454
459
|
}
|
|
455
460
|
return Math.round(seconds * 1000);
|
|
456
461
|
}
|
|
462
|
+
function assertFollowupSupported({ engine, model, baseUrl, azureEndpoint, }) {
|
|
463
|
+
if (engine !== 'api') {
|
|
464
|
+
throw new Error('--followup requires --engine api.');
|
|
465
|
+
}
|
|
466
|
+
if (model.startsWith('gemini') || model.startsWith('claude')) {
|
|
467
|
+
throw new Error(`--followup is only supported for OpenAI Responses API runs. Model ${model} uses a provider client without previous_response_id support.`);
|
|
468
|
+
}
|
|
469
|
+
if (baseUrl && !azureEndpoint) {
|
|
470
|
+
throw new Error('--followup is only supported for the default OpenAI Responses API or Azure OpenAI Responses. Custom --base-url providers are not supported.');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function levenshteinDistance(a, b) {
|
|
474
|
+
if (a === b)
|
|
475
|
+
return 0;
|
|
476
|
+
if (a.length === 0)
|
|
477
|
+
return b.length;
|
|
478
|
+
if (b.length === 0)
|
|
479
|
+
return a.length;
|
|
480
|
+
const previous = new Array(b.length + 1);
|
|
481
|
+
const current = new Array(b.length + 1);
|
|
482
|
+
for (let j = 0; j <= b.length; j += 1) {
|
|
483
|
+
previous[j] = j;
|
|
484
|
+
}
|
|
485
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
486
|
+
current[0] = i;
|
|
487
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
488
|
+
const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
489
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + substitutionCost);
|
|
490
|
+
}
|
|
491
|
+
for (let j = 0; j <= b.length; j += 1) {
|
|
492
|
+
previous[j] = current[j];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return previous[b.length];
|
|
496
|
+
}
|
|
497
|
+
function scoreSessionSimilarity(input, candidate) {
|
|
498
|
+
if (input === candidate)
|
|
499
|
+
return 1;
|
|
500
|
+
if (candidate.startsWith(input) || input.startsWith(candidate))
|
|
501
|
+
return 0.95;
|
|
502
|
+
if (candidate.includes(input) || input.includes(candidate))
|
|
503
|
+
return 0.8;
|
|
504
|
+
const distance = levenshteinDistance(input, candidate);
|
|
505
|
+
const maxLength = Math.max(input.length, candidate.length);
|
|
506
|
+
if (maxLength === 0)
|
|
507
|
+
return 0;
|
|
508
|
+
return Math.max(0, 1 - distance / maxLength);
|
|
509
|
+
}
|
|
510
|
+
async function suggestFollowupSessionIds(input, limit = 3) {
|
|
511
|
+
const normalizedInput = input.trim().toLowerCase();
|
|
512
|
+
if (!normalizedInput)
|
|
513
|
+
return [];
|
|
514
|
+
const sessions = await sessionStore.listSessions().catch(() => []);
|
|
515
|
+
const seen = new Set();
|
|
516
|
+
const ranked = sessions
|
|
517
|
+
.map((meta) => meta.id)
|
|
518
|
+
.filter((id) => typeof id === 'string' && id.length > 0)
|
|
519
|
+
.filter((id) => {
|
|
520
|
+
if (seen.has(id))
|
|
521
|
+
return false;
|
|
522
|
+
seen.add(id);
|
|
523
|
+
return true;
|
|
524
|
+
})
|
|
525
|
+
.map((id) => ({ id, score: scoreSessionSimilarity(normalizedInput, id.toLowerCase()) }))
|
|
526
|
+
.filter((entry) => entry.score >= 0.45)
|
|
527
|
+
.sort((a, b) => b.score - a.score)
|
|
528
|
+
.slice(0, limit);
|
|
529
|
+
return ranked.map((entry) => entry.id);
|
|
530
|
+
}
|
|
531
|
+
async function resolveFollowupReference(value, followupModel) {
|
|
532
|
+
const trimmed = value.trim();
|
|
533
|
+
if (trimmed.length === 0) {
|
|
534
|
+
throw new Error('--followup requires a session id or response id.');
|
|
535
|
+
}
|
|
536
|
+
if (trimmed.startsWith('resp_')) {
|
|
537
|
+
return { responseId: trimmed };
|
|
538
|
+
}
|
|
539
|
+
// Treat as oracle session id (slug).
|
|
540
|
+
const meta = await sessionStore.readSession(trimmed);
|
|
541
|
+
if (!meta) {
|
|
542
|
+
const suggestions = await suggestFollowupSessionIds(trimmed);
|
|
543
|
+
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.map((id) => `"${id}"`).join(', ')}?` : '';
|
|
544
|
+
throw new Error(`No session found with ID ${trimmed}.${suggestionText} Run "oracle status --hours 72 --limit 20" to list recent sessions.`);
|
|
545
|
+
}
|
|
546
|
+
const fromMetadata = extractResponseIdFromSession(meta, followupModel);
|
|
547
|
+
if (fromMetadata) {
|
|
548
|
+
return { responseId: fromMetadata, sessionId: meta.id };
|
|
549
|
+
}
|
|
550
|
+
// Fallback: scrape the log for a response id (covers older sessions / edge cases).
|
|
551
|
+
const logText = await sessionStore.readLog(trimmed).catch(() => '');
|
|
552
|
+
const matches = logText.match(/resp_[A-Za-z0-9]+/g) ?? [];
|
|
553
|
+
const last = matches.length > 0 ? matches[matches.length - 1] : null;
|
|
554
|
+
if (last) {
|
|
555
|
+
return { responseId: last, sessionId: meta.id };
|
|
556
|
+
}
|
|
557
|
+
throw new Error(`Session ${trimmed} does not contain a stored response id. Ensure the original run produced a Responses API response id (background/store helps).`);
|
|
558
|
+
}
|
|
559
|
+
function extractResponseIdFromSession(meta, followupModel) {
|
|
560
|
+
// Single-model sessions store response metadata at the session root.
|
|
561
|
+
const rootResponse = meta.response ?? null;
|
|
562
|
+
const rootResponseId = rootResponse?.responseId ?? rootResponse?.id;
|
|
563
|
+
if (rootResponseId && rootResponseId.startsWith('resp_')) {
|
|
564
|
+
return rootResponseId;
|
|
565
|
+
}
|
|
566
|
+
const runs = Array.isArray(meta.models) ? meta.models : [];
|
|
567
|
+
if (runs.length === 0) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const pickRun = () => {
|
|
571
|
+
if (followupModel) {
|
|
572
|
+
return runs.find((r) => r.model === followupModel) ?? null;
|
|
573
|
+
}
|
|
574
|
+
return runs.length === 1 ? runs[0] : null;
|
|
575
|
+
};
|
|
576
|
+
const chosen = pickRun();
|
|
577
|
+
if (!chosen) {
|
|
578
|
+
const models = runs.map((r) => r.model).join(', ');
|
|
579
|
+
throw new Error(followupModel
|
|
580
|
+
? `Session ${meta.id} has no model named ${followupModel}. Available: ${models}`
|
|
581
|
+
: `Session ${meta.id} has multiple model runs. Re-run with --followup-model. Available: ${models}`);
|
|
582
|
+
}
|
|
583
|
+
const runResponse = chosen.response ?? null;
|
|
584
|
+
const runResponseId = runResponse?.responseId ?? runResponse?.id;
|
|
585
|
+
return runResponseId && runResponseId.startsWith('resp_') ? runResponseId : null;
|
|
586
|
+
}
|
|
457
587
|
function buildRunOptionsFromMetadata(metadata) {
|
|
458
588
|
const stored = metadata.options ?? {};
|
|
459
589
|
return {
|
|
460
590
|
prompt: stored.prompt ?? '',
|
|
461
591
|
model: stored.model ?? DEFAULT_MODEL,
|
|
462
592
|
models: stored.models,
|
|
593
|
+
previousResponseId: stored.previousResponseId,
|
|
463
594
|
effectiveModelId: stored.effectiveModelId ?? stored.model,
|
|
464
595
|
file: stored.file ?? [],
|
|
596
|
+
maxFileSizeBytes: stored.maxFileSizeBytes,
|
|
465
597
|
slug: stored.slug,
|
|
466
598
|
filesReport: stored.filesReport,
|
|
467
599
|
maxInput: stored.maxInput,
|
|
@@ -664,6 +796,14 @@ async function runRootCommand(options) {
|
|
|
664
796
|
throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
|
|
665
797
|
}
|
|
666
798
|
const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
|
|
799
|
+
const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model === 'gemini-3.1-pro');
|
|
800
|
+
if ((userForcedBrowser || userConfig.engine === 'browser') && includesGeminiApiOnly) {
|
|
801
|
+
throw new Error('gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.');
|
|
802
|
+
}
|
|
803
|
+
if (engine === 'browser' && includesGeminiApiOnly) {
|
|
804
|
+
console.log(chalk.dim('gemini-3.1-pro is API-only today; switching to API.'));
|
|
805
|
+
engine = 'api';
|
|
806
|
+
}
|
|
667
807
|
const effectiveModelId = resolvedModel.startsWith('gemini')
|
|
668
808
|
? resolveGeminiModelId(resolvedModel)
|
|
669
809
|
: isKnownModel(resolvedModel)
|
|
@@ -672,6 +812,7 @@ async function runRootCommand(options) {
|
|
|
672
812
|
const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
|
|
673
813
|
const { models: _rawModels, ...optionsWithoutModels } = options;
|
|
674
814
|
const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
|
|
815
|
+
resolvedOptions.maxFileSizeBytes = resolveConfiguredMaxFileSizeBytes(userConfig, process.env);
|
|
675
816
|
if (normalizedMultiModels.length > 0) {
|
|
676
817
|
resolvedOptions.models = normalizedMultiModels;
|
|
677
818
|
}
|
|
@@ -746,6 +887,21 @@ async function runRootCommand(options) {
|
|
|
746
887
|
options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
|
|
747
888
|
}
|
|
748
889
|
resolvedOptions.prompt = options.prompt;
|
|
890
|
+
if (options.followup) {
|
|
891
|
+
assertFollowupSupported({
|
|
892
|
+
engine,
|
|
893
|
+
model: resolvedModel,
|
|
894
|
+
baseUrl: resolvedBaseUrl,
|
|
895
|
+
azureEndpoint: resolvedOptions.azure?.endpoint,
|
|
896
|
+
});
|
|
897
|
+
if (normalizedMultiModels.length > 0) {
|
|
898
|
+
throw new Error('--followup cannot be combined with --models.');
|
|
899
|
+
}
|
|
900
|
+
const followup = await resolveFollowupReference(options.followup, options.followupModel);
|
|
901
|
+
resolvedOptions.previousResponseId = followup.responseId;
|
|
902
|
+
resolvedOptions.followupSessionId = followup.sessionId;
|
|
903
|
+
resolvedOptions.followupModel = options.followupModel;
|
|
904
|
+
}
|
|
749
905
|
const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
|
|
750
906
|
if (engine === 'browser') {
|
|
751
907
|
await runBrowserPreview({
|
|
@@ -784,6 +940,21 @@ async function runRootCommand(options) {
|
|
|
784
940
|
options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
|
|
785
941
|
}
|
|
786
942
|
resolvedOptions.prompt = options.prompt;
|
|
943
|
+
if (options.followup) {
|
|
944
|
+
assertFollowupSupported({
|
|
945
|
+
engine,
|
|
946
|
+
model: resolvedModel,
|
|
947
|
+
baseUrl: resolvedBaseUrl,
|
|
948
|
+
azureEndpoint: resolvedOptions.azure?.endpoint,
|
|
949
|
+
});
|
|
950
|
+
if (normalizedMultiModels.length > 0) {
|
|
951
|
+
throw new Error('--followup cannot be combined with --models.');
|
|
952
|
+
}
|
|
953
|
+
const followup = await resolveFollowupReference(options.followup, options.followupModel);
|
|
954
|
+
resolvedOptions.previousResponseId = followup.responseId;
|
|
955
|
+
resolvedOptions.followupSessionId = followup.sessionId;
|
|
956
|
+
resolvedOptions.followupModel = options.followupModel;
|
|
957
|
+
}
|
|
787
958
|
const duplicateBlocked = await shouldBlockDuplicatePrompt({
|
|
788
959
|
prompt: resolvedOptions.prompt,
|
|
789
960
|
force: options.force,
|
|
@@ -798,7 +969,10 @@ async function runRootCommand(options) {
|
|
|
798
969
|
const isBrowserMode = engine === 'browser' || userForcedBrowser;
|
|
799
970
|
const filesToValidate = isBrowserMode ? options.file.filter((f) => !isMediaFile(f)) : options.file;
|
|
800
971
|
if (filesToValidate.length > 0) {
|
|
801
|
-
await readFiles(filesToValidate, {
|
|
972
|
+
await readFiles(filesToValidate, {
|
|
973
|
+
cwd: process.cwd(),
|
|
974
|
+
maxFileSizeBytes: resolvedOptions.maxFileSizeBytes,
|
|
975
|
+
});
|
|
802
976
|
}
|
|
803
977
|
}
|
|
804
978
|
const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
|
|
@@ -874,6 +1048,8 @@ async function runRootCommand(options) {
|
|
|
874
1048
|
...baseRunOptions,
|
|
875
1049
|
mode: sessionMode,
|
|
876
1050
|
browserConfig,
|
|
1051
|
+
followupSessionId: resolvedOptions.followupSessionId,
|
|
1052
|
+
followupModel: resolvedOptions.followupModel,
|
|
877
1053
|
waitPreference,
|
|
878
1054
|
youtube: options.youtube,
|
|
879
1055
|
generateImage: options.generateImage,
|
|
@@ -1022,7 +1198,10 @@ async function restartSession(sessionId, options) {
|
|
|
1022
1198
|
const isBrowserMode = engine === 'browser';
|
|
1023
1199
|
const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
|
|
1024
1200
|
if (filesToValidate.length > 0) {
|
|
1025
|
-
await readFiles(filesToValidate, {
|
|
1201
|
+
await readFiles(filesToValidate, {
|
|
1202
|
+
cwd,
|
|
1203
|
+
maxFileSizeBytes: runOptions.maxFileSizeBytes,
|
|
1204
|
+
});
|
|
1026
1205
|
}
|
|
1027
1206
|
}
|
|
1028
1207
|
enforceBrowserSearchFlag(runOptions, sessionMode, console.log);
|
|
@@ -1080,6 +1259,8 @@ async function restartSession(sessionId, options) {
|
|
|
1080
1259
|
...runOptions,
|
|
1081
1260
|
mode: sessionMode,
|
|
1082
1261
|
browserConfig,
|
|
1262
|
+
followupSessionId: storedOptions.followupSessionId,
|
|
1263
|
+
followupModel: storedOptions.followupModel,
|
|
1083
1264
|
waitPreference,
|
|
1084
1265
|
youtube: storedOptions.youtube,
|
|
1085
1266
|
generateImage: storedOptions.generateImage,
|
|
@@ -71,13 +71,15 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
71
71
|
.map((token) => normalizeText(token))
|
|
72
72
|
.filter(Boolean);
|
|
73
73
|
const targetWords = normalizedTarget.split(' ').filter(Boolean);
|
|
74
|
-
const desiredVersion = normalizedTarget.includes('5
|
|
75
|
-
? '5-
|
|
76
|
-
: normalizedTarget.includes('5
|
|
77
|
-
? '5-
|
|
78
|
-
: normalizedTarget.includes('5
|
|
79
|
-
? '5-
|
|
80
|
-
:
|
|
74
|
+
const desiredVersion = normalizedTarget.includes('5 4')
|
|
75
|
+
? '5-4'
|
|
76
|
+
: normalizedTarget.includes('5 2')
|
|
77
|
+
? '5-2'
|
|
78
|
+
: normalizedTarget.includes('5 1')
|
|
79
|
+
? '5-1'
|
|
80
|
+
: normalizedTarget.includes('5 0')
|
|
81
|
+
? '5-0'
|
|
82
|
+
: null;
|
|
81
83
|
const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
|
|
82
84
|
const wantsInstant = normalizedTarget.includes('instant');
|
|
83
85
|
const wantsThinking = normalizedTarget.includes('thinking');
|
|
@@ -87,6 +89,26 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
87
89
|
return { status: 'button-missing' };
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
const closeMenu = () => {
|
|
93
|
+
try {
|
|
94
|
+
if (dispatchClickSequence(button)) {
|
|
95
|
+
lastPointerClick = performance.now();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
try {
|
|
100
|
+
document.dispatchEvent(
|
|
101
|
+
new KeyboardEvent('keydown', {
|
|
102
|
+
key: 'Escape',
|
|
103
|
+
code: 'Escape',
|
|
104
|
+
keyCode: 27,
|
|
105
|
+
which: 27,
|
|
106
|
+
bubbles: true,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
} catch {}
|
|
110
|
+
};
|
|
111
|
+
|
|
90
112
|
const getButtonLabel = () => (button.textContent ?? '').trim();
|
|
91
113
|
if (MODEL_STRATEGY === 'current') {
|
|
92
114
|
return { status: 'already-selected', label: getButtonLabel() };
|
|
@@ -95,6 +117,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
95
117
|
const normalizedLabel = normalizeText(getButtonLabel());
|
|
96
118
|
if (!normalizedLabel) return false;
|
|
97
119
|
if (desiredVersion) {
|
|
120
|
+
if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
|
|
98
121
|
if (desiredVersion === '5-2' && !normalizedLabel.includes('5 2')) return false;
|
|
99
122
|
if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
|
|
100
123
|
if (desiredVersion === '5-0' && !normalizedLabel.includes('5 0')) return false;
|
|
@@ -159,6 +182,12 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
159
182
|
normalizedTestId.includes('gpt-5-2') ||
|
|
160
183
|
normalizedTestId.includes('gpt-5.2') ||
|
|
161
184
|
normalizedTestId.includes('gpt52');
|
|
185
|
+
const has54 =
|
|
186
|
+
normalizedTestId.includes('5-4') ||
|
|
187
|
+
normalizedTestId.includes('5.4') ||
|
|
188
|
+
normalizedTestId.includes('gpt-5-4') ||
|
|
189
|
+
normalizedTestId.includes('gpt-5.4') ||
|
|
190
|
+
normalizedTestId.includes('gpt54');
|
|
162
191
|
const has51 =
|
|
163
192
|
normalizedTestId.includes('5-1') ||
|
|
164
193
|
normalizedTestId.includes('5.1') ||
|
|
@@ -171,7 +200,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
171
200
|
normalizedTestId.includes('gpt-5-0') ||
|
|
172
201
|
normalizedTestId.includes('gpt-5.0') ||
|
|
173
202
|
normalizedTestId.includes('gpt50');
|
|
174
|
-
const candidateVersion = has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
|
|
203
|
+
const candidateVersion = has54 ? '5-4' : has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
|
|
175
204
|
// If a candidate advertises a different version, ignore it entirely.
|
|
176
205
|
if (candidateVersion && candidateVersion !== desiredVersion) {
|
|
177
206
|
return 0;
|
|
@@ -317,6 +346,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
317
346
|
const match = findBestOption();
|
|
318
347
|
if (match) {
|
|
319
348
|
if (optionIsSelected(match.node)) {
|
|
349
|
+
closeMenu();
|
|
320
350
|
resolve({ status: 'already-selected', label: getButtonLabel() || match.label });
|
|
321
351
|
return;
|
|
322
352
|
}
|
|
@@ -331,6 +361,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
331
361
|
// Wait for the top bar label to reflect the requested model; otherwise keep scanning.
|
|
332
362
|
setTimeout(() => {
|
|
333
363
|
if (buttonMatchesTarget()) {
|
|
364
|
+
closeMenu();
|
|
334
365
|
resolve({ status: 'switched', label: getButtonLabel() || match.label });
|
|
335
366
|
return;
|
|
336
367
|
}
|
|
@@ -374,6 +405,22 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
374
405
|
push(`chatgpt ${dotless}`, labelTokens);
|
|
375
406
|
push(`gpt ${base}`, labelTokens);
|
|
376
407
|
push(`gpt ${dotless}`, labelTokens);
|
|
408
|
+
// Numeric variations (5.4 ↔ 54 ↔ gpt-5-4)
|
|
409
|
+
if (base.includes('5.4') || base.includes('5-4') || base.includes('54')) {
|
|
410
|
+
push('5.4', labelTokens);
|
|
411
|
+
push('gpt-5.4', labelTokens);
|
|
412
|
+
push('gpt5.4', labelTokens);
|
|
413
|
+
push('gpt-5-4', labelTokens);
|
|
414
|
+
push('gpt5-4', labelTokens);
|
|
415
|
+
push('gpt54', labelTokens);
|
|
416
|
+
push('chatgpt 5.4', labelTokens);
|
|
417
|
+
if (!base.includes('pro')) {
|
|
418
|
+
testIdTokens.add('model-switcher-gpt-5-4');
|
|
419
|
+
}
|
|
420
|
+
testIdTokens.add('gpt-5-4');
|
|
421
|
+
testIdTokens.add('gpt5-4');
|
|
422
|
+
testIdTokens.add('gpt54');
|
|
423
|
+
}
|
|
377
424
|
// Numeric variations (5.1 ↔ 51 ↔ gpt-5-1)
|
|
378
425
|
if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
|
|
379
426
|
push('5.1', labelTokens);
|
|
@@ -436,6 +483,11 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
436
483
|
push('proresearch', labelTokens);
|
|
437
484
|
push('research grade', labelTokens);
|
|
438
485
|
push('advanced reasoning', labelTokens);
|
|
486
|
+
if (base.includes('5.4') || base.includes('5-4') || base.includes('54')) {
|
|
487
|
+
testIdTokens.add('gpt-5.4-pro');
|
|
488
|
+
testIdTokens.add('gpt-5-4-pro');
|
|
489
|
+
testIdTokens.add('gpt54pro');
|
|
490
|
+
}
|
|
439
491
|
if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
|
|
440
492
|
testIdTokens.add('gpt-5.1-pro');
|
|
441
493
|
testIdTokens.add('gpt-5-1-pro');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CLOUDFLARE_SCRIPT_SELECTOR, CLOUDFLARE_TITLE, INPUT_SELECTORS, } from '../constants.js';
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure } from '../domDebug.js';
|
|
4
|
+
import { BrowserAutomationError } from '../../oracle/errors.js';
|
|
4
5
|
export function installJavaScriptDialogAutoDismissal(Page, logger) {
|
|
5
6
|
const pageAny = Page;
|
|
6
7
|
if (typeof pageAny.on !== 'function' || typeof pageAny.handleJavaScriptDialog !== 'function') {
|
|
@@ -127,7 +128,7 @@ export async function ensureNotBlocked(Runtime, headless, logger) {
|
|
|
127
128
|
? 'Cloudflare challenge detected in headless mode. Re-run with --headful so you can solve the challenge.'
|
|
128
129
|
: 'Cloudflare challenge detected. Complete the “Just a moment…” check in the open browser, then rerun.';
|
|
129
130
|
logger('Cloudflare anti-bot page detected');
|
|
130
|
-
throw new
|
|
131
|
+
throw new BrowserAutomationError(message, { stage: 'cloudflare-challenge', headless });
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
const LOGIN_CHECK_TIMEOUT_MS = 5_000;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CHATGPT_URL = 'https://chatgpt.com/';
|
|
2
|
-
export const DEFAULT_MODEL_TARGET = 'GPT-5.
|
|
2
|
+
export const DEFAULT_MODEL_TARGET = 'GPT-5.4 Pro';
|
|
3
3
|
export const DEFAULT_MODEL_STRATEGY = 'select';
|
|
4
4
|
export const COOKIE_URLS = ['https://chatgpt.com', 'https://chat.openai.com', 'https://atlas.openai.com'];
|
|
5
5
|
export const INPUT_SELECTORS = [
|