@steipete/oracle 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +30 -1
  2. package/dist/bin/oracle-cli.js +291 -16
  3. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  4. package/dist/src/bridge/connection.js +103 -0
  5. package/dist/src/bridge/userConfigFile.js +28 -0
  6. package/dist/src/browser/actions/assistantResponse.js +85 -42
  7. package/dist/src/browser/actions/promptComposer.js +141 -32
  8. package/dist/src/browser/chromeLifecycle.js +78 -9
  9. package/dist/src/browser/config.js +14 -0
  10. package/dist/src/browser/detect.js +164 -0
  11. package/dist/src/browser/index.js +394 -24
  12. package/dist/src/browser/profileState.js +93 -0
  13. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  14. package/dist/src/cli/bridge/client.js +73 -0
  15. package/dist/src/cli/bridge/codexConfig.js +43 -0
  16. package/dist/src/cli/bridge/doctor.js +107 -0
  17. package/dist/src/cli/bridge/host.js +259 -0
  18. package/dist/src/cli/browserConfig.js +21 -0
  19. package/dist/src/cli/browserDefaults.js +21 -0
  20. package/dist/src/cli/engine.js +17 -1
  21. package/dist/src/cli/options.js +14 -0
  22. package/dist/src/cli/runOptions.js +4 -0
  23. package/dist/src/cli/sessionRunner.js +149 -0
  24. package/dist/src/cli/tui/index.js +1 -0
  25. package/dist/src/mcp/tools/consult.js +81 -15
  26. package/dist/src/mcp/tools/sessions.js +15 -6
  27. package/dist/src/mcp/types.js +4 -0
  28. package/dist/src/mcp/utils.js +12 -2
  29. package/dist/src/oracle/background.js +1 -2
  30. package/dist/src/oracle/client.js +5 -2
  31. package/dist/src/oracle/files.js +2 -2
  32. package/dist/src/oracle/modelResolver.js +33 -1
  33. package/dist/src/oracle/run.js +1 -0
  34. package/dist/src/remote/client.js +6 -5
  35. package/dist/src/remote/health.js +113 -0
  36. package/dist/src/remote/remoteServiceConfig.js +31 -0
  37. package/dist/src/remote/server.js +28 -1
  38. package/dist/src/sessionManager.js +72 -7
  39. package/dist/src/sessionStore.js +2 -2
  40. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  41. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  42. package/package.json +21 -21
  43. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  44. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md CHANGED
@@ -42,6 +42,7 @@ npx -y @steipete/oracle --engine browser --model gemini-3-pro --prompt "a cute r
42
42
  # Sessions (list and replay)
43
43
  npx -y @steipete/oracle status --hours 72
44
44
  npx -y @steipete/oracle session <id> --render
45
+ npx -y @steipete/oracle restart <id>
45
46
 
46
47
  # TUI (interactive, only for humans)
47
48
  npx -y @steipete/oracle tui
@@ -100,6 +101,24 @@ npx -y @steipete/oracle oracle-mcp
100
101
  - Sessions you can replay (`oracle status`, `oracle session <id> --render`).
101
102
  - Session logs and bundles live in `~/.oracle/sessions` (override with `ORACLE_HOME_DIR`).
102
103
 
104
+ ## Browser auto-reattach (long Pro runs)
105
+
106
+ 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.
107
+
108
+ Enable auto-reattach by setting a non-zero interval:
109
+ - `--browser-auto-reattach-delay` — wait before the first retry (e.g. `30s`)
110
+ - `--browser-auto-reattach-interval` — how often to retry (e.g. `2m`)
111
+ - `--browser-auto-reattach-timeout` — per-attempt budget (default `2m`)
112
+
113
+ ```bash
114
+ oracle --engine browser \
115
+ --browser-timeout 6m \
116
+ --browser-auto-reattach-delay 30s \
117
+ --browser-auto-reattach-interval 2m \
118
+ --browser-auto-reattach-timeout 2m \
119
+ -p "Run the long UI audit" --file "src/**/*.ts"
120
+ ```
121
+
103
122
  ## Flags you’ll actually use
104
123
 
105
124
  | Flag | Purpose |
@@ -117,8 +136,16 @@ npx -y @steipete/oracle oracle-mcp
117
136
  | `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
118
137
  | `--browser-inline-cookies[(-file)] <payload|path>` | Supply cookies without Chrome/Keychain (browser). |
119
138
  | `--browser-timeout`, `--browser-input-timeout` | Control overall/browser input timeouts (supports h/m/s/ms). |
139
+ | `--browser-recheck-delay`, `--browser-recheck-timeout` | Delayed recheck for long Pro runs: wait then retry capture after timeout (supports h/m/s/ms). |
140
+ | `--browser-reuse-wait` | Wait for a shared Chrome profile before launching (parallel browser runs). |
141
+ | `--browser-profile-lock-timeout` | Wait for the shared manual-login profile lock before sending (serializes parallel runs). |
120
142
  | `--render`, `--copy` | Print and/or copy the assembled markdown bundle. |
121
143
  | `--wait` | Block for background API runs (e.g., GPT‑5.1 Pro) instead of detaching. |
144
+ | `--timeout <seconds\|auto>` | Overall API deadline (auto = 60m for pro, 120s otherwise). |
145
+ | `--background`, `--no-background` | Force Responses API background mode (create + retrieve) for API runs. |
146
+ | `--http-timeout <ms\|s\|m\|h>` | HTTP client timeout (default 20m). |
147
+ | `--zombie-timeout <ms\|s\|m\|h>` | Override stale-session cutoff used by `oracle status`. |
148
+ | `--zombie-last-activity` | Use last log activity to detect stale sessions. |
122
149
  | `--write-output <path>` | Save only the final answer (multi-model adds `.<model>`). |
123
150
  | `--files-report` | Print per-file token usage. |
124
151
  | `--dry-run [summary\|json\|full]` | Preview without sending. |
@@ -149,7 +176,8 @@ Advanced flags
149
176
 
150
177
  | Area | Flags |
151
178
  | --- | --- |
152
- | Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-cookie-wait`, `--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` |
179
+ | Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-recheck-delay`, `--browser-recheck-timeout`, `--browser-reuse-wait`, `--browser-profile-lock-timeout`, `--browser-auto-reattach-delay`, `--browser-auto-reattach-interval`, `--browser-auto-reattach-timeout`, `--browser-cookie-wait`, `--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` |
180
+ | Run control | `--background`, `--no-background`, `--http-timeout`, `--zombie-timeout`, `--zombie-last-activity` |
153
181
  | Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |
154
182
 
155
183
  Remote browser example
@@ -171,6 +199,7 @@ oracle status --clear --hours 168
171
199
  ```
172
200
 
173
201
  ## More docs
202
+ - Bridge (Windows host → Linux client): [docs/bridge.md](docs/bridge.md)
174
203
  - Browser mode & forks: [docs/browser-mode.md](docs/browser-mode.md) (includes `oracle serve` remote service), [docs/chromium-forks.md](docs/chromium-forks.md), [docs/linux.md](docs/linux.md)
175
204
  - MCP: [docs/mcp.md](docs/mcp.md)
176
205
  - OpenAI/Azure/OpenRouter endpoints: [docs/openai-endpoints.md](docs/openai-endpoints.md), [docs/openrouter.md](docs/openrouter.md)
@@ -20,7 +20,7 @@ import { CHATGPT_URL } from '../src/browserMode.js';
20
20
  import { createRemoteBrowserExecutor } from '../src/remote/client.js';
21
21
  import { createGeminiWebExecutor } from '../src/gemini-web/index.js';
22
22
  import { applyHelpStyling } from '../src/cli/help.js';
23
- import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, mergePathLikeOptions, dedupePathInputs, } from '../src/cli/options.js';
23
+ import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from '../src/cli/options.js';
24
24
  import { copyToClipboard } from '../src/cli/clipboard.js';
25
25
  import { buildMarkdownBundle } from '../src/cli/markdownBundle.js';
26
26
  import { shouldDetachSession } from '../src/cli/detach.js';
@@ -46,9 +46,20 @@ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, }
46
46
  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
+ import { resolveRemoteServiceConfig } from '../src/remote/remoteServiceConfig.js';
49
50
  const VERSION = getCliVersion();
50
51
  const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
51
- const rawCliArgs = process.argv.slice(2);
52
+ const LEGACY_FLAG_ALIASES = new Map([
53
+ ['--[no-]notify', '--notify'],
54
+ ['--[no-]notify-sound', '--notify-sound'],
55
+ ['--[no-]background', '--background'],
56
+ ]);
57
+ const normalizedArgv = process.argv.map((arg, index) => {
58
+ if (index < 2)
59
+ return arg;
60
+ return LEGACY_FLAG_ALIASES.get(arg) ?? arg;
61
+ });
62
+ const rawCliArgs = normalizedArgv.slice(2);
52
63
  const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
53
64
  const isTty = process.stdout.isTTY;
54
65
  const program = new Command();
@@ -120,12 +131,22 @@ program
120
131
  .addOption(new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp())
121
132
  .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
122
133
  .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
123
- .addOption(new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
124
- .default(undefined))
125
- .addOption(new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
134
+ .addOption(new Option('--notify', 'Desktop notification when a session finishes (default on unless CI/SSH).').default(undefined))
135
+ .addOption(new Option('--no-notify', 'Disable desktop notifications.').default(undefined))
136
+ .addOption(new Option('--notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
137
+ .addOption(new Option('--no-notify-sound', 'Disable notification sounds.').default(undefined))
126
138
  .addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.2-pro, 120s otherwise).')
127
139
  .argParser(parseTimeoutOption)
128
140
  .default('auto'))
141
+ .addOption(new Option('--background', 'Use Responses API background mode (create + retrieve) for API runs.').default(undefined))
142
+ .addOption(new Option('--no-background', 'Disable Responses API background mode.').default(undefined))
143
+ .addOption(new Option('--http-timeout <ms|s|m|h>', 'HTTP client timeout for API requests (default 20m).')
144
+ .argParser((value) => parseDurationOption(value, 'HTTP timeout'))
145
+ .default(undefined))
146
+ .addOption(new Option('--zombie-timeout <ms|s|m|h>', 'Override stale-session cutoff used by `oracle status` (default 60m).')
147
+ .argParser((value) => parseDurationOption(value, 'Zombie timeout'))
148
+ .default(undefined))
149
+ .option('--zombie-last-activity', 'Base stale-session detection on last log activity instead of start time.', false)
129
150
  .addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
130
151
  .hideHelp()
131
152
  .choices(['summary', 'json', 'full'])
@@ -162,7 +183,14 @@ program
162
183
  .addOption(new Option('--chatgpt-url <url>', `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
163
184
  .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
164
185
  .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
165
- .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
186
+ .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 60s).').hideHelp())
187
+ .addOption(new Option('--browser-recheck-delay <ms|s|m|h>', 'After an assistant timeout, wait this long then revisit the conversation to retry capture.').hideHelp())
188
+ .addOption(new Option('--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt (default 120s).').hideHelp())
189
+ .addOption(new Option('--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile to appear before launching a new one (helps parallel runs).').hideHelp())
190
+ .addOption(new Option('--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the shared manual-login profile lock before sending (serializes parallel runs).').hideHelp())
191
+ .addOption(new Option('--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before starting periodic auto-reattach attempts after a timeout.').hideHelp())
192
+ .addOption(new Option('--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).').hideHelp())
193
+ .addOption(new Option('--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt (default 120s).').hideHelp())
166
194
  .addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
167
195
  .addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
168
196
  .argParser(parseIntOption))
@@ -219,14 +247,73 @@ program
219
247
  .option('--host <address>', 'Interface to bind (default 0.0.0.0).')
220
248
  .option('--port <number>', 'Port to listen on (default random).', parseIntOption)
221
249
  .option('--token <value>', 'Access token clients must provide (random if omitted).')
250
+ .option('--manual-login', 'Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).', false)
251
+ .option('--manual-login-profile-dir <path>', 'Chrome profile directory for manual login (default ~/.oracle/browser-profile).')
222
252
  .action(async (commandOptions) => {
223
253
  const { serveRemote } = await import('../src/remote/server.js');
224
254
  await serveRemote({
225
255
  host: commandOptions.host,
226
256
  port: commandOptions.port,
227
257
  token: commandOptions.token,
258
+ manualLoginDefault: commandOptions.manualLogin,
259
+ manualLoginProfileDir: commandOptions.manualLoginProfileDir,
228
260
  });
229
261
  });
262
+ const bridgeCommand = program.command('bridge').description('Bridge a Windows-hosted ChatGPT session to Linux clients.');
263
+ bridgeCommand
264
+ .command('host')
265
+ .description('Start a secure oracle serve host (optionally with an SSH reverse tunnel).')
266
+ .option('--bind <host:port>', 'Local bind address for the host service (default 127.0.0.1:9473).')
267
+ .option('--token <token|auto>', 'Service access token (default auto).', 'auto')
268
+ .option('--write-connection <path>', 'Write a connection artifact JSON (default ~/.oracle/bridge-connection.json).')
269
+ .option('--ssh <user@host>', 'Maintain an SSH reverse tunnel to the Linux host (ssh -N -R ...).')
270
+ .option('--ssh-remote-port <port>', 'Remote port to bind on the Linux host (default matches --bind port).', parseIntOption)
271
+ .option('--ssh-identity <path>', 'SSH identity file (ssh -i).')
272
+ .option('--ssh-extra-args <args>', 'Extra args passed to ssh (quoted string).')
273
+ .option('--background', 'Run the host in the background and write pid/log files.', false)
274
+ .option('--foreground', 'Run the host in the foreground (default).', false)
275
+ .option('--print', 'Print the client connection string (includes token).', false)
276
+ .option('--print-token', 'Print only the token.', false)
277
+ .action(async (commandOptions) => {
278
+ const { runBridgeHost } = await import('../src/cli/bridge/host.js');
279
+ await runBridgeHost(commandOptions);
280
+ });
281
+ bridgeCommand
282
+ .command('client')
283
+ .description('Configure this machine to use a remote oracle serve host.')
284
+ .requiredOption('--connect <connection>', 'Connection string or path to bridge-connection.json.')
285
+ .option('--config <path>', 'Override the oracle config file location (default ~/.oracle/config.json).')
286
+ .option('--no-write-config', 'Do not write ~/.oracle/config.json (just validate).')
287
+ .option('--no-test', 'Skip remote /health check.')
288
+ .option('--print-env', 'Print env var exports (includes token).', false)
289
+ .action(async (commandOptions) => {
290
+ const { runBridgeClient } = await import('../src/cli/bridge/client.js');
291
+ await runBridgeClient(commandOptions);
292
+ });
293
+ bridgeCommand
294
+ .command('doctor')
295
+ .description('Diagnose bridge connectivity and browser engine prerequisites.')
296
+ .option('--verbose', 'Show extra diagnostics.', false)
297
+ .action(async (commandOptions) => {
298
+ const { runBridgeDoctor } = await import('../src/cli/bridge/doctor.js');
299
+ await runBridgeDoctor(commandOptions);
300
+ });
301
+ bridgeCommand
302
+ .command('codex-config')
303
+ .description('Print a Codex CLI MCP server config snippet for oracle-mcp.')
304
+ .option('--print-token', 'Include ORACLE_REMOTE_TOKEN in the snippet.', false)
305
+ .action(async (commandOptions) => {
306
+ const { runBridgeCodexConfig } = await import('../src/cli/bridge/codexConfig.js');
307
+ await runBridgeCodexConfig(commandOptions);
308
+ });
309
+ bridgeCommand
310
+ .command('claude-config')
311
+ .description('Print a Claude Code MCP config snippet (.mcp.json) for oracle-mcp.')
312
+ .option('--print-token', 'Include ORACLE_REMOTE_TOKEN in the snippet.', false)
313
+ .action(async (commandOptions) => {
314
+ const { runBridgeClaudeConfig } = await import('../src/cli/bridge/claudeConfig.js');
315
+ await runBridgeClaudeConfig(commandOptions);
316
+ });
230
317
  program
231
318
  .command('tui')
232
319
  .description('Launch the interactive terminal UI for humans (no automation).')
@@ -299,6 +386,17 @@ const statusCommand = program
299
386
  showExamples,
300
387
  });
301
388
  });
389
+ program
390
+ .command('restart <id>')
391
+ .description('Re-run a stored session as a new session (clones options).')
392
+ .addOption(new Option('--wait').default(undefined))
393
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
394
+ .option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.')
395
+ .option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.')
396
+ .action(async (sessionId, _options, cmd) => {
397
+ const restartOptions = cmd.opts();
398
+ await restartSession(sessionId, restartOptions);
399
+ });
302
400
  function buildRunOptions(options, overrides = {}) {
303
401
  if (!options.prompt) {
304
402
  throw new Error('Prompt is required.');
@@ -323,6 +421,9 @@ function buildRunOptions(options, overrides = {}) {
323
421
  maxOutput: overrides.maxOutput ?? options.maxOutput,
324
422
  system: overrides.system ?? options.system,
325
423
  timeoutSeconds: overrides.timeoutSeconds ?? options.timeout,
424
+ httpTimeoutMs: overrides.httpTimeoutMs ?? options.httpTimeout,
425
+ zombieTimeoutMs: overrides.zombieTimeoutMs ?? options.zombieTimeout,
426
+ zombieUseLastActivity: overrides.zombieUseLastActivity ?? options.zombieLastActivity,
326
427
  silent: overrides.silent ?? options.silent,
327
428
  search: overrides.search ?? options.search,
328
429
  preview: overrides.preview ?? undefined,
@@ -373,6 +474,10 @@ function buildRunOptionsFromMetadata(metadata) {
373
474
  apiKey: undefined,
374
475
  baseUrl: normalizeBaseUrl(stored.baseUrl),
375
476
  azure: stored.azure,
477
+ timeoutSeconds: stored.timeoutSeconds,
478
+ httpTimeoutMs: stored.httpTimeoutMs,
479
+ zombieTimeoutMs: stored.zombieTimeoutMs,
480
+ zombieUseLastActivity: stored.zombieUseLastActivity,
376
481
  sessionId: metadata.id,
377
482
  verbose: stored.verbose,
378
483
  heartbeatIntervalMs: stored.heartbeatIntervalMs,
@@ -446,8 +551,14 @@ async function runRootCommand(options) {
446
551
  }
447
552
  };
448
553
  applyRetentionOption();
449
- const remoteHost = options.remoteHost ?? userConfig.remoteHost ?? userConfig.remote?.host ?? process.env.ORACLE_REMOTE_HOST;
450
- const remoteToken = options.remoteToken ?? userConfig.remoteToken ?? userConfig.remote?.token ?? process.env.ORACLE_REMOTE_TOKEN;
554
+ const remoteConfig = resolveRemoteServiceConfig({
555
+ cliHost: options.remoteHost,
556
+ cliToken: options.remoteToken,
557
+ userConfig,
558
+ env: process.env,
559
+ });
560
+ const remoteHost = remoteConfig.host;
561
+ const remoteToken = remoteConfig.token;
451
562
  if (remoteHost) {
452
563
  console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
453
564
  }
@@ -572,11 +683,10 @@ async function runRootCommand(options) {
572
683
  // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
573
684
  let waitPreference = resolveWaitFlag({
574
685
  waitFlag: options.wait,
575
- noWaitFlag: options.noWait,
576
686
  model: resolvedModel,
577
687
  engine,
578
688
  });
579
- if (remoteHost && !waitPreference) {
689
+ if (remoteHost && waitPreference === false) {
580
690
  console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
581
691
  waitPreference = true;
582
692
  }
@@ -752,7 +862,7 @@ async function runRootCommand(options) {
752
862
  const baseRunOptions = buildRunOptions(resolvedOptions, {
753
863
  preview: false,
754
864
  previewMode: undefined,
755
- background: userConfig.background ?? resolvedOptions.background,
865
+ background: resolvedOptions.background ?? userConfig.background,
756
866
  baseUrl: resolvedBaseUrl,
757
867
  });
758
868
  enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
@@ -764,6 +874,13 @@ async function runRootCommand(options) {
764
874
  ...baseRunOptions,
765
875
  mode: sessionMode,
766
876
  browserConfig,
877
+ waitPreference,
878
+ youtube: options.youtube,
879
+ generateImage: options.generateImage,
880
+ editImage: options.editImage,
881
+ outputPath: options.output,
882
+ aspectRatio: options.aspect,
883
+ geminiShowThoughts: options.geminiShowThoughts,
767
884
  }, process.cwd(), notifications);
768
885
  const liveRunOptions = {
769
886
  ...baseRunOptions,
@@ -805,7 +922,7 @@ async function runRootCommand(options) {
805
922
  await attachSession(sessionMeta.id, { suppressMetadata: true });
806
923
  }
807
924
  }
808
- async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps) {
925
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
809
926
  const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
810
927
  let headerAugmented = false;
811
928
  const combinedLog = (message = '') => {
@@ -834,7 +951,7 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
834
951
  runOptions,
835
952
  mode,
836
953
  browserConfig,
837
- cwd: process.cwd(),
954
+ cwd,
838
955
  log: combinedLog,
839
956
  write: combinedWrite,
840
957
  version: VERSION,
@@ -877,6 +994,140 @@ async function launchDetachedSession(sessionId) {
877
994
  }
878
995
  });
879
996
  }
997
+ async function restartSession(sessionId, options) {
998
+ const metadata = await sessionStore.readSession(sessionId);
999
+ if (!metadata) {
1000
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
1001
+ process.exitCode = 1;
1002
+ return;
1003
+ }
1004
+ const runOptions = buildRunOptionsFromMetadata(metadata);
1005
+ if (!runOptions.prompt) {
1006
+ console.error(chalk.red(`Session ${sessionId} has no stored prompt; cannot restart.`));
1007
+ process.exitCode = 1;
1008
+ return;
1009
+ }
1010
+ const sessionMode = getSessionMode(metadata);
1011
+ const engine = sessionMode === 'browser' ? 'browser' : 'api';
1012
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
1013
+ if (sessionMode === 'browser' && !browserConfig) {
1014
+ console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
1015
+ process.exitCode = 1;
1016
+ return;
1017
+ }
1018
+ const userConfig = (await loadUserConfig()).config;
1019
+ const cwd = metadata.cwd ?? process.cwd();
1020
+ const storedOptions = metadata.options ?? {};
1021
+ if (runOptions.file && runOptions.file.length > 0) {
1022
+ const isBrowserMode = engine === 'browser';
1023
+ const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
1024
+ if (filesToValidate.length > 0) {
1025
+ await readFiles(filesToValidate, { cwd });
1026
+ }
1027
+ }
1028
+ enforceBrowserSearchFlag(runOptions, sessionMode, console.log);
1029
+ let waitPreference = resolveRestartWaitPreference({
1030
+ waitFlag: options.wait,
1031
+ storedPreference: storedOptions.waitPreference,
1032
+ model: runOptions.model,
1033
+ engine,
1034
+ });
1035
+ const remoteConfig = resolveRemoteServiceConfig({
1036
+ cliHost: options.remoteHost,
1037
+ cliToken: options.remoteToken,
1038
+ userConfig,
1039
+ env: process.env,
1040
+ });
1041
+ const remoteHost = remoteConfig.host;
1042
+ const remoteToken = remoteConfig.token;
1043
+ if (remoteHost && engine !== 'browser') {
1044
+ throw new Error('--remote-host requires a browser session.');
1045
+ }
1046
+ if (remoteHost) {
1047
+ console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
1048
+ }
1049
+ if (remoteHost && waitPreference === false) {
1050
+ console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
1051
+ waitPreference = true;
1052
+ }
1053
+ let browserDeps;
1054
+ if (browserConfig && remoteHost) {
1055
+ browserDeps = {
1056
+ executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
1057
+ };
1058
+ console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1059
+ }
1060
+ else if (browserConfig && runOptions.model.startsWith('gemini')) {
1061
+ browserDeps = {
1062
+ executeBrowser: createGeminiWebExecutor({
1063
+ youtube: storedOptions.youtube,
1064
+ generateImage: storedOptions.generateImage,
1065
+ editImage: storedOptions.editImage,
1066
+ outputPath: storedOptions.outputPath,
1067
+ aspectRatio: storedOptions.aspectRatio,
1068
+ showThoughts: storedOptions.geminiShowThoughts,
1069
+ }),
1070
+ };
1071
+ console.log(chalk.dim('Using Gemini web client for browser automation'));
1072
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
1073
+ console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
1074
+ }
1075
+ }
1076
+ const remoteExecutionActive = Boolean(browserDeps);
1077
+ await sessionStore.ensureStorage();
1078
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
1079
+ const sessionMeta = await sessionStore.createSession({
1080
+ ...runOptions,
1081
+ mode: sessionMode,
1082
+ browserConfig,
1083
+ waitPreference,
1084
+ youtube: storedOptions.youtube,
1085
+ generateImage: storedOptions.generateImage,
1086
+ editImage: storedOptions.editImage,
1087
+ outputPath: storedOptions.outputPath,
1088
+ aspectRatio: storedOptions.aspectRatio,
1089
+ geminiShowThoughts: storedOptions.geminiShowThoughts,
1090
+ }, cwd, notifications, sessionId);
1091
+ const liveRunOptions = {
1092
+ ...runOptions,
1093
+ sessionId: sessionMeta.id,
1094
+ effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
1095
+ };
1096
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1097
+ const detachAllowed = remoteExecutionActive
1098
+ ? false
1099
+ : shouldDetachSession({
1100
+ engine,
1101
+ model: runOptions.model,
1102
+ waitPreference,
1103
+ disableDetachEnv,
1104
+ });
1105
+ const detached = !detachAllowed
1106
+ ? false
1107
+ : await launchDetachedSession(sessionMeta.id).catch((error) => {
1108
+ const message = error instanceof Error ? error.message : String(error);
1109
+ console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
1110
+ return false;
1111
+ });
1112
+ if (!waitPreference) {
1113
+ if (!detached) {
1114
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1115
+ process.exitCode = 1;
1116
+ return;
1117
+ }
1118
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
1119
+ console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
1120
+ return;
1121
+ }
1122
+ if (detached === false) {
1123
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps, cwd);
1124
+ return;
1125
+ }
1126
+ if (detached) {
1127
+ console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
1128
+ await attachSession(sessionMeta.id, { suppressMetadata: true });
1129
+ }
1130
+ }
880
1131
  async function executeSession(sessionId) {
881
1132
  const metadata = await sessionStore.readSession(sessionId);
882
1133
  if (!metadata) {
@@ -927,6 +1178,13 @@ function printDebugHelp(cliName) {
927
1178
  ['--browser-url <url>', 'Alias for --chatgpt-url.'],
928
1179
  ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
929
1180
  ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
1181
+ ['--browser-recheck-delay <ms|s|m|h>', 'After timeout, wait then revisit the conversation to retry capture.'],
1182
+ ['--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt.'],
1183
+ ['--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile before launching (parallel runs).'],
1184
+ ['--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the manual-login profile lock before sending.'],
1185
+ ['--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before periodic auto-reattach attempts after a timeout.'],
1186
+ ['--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).'],
1187
+ ['--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt.'],
930
1188
  ['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
931
1189
  ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
932
1190
  ['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
@@ -944,19 +1202,36 @@ function printDebugOptionGroup(entries) {
944
1202
  console.log(` ${label}${description}`);
945
1203
  });
946
1204
  }
947
- function resolveWaitFlag({ waitFlag, noWaitFlag, model, engine, }) {
1205
+ function resolveWaitFlag({ waitFlag, model, engine, }) {
948
1206
  if (waitFlag === true)
949
1207
  return true;
950
- if (noWaitFlag === true)
1208
+ if (waitFlag === false)
951
1209
  return false;
952
1210
  return defaultWaitPreference(model, engine);
953
1211
  }
1212
+ function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engine, }) {
1213
+ if (waitFlag === true)
1214
+ return true;
1215
+ if (waitFlag === false)
1216
+ return false;
1217
+ if (typeof storedPreference === 'boolean')
1218
+ return storedPreference;
1219
+ return defaultWaitPreference(model, engine);
1220
+ }
1221
+ function resolveEffectiveModelIdForRun(model, stored) {
1222
+ if (stored)
1223
+ return stored;
1224
+ if (model.startsWith('gemini')) {
1225
+ return resolveGeminiModelId(model);
1226
+ }
1227
+ return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
1228
+ }
954
1229
  program.action(async function () {
955
1230
  const options = this.optsWithGlobals();
956
1231
  await runRootCommand(options);
957
1232
  });
958
1233
  async function main() {
959
- const parsePromise = program.parseAsync(process.argv);
1234
+ const parsePromise = program.parseAsync(normalizedArgv);
960
1235
  const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
961
1236
  const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
962
1237
  if (result === 'sigint') {
@@ -0,0 +1,53 @@
1
+ import puppeteer from 'puppeteer-core';
2
+ const port = parseInt(process.argv[2] || '52990', 10);
3
+ async function main() {
4
+ const browser = await puppeteer.connect({
5
+ browserURL: `http://127.0.0.1:${port}`,
6
+ defaultViewport: null,
7
+ });
8
+ const pages = await browser.pages();
9
+ let targetPage = null;
10
+ for (const page of pages) {
11
+ const url = page.url();
12
+ if (url.includes('chatgpt.com/c/')) {
13
+ targetPage = page;
14
+ break;
15
+ }
16
+ }
17
+ if (!targetPage) {
18
+ console.error('ChatGPT conversation page not found');
19
+ process.exit(1);
20
+ }
21
+ console.error('Found page:', await targetPage.url());
22
+ // Extract the last assistant message
23
+ const content = (await targetPage.evaluate(() => {
24
+ // Try multiple selectors for ChatGPT's assistant messages
25
+ const selectors = [
26
+ '[data-message-author-role="assistant"] .markdown',
27
+ '[data-message-author-role="assistant"]',
28
+ '.agent-turn .markdown',
29
+ '.agent-turn',
30
+ ];
31
+ for (const selector of selectors) {
32
+ const elements = document.querySelectorAll(selector);
33
+ if (elements.length > 0) {
34
+ const lastEl = elements[elements.length - 1];
35
+ return {
36
+ selector,
37
+ count: elements.length,
38
+ text: lastEl.innerText,
39
+ };
40
+ }
41
+ }
42
+ // Debug: show what's on the page
43
+ const body = document.body.innerHTML;
44
+ return { error: 'No messages found', bodyLength: body.length, sample: body.slice(0, 2000) };
45
+ }));
46
+ if ('error' in content) {
47
+ console.error('Error:', JSON.stringify(content, null, 2));
48
+ process.exit(1);
49
+ }
50
+ console.log(content.text);
51
+ browser.disconnect();
52
+ }
53
+ main().catch(console.error);