@steipete/oracle 0.9.0 → 0.10.0

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