@steipete/oracle 0.8.5 → 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.
Files changed (49) hide show
  1. package/README.md +99 -5
  2. package/dist/bin/oracle-cli.js +376 -13
  3. package/dist/src/browser/actions/assistantResponse.js +72 -37
  4. package/dist/src/browser/actions/modelSelection.js +60 -8
  5. package/dist/src/browser/actions/navigation.js +2 -1
  6. package/dist/src/browser/actions/promptComposer.js +141 -32
  7. package/dist/src/browser/chromeLifecycle.js +25 -9
  8. package/dist/src/browser/config.js +14 -0
  9. package/dist/src/browser/constants.js +1 -1
  10. package/dist/src/browser/index.js +414 -43
  11. package/dist/src/browser/profileState.js +93 -0
  12. package/dist/src/browser/providerDomFlow.js +17 -0
  13. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  14. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +245 -0
  15. package/dist/src/browser/providers/index.js +2 -0
  16. package/dist/src/cli/browserConfig.js +33 -6
  17. package/dist/src/cli/browserDefaults.js +21 -0
  18. package/dist/src/cli/detach.js +5 -2
  19. package/dist/src/cli/fileSize.js +11 -0
  20. package/dist/src/cli/help.js +3 -3
  21. package/dist/src/cli/markdownBundle.js +5 -1
  22. package/dist/src/cli/options.js +40 -3
  23. package/dist/src/cli/runOptions.js +11 -3
  24. package/dist/src/cli/sessionDisplay.js +91 -2
  25. package/dist/src/cli/sessionLineage.js +56 -0
  26. package/dist/src/cli/sessionRunner.js +169 -2
  27. package/dist/src/cli/sessionTable.js +2 -1
  28. package/dist/src/cli/tui/index.js +3 -0
  29. package/dist/src/gemini-web/browserSessionManager.js +76 -0
  30. package/dist/src/gemini-web/client.js +16 -5
  31. package/dist/src/gemini-web/executionClients.js +1 -0
  32. package/dist/src/gemini-web/executionMode.js +18 -0
  33. package/dist/src/gemini-web/executor.js +273 -120
  34. package/dist/src/mcp/tools/consult.js +35 -21
  35. package/dist/src/oracle/client.js +42 -13
  36. package/dist/src/oracle/config.js +43 -7
  37. package/dist/src/oracle/errors.js +2 -2
  38. package/dist/src/oracle/files.js +20 -5
  39. package/dist/src/oracle/gemini.js +3 -0
  40. package/dist/src/oracle/modelResolver.js +33 -1
  41. package/dist/src/oracle/request.js +7 -2
  42. package/dist/src/oracle/run.js +22 -12
  43. package/dist/src/sessionManager.js +13 -2
  44. package/dist/src/sessionStore.js +2 -2
  45. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  46. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  47. package/package.json +24 -24
  48. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  49. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -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.2 Pro / GPT-5.2 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
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('-f, --file <paths...>', 'Files/directories or glob patterns to attach (prefix with !pattern to exclude). Files larger than 1 MB are rejected automatically.', collectPaths, [])
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.2-pro default; also supports gpt-5.1-pro alias). Also gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).', normalizeModelOption)
127
- .addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.2-pro,gemini-3-pro").')
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.2-pro, 120s otherwise).')
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))
@@ -183,7 +186,14 @@ program
183
186
  .addOption(new Option('--chatgpt-url <url>', `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
184
187
  .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
185
188
  .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
186
- .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
189
+ .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 60s).').hideHelp())
190
+ .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())
191
+ .addOption(new Option('--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt (default 120s).').hideHelp())
192
+ .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())
193
+ .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())
194
+ .addOption(new Option('--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before starting periodic auto-reattach attempts after a timeout.').hideHelp())
195
+ .addOption(new Option('--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).').hideHelp())
196
+ .addOption(new Option('--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt (default 120s).').hideHelp())
187
197
  .addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
188
198
  .addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
189
199
  .argParser(parseIntOption))
@@ -379,6 +389,17 @@ const statusCommand = program
379
389
  showExamples,
380
390
  });
381
391
  });
392
+ program
393
+ .command('restart <id>')
394
+ .description('Re-run a stored session as a new session (clones options).')
395
+ .addOption(new Option('--wait').default(undefined))
396
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
397
+ .option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.')
398
+ .option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.')
399
+ .action(async (sessionId, _options, cmd) => {
400
+ const restartOptions = cmd.opts();
401
+ await restartSession(sessionId, restartOptions);
402
+ });
382
403
  function buildRunOptions(options, overrides = {}) {
383
404
  if (!options.prompt) {
384
405
  throw new Error('Prompt is required.');
@@ -395,8 +416,10 @@ function buildRunOptions(options, overrides = {}) {
395
416
  prompt: options.prompt,
396
417
  model: options.model,
397
418
  models: overrides.models ?? options.models,
419
+ previousResponseId: overrides.previousResponseId ?? options.previousResponseId,
398
420
  effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
399
421
  file: overrides.file ?? options.file ?? [],
422
+ maxFileSizeBytes: overrides.maxFileSizeBytes ?? options.maxFileSizeBytes,
400
423
  slug: overrides.slug ?? options.slug,
401
424
  filesReport: overrides.filesReport ?? options.filesReport,
402
425
  maxInput: overrides.maxInput ?? options.maxInput,
@@ -436,14 +459,141 @@ function resolveHeartbeatIntervalMs(seconds) {
436
459
  }
437
460
  return Math.round(seconds * 1000);
438
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
+ }
439
587
  function buildRunOptionsFromMetadata(metadata) {
440
588
  const stored = metadata.options ?? {};
441
589
  return {
442
590
  prompt: stored.prompt ?? '',
443
591
  model: stored.model ?? DEFAULT_MODEL,
444
592
  models: stored.models,
593
+ previousResponseId: stored.previousResponseId,
445
594
  effectiveModelId: stored.effectiveModelId ?? stored.model,
446
595
  file: stored.file ?? [],
596
+ maxFileSizeBytes: stored.maxFileSizeBytes,
447
597
  slug: stored.slug,
448
598
  filesReport: stored.filesReport,
449
599
  maxInput: stored.maxInput,
@@ -646,6 +796,14 @@ async function runRootCommand(options) {
646
796
  throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
647
797
  }
648
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
+ }
649
807
  const effectiveModelId = resolvedModel.startsWith('gemini')
650
808
  ? resolveGeminiModelId(resolvedModel)
651
809
  : isKnownModel(resolvedModel)
@@ -654,6 +812,7 @@ async function runRootCommand(options) {
654
812
  const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
655
813
  const { models: _rawModels, ...optionsWithoutModels } = options;
656
814
  const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
815
+ resolvedOptions.maxFileSizeBytes = resolveConfiguredMaxFileSizeBytes(userConfig, process.env);
657
816
  if (normalizedMultiModels.length > 0) {
658
817
  resolvedOptions.models = normalizedMultiModels;
659
818
  }
@@ -665,11 +824,10 @@ async function runRootCommand(options) {
665
824
  // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
666
825
  let waitPreference = resolveWaitFlag({
667
826
  waitFlag: options.wait,
668
- noWaitFlag: options.noWait,
669
827
  model: resolvedModel,
670
828
  engine,
671
829
  });
672
- if (remoteHost && !waitPreference) {
830
+ if (remoteHost && waitPreference === false) {
673
831
  console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
674
832
  waitPreference = true;
675
833
  }
@@ -729,6 +887,21 @@ async function runRootCommand(options) {
729
887
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
730
888
  }
731
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
+ }
732
905
  const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
733
906
  if (engine === 'browser') {
734
907
  await runBrowserPreview({
@@ -767,6 +940,21 @@ async function runRootCommand(options) {
767
940
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
768
941
  }
769
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
+ }
770
958
  const duplicateBlocked = await shouldBlockDuplicatePrompt({
771
959
  prompt: resolvedOptions.prompt,
772
960
  force: options.force,
@@ -781,7 +969,10 @@ async function runRootCommand(options) {
781
969
  const isBrowserMode = engine === 'browser' || userForcedBrowser;
782
970
  const filesToValidate = isBrowserMode ? options.file.filter((f) => !isMediaFile(f)) : options.file;
783
971
  if (filesToValidate.length > 0) {
784
- await readFiles(filesToValidate, { cwd: process.cwd() });
972
+ await readFiles(filesToValidate, {
973
+ cwd: process.cwd(),
974
+ maxFileSizeBytes: resolvedOptions.maxFileSizeBytes,
975
+ });
785
976
  }
786
977
  }
787
978
  const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
@@ -857,6 +1048,15 @@ async function runRootCommand(options) {
857
1048
  ...baseRunOptions,
858
1049
  mode: sessionMode,
859
1050
  browserConfig,
1051
+ followupSessionId: resolvedOptions.followupSessionId,
1052
+ followupModel: resolvedOptions.followupModel,
1053
+ waitPreference,
1054
+ youtube: options.youtube,
1055
+ generateImage: options.generateImage,
1056
+ editImage: options.editImage,
1057
+ outputPath: options.output,
1058
+ aspectRatio: options.aspect,
1059
+ geminiShowThoughts: options.geminiShowThoughts,
860
1060
  }, process.cwd(), notifications);
861
1061
  const liveRunOptions = {
862
1062
  ...baseRunOptions,
@@ -898,7 +1098,7 @@ async function runRootCommand(options) {
898
1098
  await attachSession(sessionMeta.id, { suppressMetadata: true });
899
1099
  }
900
1100
  }
901
- async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps) {
1101
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
902
1102
  const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
903
1103
  let headerAugmented = false;
904
1104
  const combinedLog = (message = '') => {
@@ -927,7 +1127,7 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
927
1127
  runOptions,
928
1128
  mode,
929
1129
  browserConfig,
930
- cwd: process.cwd(),
1130
+ cwd,
931
1131
  log: combinedLog,
932
1132
  write: combinedWrite,
933
1133
  version: VERSION,
@@ -970,6 +1170,145 @@ async function launchDetachedSession(sessionId) {
970
1170
  }
971
1171
  });
972
1172
  }
1173
+ async function restartSession(sessionId, options) {
1174
+ const metadata = await sessionStore.readSession(sessionId);
1175
+ if (!metadata) {
1176
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
1177
+ process.exitCode = 1;
1178
+ return;
1179
+ }
1180
+ const runOptions = buildRunOptionsFromMetadata(metadata);
1181
+ if (!runOptions.prompt) {
1182
+ console.error(chalk.red(`Session ${sessionId} has no stored prompt; cannot restart.`));
1183
+ process.exitCode = 1;
1184
+ return;
1185
+ }
1186
+ const sessionMode = getSessionMode(metadata);
1187
+ const engine = sessionMode === 'browser' ? 'browser' : 'api';
1188
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
1189
+ if (sessionMode === 'browser' && !browserConfig) {
1190
+ console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
1191
+ process.exitCode = 1;
1192
+ return;
1193
+ }
1194
+ const userConfig = (await loadUserConfig()).config;
1195
+ const cwd = metadata.cwd ?? process.cwd();
1196
+ const storedOptions = metadata.options ?? {};
1197
+ if (runOptions.file && runOptions.file.length > 0) {
1198
+ const isBrowserMode = engine === 'browser';
1199
+ const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
1200
+ if (filesToValidate.length > 0) {
1201
+ await readFiles(filesToValidate, {
1202
+ cwd,
1203
+ maxFileSizeBytes: runOptions.maxFileSizeBytes,
1204
+ });
1205
+ }
1206
+ }
1207
+ enforceBrowserSearchFlag(runOptions, sessionMode, console.log);
1208
+ let waitPreference = resolveRestartWaitPreference({
1209
+ waitFlag: options.wait,
1210
+ storedPreference: storedOptions.waitPreference,
1211
+ model: runOptions.model,
1212
+ engine,
1213
+ });
1214
+ const remoteConfig = resolveRemoteServiceConfig({
1215
+ cliHost: options.remoteHost,
1216
+ cliToken: options.remoteToken,
1217
+ userConfig,
1218
+ env: process.env,
1219
+ });
1220
+ const remoteHost = remoteConfig.host;
1221
+ const remoteToken = remoteConfig.token;
1222
+ if (remoteHost && engine !== 'browser') {
1223
+ throw new Error('--remote-host requires a browser session.');
1224
+ }
1225
+ if (remoteHost) {
1226
+ console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
1227
+ }
1228
+ if (remoteHost && waitPreference === false) {
1229
+ console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
1230
+ waitPreference = true;
1231
+ }
1232
+ let browserDeps;
1233
+ if (browserConfig && remoteHost) {
1234
+ browserDeps = {
1235
+ executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
1236
+ };
1237
+ console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1238
+ }
1239
+ else if (browserConfig && runOptions.model.startsWith('gemini')) {
1240
+ browserDeps = {
1241
+ executeBrowser: createGeminiWebExecutor({
1242
+ youtube: storedOptions.youtube,
1243
+ generateImage: storedOptions.generateImage,
1244
+ editImage: storedOptions.editImage,
1245
+ outputPath: storedOptions.outputPath,
1246
+ aspectRatio: storedOptions.aspectRatio,
1247
+ showThoughts: storedOptions.geminiShowThoughts,
1248
+ }),
1249
+ };
1250
+ console.log(chalk.dim('Using Gemini web client for browser automation'));
1251
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
1252
+ console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
1253
+ }
1254
+ }
1255
+ const remoteExecutionActive = Boolean(browserDeps);
1256
+ await sessionStore.ensureStorage();
1257
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
1258
+ const sessionMeta = await sessionStore.createSession({
1259
+ ...runOptions,
1260
+ mode: sessionMode,
1261
+ browserConfig,
1262
+ followupSessionId: storedOptions.followupSessionId,
1263
+ followupModel: storedOptions.followupModel,
1264
+ waitPreference,
1265
+ youtube: storedOptions.youtube,
1266
+ generateImage: storedOptions.generateImage,
1267
+ editImage: storedOptions.editImage,
1268
+ outputPath: storedOptions.outputPath,
1269
+ aspectRatio: storedOptions.aspectRatio,
1270
+ geminiShowThoughts: storedOptions.geminiShowThoughts,
1271
+ }, cwd, notifications, sessionId);
1272
+ const liveRunOptions = {
1273
+ ...runOptions,
1274
+ sessionId: sessionMeta.id,
1275
+ effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
1276
+ };
1277
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1278
+ const detachAllowed = remoteExecutionActive
1279
+ ? false
1280
+ : shouldDetachSession({
1281
+ engine,
1282
+ model: runOptions.model,
1283
+ waitPreference,
1284
+ disableDetachEnv,
1285
+ });
1286
+ const detached = !detachAllowed
1287
+ ? false
1288
+ : await launchDetachedSession(sessionMeta.id).catch((error) => {
1289
+ const message = error instanceof Error ? error.message : String(error);
1290
+ console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
1291
+ return false;
1292
+ });
1293
+ if (!waitPreference) {
1294
+ if (!detached) {
1295
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1296
+ process.exitCode = 1;
1297
+ return;
1298
+ }
1299
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
1300
+ console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
1301
+ return;
1302
+ }
1303
+ if (detached === false) {
1304
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps, cwd);
1305
+ return;
1306
+ }
1307
+ if (detached) {
1308
+ console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
1309
+ await attachSession(sessionMeta.id, { suppressMetadata: true });
1310
+ }
1311
+ }
973
1312
  async function executeSession(sessionId) {
974
1313
  const metadata = await sessionStore.readSession(sessionId);
975
1314
  if (!metadata) {
@@ -1020,6 +1359,13 @@ function printDebugHelp(cliName) {
1020
1359
  ['--browser-url <url>', 'Alias for --chatgpt-url.'],
1021
1360
  ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
1022
1361
  ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
1362
+ ['--browser-recheck-delay <ms|s|m|h>', 'After timeout, wait then revisit the conversation to retry capture.'],
1363
+ ['--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt.'],
1364
+ ['--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile before launching (parallel runs).'],
1365
+ ['--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the manual-login profile lock before sending.'],
1366
+ ['--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before periodic auto-reattach attempts after a timeout.'],
1367
+ ['--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).'],
1368
+ ['--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt.'],
1023
1369
  ['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
1024
1370
  ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
1025
1371
  ['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
@@ -1037,13 +1383,30 @@ function printDebugOptionGroup(entries) {
1037
1383
  console.log(` ${label}${description}`);
1038
1384
  });
1039
1385
  }
1040
- function resolveWaitFlag({ waitFlag, noWaitFlag, model, engine, }) {
1386
+ function resolveWaitFlag({ waitFlag, model, engine, }) {
1041
1387
  if (waitFlag === true)
1042
1388
  return true;
1043
- if (noWaitFlag === true)
1389
+ if (waitFlag === false)
1044
1390
  return false;
1045
1391
  return defaultWaitPreference(model, engine);
1046
1392
  }
1393
+ function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engine, }) {
1394
+ if (waitFlag === true)
1395
+ return true;
1396
+ if (waitFlag === false)
1397
+ return false;
1398
+ if (typeof storedPreference === 'boolean')
1399
+ return storedPreference;
1400
+ return defaultWaitPreference(model, engine);
1401
+ }
1402
+ function resolveEffectiveModelIdForRun(model, stored) {
1403
+ if (stored)
1404
+ return stored;
1405
+ if (model.startsWith('gemini')) {
1406
+ return resolveGeminiModelId(model);
1407
+ }
1408
+ return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
1409
+ }
1047
1410
  program.action(async function () {
1048
1411
  const options = this.optsWithGlobals();
1049
1412
  await runRootCommand(options);