@steipete/oracle 0.8.6 → 0.10.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 (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -1,58 +1,60 @@
1
1
  #!/usr/bin/env node
2
- import 'dotenv/config';
3
- import { spawn } from 'node:child_process';
4
- import { fileURLToPath } from 'node:url';
5
- import { once } from 'node:events';
6
- import { Command, Option } from 'commander';
2
+ import "dotenv/config";
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { once } from "node:events";
6
+ import { Command, Option } from "commander";
7
7
  // Allow `npx @steipete/oracle oracle-mcp` to resolve the MCP server even though npx runs the default binary.
8
- if (process.argv[2] === 'oracle-mcp') {
9
- const { startMcpServer } = await import('../src/mcp/server.js');
8
+ if (process.argv[2] === "oracle-mcp") {
9
+ const { startMcpServer } = await import("../src/mcp/server.js");
10
10
  await startMcpServer();
11
11
  process.exit(0);
12
12
  }
13
- import { resolveEngine, defaultWaitPreference } from '../src/cli/engine.js';
14
- import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
15
- import chalk from 'chalk';
16
- import { sessionStore, pruneOldSessions } from '../src/sessionStore.js';
17
- import { DEFAULT_MODEL, MODEL_CONFIGS, readFiles, estimateRequestTokens, buildRequestBody } from '../src/oracle.js';
18
- import { isKnownModel } from '../src/oracle/modelResolver.js';
19
- import { CHATGPT_URL } from '../src/browserMode.js';
20
- import { createRemoteBrowserExecutor } from '../src/remote/client.js';
21
- import { createGeminiWebExecutor } from '../src/gemini-web/index.js';
22
- import { applyHelpStyling } from '../src/cli/help.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
- import { copyToClipboard } from '../src/cli/clipboard.js';
25
- import { buildMarkdownBundle } from '../src/cli/markdownBundle.js';
26
- import { shouldDetachSession } from '../src/cli/detach.js';
27
- import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
28
- import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
29
- import { performSessionRun } from '../src/cli/sessionRunner.js';
30
- import { isMediaFile } from '../src/browser/prompt.js';
31
- import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
32
- import { formatCompactNumber } from '../src/cli/format.js';
33
- import { formatIntroLine } from '../src/cli/tagline.js';
34
- import { warnIfOversizeBundle } from '../src/cli/bundleWarnings.js';
35
- import { formatRenderedMarkdown } from '../src/cli/renderOutput.js';
36
- import { resolveRenderFlag, resolveRenderPlain } from '../src/cli/renderFlags.js';
37
- import { resolveGeminiModelId } from '../src/oracle/gemini.js';
38
- import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
39
- import { isErrorLogged } from '../src/cli/errorUtils.js';
40
- import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
41
- import { resolveOutputPath } from '../src/cli/writeOutputPath.js';
42
- import { getCliVersion } from '../src/version.js';
43
- import { runDryRunSummary, runBrowserPreview } from '../src/cli/dryRun.js';
44
- import { launchTui } from '../src/cli/tui/index.js';
45
- import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from '../src/cli/notifier.js';
46
- import { loadUserConfig } from '../src/config.js';
47
- import { applyBrowserDefaultsFromConfig } from '../src/cli/browserDefaults.js';
48
- import { shouldBlockDuplicatePrompt } from '../src/cli/duplicatePromptGuard.js';
49
- import { resolveRemoteServiceConfig } from '../src/remote/remoteServiceConfig.js';
13
+ import { resolveEngine, defaultWaitPreference } from "../src/cli/engine.js";
14
+ import { shouldRequirePrompt } from "../src/cli/promptRequirement.js";
15
+ import { resolveDashPrompt } from "../src/cli/stdin.js";
16
+ import chalk from "chalk";
17
+ import { sessionStore, pruneOldSessions } from "../src/sessionStore.js";
18
+ import { DEFAULT_MODEL, MODEL_CONFIGS, readFiles, estimateRequestTokens, buildRequestBody, } from "../src/oracle.js";
19
+ import { isKnownModel } from "../src/oracle/modelResolver.js";
20
+ import { CHATGPT_URL } from "../src/browserMode.js";
21
+ import { createRemoteBrowserExecutor } from "../src/remote/client.js";
22
+ import { createGeminiWebExecutor } from "../src/gemini-web/index.js";
23
+ import { applyHelpStyling } from "../src/cli/help.js";
24
+ import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
25
+ import { copyToClipboard } from "../src/cli/clipboard.js";
26
+ import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
27
+ import { shouldDetachSession } from "../src/cli/detach.js";
28
+ import { applyHiddenAliases } from "../src/cli/hiddenAliases.js";
29
+ import { buildBrowserConfig, resolveBrowserModelLabel } from "../src/cli/browserConfig.js";
30
+ import { performSessionRun } from "../src/cli/sessionRunner.js";
31
+ import { isMediaFile } from "../src/browser/prompt.js";
32
+ import { attachSession, showStatus, formatCompletionSummary } from "../src/cli/sessionDisplay.js";
33
+ import { formatCompactNumber } from "../src/cli/format.js";
34
+ import { formatIntroLine } from "../src/cli/tagline.js";
35
+ import { warnIfOversizeBundle } from "../src/cli/bundleWarnings.js";
36
+ import { formatRenderedMarkdown } from "../src/cli/renderOutput.js";
37
+ import { resolveRenderFlag, resolveRenderPlain } from "../src/cli/renderFlags.js";
38
+ import { resolveGeminiModelId } from "../src/oracle/gemini.js";
39
+ import { handleSessionCommand, formatSessionCleanupMessage, } from "../src/cli/sessionCommand.js";
40
+ import { isErrorLogged } from "../src/cli/errorUtils.js";
41
+ import { handleSessionAlias, handleStatusFlag } from "../src/cli/rootAlias.js";
42
+ import { resolveOutputPath } from "../src/cli/writeOutputPath.js";
43
+ import { getCliVersion } from "../src/version.js";
44
+ import { runDryRunSummary, runBrowserPreview } from "../src/cli/dryRun.js";
45
+ import { launchTui } from "../src/cli/tui/index.js";
46
+ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from "../src/cli/notifier.js";
47
+ import { loadUserConfig } from "../src/config.js";
48
+ import { applyBrowserDefaultsFromConfig } from "../src/cli/browserDefaults.js";
49
+ import { shouldBlockDuplicatePrompt } from "../src/cli/duplicatePromptGuard.js";
50
+ import { resolveRemoteServiceConfig } from "../src/remote/remoteServiceConfig.js";
51
+ import { resolveConfiguredMaxFileSizeBytes } from "../src/cli/fileSize.js";
50
52
  const VERSION = getCliVersion();
51
53
  const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
52
54
  const LEGACY_FLAG_ALIASES = new Map([
53
- ['--[no-]notify', '--notify'],
54
- ['--[no-]notify-sound', '--notify-sound'],
55
- ['--[no-]background', '--background'],
55
+ ["--[no-]notify", "--notify"],
56
+ ["--[no-]notify-sound", "--notify-sound"],
57
+ ["--[no-]background", "--background"],
56
58
  ]);
57
59
  const normalizedArgv = process.argv.map((arg, index) => {
58
60
  if (index < 2)
@@ -64,18 +66,18 @@ const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : raw
64
66
  const isTty = process.stdout.isTTY;
65
67
  const program = new Command();
66
68
  let introPrinted = false;
67
- program.hook('preAction', () => {
69
+ program.hook("preAction", () => {
68
70
  if (introPrinted)
69
71
  return;
70
72
  console.log(formatIntroLine(VERSION, { env: process.env, richTty: isTty }));
71
73
  introPrinted = true;
72
74
  });
73
75
  applyHelpStyling(program, VERSION, isTty);
74
- program.hook('preAction', (thisCommand) => {
76
+ program.hook("preAction", async (thisCommand) => {
75
77
  if (thisCommand !== program) {
76
78
  return;
77
79
  }
78
- if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
80
+ if (userCliArgs.some((arg) => arg === "--help" || arg === "-h")) {
79
81
  return;
80
82
  }
81
83
  if (userCliArgs.length === 0) {
@@ -87,7 +89,12 @@ program.hook('preAction', (thisCommand) => {
87
89
  const positional = thisCommand.args?.[0];
88
90
  if (!opts.prompt && positional) {
89
91
  opts.prompt = positional;
90
- thisCommand.setOptionValue('prompt', positional);
92
+ thisCommand.setOptionValue("prompt", positional);
93
+ }
94
+ const resolvedPrompt = await resolveDashPrompt(opts.prompt);
95
+ if (resolvedPrompt !== opts.prompt) {
96
+ opts.prompt = resolvedPrompt;
97
+ thisCommand.setOptionValue("prompt", resolvedPrompt);
91
98
  }
92
99
  if (shouldRequirePrompt(userCliArgs, opts)) {
93
100
  console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
@@ -97,139 +104,146 @@ program.hook('preAction', (thisCommand) => {
97
104
  }
98
105
  });
99
106
  program
100
- .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.')
107
+ .name("oracle")
108
+ .description("One-shot GPT-5.5 Pro / GPT-5.5 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.")
102
109
  .version(VERSION)
103
- .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
104
- .option('-p, --prompt <text>', 'User prompt to send to the model.')
105
- .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
- .addOption(new Option('--include <paths...>', 'Alias for --file.')
110
+ .argument("[prompt]", "Prompt text (shorthand for --prompt).")
111
+ .option("-p, --prompt <text>", "User prompt to send to the model.")
112
+ .addOption(new Option("--message <text>", "Alias for --prompt.").hideHelp())
113
+ .option("--followup <sessionId|responseId>", "Continue an OpenAI/Azure Responses API run from a stored response id (resp_...) or from a stored oracle session id.")
114
+ .option("--followup-model <model>", "When following up a multi-model session, choose which model response to continue from.")
115
+ .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, [])
116
+ .addOption(new Option("--include <paths...>", "Alias for --file.")
108
117
  .argParser(collectPaths)
109
118
  .default([])
110
119
  .hideHelp())
111
- .addOption(new Option('--files <paths...>', 'Alias for --file.')
120
+ .addOption(new Option("--files <paths...>", "Alias for --file.")
112
121
  .argParser(collectPaths)
113
122
  .default([])
114
123
  .hideHelp())
115
- .addOption(new Option('--path <paths...>', 'Alias for --file.')
124
+ .addOption(new Option("--path <paths...>", "Alias for --file.")
116
125
  .argParser(collectPaths)
117
126
  .default([])
118
127
  .hideHelp())
119
- .addOption(new Option('--paths <paths...>', 'Alias for --file.')
128
+ .addOption(new Option("--paths <paths...>", "Alias for --file.")
120
129
  .argParser(collectPaths)
121
130
  .default([])
122
131
  .hideHelp())
123
- .addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
124
- .addOption(new Option('--copy').hideHelp().default(false))
125
- .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").')
132
+ .addOption(new Option("--copy-markdown", "Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.").default(false))
133
+ .addOption(new Option("--copy").hideHelp().default(false))
134
+ .option("-s, --slug <words>", "Custom session slug (3-5 words).")
135
+ .option("-m, --model <model>", 'Model to target (gpt-5.5-pro default). Also gpt-5.5, gpt-5.4-pro, 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.6-sonnet, claude-4.1-opus, or ChatGPT labels like "5.5 Pro" / "5.2 Thinking" for browser runs).', normalizeModelOption)
136
+ .addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
128
137
  .argParser(collectModelList)
129
138
  .default([]))
130
- .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']))
131
- .addOption(new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp())
132
- .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
133
- .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
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))
138
- .addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.2-pro, 120s otherwise).')
139
+ .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"]))
140
+ .addOption(new Option("--mode <mode>", "Alias for --engine (api | browser).")
141
+ .choices(["api", "browser"])
142
+ .hideHelp())
143
+ .option("--files-report", "Show token usage per attached file (also prints automatically when files exceed the token budget).", false)
144
+ .option("-v, --verbose", "Enable verbose logging for all operations.", false)
145
+ .addOption(new Option("--notify", "Desktop notification when a session finishes (default on unless CI/SSH).").default(undefined))
146
+ .addOption(new Option("--no-notify", "Disable desktop notifications.").default(undefined))
147
+ .addOption(new Option("--notify-sound", "Play a notification sound on completion (default off).").default(undefined))
148
+ .addOption(new Option("--no-notify-sound", "Disable notification sounds.").default(undefined))
149
+ .addOption(new Option("--timeout <seconds|auto>", "Overall timeout before aborting the API call (auto = 60m for Pro models, 120s otherwise).")
139
150
  .argParser(parseTimeoutOption)
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'))
151
+ .default("auto"))
152
+ .addOption(new Option("--background", "Use Responses API background mode (create + retrieve) for API runs.").default(undefined))
153
+ .addOption(new Option("--no-background", "Disable Responses API background mode.").default(undefined))
154
+ .addOption(new Option("--http-timeout <ms|s|m|h>", "HTTP client timeout for API requests (default 20m).")
155
+ .argParser((value) => parseDurationOption(value, "HTTP timeout"))
145
156
  .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'))
157
+ .addOption(new Option("--zombie-timeout <ms|s|m|h>", "Override stale-session cutoff used by `oracle status` (default 60m).")
158
+ .argParser((value) => parseDurationOption(value, "Zombie timeout"))
148
159
  .default(undefined))
149
- .option('--zombie-last-activity', 'Base stale-session detection on last log activity instead of start time.', false)
150
- .addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
160
+ .option("--zombie-last-activity", "Base stale-session detection on last log activity instead of start time.", false)
161
+ .addOption(new Option("--preview [mode]", "(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.")
151
162
  .hideHelp()
152
- .choices(['summary', 'json', 'full'])
153
- .preset('summary'))
154
- .addOption(new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
155
- .choices(['summary', 'json', 'full'])
156
- .preset('summary')
163
+ .choices(["summary", "json", "full"])
164
+ .preset("summary"))
165
+ .addOption(new Option("--dry-run [mode]", "Preview without calling the model (summary | json | full).")
166
+ .choices(["summary", "json", "full"])
167
+ .preset("summary")
157
168
  .default(false))
158
- .addOption(new Option('--exec-session <id>').hideHelp())
159
- .addOption(new Option('--session <id>').hideHelp())
160
- .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
161
- .option('--render-markdown', 'Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.', false)
162
- .option('--render', 'Alias for --render-markdown.', false)
163
- .option('--render-plain', 'Render markdown without ANSI/highlighting (use plain text even in a TTY).', false)
164
- .option('--write-output <path>', 'Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).')
165
- .option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
166
- .addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
169
+ .addOption(new Option("--exec-session <id>").hideHelp())
170
+ .addOption(new Option("--session <id>").hideHelp())
171
+ .addOption(new Option("--status", "Show stored sessions (alias for `oracle status`).")
172
+ .default(false)
173
+ .hideHelp())
174
+ .option("--render-markdown", "Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.", false)
175
+ .option("--render", "Alias for --render-markdown.", false)
176
+ .option("--render-plain", "Render markdown without ANSI/highlighting (use plain text even in a TTY).", false)
177
+ .option("--write-output <path>", "Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).")
178
+ .option("--verbose-render", "Show render/TTY diagnostics when replaying sessions.", false)
179
+ .addOption(new Option("--search <mode>", "Set server-side search behavior (on/off).")
167
180
  .argParser(parseSearchOption)
168
181
  .hideHelp())
169
- .addOption(new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
182
+ .addOption(new Option("--max-input <tokens>", "Override the input token budget for the selected model.")
170
183
  .argParser(parseIntOption)
171
184
  .hideHelp())
172
- .addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
185
+ .addOption(new Option("--max-output <tokens>", "Override the max output tokens for the selected model.")
173
186
  .argParser(parseIntOption)
174
187
  .hideHelp())
175
- .option('--base-url <url>', 'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).')
176
- .option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
177
- .option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
178
- .option('--azure-api-version <version>', 'Azure OpenAI API Version.')
179
- .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
180
- .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
181
- .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
182
- .addOption(new Option('--browser-cookie-path <path>', 'Explicit Chrome/Chromium cookie DB path for session reuse.'))
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}).`))
184
- .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
185
- .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 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())
194
- .addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
195
- .addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
196
- .argParser(parseIntOption))
197
- .addOption(new Option('--browser-debug-port <port>', '(alias) Use a fixed Chrome DevTools port.').argParser(parseIntOption).hideHelp())
198
- .addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
199
- .addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
200
- .addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
201
- .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
202
- .addOption(new Option('--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.').hideHelp())
203
- .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
204
- .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
205
- .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
206
- .addOption(new Option('--browser-model-strategy <mode>', 'ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.').choices(['select', 'current', 'ignore']))
207
- .addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
208
- .choices(['light', 'standard', 'extended', 'heavy'])
188
+ .option("--base-url <url>", "Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).")
189
+ .option("--azure-endpoint <url>", "Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).")
190
+ .option("--azure-deployment <name>", "Azure OpenAI Deployment Name.")
191
+ .option("--azure-api-version <version>", "Azure OpenAI API Version.")
192
+ .addOption(new Option("--browser", "(deprecated) Use --engine browser instead.").default(false).hideHelp())
193
+ .addOption(new Option("--browser-chrome-profile <name>", "Chrome profile name/path for cookie reuse.").hideHelp())
194
+ .addOption(new Option("--browser-chrome-path <path>", "Explicit Chrome or Chromium executable path.").hideHelp())
195
+ .addOption(new Option("--browser-cookie-path <path>", "Explicit Chrome/Chromium cookie DB path for session reuse."))
196
+ .addOption(new Option("--chatgpt-url <url>", `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
197
+ .addOption(new Option("--browser-url <url>", `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
198
+ .addOption(new Option("--browser-timeout <ms|s|m>", "Maximum time to wait for an answer (default 1200s / 20m).").hideHelp())
199
+ .addOption(new Option("--browser-input-timeout <ms|s|m>", "Maximum time to wait for the prompt textarea (default 60s).").hideHelp())
200
+ .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())
201
+ .addOption(new Option("--browser-recheck-timeout <ms|s|m|h>", "Time budget for the delayed recheck attempt (default 120s).").hideHelp())
202
+ .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())
203
+ .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())
204
+ .addOption(new Option("--browser-auto-reattach-delay <ms|s|m|h>", "Delay before starting periodic auto-reattach attempts after a timeout.").hideHelp())
205
+ .addOption(new Option("--browser-auto-reattach-interval <ms|s|m|h>", "Interval between auto-reattach attempts (0 disables).").hideHelp())
206
+ .addOption(new Option("--browser-auto-reattach-timeout <ms|s|m|h>", "Time budget for each auto-reattach attempt (default 120s).").hideHelp())
207
+ .addOption(new Option("--browser-cookie-wait <ms|s|m>", "Wait before retrying cookie sync when Chrome cookies are empty or locked.").hideHelp())
208
+ .addOption(new Option("--browser-port <port>", "Use a fixed Chrome DevTools port (helpful on WSL firewalls).").argParser(parseIntOption))
209
+ .addOption(new Option("--browser-debug-port <port>", "(alias) Use a fixed Chrome DevTools port.")
210
+ .argParser(parseIntOption)
211
+ .hideHelp())
212
+ .addOption(new Option("--browser-cookie-names <names>", "Comma-separated cookie allowlist for sync.").hideHelp())
213
+ .addOption(new Option("--browser-inline-cookies <jsonOrBase64>", "Inline cookies payload (JSON array or base64-encoded JSON).").hideHelp())
214
+ .addOption(new Option("--browser-inline-cookies-file <path>", "Load inline cookies from file (JSON or base64 JSON).").hideHelp())
215
+ .addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
216
+ .addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
217
+ .addOption(new Option("--browser-headless", "Launch Chrome in headless mode.").hideHelp())
218
+ .addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
219
+ .addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
220
+ .addOption(new Option("--browser-model-strategy <mode>", "ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.").choices(["select", "current", "ignore"]))
221
+ .addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.")
222
+ .choices(["light", "standard", "extended", "heavy"])
209
223
  .hideHelp())
210
- .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
211
- .addOption(new Option('--browser-attachments <mode>', 'How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.')
212
- .choices(['auto', 'never', 'always'])
213
- .default('auto'))
214
- .addOption(new Option('--remote-chrome <host:port>', 'Connect to remote Chrome DevTools Protocol (e.g., 192.168.1.10:9222 or [2001:db8::1]:9222 for IPv6).'))
215
- .addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
216
- .addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
217
- .addOption(new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false))
218
- .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
219
- .addOption(new Option('--youtube <url>', 'YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com).'))
220
- .addOption(new Option('--generate-image <file>', 'Generate image and save to file (Gemini web/cookie mode only; requires gemini.google.com Chrome cookies).'))
221
- .addOption(new Option('--edit-image <file>', 'Edit existing image (use with --output, Gemini web/cookie mode only).'))
222
- .addOption(new Option('--output <file>', 'Output file path for image operations (Gemini web/cookie mode only).'))
223
- .addOption(new Option('--aspect <ratio>', 'Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only).'))
224
- .addOption(new Option('--gemini-show-thoughts', 'Display Gemini thinking process (Gemini web/cookie mode only).').default(false))
225
- .option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
226
- .option('--force', 'Force start a new session even if an identical prompt is already running.', false)
227
- .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
228
- .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
229
- .addOption(new Option('--wait').default(undefined))
230
- .addOption(new Option('--no-wait').default(undefined).hideHelp())
231
- .showHelpAfterError('(use --help for usage)');
232
- program.addHelpText('after', `
224
+ .addOption(new Option("--browser-allow-cookie-errors", "Continue even if Chrome cookies cannot be copied.").hideHelp())
225
+ .addOption(new Option("--browser-attachments <mode>", "How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.")
226
+ .choices(["auto", "never", "always"])
227
+ .default("auto"))
228
+ .addOption(new Option("--remote-chrome <host:port>", "Connect to remote Chrome DevTools Protocol (e.g., 192.168.1.10:9222 or [2001:db8::1]:9222 for IPv6)."))
229
+ .addOption(new Option("--remote-host <host:port>", "Delegate browser runs to a remote `oracle serve` instance."))
230
+ .addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
231
+ .addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
232
+ .addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
233
+ .addOption(new Option("--youtube <url>", "YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com)."))
234
+ .addOption(new Option("--generate-image <file>", "Generate image and save to file (Gemini web/cookie mode only; requires gemini.google.com Chrome cookies)."))
235
+ .addOption(new Option("--edit-image <file>", "Edit existing image (use with --output, Gemini web/cookie mode only)."))
236
+ .addOption(new Option("--output <file>", "Output file path for image operations (Gemini web/cookie mode only)."))
237
+ .addOption(new Option("--aspect <ratio>", "Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only)."))
238
+ .addOption(new Option("--gemini-show-thoughts", "Display Gemini thinking process (Gemini web/cookie mode only).").default(false))
239
+ .option("--retain-hours <hours>", "Prune stored sessions older than this many hours before running (set 0 to disable).", parseFloatOption)
240
+ .option("--force", "Force start a new session even if an identical prompt is already running.", false)
241
+ .option("--debug-help", "Show the advanced/debug option set and exit.", false)
242
+ .option("--heartbeat <seconds>", "Emit periodic in-progress updates (0 to disable).", parseHeartbeatOption, 30)
243
+ .addOption(new Option("--wait").default(undefined))
244
+ .addOption(new Option("--no-wait").default(undefined).hideHelp())
245
+ .showHelpAfterError("(use --help for usage)");
246
+ program.addHelpText("after", `
233
247
  Examples:
234
248
  # Quick API run with two files
235
249
  oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
@@ -242,15 +256,15 @@ Examples:
242
256
  oracle --render --copy -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
243
257
  `);
244
258
  program
245
- .command('serve')
246
- .description('Run Oracle browser automation as a remote service for other machines.')
247
- .option('--host <address>', 'Interface to bind (default 0.0.0.0).')
248
- .option('--port <number>', 'Port to listen on (default random).', parseIntOption)
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).')
259
+ .command("serve")
260
+ .description("Run Oracle browser automation as a remote service for other machines.")
261
+ .option("--host <address>", "Interface to bind (default 0.0.0.0).")
262
+ .option("--port <number>", "Port to listen on (default random).", parseIntOption)
263
+ .option("--token <value>", "Access token clients must provide (random if omitted).")
264
+ .option("--manual-login", "Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).", false)
265
+ .option("--manual-login-profile-dir <path>", "Chrome profile directory for manual login (default ~/.oracle/browser-profile).")
252
266
  .action(async (commandOptions) => {
253
- const { serveRemote } = await import('../src/remote/server.js');
267
+ const { serveRemote } = await import("../src/remote/server.js");
254
268
  await serveRemote({
255
269
  host: commandOptions.host,
256
270
  port: commandOptions.port,
@@ -259,119 +273,122 @@ program
259
273
  manualLoginProfileDir: commandOptions.manualLoginProfileDir,
260
274
  });
261
275
  });
262
- const bridgeCommand = program.command('bridge').description('Bridge a Windows-hosted ChatGPT session to Linux clients.');
276
+ const bridgeCommand = program
277
+ .command("bridge")
278
+ .description("Bridge a Windows-hosted ChatGPT session to Linux clients.");
263
279
  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)
280
+ .command("host")
281
+ .description("Start a secure oracle serve host (optionally with an SSH reverse tunnel).")
282
+ .option("--bind <host:port>", "Local bind address for the host service (default 127.0.0.1:9473).")
283
+ .option("--token <token|auto>", "Service access token (default auto).", "auto")
284
+ .option("--write-connection <path>", "Write a connection artifact JSON (default ~/.oracle/bridge-connection.json).")
285
+ .option("--ssh <user@host>", "Maintain an SSH reverse tunnel to the Linux host (ssh -N -R ...).")
286
+ .option("--ssh-remote-port <port>", "Remote port to bind on the Linux host (default matches --bind port).", parseIntOption)
287
+ .option("--ssh-identity <path>", "SSH identity file (ssh -i).")
288
+ .option("--ssh-extra-args <args>", "Extra args passed to ssh (quoted string).")
289
+ .option("--background", "Run the host in the background and write pid/log files.", false)
290
+ .option("--foreground", "Run the host in the foreground (default).", false)
291
+ .option("--print", "Print the client connection string (includes token).", false)
292
+ .option("--print-token", "Print only the token.", false)
277
293
  .action(async (commandOptions) => {
278
- const { runBridgeHost } = await import('../src/cli/bridge/host.js');
294
+ const { runBridgeHost } = await import("../src/cli/bridge/host.js");
279
295
  await runBridgeHost(commandOptions);
280
296
  });
281
297
  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)
298
+ .command("client")
299
+ .description("Configure this machine to use a remote oracle serve host.")
300
+ .requiredOption("--connect <connection>", "Connection string or path to bridge-connection.json.")
301
+ .option("--config <path>", "Override the oracle config file location (default ~/.oracle/config.json).")
302
+ .option("--no-write-config", "Do not write ~/.oracle/config.json (just validate).")
303
+ .option("--no-test", "Skip remote /health check.")
304
+ .option("--print-env", "Print env var exports (includes token).", false)
289
305
  .action(async (commandOptions) => {
290
- const { runBridgeClient } = await import('../src/cli/bridge/client.js');
306
+ const { runBridgeClient } = await import("../src/cli/bridge/client.js");
291
307
  await runBridgeClient(commandOptions);
292
308
  });
293
309
  bridgeCommand
294
- .command('doctor')
295
- .description('Diagnose bridge connectivity and browser engine prerequisites.')
296
- .option('--verbose', 'Show extra diagnostics.', false)
310
+ .command("doctor")
311
+ .description("Diagnose bridge connectivity and browser engine prerequisites.")
312
+ .option("--verbose", "Show extra diagnostics.", false)
297
313
  .action(async (commandOptions) => {
298
- const { runBridgeDoctor } = await import('../src/cli/bridge/doctor.js');
314
+ const { runBridgeDoctor } = await import("../src/cli/bridge/doctor.js");
299
315
  await runBridgeDoctor(commandOptions);
300
316
  });
301
317
  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)
318
+ .command("codex-config")
319
+ .description("Print a Codex CLI MCP server config snippet for oracle-mcp.")
320
+ .option("--print-token", "Include ORACLE_REMOTE_TOKEN in the snippet.", false)
305
321
  .action(async (commandOptions) => {
306
- const { runBridgeCodexConfig } = await import('../src/cli/bridge/codexConfig.js');
322
+ const { runBridgeCodexConfig } = await import("../src/cli/bridge/codexConfig.js");
307
323
  await runBridgeCodexConfig(commandOptions);
308
324
  });
309
325
  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)
326
+ .command("claude-config")
327
+ .description("Print a Claude Code MCP config snippet (.mcp.json) for oracle-mcp.")
328
+ .option("--print-token", "Include ORACLE_REMOTE_TOKEN in the snippet.", false)
313
329
  .action(async (commandOptions) => {
314
- const { runBridgeClaudeConfig } = await import('../src/cli/bridge/claudeConfig.js');
330
+ const { runBridgeClaudeConfig } = await import("../src/cli/bridge/claudeConfig.js");
315
331
  await runBridgeClaudeConfig(commandOptions);
316
332
  });
317
333
  program
318
- .command('tui')
319
- .description('Launch the interactive terminal UI for humans (no automation).')
334
+ .command("tui")
335
+ .description("Launch the interactive terminal UI for humans (no automation).")
320
336
  .action(async () => {
321
337
  await sessionStore.ensureStorage();
322
338
  await launchTui({ version: VERSION, printIntro: false });
323
339
  });
324
- const sessionCommand = program
325
- .command('session [id]')
326
- .description('Attach to a stored session or list recent sessions when no ID is provided.')
327
- .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
328
- .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
329
- .option('--all', 'Include all stored sessions regardless of age.', false)
330
- .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
331
- .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
332
- .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
333
- .option('--render-markdown', 'Alias for --render.', false)
334
- .option('--model <name>', 'Filter sessions/output for a specific model.', '')
335
- .option('--path', 'Print the stored session paths instead of attaching.', false)
336
- .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
340
+ program
341
+ .command("session [id]")
342
+ .description("Attach to a stored session or list recent sessions when no ID is provided.")
343
+ .option("--hours <hours>", "Look back this many hours when listing sessions (default 24).", parseFloatOption, 24)
344
+ .option("--limit <count>", "Maximum sessions to show when listing (max 1000).", parseIntOption, 100)
345
+ .option("--all", "Include all stored sessions regardless of age.", false)
346
+ .option("--clear", "Delete stored sessions older than the provided window (24h default).", false)
347
+ .option("--hide-prompt", "Hide stored prompt when displaying a session.", false)
348
+ .option("--render", "Render completed session output as markdown (rich TTY only).", false)
349
+ .option("--render-markdown", "Alias for --render.", false)
350
+ .option("--model <name>", "Filter sessions/output for a specific model.", "")
351
+ .option("--path", "Print the stored session paths instead of attaching.", false)
352
+ .addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
337
353
  .action(async (sessionId, _options, cmd) => {
338
354
  await handleSessionCommand(sessionId, cmd);
339
355
  });
340
- const statusCommand = program
341
- .command('status [id]')
342
- .description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
343
- .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
344
- .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
345
- .option('--all', 'Include all stored sessions regardless of age.', false)
346
- .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
347
- .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
348
- .option('--render-markdown', 'Alias for --render.', false)
349
- .option('--model <name>', 'Filter sessions/output for a specific model.', '')
350
- .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
351
- .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
356
+ program
357
+ .command("status [id]")
358
+ .description("List recent sessions (24h window by default) or attach to a session when an ID is provided.")
359
+ .option("--hours <hours>", "Look back this many hours (default 24).", parseFloatOption, 24)
360
+ .option("--limit <count>", "Maximum sessions to show (max 1000).", parseIntOption, 100)
361
+ .option("--all", "Include all stored sessions regardless of age.", false)
362
+ .option("--clear", "Delete stored sessions older than the provided window (24h default).", false)
363
+ .option("--render", "Render completed session output as markdown (rich TTY only).", false)
364
+ .option("--render-markdown", "Alias for --render.", false)
365
+ .option("--model <name>", "Filter sessions/output for a specific model.", "")
366
+ .option("--hide-prompt", "Hide stored prompt when displaying a session.", false)
367
+ .addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
352
368
  .action(async (sessionId, _options, command) => {
353
369
  const statusOptions = command.opts();
354
370
  const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
355
371
  if (clearRequested) {
356
372
  if (sessionId) {
357
- console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
373
+ console.error("Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.");
358
374
  process.exitCode = 1;
359
375
  return;
360
376
  }
361
377
  const hours = statusOptions.hours;
362
378
  const includeAll = statusOptions.all;
363
379
  const result = await sessionStore.deleteOlderThan({ hours, includeAll });
364
- const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
380
+ const scope = includeAll ? "all stored sessions" : `sessions older than ${hours}h`;
365
381
  console.log(formatSessionCleanupMessage(result, scope));
366
382
  return;
367
383
  }
368
- if (sessionId === 'clear' || sessionId === 'clean') {
384
+ if (sessionId === "clear" || sessionId === "clean") {
369
385
  console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
370
386
  process.exitCode = 1;
371
387
  return;
372
388
  }
373
389
  if (sessionId) {
374
- const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
390
+ const autoRender = !command.getOptionValueSource?.("render") &&
391
+ !command.getOptionValueSource?.("renderMarkdown")
375
392
  ? process.stdout.isTTY
376
393
  : false;
377
394
  const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
@@ -387,19 +404,19 @@ const statusCommand = program
387
404
  });
388
405
  });
389
406
  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.')
407
+ .command("restart <id>")
408
+ .description("Re-run a stored session as a new session (clones options).")
409
+ .addOption(new Option("--wait").default(undefined))
410
+ .addOption(new Option("--no-wait").default(undefined).hideHelp())
411
+ .option("--remote-host <host:port>", "Delegate browser runs to a remote `oracle serve` instance.")
412
+ .option("--remote-token <token>", "Access token for the remote `oracle serve` instance.")
396
413
  .action(async (sessionId, _options, cmd) => {
397
414
  const restartOptions = cmd.opts();
398
415
  await restartSession(sessionId, restartOptions);
399
416
  });
400
417
  function buildRunOptions(options, overrides = {}) {
401
418
  if (!options.prompt) {
402
- throw new Error('Prompt is required.');
419
+ throw new Error("Prompt is required.");
403
420
  }
404
421
  const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
405
422
  const azure = options.azureEndpoint || overrides.azure?.endpoint
@@ -413,8 +430,10 @@ function buildRunOptions(options, overrides = {}) {
413
430
  prompt: options.prompt,
414
431
  model: options.model,
415
432
  models: overrides.models ?? options.models,
433
+ previousResponseId: overrides.previousResponseId ?? options.previousResponseId,
416
434
  effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
417
435
  file: overrides.file ?? options.file ?? [],
436
+ maxFileSizeBytes: overrides.maxFileSizeBytes ?? options.maxFileSizeBytes,
418
437
  slug: overrides.slug ?? options.slug,
419
438
  filesReport: overrides.filesReport ?? options.filesReport,
420
439
  maxInput: overrides.maxInput ?? options.maxInput,
@@ -434,7 +453,9 @@ function buildRunOptions(options, overrides = {}) {
434
453
  sessionId: overrides.sessionId ?? options.sessionId,
435
454
  verbose: overrides.verbose ?? options.verbose,
436
455
  heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
437
- browserAttachments: overrides.browserAttachments ?? options.browserAttachments ?? 'auto',
456
+ browserAttachments: overrides.browserAttachments ??
457
+ options.browserAttachments ??
458
+ "auto",
438
459
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
439
460
  browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
440
461
  background: overrides.background ?? undefined,
@@ -443,25 +464,154 @@ function buildRunOptions(options, overrides = {}) {
443
464
  };
444
465
  }
445
466
  export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
446
- if (sessionMode === 'browser' && runOptions.search === false) {
447
- logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
467
+ if (sessionMode === "browser" && runOptions.search === false) {
468
+ logFn(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
448
469
  runOptions.search = undefined;
449
470
  }
450
471
  }
451
472
  function resolveHeartbeatIntervalMs(seconds) {
452
- if (typeof seconds !== 'number' || seconds <= 0) {
473
+ if (typeof seconds !== "number" || seconds <= 0) {
453
474
  return undefined;
454
475
  }
455
476
  return Math.round(seconds * 1000);
456
477
  }
478
+ function assertFollowupSupported({ engine, model, baseUrl, azureEndpoint, }) {
479
+ if (engine !== "api") {
480
+ throw new Error("--followup requires --engine api.");
481
+ }
482
+ if (model.startsWith("gemini") || model.startsWith("claude")) {
483
+ throw new Error(`--followup is only supported for OpenAI Responses API runs. Model ${model} uses a provider client without previous_response_id support.`);
484
+ }
485
+ if (baseUrl && !azureEndpoint) {
486
+ throw new Error("--followup is only supported for the default OpenAI Responses API or Azure OpenAI Responses. Custom --base-url providers are not supported.");
487
+ }
488
+ }
489
+ function levenshteinDistance(a, b) {
490
+ if (a === b)
491
+ return 0;
492
+ if (a.length === 0)
493
+ return b.length;
494
+ if (b.length === 0)
495
+ return a.length;
496
+ const previous = Array.from({ length: b.length + 1 });
497
+ const current = Array.from({ length: b.length + 1 });
498
+ for (let j = 0; j <= b.length; j += 1) {
499
+ previous[j] = j;
500
+ }
501
+ for (let i = 1; i <= a.length; i += 1) {
502
+ current[0] = i;
503
+ for (let j = 1; j <= b.length; j += 1) {
504
+ const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
505
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + substitutionCost);
506
+ }
507
+ for (let j = 0; j <= b.length; j += 1) {
508
+ previous[j] = current[j];
509
+ }
510
+ }
511
+ return previous[b.length];
512
+ }
513
+ function scoreSessionSimilarity(input, candidate) {
514
+ if (input === candidate)
515
+ return 1;
516
+ if (candidate.startsWith(input) || input.startsWith(candidate))
517
+ return 0.95;
518
+ if (candidate.includes(input) || input.includes(candidate))
519
+ return 0.8;
520
+ const distance = levenshteinDistance(input, candidate);
521
+ const maxLength = Math.max(input.length, candidate.length);
522
+ if (maxLength === 0)
523
+ return 0;
524
+ return Math.max(0, 1 - distance / maxLength);
525
+ }
526
+ async function suggestFollowupSessionIds(input, limit = 3) {
527
+ const normalizedInput = input.trim().toLowerCase();
528
+ if (!normalizedInput)
529
+ return [];
530
+ const sessions = await sessionStore.listSessions().catch(() => []);
531
+ const seen = new Set();
532
+ const ranked = sessions
533
+ .map((meta) => meta.id)
534
+ .filter((id) => typeof id === "string" && id.length > 0)
535
+ .filter((id) => {
536
+ if (seen.has(id))
537
+ return false;
538
+ seen.add(id);
539
+ return true;
540
+ })
541
+ .map((id) => ({ id, score: scoreSessionSimilarity(normalizedInput, id.toLowerCase()) }))
542
+ .filter((entry) => entry.score >= 0.45)
543
+ .sort((a, b) => b.score - a.score)
544
+ .slice(0, limit);
545
+ return ranked.map((entry) => entry.id);
546
+ }
547
+ async function resolveFollowupReference(value, followupModel) {
548
+ const trimmed = value.trim();
549
+ if (trimmed.length === 0) {
550
+ throw new Error("--followup requires a session id or response id.");
551
+ }
552
+ if (trimmed.startsWith("resp_")) {
553
+ return { responseId: trimmed };
554
+ }
555
+ // Treat as oracle session id (slug).
556
+ const meta = await sessionStore.readSession(trimmed);
557
+ if (!meta) {
558
+ const suggestions = await suggestFollowupSessionIds(trimmed);
559
+ const suggestionText = suggestions.length > 0
560
+ ? ` Did you mean: ${suggestions.map((id) => `"${id}"`).join(", ")}?`
561
+ : "";
562
+ throw new Error(`No session found with ID ${trimmed}.${suggestionText} Run "oracle status --hours 72 --limit 20" to list recent sessions.`);
563
+ }
564
+ const fromMetadata = extractResponseIdFromSession(meta, followupModel);
565
+ if (fromMetadata) {
566
+ return { responseId: fromMetadata, sessionId: meta.id };
567
+ }
568
+ // Fallback: scrape the log for a response id (covers older sessions / edge cases).
569
+ const logText = await sessionStore.readLog(trimmed).catch(() => "");
570
+ const matches = logText.match(/resp_[A-Za-z0-9]+/g) ?? [];
571
+ const last = matches.length > 0 ? matches[matches.length - 1] : null;
572
+ if (last) {
573
+ return { responseId: last, sessionId: meta.id };
574
+ }
575
+ 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).`);
576
+ }
577
+ function extractResponseIdFromSession(meta, followupModel) {
578
+ // Single-model sessions store response metadata at the session root.
579
+ const rootResponse = meta.response ?? null;
580
+ const rootResponseId = rootResponse?.responseId ?? rootResponse?.id;
581
+ if (rootResponseId && rootResponseId.startsWith("resp_")) {
582
+ return rootResponseId;
583
+ }
584
+ const runs = Array.isArray(meta.models) ? meta.models : [];
585
+ if (runs.length === 0) {
586
+ return null;
587
+ }
588
+ const pickRun = () => {
589
+ if (followupModel) {
590
+ return runs.find((r) => r.model === followupModel) ?? null;
591
+ }
592
+ return runs.length === 1 ? runs[0] : null;
593
+ };
594
+ const chosen = pickRun();
595
+ if (!chosen) {
596
+ const models = runs.map((r) => r.model).join(", ");
597
+ throw new Error(followupModel
598
+ ? `Session ${meta.id} has no model named ${followupModel}. Available: ${models}`
599
+ : `Session ${meta.id} has multiple model runs. Re-run with --followup-model. Available: ${models}`);
600
+ }
601
+ const runResponse = chosen.response ?? null;
602
+ const runResponseId = runResponse?.responseId ?? runResponse?.id;
603
+ return runResponseId && runResponseId.startsWith("resp_") ? runResponseId : null;
604
+ }
457
605
  function buildRunOptionsFromMetadata(metadata) {
458
606
  const stored = metadata.options ?? {};
459
607
  return {
460
- prompt: stored.prompt ?? '',
608
+ prompt: stored.prompt ?? "",
461
609
  model: stored.model ?? DEFAULT_MODEL,
462
610
  models: stored.models,
611
+ previousResponseId: stored.previousResponseId,
463
612
  effectiveModelId: stored.effectiveModelId ?? stored.model,
464
613
  file: stored.file ?? [],
614
+ maxFileSizeBytes: stored.maxFileSizeBytes,
465
615
  slug: stored.slug,
466
616
  filesReport: stored.filesReport,
467
617
  maxInput: stored.maxInput,
@@ -490,36 +640,36 @@ function buildRunOptionsFromMetadata(metadata) {
490
640
  };
491
641
  }
492
642
  function getSessionMode(metadata) {
493
- return metadata.mode ?? metadata.options?.mode ?? 'api';
643
+ return metadata.mode ?? metadata.options?.mode ?? "api";
494
644
  }
495
645
  function getBrowserConfigFromMetadata(metadata) {
496
646
  return metadata.options?.browserConfig ?? metadata.browser?.config;
497
647
  }
498
648
  async function runRootCommand(options) {
499
- if (process.env.ORACLE_FORCE_TUI === '1') {
649
+ if (process.env.ORACLE_FORCE_TUI === "1") {
500
650
  await sessionStore.ensureStorage();
501
651
  await launchTui({ version: VERSION, printIntro: false });
502
652
  return;
503
653
  }
504
654
  const userConfig = (await loadUserConfig()).config;
505
- const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
655
+ const helpRequested = rawCliArgs.some((arg) => arg === "--help" || arg === "-h");
506
656
  const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
507
657
  if (multiModelProvided) {
508
- const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? '');
658
+ const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? "");
509
659
  if (modelFromConfigOrCli) {
510
- throw new Error('--models cannot be combined with --model.');
660
+ throw new Error("--models cannot be combined with --model.");
511
661
  }
512
662
  }
513
663
  const optionUsesDefault = (name) => {
514
664
  // Commander reports undefined for untouched options, so treat undefined/default the same
515
665
  const source = program.getOptionValueSource?.(name);
516
- return source == null || source === 'default';
666
+ return source == null || source === "default";
517
667
  };
518
668
  if (helpRequested) {
519
669
  if (options.verbose) {
520
- console.log('');
670
+ console.log("");
521
671
  printDebugHelp(program.name());
522
- console.log('');
672
+ console.log("");
523
673
  }
524
674
  program.help({ error: false });
525
675
  return;
@@ -529,8 +679,8 @@ async function runRootCommand(options) {
529
679
  if (mergedFileInputs.length > 0) {
530
680
  const { deduped, duplicates } = dedupePathInputs(mergedFileInputs, { cwd: process.cwd() });
531
681
  if (duplicates.length > 0) {
532
- const preview = duplicates.slice(0, 8).join(', ');
533
- const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : '';
682
+ const preview = duplicates.slice(0, 8).join(", ");
683
+ const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : "";
534
684
  console.log(chalk.dim(`Ignoring duplicate --file inputs: ${preview}${suffix}`));
535
685
  }
536
686
  options.file = deduped;
@@ -539,11 +689,11 @@ async function runRootCommand(options) {
539
689
  const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
540
690
  const renderPlain = resolveRenderPlain(options.renderPlain, options.render, options.renderMarkdown);
541
691
  const applyRetentionOption = () => {
542
- if (optionUsesDefault('retainHours') && typeof userConfig.sessionRetentionHours === 'number') {
692
+ if (optionUsesDefault("retainHours") && typeof userConfig.sessionRetentionHours === "number") {
543
693
  options.retainHours = userConfig.sessionRetentionHours;
544
694
  }
545
695
  const envRetention = process.env.ORACLE_RETAIN_HOURS;
546
- if (optionUsesDefault('retainHours') && envRetention) {
696
+ if (optionUsesDefault("retainHours") && envRetention) {
547
697
  const parsed = Number.parseFloat(envRetention);
548
698
  if (!Number.isNaN(parsed)) {
549
699
  options.retainHours = parsed;
@@ -563,11 +713,11 @@ async function runRootCommand(options) {
563
713
  console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
564
714
  }
565
715
  if (userCliArgs.length === 0) {
566
- console.log(chalk.yellow('No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI.'));
716
+ console.log(chalk.yellow("No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI."));
567
717
  program.outputHelp();
568
718
  return;
569
719
  }
570
- const retentionHours = typeof options.retainHours === 'number' ? options.retainHours : undefined;
720
+ const retentionHours = typeof options.retainHours === "number" ? options.retainHours : undefined;
571
721
  await sessionStore.ensureStorage();
572
722
  await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
573
723
  if (options.debugHelp) {
@@ -575,35 +725,39 @@ async function runRootCommand(options) {
575
725
  return;
576
726
  }
577
727
  if (options.dryRun && options.renderMarkdown) {
578
- throw new Error('--dry-run cannot be combined with --render-markdown.');
728
+ throw new Error("--dry-run cannot be combined with --render-markdown.");
579
729
  }
580
730
  const preferredEngine = options.engine ?? userConfig.engine;
581
- let engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
731
+ let engine = resolveEngine({
732
+ engine: preferredEngine,
733
+ browserFlag: options.browser,
734
+ env: process.env,
735
+ });
582
736
  if (options.browser) {
583
- console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
737
+ console.log(chalk.yellow("`--browser` is deprecated; use `--engine browser` instead."));
584
738
  }
585
- if (optionUsesDefault('model') && userConfig.model) {
739
+ if (optionUsesDefault("model") && userConfig.model) {
586
740
  options.model = userConfig.model;
587
741
  }
588
- if (optionUsesDefault('search') && userConfig.search) {
589
- options.search = userConfig.search === 'on';
742
+ if (optionUsesDefault("search") && userConfig.search) {
743
+ options.search = userConfig.search === "on";
590
744
  }
591
- if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
745
+ if (optionUsesDefault("filesReport") && userConfig.filesReport != null) {
592
746
  options.filesReport = Boolean(userConfig.filesReport);
593
747
  }
594
- if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
748
+ if (optionUsesDefault("heartbeat") && typeof userConfig.heartbeatSeconds === "number") {
595
749
  options.heartbeat = userConfig.heartbeatSeconds;
596
750
  }
597
- if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
751
+ if (optionUsesDefault("baseUrl") && userConfig.apiBaseUrl) {
598
752
  options.baseUrl = userConfig.apiBaseUrl;
599
753
  }
600
- if (remoteHost && engine !== 'browser') {
601
- throw new Error('--remote-host requires --engine browser.');
754
+ if (remoteHost && engine !== "browser") {
755
+ throw new Error("--remote-host requires --engine browser.");
602
756
  }
603
757
  if (remoteHost && options.remoteChrome) {
604
- throw new Error('--remote-host cannot be combined with --remote-chrome.');
758
+ throw new Error("--remote-host cannot be combined with --remote-chrome.");
605
759
  }
606
- if (optionUsesDefault('azureEndpoint')) {
760
+ if (optionUsesDefault("azureEndpoint")) {
607
761
  if (process.env.AZURE_OPENAI_ENDPOINT) {
608
762
  options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
609
763
  }
@@ -611,7 +765,7 @@ async function runRootCommand(options) {
611
765
  options.azureEndpoint = userConfig.azure.endpoint;
612
766
  }
613
767
  }
614
- if (optionUsesDefault('azureDeployment')) {
768
+ if (optionUsesDefault("azureDeployment")) {
615
769
  if (process.env.AZURE_OPENAI_DEPLOYMENT) {
616
770
  options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
617
771
  }
@@ -619,7 +773,7 @@ async function runRootCommand(options) {
619
773
  options.azureDeployment = userConfig.azure.deployment;
620
774
  }
621
775
  }
622
- if (optionUsesDefault('azureApiVersion')) {
776
+ if (optionUsesDefault("azureApiVersion")) {
623
777
  if (process.env.AZURE_OPENAI_API_VERSION) {
624
778
  options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
625
779
  }
@@ -630,48 +784,57 @@ async function runRootCommand(options) {
630
784
  const normalizedMultiModels = multiModelProvided
631
785
  ? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
632
786
  : [];
633
- const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? '' : DEFAULT_MODEL);
787
+ const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? "" : DEFAULT_MODEL);
634
788
  const resolvedModelCandidate = multiModelProvided
635
789
  ? normalizedMultiModels[0]
636
- : engine === 'browser'
790
+ : engine === "browser"
637
791
  ? inferModelFromLabel(cliModelArg || DEFAULT_MODEL)
638
792
  : resolveApiModel(cliModelArg || DEFAULT_MODEL);
639
793
  const primaryModelCandidate = normalizedMultiModels[0] ?? resolvedModelCandidate;
640
- const isGemini = primaryModelCandidate.startsWith('gemini');
641
- const isCodex = primaryModelCandidate.startsWith('gpt-5.1-codex');
642
- const isClaude = primaryModelCandidate.startsWith('claude');
643
- const userForcedBrowser = options.browser || options.engine === 'browser';
644
- const isBrowserCompatible = (model) => model.startsWith('gpt-') || model.startsWith('gemini');
645
- const hasNonBrowserCompatibleTarget = (engine === 'browser' || userForcedBrowser) &&
794
+ const isGemini = primaryModelCandidate.startsWith("gemini");
795
+ const isCodex = primaryModelCandidate.startsWith("gpt-5.1-codex");
796
+ const isClaude = primaryModelCandidate.startsWith("claude");
797
+ const userForcedBrowser = options.browser || options.engine === "browser";
798
+ const isBrowserCompatible = (model) => model.startsWith("gpt-") || model.startsWith("gemini");
799
+ const hasNonBrowserCompatibleTarget = (engine === "browser" || userForcedBrowser) &&
646
800
  (normalizedMultiModels.length > 0
647
801
  ? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
648
802
  : !isBrowserCompatible(resolvedModelCandidate));
649
803
  if (hasNonBrowserCompatibleTarget) {
650
- throw new Error('Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.');
804
+ throw new Error("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.");
651
805
  }
652
- if (isClaude && engine === 'browser') {
653
- console.log(chalk.dim('Browser engine is not supported for Claude models; switching to API.'));
654
- engine = 'api';
806
+ if (isClaude && engine === "browser") {
807
+ console.log(chalk.dim("Browser engine is not supported for Claude models; switching to API."));
808
+ engine = "api";
655
809
  }
656
- if (isCodex && engine === 'browser') {
657
- console.log(chalk.dim('Browser engine is not supported for gpt-5.1-codex; switching to API.'));
658
- engine = 'api';
810
+ if (isCodex && engine === "browser") {
811
+ console.log(chalk.dim("Browser engine is not supported for gpt-5.1-codex; switching to API."));
812
+ engine = "api";
659
813
  }
660
814
  if (normalizedMultiModels.length > 0) {
661
- engine = 'api';
815
+ engine = "api";
662
816
  }
663
817
  if (remoteHost && normalizedMultiModels.length > 0) {
664
- throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
818
+ throw new Error("--remote-host does not support --models yet. Use API engine locally instead.");
665
819
  }
666
820
  const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
667
- const effectiveModelId = resolvedModel.startsWith('gemini')
821
+ const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model === "gemini-3.1-pro");
822
+ if ((userForcedBrowser || userConfig.engine === "browser") && includesGeminiApiOnly) {
823
+ throw new Error("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.");
824
+ }
825
+ if (engine === "browser" && includesGeminiApiOnly) {
826
+ console.log(chalk.dim("gemini-3.1-pro is API-only today; switching to API."));
827
+ engine = "api";
828
+ }
829
+ const effectiveModelId = resolvedModel.startsWith("gemini")
668
830
  ? resolveGeminiModelId(resolvedModel)
669
831
  : isKnownModel(resolvedModel)
670
- ? MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel
832
+ ? (MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel)
671
833
  : resolvedModel;
672
834
  const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
673
835
  const { models: _rawModels, ...optionsWithoutModels } = options;
674
836
  const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
837
+ resolvedOptions.maxFileSizeBytes = resolveConfiguredMaxFileSizeBytes(userConfig, process.env);
675
838
  if (normalizedMultiModels.length > 0) {
676
839
  resolvedOptions.models = normalizedMultiModels;
677
840
  }
@@ -687,7 +850,7 @@ async function runRootCommand(options) {
687
850
  engine,
688
851
  });
689
852
  if (remoteHost && waitPreference === false) {
690
- console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
853
+ console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
691
854
  waitPreference = true;
692
855
  }
693
856
  if (await handleStatusFlag(options, { attachSession, showStatus })) {
@@ -702,10 +865,12 @@ async function runRootCommand(options) {
702
865
  }
703
866
  if (renderMarkdown || copyMarkdown) {
704
867
  if (!options.prompt) {
705
- throw new Error('Prompt is required when using --render-markdown or --copy-markdown.');
868
+ throw new Error("Prompt is required when using --render-markdown or --copy-markdown.");
706
869
  }
707
870
  const bundle = await buildMarkdownBundle({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
708
- const modelConfig = isKnownModel(resolvedModel) ? MODEL_CONFIGS[resolvedModel] : MODEL_CONFIGS['gpt-5.1'];
871
+ const modelConfig = isKnownModel(resolvedModel)
872
+ ? MODEL_CONFIGS[resolvedModel]
873
+ : MODEL_CONFIGS["gpt-5.1"];
709
874
  const requestBody = buildRequestBody({
710
875
  modelConfig,
711
876
  systemPrompt: bundle.systemPrompt,
@@ -722,17 +887,19 @@ async function runRootCommand(options) {
722
887
  ? bundle.markdown
723
888
  : await formatRenderedMarkdown(bundle.markdown, { richTty: isTty });
724
889
  // Trim trailing newlines from the rendered bundle so we print exactly one blank before the summary line.
725
- console.log(output.replace(/\n+$/u, ''));
890
+ console.log(output.replace(/\n+$/u, ""));
726
891
  }
727
892
  if (copyMarkdown) {
728
893
  const result = await copyToClipboard(bundle.markdown);
729
894
  if (result.success) {
730
- const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : '';
895
+ const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : "";
731
896
  const summary = `Copied markdown to clipboard (~${formatCompactNumber(estimatedTokens)} tokens${filesPart}).`;
732
897
  console.log(chalk.green(summary));
733
898
  }
734
899
  else {
735
- const reason = result.error instanceof Error ? result.error.message : String(result.error ?? 'unknown error');
900
+ const reason = result.error instanceof Error
901
+ ? result.error.message
902
+ : String(result.error ?? "unknown error");
736
903
  console.log(chalk.dim(`Copy failed (${reason}); markdown not printed. Re-run with --render-markdown if you need the content.`));
737
904
  }
738
905
  }
@@ -740,14 +907,33 @@ async function runRootCommand(options) {
740
907
  }
741
908
  if (previewMode) {
742
909
  if (!options.prompt) {
743
- throw new Error('Prompt is required when using --dry-run/preview.');
910
+ throw new Error("Prompt is required when using --dry-run/preview.");
744
911
  }
745
912
  if (userConfig.promptSuffix) {
746
913
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
747
914
  }
748
915
  resolvedOptions.prompt = options.prompt;
749
- const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
750
- if (engine === 'browser') {
916
+ if (options.followup) {
917
+ assertFollowupSupported({
918
+ engine,
919
+ model: resolvedModel,
920
+ baseUrl: resolvedBaseUrl,
921
+ azureEndpoint: resolvedOptions.azure?.endpoint,
922
+ });
923
+ if (normalizedMultiModels.length > 0) {
924
+ throw new Error("--followup cannot be combined with --models.");
925
+ }
926
+ const followup = await resolveFollowupReference(options.followup, options.followupModel);
927
+ resolvedOptions.previousResponseId = followup.responseId;
928
+ resolvedOptions.followupSessionId = followup.sessionId;
929
+ resolvedOptions.followupModel = options.followupModel;
930
+ }
931
+ const runOptions = buildRunOptions(resolvedOptions, {
932
+ preview: true,
933
+ previewMode,
934
+ baseUrl: resolvedBaseUrl,
935
+ });
936
+ if (engine === "browser") {
751
937
  await runBrowserPreview({
752
938
  runOptions,
753
939
  cwd: process.cwd(),
@@ -758,7 +944,7 @@ async function runRootCommand(options) {
758
944
  return;
759
945
  }
760
946
  // API dry-run/preview path
761
- if (previewMode === 'summary') {
947
+ if (previewMode === "summary") {
762
948
  await runDryRunSummary({
763
949
  engine,
764
950
  runOptions,
@@ -778,12 +964,27 @@ async function runRootCommand(options) {
778
964
  return;
779
965
  }
780
966
  if (!options.prompt) {
781
- throw new Error('Prompt is required when starting a new session.');
967
+ throw new Error("Prompt is required when starting a new session.");
782
968
  }
783
969
  if (userConfig.promptSuffix) {
784
970
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
785
971
  }
786
972
  resolvedOptions.prompt = options.prompt;
973
+ if (options.followup) {
974
+ assertFollowupSupported({
975
+ engine,
976
+ model: resolvedModel,
977
+ baseUrl: resolvedBaseUrl,
978
+ azureEndpoint: resolvedOptions.azure?.endpoint,
979
+ });
980
+ if (normalizedMultiModels.length > 0) {
981
+ throw new Error("--followup cannot be combined with --models.");
982
+ }
983
+ const followup = await resolveFollowupReference(options.followup, options.followupModel);
984
+ resolvedOptions.previousResponseId = followup.responseId;
985
+ resolvedOptions.followupSessionId = followup.sessionId;
986
+ resolvedOptions.followupModel = options.followupModel;
987
+ }
787
988
  const duplicateBlocked = await shouldBlockDuplicatePrompt({
788
989
  prompt: resolvedOptions.prompt,
789
990
  force: options.force,
@@ -795,10 +996,15 @@ async function runRootCommand(options) {
795
996
  return;
796
997
  }
797
998
  if (options.file && options.file.length > 0) {
798
- const isBrowserMode = engine === 'browser' || userForcedBrowser;
799
- const filesToValidate = isBrowserMode ? options.file.filter((f) => !isMediaFile(f)) : options.file;
999
+ const isBrowserMode = engine === "browser" || userForcedBrowser;
1000
+ const filesToValidate = isBrowserMode
1001
+ ? options.file.filter((f) => !isMediaFile(f))
1002
+ : options.file;
800
1003
  if (filesToValidate.length > 0) {
801
- await readFiles(filesToValidate, { cwd: process.cwd() });
1004
+ await readFiles(filesToValidate, {
1005
+ cwd: process.cwd(),
1006
+ maxFileSizeBytes: resolvedOptions.maxFileSizeBytes,
1007
+ });
802
1008
  }
803
1009
  }
804
1010
  const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
@@ -809,9 +1015,9 @@ async function runRootCommand(options) {
809
1015
  env: process.env,
810
1016
  config: userConfig.notify,
811
1017
  });
812
- const sessionMode = engine === 'browser' ? 'browser' : 'api';
813
- const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
814
- const browserConfig = sessionMode === 'browser'
1018
+ const sessionMode = engine === "browser" ? "browser" : "api";
1019
+ const browserModelLabelOverride = sessionMode === "browser" ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
1020
+ const browserConfig = sessionMode === "browser"
815
1021
  ? await buildBrowserConfig({
816
1022
  ...options,
817
1023
  model: resolvedModel,
@@ -825,7 +1031,7 @@ async function runRootCommand(options) {
825
1031
  };
826
1032
  console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
827
1033
  }
828
- else if (browserConfig && resolvedModel.startsWith('gemini')) {
1034
+ else if (browserConfig && resolvedModel.startsWith("gemini")) {
829
1035
  browserDeps = {
830
1036
  executeBrowser: createGeminiWebExecutor({
831
1037
  youtube: options.youtube,
@@ -836,9 +1042,9 @@ async function runRootCommand(options) {
836
1042
  showThoughts: options.geminiShowThoughts,
837
1043
  }),
838
1044
  };
839
- console.log(chalk.dim('Using Gemini web client for browser automation'));
840
- if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
841
- console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
1045
+ console.log(chalk.dim("Using Gemini web client for browser automation"));
1046
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== "select") {
1047
+ console.log(chalk.dim("Browser model strategy is ignored for Gemini web runs."));
842
1048
  }
843
1049
  }
844
1050
  const remoteExecutionActive = Boolean(browserDeps);
@@ -866,14 +1072,16 @@ async function runRootCommand(options) {
866
1072
  baseUrl: resolvedBaseUrl,
867
1073
  });
868
1074
  enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
869
- if (sessionMode === 'browser' && baseRunOptions.search === false) {
870
- console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
1075
+ if (sessionMode === "browser" && baseRunOptions.search === false) {
1076
+ console.log(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
871
1077
  baseRunOptions.search = undefined;
872
1078
  }
873
1079
  const sessionMeta = await sessionStore.createSession({
874
1080
  ...baseRunOptions,
875
1081
  mode: sessionMode,
876
1082
  browserConfig,
1083
+ followupSessionId: resolvedOptions.followupSessionId,
1084
+ followupModel: resolvedOptions.followupModel,
877
1085
  waitPreference,
878
1086
  youtube: options.youtube,
879
1087
  generateImage: options.generateImage,
@@ -887,7 +1095,7 @@ async function runRootCommand(options) {
887
1095
  sessionId: sessionMeta.id,
888
1096
  effectiveModelId,
889
1097
  };
890
- const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1098
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
891
1099
  const detachAllowed = remoteExecutionActive
892
1100
  ? false
893
1101
  : shouldDetachSession({
@@ -905,12 +1113,12 @@ async function runRootCommand(options) {
905
1113
  });
906
1114
  if (!waitPreference) {
907
1115
  if (!detached) {
908
- console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1116
+ console.log(chalk.red("Unable to start in background; use --wait to run inline."));
909
1117
  process.exitCode = 1;
910
1118
  return;
911
1119
  }
912
1120
  console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
913
- console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
1121
+ console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
914
1122
  return;
915
1123
  }
916
1124
  if (detached === false) {
@@ -925,8 +1133,8 @@ async function runRootCommand(options) {
925
1133
  async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
926
1134
  const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
927
1135
  let headerAugmented = false;
928
- const combinedLog = (message = '') => {
929
- if (!headerAugmented && message.startsWith('oracle (')) {
1136
+ const combinedLog = (message = "") => {
1137
+ if (!headerAugmented && message.startsWith("oracle (")) {
930
1138
  headerAugmented = true;
931
1139
  if (showReattachHint) {
932
1140
  console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
@@ -955,21 +1163,19 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
955
1163
  log: combinedLog,
956
1164
  write: combinedWrite,
957
1165
  version: VERSION,
958
- notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
1166
+ notifications: notifications ??
1167
+ deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
959
1168
  browserDeps,
960
1169
  });
961
1170
  const latest = await sessionStore.readSession(sessionMeta.id);
962
1171
  if (!suppressSummary) {
963
1172
  const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
964
1173
  if (summary) {
965
- console.log('\n' + chalk.green.bold(summary));
1174
+ console.log("\n" + chalk.green.bold(summary));
966
1175
  logLine(summary); // plain text in log, colored on stdout
967
1176
  }
968
1177
  }
969
1178
  }
970
- catch (error) {
971
- throw error;
972
- }
973
1179
  finally {
974
1180
  stream.end();
975
1181
  }
@@ -977,14 +1183,14 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
977
1183
  async function launchDetachedSession(sessionId) {
978
1184
  return new Promise((resolve, reject) => {
979
1185
  try {
980
- const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
1186
+ const args = ["--", CLI_ENTRYPOINT, "--exec-session", sessionId];
981
1187
  const child = spawn(process.execPath, args, {
982
1188
  detached: true,
983
- stdio: 'ignore',
1189
+ stdio: "ignore",
984
1190
  env: process.env,
985
1191
  });
986
- child.once('error', reject);
987
- child.once('spawn', () => {
1192
+ child.once("error", reject);
1193
+ child.once("spawn", () => {
988
1194
  child.unref();
989
1195
  resolve(true);
990
1196
  });
@@ -1008,9 +1214,9 @@ async function restartSession(sessionId, options) {
1008
1214
  return;
1009
1215
  }
1010
1216
  const sessionMode = getSessionMode(metadata);
1011
- const engine = sessionMode === 'browser' ? 'browser' : 'api';
1217
+ const engine = sessionMode === "browser" ? "browser" : "api";
1012
1218
  const browserConfig = getBrowserConfigFromMetadata(metadata);
1013
- if (sessionMode === 'browser' && !browserConfig) {
1219
+ if (sessionMode === "browser" && !browserConfig) {
1014
1220
  console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
1015
1221
  process.exitCode = 1;
1016
1222
  return;
@@ -1019,10 +1225,15 @@ async function restartSession(sessionId, options) {
1019
1225
  const cwd = metadata.cwd ?? process.cwd();
1020
1226
  const storedOptions = metadata.options ?? {};
1021
1227
  if (runOptions.file && runOptions.file.length > 0) {
1022
- const isBrowserMode = engine === 'browser';
1023
- const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
1228
+ const isBrowserMode = engine === "browser";
1229
+ const filesToValidate = isBrowserMode
1230
+ ? runOptions.file.filter((f) => !isMediaFile(f))
1231
+ : runOptions.file;
1024
1232
  if (filesToValidate.length > 0) {
1025
- await readFiles(filesToValidate, { cwd });
1233
+ await readFiles(filesToValidate, {
1234
+ cwd,
1235
+ maxFileSizeBytes: runOptions.maxFileSizeBytes,
1236
+ });
1026
1237
  }
1027
1238
  }
1028
1239
  enforceBrowserSearchFlag(runOptions, sessionMode, console.log);
@@ -1040,14 +1251,14 @@ async function restartSession(sessionId, options) {
1040
1251
  });
1041
1252
  const remoteHost = remoteConfig.host;
1042
1253
  const remoteToken = remoteConfig.token;
1043
- if (remoteHost && engine !== 'browser') {
1044
- throw new Error('--remote-host requires a browser session.');
1254
+ if (remoteHost && engine !== "browser") {
1255
+ throw new Error("--remote-host requires a browser session.");
1045
1256
  }
1046
1257
  if (remoteHost) {
1047
1258
  console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
1048
1259
  }
1049
1260
  if (remoteHost && waitPreference === false) {
1050
- console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
1261
+ console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
1051
1262
  waitPreference = true;
1052
1263
  }
1053
1264
  let browserDeps;
@@ -1057,7 +1268,7 @@ async function restartSession(sessionId, options) {
1057
1268
  };
1058
1269
  console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1059
1270
  }
1060
- else if (browserConfig && runOptions.model.startsWith('gemini')) {
1271
+ else if (browserConfig && runOptions.model.startsWith("gemini")) {
1061
1272
  browserDeps = {
1062
1273
  executeBrowser: createGeminiWebExecutor({
1063
1274
  youtube: storedOptions.youtube,
@@ -1068,9 +1279,9 @@ async function restartSession(sessionId, options) {
1068
1279
  showThoughts: storedOptions.geminiShowThoughts,
1069
1280
  }),
1070
1281
  };
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.'));
1282
+ console.log(chalk.dim("Using Gemini web client for browser automation"));
1283
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== "select") {
1284
+ console.log(chalk.dim("Browser model strategy is ignored for Gemini web runs."));
1074
1285
  }
1075
1286
  }
1076
1287
  const remoteExecutionActive = Boolean(browserDeps);
@@ -1080,6 +1291,8 @@ async function restartSession(sessionId, options) {
1080
1291
  ...runOptions,
1081
1292
  mode: sessionMode,
1082
1293
  browserConfig,
1294
+ followupSessionId: storedOptions.followupSessionId,
1295
+ followupModel: storedOptions.followupModel,
1083
1296
  waitPreference,
1084
1297
  youtube: storedOptions.youtube,
1085
1298
  generateImage: storedOptions.generateImage,
@@ -1093,7 +1306,7 @@ async function restartSession(sessionId, options) {
1093
1306
  sessionId: sessionMeta.id,
1094
1307
  effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
1095
1308
  };
1096
- const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1309
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
1097
1310
  const detachAllowed = remoteExecutionActive
1098
1311
  ? false
1099
1312
  : shouldDetachSession({
@@ -1111,12 +1324,12 @@ async function restartSession(sessionId, options) {
1111
1324
  });
1112
1325
  if (!waitPreference) {
1113
1326
  if (!detached) {
1114
- console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1327
+ console.log(chalk.red("Unable to start in background; use --wait to run inline."));
1115
1328
  process.exitCode = 1;
1116
1329
  return;
1117
1330
  }
1118
1331
  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.'));
1332
+ console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
1120
1333
  return;
1121
1334
  }
1122
1335
  if (detached === false) {
@@ -1162,37 +1375,58 @@ async function executeSession(sessionId) {
1162
1375
  }
1163
1376
  }
1164
1377
  function printDebugHelp(cliName) {
1165
- console.log(chalk.bold('Advanced Options'));
1378
+ console.log(chalk.bold("Advanced Options"));
1166
1379
  printDebugOptionGroup([
1167
- ['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
1168
- ['--max-input <tokens>', 'Override the input token budget.'],
1169
- ['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
1380
+ ["--search <on|off>", "Enable or disable the server-side search tool (default on)."],
1381
+ ["--max-input <tokens>", "Override the input token budget."],
1382
+ ["--max-output <tokens>", "Override the max output tokens (model default otherwise)."],
1170
1383
  ]);
1171
- console.log('');
1172
- console.log(chalk.bold('Browser Options'));
1384
+ console.log("");
1385
+ console.log(chalk.bold("Browser Options"));
1173
1386
  printDebugOptionGroup([
1174
- ['--chatgpt-url <url>', 'Override the ChatGPT web URL (workspace/folder targets).'],
1175
- ['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
1176
- ['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
1177
- ['--browser-cookie-path <path>', 'Use a specific Chrome/Chromium cookie store file.'],
1178
- ['--browser-url <url>', 'Alias for --chatgpt-url.'],
1179
- ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
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.'],
1188
- ['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
1189
- ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
1190
- ['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
1191
- ['--browser-headless', 'Launch Chrome in headless mode.'],
1192
- ['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
1193
- ['--browser-keep-browser', 'Leave Chrome running after completion.'],
1387
+ ["--chatgpt-url <url>", "Override the ChatGPT web URL (workspace/folder targets)."],
1388
+ ["--browser-chrome-profile <name>", "Reuse cookies from a specific Chrome profile."],
1389
+ ["--browser-chrome-path <path>", "Point to a custom Chrome/Chromium binary."],
1390
+ ["--browser-cookie-path <path>", "Use a specific Chrome/Chromium cookie store file."],
1391
+ ["--browser-url <url>", "Alias for --chatgpt-url."],
1392
+ ["--browser-timeout <ms|s|m>", "Cap total wait time for the assistant response."],
1393
+ ["--browser-input-timeout <ms|s|m>", "Cap how long we wait for the composer textarea."],
1394
+ [
1395
+ "--browser-recheck-delay <ms|s|m|h>",
1396
+ "After timeout, wait then revisit the conversation to retry capture.",
1397
+ ],
1398
+ ["--browser-recheck-timeout <ms|s|m|h>", "Time budget for the delayed recheck attempt."],
1399
+ [
1400
+ "--browser-reuse-wait <ms|s|m|h>",
1401
+ "Wait for a shared Chrome profile before launching (parallel runs).",
1402
+ ],
1403
+ [
1404
+ "--browser-profile-lock-timeout <ms|s|m|h>",
1405
+ "Wait for the manual-login profile lock before sending.",
1406
+ ],
1407
+ [
1408
+ "--browser-auto-reattach-delay <ms|s|m|h>",
1409
+ "Delay before periodic auto-reattach attempts after a timeout.",
1410
+ ],
1411
+ [
1412
+ "--browser-auto-reattach-interval <ms|s|m|h>",
1413
+ "Interval between auto-reattach attempts (0 disables).",
1414
+ ],
1415
+ ["--browser-auto-reattach-timeout <ms|s|m|h>", "Time budget for each auto-reattach attempt."],
1416
+ [
1417
+ "--browser-cookie-wait <ms|s|m>",
1418
+ "Wait before retrying cookie sync when Chrome cookies are empty or locked.",
1419
+ ],
1420
+ ["--browser-no-cookie-sync", "Skip copying cookies from your main profile."],
1421
+ [
1422
+ "--browser-manual-login",
1423
+ "Skip cookie copy; reuse a persistent automation profile and log in manually.",
1424
+ ],
1425
+ ["--browser-headless", "Launch Chrome in headless mode."],
1426
+ ["--browser-hide-window", "Hide the Chrome window (macOS headful only)."],
1427
+ ["--browser-keep-browser", "Leave Chrome running after completion."],
1194
1428
  ]);
1195
- console.log('');
1429
+ console.log("");
1196
1430
  console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
1197
1431
  }
1198
1432
  function printDebugOptionGroup(entries) {
@@ -1214,17 +1448,17 @@ function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engin
1214
1448
  return true;
1215
1449
  if (waitFlag === false)
1216
1450
  return false;
1217
- if (typeof storedPreference === 'boolean')
1451
+ if (typeof storedPreference === "boolean")
1218
1452
  return storedPreference;
1219
1453
  return defaultWaitPreference(model, engine);
1220
1454
  }
1221
1455
  function resolveEffectiveModelIdForRun(model, stored) {
1222
1456
  if (stored)
1223
1457
  return stored;
1224
- if (model.startsWith('gemini')) {
1458
+ if (model.startsWith("gemini")) {
1225
1459
  return resolveGeminiModelId(model);
1226
1460
  }
1227
- return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
1461
+ return isKnownModel(model) ? (MODEL_CONFIGS[model].apiModel ?? model) : model;
1228
1462
  }
1229
1463
  program.action(async function () {
1230
1464
  const options = this.optsWithGlobals();
@@ -1232,21 +1466,21 @@ program.action(async function () {
1232
1466
  });
1233
1467
  async function main() {
1234
1468
  const parsePromise = program.parseAsync(normalizedArgv);
1235
- const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
1236
- const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
1237
- if (result === 'sigint') {
1238
- console.log(chalk.yellow('\nCancelled.'));
1469
+ const sigintPromise = once(process, "SIGINT").then(() => "sigint");
1470
+ const result = await Promise.race([parsePromise.then(() => "parsed"), sigintPromise]);
1471
+ if (result === "sigint") {
1472
+ console.log(chalk.yellow("\nCancelled."));
1239
1473
  process.exitCode = 130;
1240
1474
  }
1241
1475
  }
1242
1476
  void main().catch((error) => {
1243
1477
  if (error instanceof Error) {
1244
1478
  if (!isErrorLogged(error)) {
1245
- console.error(chalk.red(''), error.message);
1479
+ console.error(chalk.red(""), error.message);
1246
1480
  }
1247
1481
  }
1248
1482
  else {
1249
- console.error(chalk.red(''), error);
1483
+ console.error(chalk.red(""), error);
1250
1484
  }
1251
1485
  process.exitCode = 1;
1252
1486
  });