@steipete/oracle 0.9.0 → 0.11.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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  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/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,59 +1,61 @@
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';
50
- import { resolveConfiguredMaxFileSizeBytes } from '../src/cli/fileSize.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, collectTextValues, 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 { showBrowserTabsStatus } from "../src/cli/browserTabs.js";
44
+ import { getCliVersion } from "../src/version.js";
45
+ import { runDryRunSummary, runBrowserPreview } from "../src/cli/dryRun.js";
46
+ import { launchTui } from "../src/cli/tui/index.js";
47
+ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from "../src/cli/notifier.js";
48
+ import { loadUserConfig } from "../src/config.js";
49
+ import { applyBrowserDefaultsFromConfig } from "../src/cli/browserDefaults.js";
50
+ import { shouldBlockDuplicatePrompt } from "../src/cli/duplicatePromptGuard.js";
51
+ import { resolveRemoteServiceConfig } from "../src/remote/remoteServiceConfig.js";
52
+ import { resolveConfiguredMaxFileSizeBytes } from "../src/cli/fileSize.js";
51
53
  const VERSION = getCliVersion();
52
54
  const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
53
55
  const LEGACY_FLAG_ALIASES = new Map([
54
- ['--[no-]notify', '--notify'],
55
- ['--[no-]notify-sound', '--notify-sound'],
56
- ['--[no-]background', '--background'],
56
+ ["--[no-]notify", "--notify"],
57
+ ["--[no-]notify-sound", "--notify-sound"],
58
+ ["--[no-]background", "--background"],
57
59
  ]);
58
60
  const normalizedArgv = process.argv.map((arg, index) => {
59
61
  if (index < 2)
@@ -63,20 +65,24 @@ const normalizedArgv = process.argv.map((arg, index) => {
63
65
  const rawCliArgs = normalizedArgv.slice(2);
64
66
  const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
65
67
  const isTty = process.stdout.isTTY;
68
+ const suppressIntro = userCliArgs[0] === "bridge" &&
69
+ (userCliArgs[1] === "codex-config" || userCliArgs[1] === "claude-config");
66
70
  const program = new Command();
67
71
  let introPrinted = false;
68
- program.hook('preAction', () => {
72
+ program.hook("preAction", () => {
73
+ if (suppressIntro)
74
+ return;
69
75
  if (introPrinted)
70
76
  return;
71
77
  console.log(formatIntroLine(VERSION, { env: process.env, richTty: isTty }));
72
78
  introPrinted = true;
73
79
  });
74
80
  applyHelpStyling(program, VERSION, isTty);
75
- program.hook('preAction', (thisCommand) => {
81
+ program.hook("preAction", async (thisCommand) => {
76
82
  if (thisCommand !== program) {
77
83
  return;
78
84
  }
79
- if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
85
+ if (userCliArgs.some((arg) => arg === "--help" || arg === "-h")) {
80
86
  return;
81
87
  }
82
88
  if (userCliArgs.length === 0) {
@@ -88,7 +94,12 @@ program.hook('preAction', (thisCommand) => {
88
94
  const positional = thisCommand.args?.[0];
89
95
  if (!opts.prompt && positional) {
90
96
  opts.prompt = positional;
91
- thisCommand.setOptionValue('prompt', positional);
97
+ thisCommand.setOptionValue("prompt", positional);
98
+ }
99
+ const resolvedPrompt = await resolveDashPrompt(opts.prompt);
100
+ if (resolvedPrompt !== opts.prompt) {
101
+ opts.prompt = resolvedPrompt;
102
+ thisCommand.setOptionValue("prompt", resolvedPrompt);
92
103
  }
93
104
  if (shouldRequirePrompt(userCliArgs, opts)) {
94
105
  console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
@@ -98,141 +109,154 @@ program.hook('preAction', (thisCommand) => {
98
109
  }
99
110
  });
100
111
  program
101
- .name('oracle')
102
- .description('One-shot GPT-5.4 Pro / GPT-5.4 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
112
+ .name("oracle")
113
+ .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.")
103
114
  .version(VERSION)
104
- .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
105
- .option('-p, --prompt <text>', 'User prompt to send to the model.')
106
- .addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
107
- .option('--followup <sessionId|responseId>', 'Continue an OpenAI/Azure Responses API run from a stored response id (resp_...) or from a stored oracle session id.')
108
- .option('--followup-model <model>', 'When following up a multi-model session, choose which model response to continue from.')
109
- .option('-f, --file <paths...>', 'Files/directories or glob patterns to attach (prefix with !pattern to exclude). Oversized files are rejected automatically (default cap: 1 MB; configurable via ORACLE_MAX_FILE_SIZE_BYTES or config.maxFileSizeBytes).', collectPaths, [])
110
- .addOption(new Option('--include <paths...>', 'Alias for --file.')
115
+ .argument("[prompt]", "Prompt text (shorthand for --prompt).")
116
+ .option("-p, --prompt <text>", "User prompt to send to the model.")
117
+ .addOption(new Option("--message <text>", "Alias for --prompt.").hideHelp())
118
+ .option("--followup <sessionId|responseId>", "Continue an OpenAI/Azure Responses API run from a stored response id (resp_...) or from a stored oracle session id.")
119
+ .option("--followup-model <model>", "When following up a multi-model session, choose which model response to continue from.")
120
+ .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, [])
121
+ .addOption(new Option("--include <paths...>", "Alias for --file.")
111
122
  .argParser(collectPaths)
112
123
  .default([])
113
124
  .hideHelp())
114
- .addOption(new Option('--files <paths...>', 'Alias for --file.')
125
+ .addOption(new Option("--files <paths...>", "Alias for --file.")
115
126
  .argParser(collectPaths)
116
127
  .default([])
117
128
  .hideHelp())
118
- .addOption(new Option('--path <paths...>', 'Alias for --file.')
129
+ .addOption(new Option("--path <paths...>", "Alias for --file.")
119
130
  .argParser(collectPaths)
120
131
  .default([])
121
132
  .hideHelp())
122
- .addOption(new Option('--paths <paths...>', 'Alias for --file.')
133
+ .addOption(new Option("--paths <paths...>", "Alias for --file.")
123
134
  .argParser(collectPaths)
124
135
  .default([])
125
136
  .hideHelp())
126
- .addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
127
- .addOption(new Option('--copy').hideHelp().default(false))
128
- .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
129
- .option('-m, --model <model>', 'Model to target (gpt-5.4-pro default). Also gpt-5.4, gpt-5.1-pro, gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3.1-pro API-only, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).', normalizeModelOption)
130
- .addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.4-pro,gemini-3-pro").')
137
+ .addOption(new Option("--copy-markdown", "Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.").default(false))
138
+ .addOption(new Option("--copy").hideHelp().default(false))
139
+ .option("-s, --slug <words>", "Custom session slug (3-5 words).")
140
+ .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)
141
+ .addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
131
142
  .argParser(collectModelList)
132
143
  .default([]))
133
- .addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
134
- .addOption(new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp())
135
- .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
136
- .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
137
- .addOption(new Option('--notify', 'Desktop notification when a session finishes (default on unless CI/SSH).').default(undefined))
138
- .addOption(new Option('--no-notify', 'Disable desktop notifications.').default(undefined))
139
- .addOption(new Option('--notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
140
- .addOption(new Option('--no-notify-sound', 'Disable notification sounds.').default(undefined))
141
- .addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.4-pro, 120s otherwise).')
144
+ .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"]))
145
+ .addOption(new Option("--mode <mode>", "Alias for --engine (api | browser).")
146
+ .choices(["api", "browser"])
147
+ .hideHelp())
148
+ .option("--files-report", "Show token usage per attached file (also prints automatically when files exceed the token budget).", false)
149
+ .option("-v, --verbose", "Enable verbose logging for all operations.", false)
150
+ .addOption(new Option("--notify", "Desktop notification when a session finishes (default on unless CI/SSH).").default(undefined))
151
+ .addOption(new Option("--no-notify", "Disable desktop notifications.").default(undefined))
152
+ .addOption(new Option("--notify-sound", "Play a notification sound on completion (default off).").default(undefined))
153
+ .addOption(new Option("--no-notify-sound", "Disable notification sounds.").default(undefined))
154
+ .addOption(new Option("--timeout <seconds|auto>", "Overall timeout before aborting the API call (auto = 60m for Pro models, 120s otherwise).")
142
155
  .argParser(parseTimeoutOption)
143
- .default('auto'))
144
- .addOption(new Option('--background', 'Use Responses API background mode (create + retrieve) for API runs.').default(undefined))
145
- .addOption(new Option('--no-background', 'Disable Responses API background mode.').default(undefined))
146
- .addOption(new Option('--http-timeout <ms|s|m|h>', 'HTTP client timeout for API requests (default 20m).')
147
- .argParser((value) => parseDurationOption(value, 'HTTP timeout'))
156
+ .default("auto"))
157
+ .addOption(new Option("--background", "Use Responses API background mode (create + retrieve) for API runs.").default(undefined))
158
+ .addOption(new Option("--no-background", "Disable Responses API background mode.").default(undefined))
159
+ .addOption(new Option("--http-timeout <ms|s|m|h>", "HTTP client timeout for API requests (default 20m).")
160
+ .argParser((value) => parseDurationOption(value, "HTTP timeout"))
148
161
  .default(undefined))
149
- .addOption(new Option('--zombie-timeout <ms|s|m|h>', 'Override stale-session cutoff used by `oracle status` (default 60m).')
150
- .argParser((value) => parseDurationOption(value, 'Zombie timeout'))
162
+ .addOption(new Option("--zombie-timeout <ms|s|m|h>", "Override stale-session cutoff used by `oracle status` (default 60m).")
163
+ .argParser((value) => parseDurationOption(value, "Zombie timeout"))
151
164
  .default(undefined))
152
- .option('--zombie-last-activity', 'Base stale-session detection on last log activity instead of start time.', false)
153
- .addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
165
+ .option("--zombie-last-activity", "Base stale-session detection on last log activity instead of start time.", false)
166
+ .addOption(new Option("--preview [mode]", "(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.")
154
167
  .hideHelp()
155
- .choices(['summary', 'json', 'full'])
156
- .preset('summary'))
157
- .addOption(new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
158
- .choices(['summary', 'json', 'full'])
159
- .preset('summary')
168
+ .choices(["summary", "json", "full"])
169
+ .preset("summary"))
170
+ .addOption(new Option("--dry-run [mode]", "Preview without calling the model (summary | json | full).")
171
+ .choices(["summary", "json", "full"])
172
+ .preset("summary")
160
173
  .default(false))
161
- .addOption(new Option('--exec-session <id>').hideHelp())
162
- .addOption(new Option('--session <id>').hideHelp())
163
- .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
164
- .option('--render-markdown', 'Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.', false)
165
- .option('--render', 'Alias for --render-markdown.', false)
166
- .option('--render-plain', 'Render markdown without ANSI/highlighting (use plain text even in a TTY).', false)
167
- .option('--write-output <path>', 'Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).')
168
- .option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
169
- .addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
174
+ .addOption(new Option("--exec-session <id>").hideHelp())
175
+ .addOption(new Option("--session <id>").hideHelp())
176
+ .addOption(new Option("--status", "Show stored sessions (alias for `oracle status`).")
177
+ .default(false)
178
+ .hideHelp())
179
+ .option("--render-markdown", "Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.", false)
180
+ .option("--render", "Alias for --render-markdown.", false)
181
+ .option("--render-plain", "Render markdown without ANSI/highlighting (use plain text even in a TTY).", false)
182
+ .option("--write-output <path>", "Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).")
183
+ .option("--verbose-render", "Show render/TTY diagnostics when replaying sessions.", false)
184
+ .addOption(new Option("--search <mode>", "Set server-side search behavior (on/off).")
170
185
  .argParser(parseSearchOption)
171
186
  .hideHelp())
172
- .addOption(new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
187
+ .addOption(new Option("--max-input <tokens>", "Override the input token budget for the selected model.")
188
+ .argParser(parseIntOption)
189
+ .hideHelp())
190
+ .addOption(new Option("--max-output <tokens>", "Override the max output tokens for the selected model.")
173
191
  .argParser(parseIntOption)
174
192
  .hideHelp())
175
- .addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
193
+ .option("--base-url <url>", "Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).")
194
+ .option("--azure-endpoint <url>", "Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).")
195
+ .option("--azure-deployment <name>", "Azure OpenAI Deployment Name.")
196
+ .option("--azure-api-version <version>", "Azure OpenAI API Version.")
197
+ .addOption(new Option("--browser", "(deprecated) Use --engine browser instead.").default(false).hideHelp())
198
+ .addOption(new Option("--browser-chrome-profile <name>", "Chrome profile name/path for cookie reuse.").hideHelp())
199
+ .addOption(new Option("--browser-chrome-path <path>", "Explicit Chrome or Chromium executable path.").hideHelp())
200
+ .addOption(new Option("--browser-cookie-path <path>", "Explicit Chrome/Chromium cookie DB path for session reuse."))
201
+ .addOption(new Option("--browser-attach-running", "Attach to a running local browser session instead of launching Chrome (defaults to 127.0.0.1:9222; combine with --remote-chrome to hint a different host:port)."))
202
+ .addOption(new Option("--chatgpt-url <url>", `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
203
+ .addOption(new Option("--browser-url <url>", `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
204
+ .addOption(new Option("--browser-timeout <ms|s|m>", "Maximum time to wait for an answer (default 1200s / 20m).").hideHelp())
205
+ .addOption(new Option("--browser-input-timeout <ms|s|m>", "Maximum time to wait for the prompt textarea (default 60s).").hideHelp())
206
+ .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())
207
+ .addOption(new Option("--browser-recheck-timeout <ms|s|m|h>", "Time budget for the delayed recheck attempt (default 120s).").hideHelp())
208
+ .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())
209
+ .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())
210
+ .addOption(new Option("--browser-max-concurrent-tabs <n>", "Soft limit for concurrent ChatGPT tabs sharing one manual-login profile (default 3).").hideHelp())
211
+ .addOption(new Option("--browser-auto-reattach-delay <ms|s|m|h>", "Delay before starting periodic auto-reattach attempts after a timeout.").hideHelp())
212
+ .addOption(new Option("--browser-auto-reattach-interval <ms|s|m|h>", "Interval between auto-reattach attempts (0 disables).").hideHelp())
213
+ .addOption(new Option("--browser-auto-reattach-timeout <ms|s|m|h>", "Time budget for each auto-reattach attempt (default 120s).").hideHelp())
214
+ .addOption(new Option("--browser-cookie-wait <ms|s|m>", "Wait before retrying cookie sync when Chrome cookies are empty or locked.").hideHelp())
215
+ .addOption(new Option("--browser-port <port>", "Use a fixed Chrome DevTools port (helpful on WSL firewalls).").argParser(parseIntOption))
216
+ .addOption(new Option("--browser-debug-port <port>", "(alias) Use a fixed Chrome DevTools port.")
176
217
  .argParser(parseIntOption)
177
218
  .hideHelp())
178
- .option('--base-url <url>', 'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).')
179
- .option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
180
- .option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
181
- .option('--azure-api-version <version>', 'Azure OpenAI API Version.')
182
- .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
183
- .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
184
- .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
185
- .addOption(new Option('--browser-cookie-path <path>', 'Explicit Chrome/Chromium cookie DB path for session reuse.'))
186
- .addOption(new Option('--chatgpt-url <url>', `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
187
- .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
188
- .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
189
- .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 60s).').hideHelp())
190
- .addOption(new Option('--browser-recheck-delay <ms|s|m|h>', 'After an assistant timeout, wait this long then revisit the conversation to retry capture.').hideHelp())
191
- .addOption(new Option('--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt (default 120s).').hideHelp())
192
- .addOption(new Option('--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile to appear before launching a new one (helps parallel runs).').hideHelp())
193
- .addOption(new Option('--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the shared manual-login profile lock before sending (serializes parallel runs).').hideHelp())
194
- .addOption(new Option('--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before starting periodic auto-reattach attempts after a timeout.').hideHelp())
195
- .addOption(new Option('--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).').hideHelp())
196
- .addOption(new Option('--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt (default 120s).').hideHelp())
197
- .addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
198
- .addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
199
- .argParser(parseIntOption))
200
- .addOption(new Option('--browser-debug-port <port>', '(alias) Use a fixed Chrome DevTools port.').argParser(parseIntOption).hideHelp())
201
- .addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
202
- .addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
203
- .addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
204
- .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
205
- .addOption(new Option('--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.').hideHelp())
206
- .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
207
- .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
208
- .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
209
- .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']))
210
- .addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
211
- .choices(['light', 'standard', 'extended', 'heavy'])
219
+ .addOption(new Option("--browser-cookie-names <names>", "Comma-separated cookie allowlist for sync.").hideHelp())
220
+ .addOption(new Option("--browser-inline-cookies <jsonOrBase64>", "Inline cookies payload (JSON array or base64-encoded JSON).").hideHelp())
221
+ .addOption(new Option("--browser-inline-cookies-file <path>", "Load inline cookies from file (JSON or base64 JSON).").hideHelp())
222
+ .addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
223
+ .addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
224
+ .addOption(new Option("--browser-headless", "Launch Chrome in headless mode.").hideHelp())
225
+ .addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
226
+ .addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
227
+ .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"]))
228
+ .addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.")
229
+ .choices(["light", "standard", "extended", "heavy"])
212
230
  .hideHelp())
213
- .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
214
- .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.')
215
- .choices(['auto', 'never', 'always'])
216
- .default('auto'))
217
- .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).'))
218
- .addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
219
- .addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
220
- .addOption(new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false))
221
- .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
222
- .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).'))
223
- .addOption(new Option('--generate-image <file>', 'Generate image and save to file (Gemini web/cookie mode only; requires gemini.google.com Chrome cookies).'))
224
- .addOption(new Option('--edit-image <file>', 'Edit existing image (use with --output, Gemini web/cookie mode only).'))
225
- .addOption(new Option('--output <file>', 'Output file path for image operations (Gemini web/cookie mode only).'))
226
- .addOption(new Option('--aspect <ratio>', 'Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only).'))
227
- .addOption(new Option('--gemini-show-thoughts', 'Display Gemini thinking process (Gemini web/cookie mode only).').default(false))
228
- .option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
229
- .option('--force', 'Force start a new session even if an identical prompt is already running.', false)
230
- .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
231
- .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
232
- .addOption(new Option('--wait').default(undefined))
233
- .addOption(new Option('--no-wait').default(undefined).hideHelp())
234
- .showHelpAfterError('(use --help for usage)');
235
- program.addHelpText('after', `
231
+ .addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates ChatGPT Deep Research.").choices(["off", "deep"]))
232
+ .addOption(new Option("--browser-archive <mode>", "Archive completed ChatGPT browser conversations after local artifacts are saved (auto archives successful non-project one-shots only).").choices(["auto", "always", "never"]))
233
+ .addOption(new Option("--browser-follow-up <prompt>", "Submit an additional prompt in the same ChatGPT browser conversation after the initial answer; repeat for multi-turn consults.")
234
+ .argParser(collectTextValues)
235
+ .default([]))
236
+ .addOption(new Option("--browser-allow-cookie-errors", "Continue even if Chrome cookies cannot be copied.").hideHelp())
237
+ .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.")
238
+ .choices(["auto", "never", "always"])
239
+ .default("auto"))
240
+ .addOption(new Option("--remote-chrome <host:port>", "Connect to remote Chrome DevTools Protocol, or when combined with --browser-attach-running use this host:port as the local attach hint."))
241
+ .option("--browser-tab <ref>", "Reuse an existing ChatGPT tab by ref (current, target id, full URL, or title substring) instead of opening a new tab.")
242
+ .addOption(new Option("--remote-host <host:port>", "Delegate browser runs to a remote `oracle serve` instance."))
243
+ .addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
244
+ .addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
245
+ .addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
246
+ .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)."))
247
+ .addOption(new Option("--generate-image <file>", "Generate image and save to file (Gemini browser mode; ChatGPT browser mode saves downloadable image artifacts when present)."))
248
+ .addOption(new Option("--edit-image <file>", "Edit existing image (Gemini browser mode; for ChatGPT attach source images with --file and use --generate-image for output)."))
249
+ .addOption(new Option("--output <file>", "Output file path for image operations."))
250
+ .addOption(new Option("--aspect <ratio>", "Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only)."))
251
+ .addOption(new Option("--gemini-show-thoughts", "Display Gemini thinking process (Gemini web/cookie mode only).").default(false))
252
+ .option("--retain-hours <hours>", "Prune stored sessions older than this many hours before running (set 0 to disable).", parseFloatOption)
253
+ .option("--force", "Force start a new session even if an identical prompt is already running.", false)
254
+ .option("--debug-help", "Show the advanced/debug option set and exit.", false)
255
+ .option("--heartbeat <seconds>", "Emit periodic in-progress updates (0 to disable).", parseHeartbeatOption, 30)
256
+ .addOption(new Option("--wait").default(undefined))
257
+ .addOption(new Option("--no-wait").default(undefined).hideHelp())
258
+ .showHelpAfterError("(use --help for usage)");
259
+ program.addHelpText("after", `
236
260
  Examples:
237
261
  # Quick API run with two files
238
262
  oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
@@ -245,15 +269,15 @@ Examples:
245
269
  oracle --render --copy -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
246
270
  `);
247
271
  program
248
- .command('serve')
249
- .description('Run Oracle browser automation as a remote service for other machines.')
250
- .option('--host <address>', 'Interface to bind (default 0.0.0.0).')
251
- .option('--port <number>', 'Port to listen on (default random).', parseIntOption)
252
- .option('--token <value>', 'Access token clients must provide (random if omitted).')
253
- .option('--manual-login', 'Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).', false)
254
- .option('--manual-login-profile-dir <path>', 'Chrome profile directory for manual login (default ~/.oracle/browser-profile).')
272
+ .command("serve")
273
+ .description("Run Oracle browser automation as a remote service for other machines.")
274
+ .option("--host <address>", "Interface to bind (default 0.0.0.0).")
275
+ .option("--port <number>", "Port to listen on (default random).", parseIntOption)
276
+ .option("--token <value>", "Access token clients must provide (random if omitted).")
277
+ .option("--manual-login", "Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).", false)
278
+ .option("--manual-login-profile-dir <path>", "Chrome profile directory for manual login (default ~/.oracle/browser-profile).")
255
279
  .action(async (commandOptions) => {
256
- const { serveRemote } = await import('../src/remote/server.js');
280
+ const { serveRemote } = await import("../src/remote/server.js");
257
281
  await serveRemote({
258
282
  host: commandOptions.host,
259
283
  port: commandOptions.port,
@@ -262,119 +286,180 @@ program
262
286
  manualLoginProfileDir: commandOptions.manualLoginProfileDir,
263
287
  });
264
288
  });
265
- const bridgeCommand = program.command('bridge').description('Bridge a Windows-hosted ChatGPT session to Linux clients.');
289
+ const projectSourcesCommand = program
290
+ .command("project-sources")
291
+ .description("Manage ChatGPT Project Sources as explicit shared project context.");
292
+ function addProjectSourcesCommonOptions(command) {
293
+ return command
294
+ .option("--chatgpt-url <url>", "ChatGPT project URL ending in /project (or browser.chatgptUrl config).")
295
+ .addOption(new Option("--browser-manual-login", "Reuse a persistent signed-in Chrome profile.").default(undefined))
296
+ .option("--browser-manual-login-profile-dir <path>", "Persistent Chrome profile directory.")
297
+ .option("--browser-timeout <duration>", "Overall browser timeout (e.g. 10m, 1h).")
298
+ .option("--browser-input-timeout <duration>", "Timeout waiting for the Project Sources UI.")
299
+ .option("--browser-profile-lock-timeout <duration>", "Timeout waiting for profile launch lock.")
300
+ .option("--browser-reuse-wait <duration>", "Wait for an existing shared Chrome to appear.")
301
+ .option("--browser-max-concurrent-tabs <n>", "Concurrent tabs allowed for the shared profile.")
302
+ .option("--browser-cookie-wait <duration>", "Wait before retrying cookie sync.")
303
+ .option("--browser-chrome-profile <profile>", "Chrome profile name for cookie sync.")
304
+ .option("--browser-chrome-path <path>", "Chrome/Chromium executable path.")
305
+ .option("--browser-cookie-path <path>", "Explicit Chrome cookie DB path.")
306
+ .option("--browser-inline-cookies <json>", "Inline ChatGPT cookies JSON.")
307
+ .option("--browser-inline-cookies-file <path>", "File containing ChatGPT cookies JSON.")
308
+ .option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.")
309
+ .option("--browser-keep-browser", "Keep Chrome running after completion.", false)
310
+ .option("--browser-hide-window", "Hide Chrome window after launch on macOS.", false)
311
+ .option("--browser-allow-cookie-errors", "Continue when cookie sync fails.", false)
312
+ .option("--max-file-size-bytes <bytes>", "Reject uploads larger than this many bytes.", parseIntOption)
313
+ .option("--json", "Print structured JSON.", false)
314
+ .option("-v, --verbose", "Enable verbose browser logging.", false);
315
+ }
316
+ addProjectSourcesCommonOptions(projectSourcesCommand
317
+ .command("list")
318
+ .description("List sources already attached to a ChatGPT Project.")).action(async function () {
319
+ const { runProjectSourcesCliCommand } = await import("../src/cli/projectSources.js");
320
+ await runProjectSourcesCliCommand("list", this.optsWithGlobals());
321
+ });
322
+ addProjectSourcesCommonOptions(projectSourcesCommand
323
+ .command("add")
324
+ .description("Upload files into a ChatGPT Project's persistent Sources tab.")
325
+ .option("-f, --file <paths...>", "Files/directories or globs to add as project sources.", collectPaths, [])
326
+ .option("--dry-run", "Validate files and show the upload plan without touching the browser.", false)).action(async function () {
327
+ const { runProjectSourcesCliCommand } = await import("../src/cli/projectSources.js");
328
+ await runProjectSourcesCliCommand("add", this.optsWithGlobals());
329
+ });
330
+ const bridgeCommand = program
331
+ .command("bridge")
332
+ .description("Bridge a Windows-hosted ChatGPT session to Linux clients.");
266
333
  bridgeCommand
267
- .command('host')
268
- .description('Start a secure oracle serve host (optionally with an SSH reverse tunnel).')
269
- .option('--bind <host:port>', 'Local bind address for the host service (default 127.0.0.1:9473).')
270
- .option('--token <token|auto>', 'Service access token (default auto).', 'auto')
271
- .option('--write-connection <path>', 'Write a connection artifact JSON (default ~/.oracle/bridge-connection.json).')
272
- .option('--ssh <user@host>', 'Maintain an SSH reverse tunnel to the Linux host (ssh -N -R ...).')
273
- .option('--ssh-remote-port <port>', 'Remote port to bind on the Linux host (default matches --bind port).', parseIntOption)
274
- .option('--ssh-identity <path>', 'SSH identity file (ssh -i).')
275
- .option('--ssh-extra-args <args>', 'Extra args passed to ssh (quoted string).')
276
- .option('--background', 'Run the host in the background and write pid/log files.', false)
277
- .option('--foreground', 'Run the host in the foreground (default).', false)
278
- .option('--print', 'Print the client connection string (includes token).', false)
279
- .option('--print-token', 'Print only the token.', false)
334
+ .command("host")
335
+ .description("Start a secure oracle serve host (optionally with an SSH reverse tunnel).")
336
+ .option("--bind <host:port>", "Local bind address for the host service (default 127.0.0.1:9473).")
337
+ .option("--token <token|auto>", "Service access token (default auto).", "auto")
338
+ .option("--write-connection <path>", "Write a connection artifact JSON (default ~/.oracle/bridge-connection.json).")
339
+ .option("--ssh <user@host>", "Maintain an SSH reverse tunnel to the Linux host (ssh -N -R ...).")
340
+ .option("--ssh-remote-port <port>", "Remote port to bind on the Linux host (default matches --bind port).", parseIntOption)
341
+ .option("--ssh-identity <path>", "SSH identity file (ssh -i).")
342
+ .option("--ssh-extra-args <args>", "Extra args passed to ssh (quoted string).")
343
+ .option("--background", "Run the host in the background and write pid/log files.", false)
344
+ .option("--foreground", "Run the host in the foreground (default).", false)
345
+ .option("--print", "Print the client connection string (includes token).", false)
346
+ .option("--print-token", "Print only the token.", false)
280
347
  .action(async (commandOptions) => {
281
- const { runBridgeHost } = await import('../src/cli/bridge/host.js');
348
+ const { runBridgeHost } = await import("../src/cli/bridge/host.js");
282
349
  await runBridgeHost(commandOptions);
283
350
  });
284
351
  bridgeCommand
285
- .command('client')
286
- .description('Configure this machine to use a remote oracle serve host.')
287
- .requiredOption('--connect <connection>', 'Connection string or path to bridge-connection.json.')
288
- .option('--config <path>', 'Override the oracle config file location (default ~/.oracle/config.json).')
289
- .option('--no-write-config', 'Do not write ~/.oracle/config.json (just validate).')
290
- .option('--no-test', 'Skip remote /health check.')
291
- .option('--print-env', 'Print env var exports (includes token).', false)
352
+ .command("client")
353
+ .description("Configure this machine to use a remote oracle serve host.")
354
+ .requiredOption("--connect <connection>", "Connection string or path to bridge-connection.json.")
355
+ .option("--config <path>", "Override the oracle config file location (default ~/.oracle/config.json).")
356
+ .option("--no-write-config", "Do not write ~/.oracle/config.json (just validate).")
357
+ .option("--no-test", "Skip remote /health check.")
358
+ .option("--print-env", "Print env var exports (includes token).", false)
292
359
  .action(async (commandOptions) => {
293
- const { runBridgeClient } = await import('../src/cli/bridge/client.js');
360
+ const { runBridgeClient } = await import("../src/cli/bridge/client.js");
294
361
  await runBridgeClient(commandOptions);
295
362
  });
296
363
  bridgeCommand
297
- .command('doctor')
298
- .description('Diagnose bridge connectivity and browser engine prerequisites.')
299
- .option('--verbose', 'Show extra diagnostics.', false)
364
+ .command("doctor")
365
+ .description("Diagnose bridge connectivity and browser engine prerequisites.")
366
+ .option("--verbose", "Show extra diagnostics.", false)
300
367
  .action(async (commandOptions) => {
301
- const { runBridgeDoctor } = await import('../src/cli/bridge/doctor.js');
368
+ const { runBridgeDoctor } = await import("../src/cli/bridge/doctor.js");
302
369
  await runBridgeDoctor(commandOptions);
303
370
  });
304
371
  bridgeCommand
305
- .command('codex-config')
306
- .description('Print a Codex CLI MCP server config snippet for oracle-mcp.')
307
- .option('--print-token', 'Include ORACLE_REMOTE_TOKEN in the snippet.', false)
372
+ .command("codex-config")
373
+ .description("Print a Codex CLI MCP server config snippet for oracle-mcp.")
374
+ .option("--print-token", "Include ORACLE_REMOTE_TOKEN in the snippet.", false)
308
375
  .action(async (commandOptions) => {
309
- const { runBridgeCodexConfig } = await import('../src/cli/bridge/codexConfig.js');
376
+ const { runBridgeCodexConfig } = await import("../src/cli/bridge/codexConfig.js");
310
377
  await runBridgeCodexConfig(commandOptions);
311
378
  });
312
379
  bridgeCommand
313
- .command('claude-config')
314
- .description('Print a Claude Code MCP config snippet (.mcp.json) for oracle-mcp.')
315
- .option('--print-token', 'Include ORACLE_REMOTE_TOKEN in the snippet.', false)
380
+ .command("claude-config")
381
+ .description("Print a Claude Code MCP config snippet (.mcp.json) for oracle-mcp.")
382
+ .option("--print-token", "Include ORACLE_REMOTE_TOKEN in the snippet.", false)
383
+ .option("--local-browser", "Use a local signed-in Chrome profile instead of a remote bridge.", false)
384
+ .option("--oracle-home-dir <path>", "Override ORACLE_HOME_DIR in the generated snippet.")
385
+ .option("--browser-profile-dir <path>", "Override ORACLE_BROWSER_PROFILE_DIR in the generated snippet.")
316
386
  .action(async (commandOptions) => {
317
- const { runBridgeClaudeConfig } = await import('../src/cli/bridge/claudeConfig.js');
387
+ const { runBridgeClaudeConfig } = await import("../src/cli/bridge/claudeConfig.js");
318
388
  await runBridgeClaudeConfig(commandOptions);
319
389
  });
320
390
  program
321
- .command('tui')
322
- .description('Launch the interactive terminal UI for humans (no automation).')
391
+ .command("tui")
392
+ .description("Launch the interactive terminal UI for humans (no automation).")
323
393
  .action(async () => {
324
394
  await sessionStore.ensureStorage();
325
395
  await launchTui({ version: VERSION, printIntro: false });
326
396
  });
327
- const sessionCommand = program
328
- .command('session [id]')
329
- .description('Attach to a stored session or list recent sessions when no ID is provided.')
330
- .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
331
- .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
332
- .option('--all', 'Include all stored sessions regardless of age.', false)
333
- .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
334
- .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
335
- .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
336
- .option('--render-markdown', 'Alias for --render.', false)
337
- .option('--model <name>', 'Filter sessions/output for a specific model.', '')
338
- .option('--path', 'Print the stored session paths instead of attaching.', false)
339
- .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
397
+ program
398
+ .command("session [id]")
399
+ .description("Attach to a stored session or list recent sessions when no ID is provided.")
400
+ .option("--hours <hours>", "Look back this many hours when listing sessions (default 24).", parseFloatOption, 24)
401
+ .option("--limit <count>", "Maximum sessions to show when listing (max 1000).", parseIntOption, 100)
402
+ .option("--all", "Include all stored sessions regardless of age.", false)
403
+ .option("--clear", "Delete stored sessions older than the provided window (24h default).", false)
404
+ .option("--hide-prompt", "Hide stored prompt when displaying a session.", false)
405
+ .option("--render", "Render completed session output as markdown (rich TTY only).", false)
406
+ .option("--render-markdown", "Alias for --render.", false)
407
+ .option("--model <name>", "Filter sessions/output for a specific model.", "")
408
+ .option("--path", "Print the stored session paths instead of attaching.", false)
409
+ .option("--harvest", "Re-read the bound browser tab and print/save the latest assistant output.", false)
410
+ .option("--live", "Tail the live browser tab for this session until it completes, stalls, or detaches.", false)
411
+ .option("--write-output <path>", "Write harvested browser output to this file (requires --harvest or --live).")
412
+ .option("--browser-tab <ref>", "Override the browser tab ref used for harvesting/live tail (current, target id, URL, or title substring).")
413
+ .addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
340
414
  .action(async (sessionId, _options, cmd) => {
341
415
  await handleSessionCommand(sessionId, cmd);
342
416
  });
343
- const statusCommand = program
344
- .command('status [id]')
345
- .description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
346
- .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
347
- .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
348
- .option('--all', 'Include all stored sessions regardless of age.', false)
349
- .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
350
- .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
351
- .option('--render-markdown', 'Alias for --render.', false)
352
- .option('--model <name>', 'Filter sessions/output for a specific model.', '')
353
- .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
354
- .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
417
+ program
418
+ .command("status [id]")
419
+ .description("List recent sessions (24h window by default) or attach to a session when an ID is provided.")
420
+ .option("--hours <hours>", "Look back this many hours (default 24).", parseFloatOption, 24)
421
+ .option("--limit <count>", "Maximum sessions to show (max 1000).", parseIntOption, 100)
422
+ .option("--all", "Include all stored sessions regardless of age.", false)
423
+ .option("--clear", "Delete stored sessions older than the provided window (24h default).", false)
424
+ .option("--render", "Render completed session output as markdown (rich TTY only).", false)
425
+ .option("--render-markdown", "Alias for --render.", false)
426
+ .option("--model <name>", "Filter sessions/output for a specific model.", "")
427
+ .option("--hide-prompt", "Hide stored prompt when displaying a session.", false)
428
+ .option("--browser-tabs", "List live ChatGPT browser tabs and known Oracle session linkage.", false)
429
+ .addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
355
430
  .action(async (sessionId, _options, command) => {
356
431
  const statusOptions = command.opts();
432
+ if (statusOptions.browserTabs) {
433
+ if (sessionId) {
434
+ console.error("Cannot combine a session ID with --browser-tabs. Remove the ID to inspect live browser tabs.");
435
+ process.exitCode = 1;
436
+ return;
437
+ }
438
+ await showBrowserTabsStatus();
439
+ return;
440
+ }
357
441
  const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
358
442
  if (clearRequested) {
359
443
  if (sessionId) {
360
- console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
444
+ console.error("Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.");
361
445
  process.exitCode = 1;
362
446
  return;
363
447
  }
364
448
  const hours = statusOptions.hours;
365
449
  const includeAll = statusOptions.all;
366
450
  const result = await sessionStore.deleteOlderThan({ hours, includeAll });
367
- const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
451
+ const scope = includeAll ? "all stored sessions" : `sessions older than ${hours}h`;
368
452
  console.log(formatSessionCleanupMessage(result, scope));
369
453
  return;
370
454
  }
371
- if (sessionId === 'clear' || sessionId === 'clean') {
455
+ if (sessionId === "clear" || sessionId === "clean") {
372
456
  console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
373
457
  process.exitCode = 1;
374
458
  return;
375
459
  }
376
460
  if (sessionId) {
377
- const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
461
+ const autoRender = !command.getOptionValueSource?.("render") &&
462
+ !command.getOptionValueSource?.("renderMarkdown")
378
463
  ? process.stdout.isTTY
379
464
  : false;
380
465
  const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
@@ -390,19 +475,19 @@ const statusCommand = program
390
475
  });
391
476
  });
392
477
  program
393
- .command('restart <id>')
394
- .description('Re-run a stored session as a new session (clones options).')
395
- .addOption(new Option('--wait').default(undefined))
396
- .addOption(new Option('--no-wait').default(undefined).hideHelp())
397
- .option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.')
398
- .option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.')
478
+ .command("restart <id>")
479
+ .description("Re-run a stored session as a new session (clones options).")
480
+ .addOption(new Option("--wait").default(undefined))
481
+ .addOption(new Option("--no-wait").default(undefined).hideHelp())
482
+ .option("--remote-host <host:port>", "Delegate browser runs to a remote `oracle serve` instance.")
483
+ .option("--remote-token <token>", "Access token for the remote `oracle serve` instance.")
399
484
  .action(async (sessionId, _options, cmd) => {
400
485
  const restartOptions = cmd.opts();
401
486
  await restartSession(sessionId, restartOptions);
402
487
  });
403
488
  function buildRunOptions(options, overrides = {}) {
404
489
  if (!options.prompt) {
405
- throw new Error('Prompt is required.');
490
+ throw new Error("Prompt is required.");
406
491
  }
407
492
  const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
408
493
  const azure = options.azureEndpoint || overrides.azure?.endpoint
@@ -439,35 +524,40 @@ function buildRunOptions(options, overrides = {}) {
439
524
  sessionId: overrides.sessionId ?? options.sessionId,
440
525
  verbose: overrides.verbose ?? options.verbose,
441
526
  heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
442
- browserAttachments: overrides.browserAttachments ?? options.browserAttachments ?? 'auto',
527
+ browserAttachments: overrides.browserAttachments ??
528
+ options.browserAttachments ??
529
+ "auto",
443
530
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
444
531
  browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
532
+ generateImage: overrides.generateImage ?? options.generateImage,
533
+ outputPath: overrides.outputPath ?? options.output,
534
+ browserFollowUps: overrides.browserFollowUps ?? options.browserFollowUp ?? [],
445
535
  background: overrides.background ?? undefined,
446
536
  renderPlain: overrides.renderPlain ?? options.renderPlain ?? false,
447
537
  writeOutputPath: overrides.writeOutputPath ?? options.writeOutputPath,
448
538
  };
449
539
  }
450
540
  export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
451
- if (sessionMode === 'browser' && runOptions.search === false) {
452
- logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
541
+ if (sessionMode === "browser" && runOptions.search === false) {
542
+ logFn(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
453
543
  runOptions.search = undefined;
454
544
  }
455
545
  }
456
546
  function resolveHeartbeatIntervalMs(seconds) {
457
- if (typeof seconds !== 'number' || seconds <= 0) {
547
+ if (typeof seconds !== "number" || seconds <= 0) {
458
548
  return undefined;
459
549
  }
460
550
  return Math.round(seconds * 1000);
461
551
  }
462
552
  function assertFollowupSupported({ engine, model, baseUrl, azureEndpoint, }) {
463
- if (engine !== 'api') {
464
- throw new Error('--followup requires --engine api.');
553
+ if (engine !== "api") {
554
+ throw new Error("--followup requires --engine api.");
465
555
  }
466
- if (model.startsWith('gemini') || model.startsWith('claude')) {
556
+ if (model.startsWith("gemini") || model.startsWith("claude")) {
467
557
  throw new Error(`--followup is only supported for OpenAI Responses API runs. Model ${model} uses a provider client without previous_response_id support.`);
468
558
  }
469
559
  if (baseUrl && !azureEndpoint) {
470
- throw new Error('--followup is only supported for the default OpenAI Responses API or Azure OpenAI Responses. Custom --base-url providers are not supported.');
560
+ throw new Error("--followup is only supported for the default OpenAI Responses API or Azure OpenAI Responses. Custom --base-url providers are not supported.");
471
561
  }
472
562
  }
473
563
  function levenshteinDistance(a, b) {
@@ -477,8 +567,8 @@ function levenshteinDistance(a, b) {
477
567
  return b.length;
478
568
  if (b.length === 0)
479
569
  return a.length;
480
- const previous = new Array(b.length + 1);
481
- const current = new Array(b.length + 1);
570
+ const previous = Array.from({ length: b.length + 1 });
571
+ const current = Array.from({ length: b.length + 1 });
482
572
  for (let j = 0; j <= b.length; j += 1) {
483
573
  previous[j] = j;
484
574
  }
@@ -515,7 +605,7 @@ async function suggestFollowupSessionIds(input, limit = 3) {
515
605
  const seen = new Set();
516
606
  const ranked = sessions
517
607
  .map((meta) => meta.id)
518
- .filter((id) => typeof id === 'string' && id.length > 0)
608
+ .filter((id) => typeof id === "string" && id.length > 0)
519
609
  .filter((id) => {
520
610
  if (seen.has(id))
521
611
  return false;
@@ -531,16 +621,18 @@ async function suggestFollowupSessionIds(input, limit = 3) {
531
621
  async function resolveFollowupReference(value, followupModel) {
532
622
  const trimmed = value.trim();
533
623
  if (trimmed.length === 0) {
534
- throw new Error('--followup requires a session id or response id.');
624
+ throw new Error("--followup requires a session id or response id.");
535
625
  }
536
- if (trimmed.startsWith('resp_')) {
626
+ if (trimmed.startsWith("resp_")) {
537
627
  return { responseId: trimmed };
538
628
  }
539
629
  // Treat as oracle session id (slug).
540
630
  const meta = await sessionStore.readSession(trimmed);
541
631
  if (!meta) {
542
632
  const suggestions = await suggestFollowupSessionIds(trimmed);
543
- const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.map((id) => `"${id}"`).join(', ')}?` : '';
633
+ const suggestionText = suggestions.length > 0
634
+ ? ` Did you mean: ${suggestions.map((id) => `"${id}"`).join(", ")}?`
635
+ : "";
544
636
  throw new Error(`No session found with ID ${trimmed}.${suggestionText} Run "oracle status --hours 72 --limit 20" to list recent sessions.`);
545
637
  }
546
638
  const fromMetadata = extractResponseIdFromSession(meta, followupModel);
@@ -548,7 +640,7 @@ async function resolveFollowupReference(value, followupModel) {
548
640
  return { responseId: fromMetadata, sessionId: meta.id };
549
641
  }
550
642
  // Fallback: scrape the log for a response id (covers older sessions / edge cases).
551
- const logText = await sessionStore.readLog(trimmed).catch(() => '');
643
+ const logText = await sessionStore.readLog(trimmed).catch(() => "");
552
644
  const matches = logText.match(/resp_[A-Za-z0-9]+/g) ?? [];
553
645
  const last = matches.length > 0 ? matches[matches.length - 1] : null;
554
646
  if (last) {
@@ -560,7 +652,7 @@ function extractResponseIdFromSession(meta, followupModel) {
560
652
  // Single-model sessions store response metadata at the session root.
561
653
  const rootResponse = meta.response ?? null;
562
654
  const rootResponseId = rootResponse?.responseId ?? rootResponse?.id;
563
- if (rootResponseId && rootResponseId.startsWith('resp_')) {
655
+ if (rootResponseId && rootResponseId.startsWith("resp_")) {
564
656
  return rootResponseId;
565
657
  }
566
658
  const runs = Array.isArray(meta.models) ? meta.models : [];
@@ -575,19 +667,19 @@ function extractResponseIdFromSession(meta, followupModel) {
575
667
  };
576
668
  const chosen = pickRun();
577
669
  if (!chosen) {
578
- const models = runs.map((r) => r.model).join(', ');
670
+ const models = runs.map((r) => r.model).join(", ");
579
671
  throw new Error(followupModel
580
672
  ? `Session ${meta.id} has no model named ${followupModel}. Available: ${models}`
581
673
  : `Session ${meta.id} has multiple model runs. Re-run with --followup-model. Available: ${models}`);
582
674
  }
583
675
  const runResponse = chosen.response ?? null;
584
676
  const runResponseId = runResponse?.responseId ?? runResponse?.id;
585
- return runResponseId && runResponseId.startsWith('resp_') ? runResponseId : null;
677
+ return runResponseId && runResponseId.startsWith("resp_") ? runResponseId : null;
586
678
  }
587
679
  function buildRunOptionsFromMetadata(metadata) {
588
680
  const stored = metadata.options ?? {};
589
681
  return {
590
- prompt: stored.prompt ?? '',
682
+ prompt: stored.prompt ?? "",
591
683
  model: stored.model ?? DEFAULT_MODEL,
592
684
  models: stored.models,
593
685
  previousResponseId: stored.previousResponseId,
@@ -616,42 +708,43 @@ function buildRunOptionsFromMetadata(metadata) {
616
708
  browserAttachments: stored.browserAttachments,
617
709
  browserInlineFiles: stored.browserInlineFiles,
618
710
  browserBundleFiles: stored.browserBundleFiles,
711
+ browserFollowUps: stored.browserFollowUps,
619
712
  background: stored.background,
620
713
  renderPlain: stored.renderPlain,
621
714
  writeOutputPath: stored.writeOutputPath,
622
715
  };
623
716
  }
624
717
  function getSessionMode(metadata) {
625
- return metadata.mode ?? metadata.options?.mode ?? 'api';
718
+ return metadata.mode ?? metadata.options?.mode ?? "api";
626
719
  }
627
720
  function getBrowserConfigFromMetadata(metadata) {
628
721
  return metadata.options?.browserConfig ?? metadata.browser?.config;
629
722
  }
630
723
  async function runRootCommand(options) {
631
- if (process.env.ORACLE_FORCE_TUI === '1') {
724
+ if (process.env.ORACLE_FORCE_TUI === "1") {
632
725
  await sessionStore.ensureStorage();
633
726
  await launchTui({ version: VERSION, printIntro: false });
634
727
  return;
635
728
  }
636
729
  const userConfig = (await loadUserConfig()).config;
637
- const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
730
+ const helpRequested = rawCliArgs.some((arg) => arg === "--help" || arg === "-h");
638
731
  const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
639
732
  if (multiModelProvided) {
640
- const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? '');
733
+ const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? "");
641
734
  if (modelFromConfigOrCli) {
642
- throw new Error('--models cannot be combined with --model.');
735
+ throw new Error("--models cannot be combined with --model.");
643
736
  }
644
737
  }
645
738
  const optionUsesDefault = (name) => {
646
739
  // Commander reports undefined for untouched options, so treat undefined/default the same
647
740
  const source = program.getOptionValueSource?.(name);
648
- return source == null || source === 'default';
741
+ return source == null || source === "default";
649
742
  };
650
743
  if (helpRequested) {
651
744
  if (options.verbose) {
652
- console.log('');
745
+ console.log("");
653
746
  printDebugHelp(program.name());
654
- console.log('');
747
+ console.log("");
655
748
  }
656
749
  program.help({ error: false });
657
750
  return;
@@ -661,8 +754,8 @@ async function runRootCommand(options) {
661
754
  if (mergedFileInputs.length > 0) {
662
755
  const { deduped, duplicates } = dedupePathInputs(mergedFileInputs, { cwd: process.cwd() });
663
756
  if (duplicates.length > 0) {
664
- const preview = duplicates.slice(0, 8).join(', ');
665
- const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : '';
757
+ const preview = duplicates.slice(0, 8).join(", ");
758
+ const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : "";
666
759
  console.log(chalk.dim(`Ignoring duplicate --file inputs: ${preview}${suffix}`));
667
760
  }
668
761
  options.file = deduped;
@@ -671,11 +764,11 @@ async function runRootCommand(options) {
671
764
  const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
672
765
  const renderPlain = resolveRenderPlain(options.renderPlain, options.render, options.renderMarkdown);
673
766
  const applyRetentionOption = () => {
674
- if (optionUsesDefault('retainHours') && typeof userConfig.sessionRetentionHours === 'number') {
767
+ if (optionUsesDefault("retainHours") && typeof userConfig.sessionRetentionHours === "number") {
675
768
  options.retainHours = userConfig.sessionRetentionHours;
676
769
  }
677
770
  const envRetention = process.env.ORACLE_RETAIN_HOURS;
678
- if (optionUsesDefault('retainHours') && envRetention) {
771
+ if (optionUsesDefault("retainHours") && envRetention) {
679
772
  const parsed = Number.parseFloat(envRetention);
680
773
  if (!Number.isNaN(parsed)) {
681
774
  options.retainHours = parsed;
@@ -695,11 +788,11 @@ async function runRootCommand(options) {
695
788
  console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
696
789
  }
697
790
  if (userCliArgs.length === 0) {
698
- console.log(chalk.yellow('No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI.'));
791
+ console.log(chalk.yellow("No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI."));
699
792
  program.outputHelp();
700
793
  return;
701
794
  }
702
- const retentionHours = typeof options.retainHours === 'number' ? options.retainHours : undefined;
795
+ const retentionHours = typeof options.retainHours === "number" ? options.retainHours : undefined;
703
796
  await sessionStore.ensureStorage();
704
797
  await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
705
798
  if (options.debugHelp) {
@@ -707,35 +800,42 @@ async function runRootCommand(options) {
707
800
  return;
708
801
  }
709
802
  if (options.dryRun && options.renderMarkdown) {
710
- throw new Error('--dry-run cannot be combined with --render-markdown.');
803
+ throw new Error("--dry-run cannot be combined with --render-markdown.");
711
804
  }
712
805
  const preferredEngine = options.engine ?? userConfig.engine;
713
- let engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
806
+ let engine = resolveEngine({
807
+ engine: preferredEngine,
808
+ browserFlag: options.browser,
809
+ env: process.env,
810
+ });
714
811
  if (options.browser) {
715
- console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
812
+ console.log(chalk.yellow("`--browser` is deprecated; use `--engine browser` instead."));
716
813
  }
717
- if (optionUsesDefault('model') && userConfig.model) {
814
+ if (optionUsesDefault("model") && userConfig.model) {
718
815
  options.model = userConfig.model;
719
816
  }
720
- if (optionUsesDefault('search') && userConfig.search) {
721
- options.search = userConfig.search === 'on';
817
+ if (optionUsesDefault("search") && userConfig.search) {
818
+ options.search = userConfig.search === "on";
722
819
  }
723
- if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
820
+ if (optionUsesDefault("filesReport") && userConfig.filesReport != null) {
724
821
  options.filesReport = Boolean(userConfig.filesReport);
725
822
  }
726
- if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
823
+ if (optionUsesDefault("heartbeat") && typeof userConfig.heartbeatSeconds === "number") {
727
824
  options.heartbeat = userConfig.heartbeatSeconds;
728
825
  }
729
- if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
826
+ if (optionUsesDefault("baseUrl") && userConfig.apiBaseUrl) {
730
827
  options.baseUrl = userConfig.apiBaseUrl;
731
828
  }
732
- if (remoteHost && engine !== 'browser') {
733
- throw new Error('--remote-host requires --engine browser.');
829
+ if (remoteHost && engine !== "browser") {
830
+ throw new Error("--remote-host requires --engine browser.");
734
831
  }
735
832
  if (remoteHost && options.remoteChrome) {
736
- throw new Error('--remote-host cannot be combined with --remote-chrome.');
833
+ throw new Error("--remote-host cannot be combined with --remote-chrome.");
834
+ }
835
+ if (options.browserTab && engine !== "browser") {
836
+ throw new Error("--browser-tab requires --engine browser.");
737
837
  }
738
- if (optionUsesDefault('azureEndpoint')) {
838
+ if (optionUsesDefault("azureEndpoint")) {
739
839
  if (process.env.AZURE_OPENAI_ENDPOINT) {
740
840
  options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
741
841
  }
@@ -743,7 +843,7 @@ async function runRootCommand(options) {
743
843
  options.azureEndpoint = userConfig.azure.endpoint;
744
844
  }
745
845
  }
746
- if (optionUsesDefault('azureDeployment')) {
846
+ if (optionUsesDefault("azureDeployment")) {
747
847
  if (process.env.AZURE_OPENAI_DEPLOYMENT) {
748
848
  options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
749
849
  }
@@ -751,7 +851,7 @@ async function runRootCommand(options) {
751
851
  options.azureDeployment = userConfig.azure.deployment;
752
852
  }
753
853
  }
754
- if (optionUsesDefault('azureApiVersion')) {
854
+ if (optionUsesDefault("azureApiVersion")) {
755
855
  if (process.env.AZURE_OPENAI_API_VERSION) {
756
856
  options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
757
857
  }
@@ -762,52 +862,56 @@ async function runRootCommand(options) {
762
862
  const normalizedMultiModels = multiModelProvided
763
863
  ? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
764
864
  : [];
765
- const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? '' : DEFAULT_MODEL);
865
+ const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? "" : DEFAULT_MODEL);
766
866
  const resolvedModelCandidate = multiModelProvided
767
867
  ? normalizedMultiModels[0]
768
- : engine === 'browser'
868
+ : engine === "browser"
769
869
  ? inferModelFromLabel(cliModelArg || DEFAULT_MODEL)
770
870
  : resolveApiModel(cliModelArg || DEFAULT_MODEL);
771
871
  const primaryModelCandidate = normalizedMultiModels[0] ?? resolvedModelCandidate;
772
- const isGemini = primaryModelCandidate.startsWith('gemini');
773
- const isCodex = primaryModelCandidate.startsWith('gpt-5.1-codex');
774
- const isClaude = primaryModelCandidate.startsWith('claude');
775
- const userForcedBrowser = options.browser || options.engine === 'browser';
776
- const isBrowserCompatible = (model) => model.startsWith('gpt-') || model.startsWith('gemini');
777
- const hasNonBrowserCompatibleTarget = (engine === 'browser' || userForcedBrowser) &&
872
+ const isGemini = primaryModelCandidate.startsWith("gemini");
873
+ const isCodex = primaryModelCandidate.startsWith("gpt-5.1-codex");
874
+ const isClaude = primaryModelCandidate.startsWith("claude");
875
+ const userForcedBrowser = options.browser || options.engine === "browser";
876
+ const isBrowserCompatible = (model) => model.startsWith("gpt-") || model.startsWith("gemini");
877
+ const hasNonBrowserCompatibleTarget = (engine === "browser" || userForcedBrowser) &&
778
878
  (normalizedMultiModels.length > 0
779
879
  ? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
780
880
  : !isBrowserCompatible(resolvedModelCandidate));
781
881
  if (hasNonBrowserCompatibleTarget) {
782
- throw new Error('Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.');
882
+ throw new Error("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.");
783
883
  }
784
- if (isClaude && engine === 'browser') {
785
- console.log(chalk.dim('Browser engine is not supported for Claude models; switching to API.'));
786
- engine = 'api';
884
+ if (isClaude && engine === "browser") {
885
+ console.log(chalk.dim("Browser engine is not supported for Claude models; switching to API."));
886
+ engine = "api";
787
887
  }
788
- if (isCodex && engine === 'browser') {
789
- console.log(chalk.dim('Browser engine is not supported for gpt-5.1-codex; switching to API.'));
790
- engine = 'api';
888
+ if (isCodex && engine === "browser") {
889
+ console.log(chalk.dim("Browser engine is not supported for gpt-5.1-codex; switching to API."));
890
+ engine = "api";
791
891
  }
792
892
  if (normalizedMultiModels.length > 0) {
793
- engine = 'api';
893
+ engine = "api";
794
894
  }
795
895
  if (remoteHost && normalizedMultiModels.length > 0) {
796
- throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
896
+ throw new Error("--remote-host does not support --models yet. Use API engine locally instead.");
797
897
  }
798
898
  const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
799
- const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model === 'gemini-3.1-pro');
800
- if ((userForcedBrowser || userConfig.engine === 'browser') && includesGeminiApiOnly) {
801
- throw new Error('gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.');
899
+ const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model === "gemini-3.1-pro");
900
+ if ((userForcedBrowser || userConfig.engine === "browser") && includesGeminiApiOnly) {
901
+ throw new Error("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.");
802
902
  }
803
- if (engine === 'browser' && includesGeminiApiOnly) {
804
- console.log(chalk.dim('gemini-3.1-pro is API-only today; switching to API.'));
805
- engine = 'api';
903
+ if (engine === "browser" && includesGeminiApiOnly) {
904
+ console.log(chalk.dim("gemini-3.1-pro is API-only today; switching to API."));
905
+ engine = "api";
806
906
  }
807
- const effectiveModelId = resolvedModel.startsWith('gemini')
907
+ const browserFollowUpCount = options.browserFollowUp?.filter((entry) => entry.trim().length > 0).length ?? 0;
908
+ if (engine !== "browser" && browserFollowUpCount > 0) {
909
+ throw new Error("--browser-follow-up requires --engine browser.");
910
+ }
911
+ const effectiveModelId = resolvedModel.startsWith("gemini")
808
912
  ? resolveGeminiModelId(resolvedModel)
809
913
  : isKnownModel(resolvedModel)
810
- ? MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel
914
+ ? (MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel)
811
915
  : resolvedModel;
812
916
  const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
813
917
  const { models: _rawModels, ...optionsWithoutModels } = options;
@@ -828,7 +932,7 @@ async function runRootCommand(options) {
828
932
  engine,
829
933
  });
830
934
  if (remoteHost && waitPreference === false) {
831
- console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
935
+ console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
832
936
  waitPreference = true;
833
937
  }
834
938
  if (await handleStatusFlag(options, { attachSession, showStatus })) {
@@ -843,10 +947,12 @@ async function runRootCommand(options) {
843
947
  }
844
948
  if (renderMarkdown || copyMarkdown) {
845
949
  if (!options.prompt) {
846
- throw new Error('Prompt is required when using --render-markdown or --copy-markdown.');
950
+ throw new Error("Prompt is required when using --render-markdown or --copy-markdown.");
847
951
  }
848
952
  const bundle = await buildMarkdownBundle({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
849
- const modelConfig = isKnownModel(resolvedModel) ? MODEL_CONFIGS[resolvedModel] : MODEL_CONFIGS['gpt-5.1'];
953
+ const modelConfig = isKnownModel(resolvedModel)
954
+ ? MODEL_CONFIGS[resolvedModel]
955
+ : MODEL_CONFIGS["gpt-5.1"];
850
956
  const requestBody = buildRequestBody({
851
957
  modelConfig,
852
958
  systemPrompt: bundle.systemPrompt,
@@ -863,25 +969,38 @@ async function runRootCommand(options) {
863
969
  ? bundle.markdown
864
970
  : await formatRenderedMarkdown(bundle.markdown, { richTty: isTty });
865
971
  // Trim trailing newlines from the rendered bundle so we print exactly one blank before the summary line.
866
- console.log(output.replace(/\n+$/u, ''));
972
+ console.log(output.replace(/\n+$/u, ""));
867
973
  }
868
974
  if (copyMarkdown) {
869
975
  const result = await copyToClipboard(bundle.markdown);
870
976
  if (result.success) {
871
- const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : '';
977
+ const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : "";
872
978
  const summary = `Copied markdown to clipboard (~${formatCompactNumber(estimatedTokens)} tokens${filesPart}).`;
873
979
  console.log(chalk.green(summary));
874
980
  }
875
981
  else {
876
- const reason = result.error instanceof Error ? result.error.message : String(result.error ?? 'unknown error');
982
+ const reason = result.error instanceof Error
983
+ ? result.error.message
984
+ : String(result.error ?? "unknown error");
877
985
  console.log(chalk.dim(`Copy failed (${reason}); markdown not printed. Re-run with --render-markdown if you need the content.`));
878
986
  }
879
987
  }
880
988
  return;
881
989
  }
990
+ const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
991
+ applyBrowserDefaultsFromConfig(options, userConfig, getSource);
992
+ const sessionMode = engine === "browser" ? "browser" : "api";
993
+ const browserModelLabelOverride = sessionMode === "browser" ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
994
+ const browserConfig = sessionMode === "browser"
995
+ ? await buildBrowserConfig({
996
+ ...options,
997
+ model: resolvedModel,
998
+ browserModelLabel: browserModelLabelOverride,
999
+ })
1000
+ : undefined;
882
1001
  if (previewMode) {
883
1002
  if (!options.prompt) {
884
- throw new Error('Prompt is required when using --dry-run/preview.');
1003
+ throw new Error("Prompt is required when using --dry-run/preview.");
885
1004
  }
886
1005
  if (userConfig.promptSuffix) {
887
1006
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
@@ -895,26 +1014,31 @@ async function runRootCommand(options) {
895
1014
  azureEndpoint: resolvedOptions.azure?.endpoint,
896
1015
  });
897
1016
  if (normalizedMultiModels.length > 0) {
898
- throw new Error('--followup cannot be combined with --models.');
1017
+ throw new Error("--followup cannot be combined with --models.");
899
1018
  }
900
1019
  const followup = await resolveFollowupReference(options.followup, options.followupModel);
901
1020
  resolvedOptions.previousResponseId = followup.responseId;
902
1021
  resolvedOptions.followupSessionId = followup.sessionId;
903
1022
  resolvedOptions.followupModel = options.followupModel;
904
1023
  }
905
- const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
906
- if (engine === 'browser') {
1024
+ const runOptions = buildRunOptions(resolvedOptions, {
1025
+ preview: true,
1026
+ previewMode,
1027
+ baseUrl: resolvedBaseUrl,
1028
+ });
1029
+ if (engine === "browser") {
907
1030
  await runBrowserPreview({
908
1031
  runOptions,
909
1032
  cwd: process.cwd(),
910
1033
  version: VERSION,
911
1034
  previewMode,
912
1035
  log: console.log,
1036
+ browserConfig,
913
1037
  }, {});
914
1038
  return;
915
1039
  }
916
1040
  // API dry-run/preview path
917
- if (previewMode === 'summary') {
1041
+ if (previewMode === "summary") {
918
1042
  await runDryRunSummary({
919
1043
  engine,
920
1044
  runOptions,
@@ -934,7 +1058,7 @@ async function runRootCommand(options) {
934
1058
  return;
935
1059
  }
936
1060
  if (!options.prompt) {
937
- throw new Error('Prompt is required when starting a new session.');
1061
+ throw new Error("Prompt is required when starting a new session.");
938
1062
  }
939
1063
  if (userConfig.promptSuffix) {
940
1064
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
@@ -948,7 +1072,7 @@ async function runRootCommand(options) {
948
1072
  azureEndpoint: resolvedOptions.azure?.endpoint,
949
1073
  });
950
1074
  if (normalizedMultiModels.length > 0) {
951
- throw new Error('--followup cannot be combined with --models.');
1075
+ throw new Error("--followup cannot be combined with --models.");
952
1076
  }
953
1077
  const followup = await resolveFollowupReference(options.followup, options.followupModel);
954
1078
  resolvedOptions.previousResponseId = followup.responseId;
@@ -957,6 +1081,7 @@ async function runRootCommand(options) {
957
1081
  }
958
1082
  const duplicateBlocked = await shouldBlockDuplicatePrompt({
959
1083
  prompt: resolvedOptions.prompt,
1084
+ browserFollowUps: resolvedOptions.browserFollowUp,
960
1085
  force: options.force,
961
1086
  sessionStore,
962
1087
  log: console.log,
@@ -966,8 +1091,10 @@ async function runRootCommand(options) {
966
1091
  return;
967
1092
  }
968
1093
  if (options.file && options.file.length > 0) {
969
- const isBrowserMode = engine === 'browser' || userForcedBrowser;
970
- const filesToValidate = isBrowserMode ? options.file.filter((f) => !isMediaFile(f)) : options.file;
1094
+ const isBrowserMode = engine === "browser" || userForcedBrowser;
1095
+ const filesToValidate = isBrowserMode
1096
+ ? options.file.filter((f) => !isMediaFile(f))
1097
+ : options.file;
971
1098
  if (filesToValidate.length > 0) {
972
1099
  await readFiles(filesToValidate, {
973
1100
  cwd: process.cwd(),
@@ -975,23 +1102,12 @@ async function runRootCommand(options) {
975
1102
  });
976
1103
  }
977
1104
  }
978
- const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
979
- applyBrowserDefaultsFromConfig(options, userConfig, getSource);
980
1105
  const notifications = resolveNotificationSettings({
981
1106
  cliNotify: options.notify,
982
1107
  cliNotifySound: options.notifySound,
983
1108
  env: process.env,
984
1109
  config: userConfig.notify,
985
1110
  });
986
- const sessionMode = engine === 'browser' ? 'browser' : 'api';
987
- const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
988
- const browserConfig = sessionMode === 'browser'
989
- ? await buildBrowserConfig({
990
- ...options,
991
- model: resolvedModel,
992
- browserModelLabel: browserModelLabelOverride,
993
- })
994
- : undefined;
995
1111
  let browserDeps;
996
1112
  if (browserConfig && remoteHost) {
997
1113
  browserDeps = {
@@ -999,7 +1115,7 @@ async function runRootCommand(options) {
999
1115
  };
1000
1116
  console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1001
1117
  }
1002
- else if (browserConfig && resolvedModel.startsWith('gemini')) {
1118
+ else if (browserConfig && resolvedModel.startsWith("gemini")) {
1003
1119
  browserDeps = {
1004
1120
  executeBrowser: createGeminiWebExecutor({
1005
1121
  youtube: options.youtube,
@@ -1010,9 +1126,9 @@ async function runRootCommand(options) {
1010
1126
  showThoughts: options.geminiShowThoughts,
1011
1127
  }),
1012
1128
  };
1013
- console.log(chalk.dim('Using Gemini web client for browser automation'));
1014
- if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
1015
- console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
1129
+ console.log(chalk.dim("Using Gemini web client for browser automation"));
1130
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== "select") {
1131
+ console.log(chalk.dim("Browser model strategy is ignored for Gemini web runs."));
1016
1132
  }
1017
1133
  }
1018
1134
  const remoteExecutionActive = Boolean(browserDeps);
@@ -1040,8 +1156,8 @@ async function runRootCommand(options) {
1040
1156
  baseUrl: resolvedBaseUrl,
1041
1157
  });
1042
1158
  enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
1043
- if (sessionMode === 'browser' && baseRunOptions.search === false) {
1044
- console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
1159
+ if (sessionMode === "browser" && baseRunOptions.search === false) {
1160
+ console.log(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
1045
1161
  baseRunOptions.search = undefined;
1046
1162
  }
1047
1163
  const sessionMeta = await sessionStore.createSession({
@@ -1063,7 +1179,7 @@ async function runRootCommand(options) {
1063
1179
  sessionId: sessionMeta.id,
1064
1180
  effectiveModelId,
1065
1181
  };
1066
- const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1182
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
1067
1183
  const detachAllowed = remoteExecutionActive
1068
1184
  ? false
1069
1185
  : shouldDetachSession({
@@ -1081,12 +1197,12 @@ async function runRootCommand(options) {
1081
1197
  });
1082
1198
  if (!waitPreference) {
1083
1199
  if (!detached) {
1084
- console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1200
+ console.log(chalk.red("Unable to start in background; use --wait to run inline."));
1085
1201
  process.exitCode = 1;
1086
1202
  return;
1087
1203
  }
1088
1204
  console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
1089
- console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
1205
+ console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
1090
1206
  return;
1091
1207
  }
1092
1208
  if (detached === false) {
@@ -1101,8 +1217,8 @@ async function runRootCommand(options) {
1101
1217
  async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
1102
1218
  const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
1103
1219
  let headerAugmented = false;
1104
- const combinedLog = (message = '') => {
1105
- if (!headerAugmented && message.startsWith('oracle (')) {
1220
+ const combinedLog = (message = "") => {
1221
+ if (!headerAugmented && message.startsWith("oracle (")) {
1106
1222
  headerAugmented = true;
1107
1223
  if (showReattachHint) {
1108
1224
  console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
@@ -1131,21 +1247,19 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
1131
1247
  log: combinedLog,
1132
1248
  write: combinedWrite,
1133
1249
  version: VERSION,
1134
- notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
1250
+ notifications: notifications ??
1251
+ deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
1135
1252
  browserDeps,
1136
1253
  });
1137
1254
  const latest = await sessionStore.readSession(sessionMeta.id);
1138
1255
  if (!suppressSummary) {
1139
1256
  const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
1140
1257
  if (summary) {
1141
- console.log('\n' + chalk.green.bold(summary));
1258
+ console.log("\n" + chalk.green.bold(summary));
1142
1259
  logLine(summary); // plain text in log, colored on stdout
1143
1260
  }
1144
1261
  }
1145
1262
  }
1146
- catch (error) {
1147
- throw error;
1148
- }
1149
1263
  finally {
1150
1264
  stream.end();
1151
1265
  }
@@ -1153,14 +1267,14 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
1153
1267
  async function launchDetachedSession(sessionId) {
1154
1268
  return new Promise((resolve, reject) => {
1155
1269
  try {
1156
- const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
1270
+ const args = ["--", CLI_ENTRYPOINT, "--exec-session", sessionId];
1157
1271
  const child = spawn(process.execPath, args, {
1158
1272
  detached: true,
1159
- stdio: 'ignore',
1273
+ stdio: "ignore",
1160
1274
  env: process.env,
1161
1275
  });
1162
- child.once('error', reject);
1163
- child.once('spawn', () => {
1276
+ child.once("error", reject);
1277
+ child.once("spawn", () => {
1164
1278
  child.unref();
1165
1279
  resolve(true);
1166
1280
  });
@@ -1184,9 +1298,9 @@ async function restartSession(sessionId, options) {
1184
1298
  return;
1185
1299
  }
1186
1300
  const sessionMode = getSessionMode(metadata);
1187
- const engine = sessionMode === 'browser' ? 'browser' : 'api';
1301
+ const engine = sessionMode === "browser" ? "browser" : "api";
1188
1302
  const browserConfig = getBrowserConfigFromMetadata(metadata);
1189
- if (sessionMode === 'browser' && !browserConfig) {
1303
+ if (sessionMode === "browser" && !browserConfig) {
1190
1304
  console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
1191
1305
  process.exitCode = 1;
1192
1306
  return;
@@ -1195,8 +1309,10 @@ async function restartSession(sessionId, options) {
1195
1309
  const cwd = metadata.cwd ?? process.cwd();
1196
1310
  const storedOptions = metadata.options ?? {};
1197
1311
  if (runOptions.file && runOptions.file.length > 0) {
1198
- const isBrowserMode = engine === 'browser';
1199
- const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
1312
+ const isBrowserMode = engine === "browser";
1313
+ const filesToValidate = isBrowserMode
1314
+ ? runOptions.file.filter((f) => !isMediaFile(f))
1315
+ : runOptions.file;
1200
1316
  if (filesToValidate.length > 0) {
1201
1317
  await readFiles(filesToValidate, {
1202
1318
  cwd,
@@ -1219,14 +1335,14 @@ async function restartSession(sessionId, options) {
1219
1335
  });
1220
1336
  const remoteHost = remoteConfig.host;
1221
1337
  const remoteToken = remoteConfig.token;
1222
- if (remoteHost && engine !== 'browser') {
1223
- throw new Error('--remote-host requires a browser session.');
1338
+ if (remoteHost && engine !== "browser") {
1339
+ throw new Error("--remote-host requires a browser session.");
1224
1340
  }
1225
1341
  if (remoteHost) {
1226
1342
  console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
1227
1343
  }
1228
1344
  if (remoteHost && waitPreference === false) {
1229
- console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
1345
+ console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
1230
1346
  waitPreference = true;
1231
1347
  }
1232
1348
  let browserDeps;
@@ -1236,7 +1352,7 @@ async function restartSession(sessionId, options) {
1236
1352
  };
1237
1353
  console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1238
1354
  }
1239
- else if (browserConfig && runOptions.model.startsWith('gemini')) {
1355
+ else if (browserConfig && runOptions.model.startsWith("gemini")) {
1240
1356
  browserDeps = {
1241
1357
  executeBrowser: createGeminiWebExecutor({
1242
1358
  youtube: storedOptions.youtube,
@@ -1247,9 +1363,9 @@ async function restartSession(sessionId, options) {
1247
1363
  showThoughts: storedOptions.geminiShowThoughts,
1248
1364
  }),
1249
1365
  };
1250
- console.log(chalk.dim('Using Gemini web client for browser automation'));
1251
- if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
1252
- console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
1366
+ console.log(chalk.dim("Using Gemini web client for browser automation"));
1367
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== "select") {
1368
+ console.log(chalk.dim("Browser model strategy is ignored for Gemini web runs."));
1253
1369
  }
1254
1370
  }
1255
1371
  const remoteExecutionActive = Boolean(browserDeps);
@@ -1274,7 +1390,7 @@ async function restartSession(sessionId, options) {
1274
1390
  sessionId: sessionMeta.id,
1275
1391
  effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
1276
1392
  };
1277
- const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1393
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
1278
1394
  const detachAllowed = remoteExecutionActive
1279
1395
  ? false
1280
1396
  : shouldDetachSession({
@@ -1292,12 +1408,12 @@ async function restartSession(sessionId, options) {
1292
1408
  });
1293
1409
  if (!waitPreference) {
1294
1410
  if (!detached) {
1295
- console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1411
+ console.log(chalk.red("Unable to start in background; use --wait to run inline."));
1296
1412
  process.exitCode = 1;
1297
1413
  return;
1298
1414
  }
1299
1415
  console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
1300
- console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
1416
+ console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
1301
1417
  return;
1302
1418
  }
1303
1419
  if (detached === false) {
@@ -1343,37 +1459,62 @@ async function executeSession(sessionId) {
1343
1459
  }
1344
1460
  }
1345
1461
  function printDebugHelp(cliName) {
1346
- console.log(chalk.bold('Advanced Options'));
1462
+ console.log(chalk.bold("Advanced Options"));
1347
1463
  printDebugOptionGroup([
1348
- ['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
1349
- ['--max-input <tokens>', 'Override the input token budget.'],
1350
- ['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
1464
+ ["--search <on|off>", "Enable or disable the server-side search tool (default on)."],
1465
+ ["--max-input <tokens>", "Override the input token budget."],
1466
+ ["--max-output <tokens>", "Override the max output tokens (model default otherwise)."],
1351
1467
  ]);
1352
- console.log('');
1353
- console.log(chalk.bold('Browser Options'));
1468
+ console.log("");
1469
+ console.log(chalk.bold("Browser Options"));
1354
1470
  printDebugOptionGroup([
1355
- ['--chatgpt-url <url>', 'Override the ChatGPT web URL (workspace/folder targets).'],
1356
- ['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
1357
- ['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
1358
- ['--browser-cookie-path <path>', 'Use a specific Chrome/Chromium cookie store file.'],
1359
- ['--browser-url <url>', 'Alias for --chatgpt-url.'],
1360
- ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
1361
- ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
1362
- ['--browser-recheck-delay <ms|s|m|h>', 'After timeout, wait then revisit the conversation to retry capture.'],
1363
- ['--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt.'],
1364
- ['--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile before launching (parallel runs).'],
1365
- ['--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the manual-login profile lock before sending.'],
1366
- ['--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before periodic auto-reattach attempts after a timeout.'],
1367
- ['--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).'],
1368
- ['--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt.'],
1369
- ['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
1370
- ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
1371
- ['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
1372
- ['--browser-headless', 'Launch Chrome in headless mode.'],
1373
- ['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
1374
- ['--browser-keep-browser', 'Leave Chrome running after completion.'],
1471
+ ["--chatgpt-url <url>", "Override the ChatGPT web URL (workspace/folder targets)."],
1472
+ ["--browser-chrome-profile <name>", "Reuse cookies from a specific Chrome profile."],
1473
+ ["--browser-chrome-path <path>", "Point to a custom Chrome/Chromium binary."],
1474
+ ["--browser-cookie-path <path>", "Use a specific Chrome/Chromium cookie store file."],
1475
+ [
1476
+ "--browser-attach-running",
1477
+ "Attach to your current Chrome session through its local remote debugging toggle.",
1478
+ ],
1479
+ ["--browser-url <url>", "Alias for --chatgpt-url."],
1480
+ ["--browser-timeout <ms|s|m>", "Cap total wait time for the assistant response."],
1481
+ ["--browser-input-timeout <ms|s|m>", "Cap how long we wait for the composer textarea."],
1482
+ [
1483
+ "--browser-recheck-delay <ms|s|m|h>",
1484
+ "After timeout, wait then revisit the conversation to retry capture.",
1485
+ ],
1486
+ ["--browser-recheck-timeout <ms|s|m|h>", "Time budget for the delayed recheck attempt."],
1487
+ [
1488
+ "--browser-reuse-wait <ms|s|m|h>",
1489
+ "Wait for a shared Chrome profile before launching (parallel runs).",
1490
+ ],
1491
+ [
1492
+ "--browser-profile-lock-timeout <ms|s|m|h>",
1493
+ "Wait for the manual-login profile lock before sending.",
1494
+ ],
1495
+ [
1496
+ "--browser-auto-reattach-delay <ms|s|m|h>",
1497
+ "Delay before periodic auto-reattach attempts after a timeout.",
1498
+ ],
1499
+ [
1500
+ "--browser-auto-reattach-interval <ms|s|m|h>",
1501
+ "Interval between auto-reattach attempts (0 disables).",
1502
+ ],
1503
+ ["--browser-auto-reattach-timeout <ms|s|m|h>", "Time budget for each auto-reattach attempt."],
1504
+ [
1505
+ "--browser-cookie-wait <ms|s|m>",
1506
+ "Wait before retrying cookie sync when Chrome cookies are empty or locked.",
1507
+ ],
1508
+ ["--browser-no-cookie-sync", "Skip copying cookies from your main profile."],
1509
+ [
1510
+ "--browser-manual-login",
1511
+ "Skip cookie copy; reuse a persistent automation profile and log in manually.",
1512
+ ],
1513
+ ["--browser-headless", "Launch Chrome in headless mode."],
1514
+ ["--browser-hide-window", "Hide the Chrome window (macOS headful only)."],
1515
+ ["--browser-keep-browser", "Leave Chrome running after completion."],
1375
1516
  ]);
1376
- console.log('');
1517
+ console.log("");
1377
1518
  console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
1378
1519
  }
1379
1520
  function printDebugOptionGroup(entries) {
@@ -1395,17 +1536,17 @@ function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engin
1395
1536
  return true;
1396
1537
  if (waitFlag === false)
1397
1538
  return false;
1398
- if (typeof storedPreference === 'boolean')
1539
+ if (typeof storedPreference === "boolean")
1399
1540
  return storedPreference;
1400
1541
  return defaultWaitPreference(model, engine);
1401
1542
  }
1402
1543
  function resolveEffectiveModelIdForRun(model, stored) {
1403
1544
  if (stored)
1404
1545
  return stored;
1405
- if (model.startsWith('gemini')) {
1546
+ if (model.startsWith("gemini")) {
1406
1547
  return resolveGeminiModelId(model);
1407
1548
  }
1408
- return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
1549
+ return isKnownModel(model) ? (MODEL_CONFIGS[model].apiModel ?? model) : model;
1409
1550
  }
1410
1551
  program.action(async function () {
1411
1552
  const options = this.optsWithGlobals();
@@ -1413,21 +1554,21 @@ program.action(async function () {
1413
1554
  });
1414
1555
  async function main() {
1415
1556
  const parsePromise = program.parseAsync(normalizedArgv);
1416
- const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
1417
- const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
1418
- if (result === 'sigint') {
1419
- console.log(chalk.yellow('\nCancelled.'));
1557
+ const sigintPromise = once(process, "SIGINT").then(() => "sigint");
1558
+ const result = await Promise.race([parsePromise.then(() => "parsed"), sigintPromise]);
1559
+ if (result === "sigint") {
1560
+ console.log(chalk.yellow("\nCancelled."));
1420
1561
  process.exitCode = 130;
1421
1562
  }
1422
1563
  }
1423
1564
  void main().catch((error) => {
1424
1565
  if (error instanceof Error) {
1425
1566
  if (!isErrorLogged(error)) {
1426
- console.error(chalk.red(''), error.message);
1567
+ console.error(chalk.red(""), error.message);
1427
1568
  }
1428
1569
  }
1429
1570
  else {
1430
- console.error(chalk.red(''), error);
1571
+ console.error(chalk.red(""), error);
1431
1572
  }
1432
1573
  process.exitCode = 1;
1433
1574
  });