@steipete/oracle 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,30 +1,30 @@
1
- import chalk from 'chalk';
2
- import kleur from 'kleur';
3
- import fs from 'node:fs/promises';
4
- import path from 'node:path';
5
- import process from 'node:process';
6
- import { performance } from 'node:perf_hooks';
7
- import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from './config.js';
8
- import { readFiles } from './files.js';
9
- import { buildPrompt, buildRequestBody } from './request.js';
10
- import { estimateRequestTokens } from './tokenEstimate.js';
11
- import { formatElapsed } from './format.js';
12
- import { formatFinishLine } from './finishLine.js';
13
- import { getFileTokenStats, printFileTokenStats } from './tokenStats.js';
14
- import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from './errors.js';
15
- import { createDefaultClientFactory, isCustomBaseUrl } from './client.js';
16
- import { formatBaseUrlForLog, maskApiKey } from './logging.js';
17
- import { startHeartbeat } from '../heartbeat.js';
18
- import { startOscProgress } from './oscProgress.js';
19
- import { createFsAdapter } from './fsAdapter.js';
20
- import { resolveGeminiModelId } from './gemini.js';
21
- import { resolveClaudeModelId } from './claude.js';
22
- import { renderMarkdownAnsi } from '../cli/markdownRenderer.js';
23
- import { createMarkdownStreamer } from 'markdansi';
24
- import { executeBackgroundResponse } from './background.js';
25
- import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from './runUtils.js';
26
- import { estimateUsdCost } from 'tokentally';
27
- import { defaultOpenRouterBaseUrl, isKnownModel, isOpenRouterBaseUrl, isProModel, resolveModelConfig, normalizeOpenRouterBaseUrl, } from './modelResolver.js';
1
+ import chalk from "chalk";
2
+ import kleur from "kleur";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { performance } from "node:perf_hooks";
7
+ import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from "./config.js";
8
+ import { readFiles } from "./files.js";
9
+ import { buildPrompt, buildRequestBody } from "./request.js";
10
+ import { estimateRequestTokens } from "./tokenEstimate.js";
11
+ import { formatElapsed } from "./format.js";
12
+ import { formatFinishLine } from "./finishLine.js";
13
+ import { getFileTokenStats, printFileTokenStats } from "./tokenStats.js";
14
+ import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from "./errors.js";
15
+ import { createDefaultClientFactory, isCustomBaseUrl } from "./client.js";
16
+ import { formatBaseUrlForLog, maskApiKey } from "./logging.js";
17
+ import { startHeartbeat } from "../heartbeat.js";
18
+ import { startOscProgress } from "./oscProgress.js";
19
+ import { createFsAdapter } from "./fsAdapter.js";
20
+ import { resolveGeminiModelId } from "./gemini.js";
21
+ import { resolveClaudeModelId } from "./claude.js";
22
+ import { renderMarkdownAnsi } from "../cli/markdownRenderer.js";
23
+ import { createMarkdownStreamer } from "markdansi";
24
+ import { executeBackgroundResponse } from "./background.js";
25
+ import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from "./runUtils.js";
26
+ import { estimateUsdCost } from "tokentally";
27
+ import { defaultOpenRouterBaseUrl, isKnownModel, isOpenRouterBaseUrl, isProModel, resolveModelConfig, normalizeOpenRouterBaseUrl, } from "./modelResolver.js";
28
28
  const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
29
29
  const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
30
30
  // Default timeout for non-pro API runs (fast models) — give them up to 120s.
@@ -36,14 +36,14 @@ const defaultWait = (ms) => new Promise((resolve) => {
36
36
  export async function runOracle(options, deps = {}) {
37
37
  const { apiKey: optionsApiKey = options.apiKey, cwd = process.cwd(), fs: fsModule = createFsAdapter(fs), log = console.log, write: sinkWrite = (_text) => true, allowStdout = true, stdoutWrite: stdoutWriteDep, now = () => performance.now(), clientFactory = createDefaultClientFactory(), client, wait = defaultWait, } = deps;
38
38
  const stdoutWrite = allowStdout
39
- ? stdoutWriteDep ?? process.stdout.write.bind(process.stdout)
39
+ ? (stdoutWriteDep ?? process.stdout.write.bind(process.stdout))
40
40
  : () => true;
41
41
  const isTty = allowStdout && isStdoutTty;
42
- const resolvedXaiBaseUrl = process.env.XAI_BASE_URL?.trim() || 'https://api.x.ai/v1';
42
+ const resolvedXaiBaseUrl = process.env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
43
43
  const openRouterApiKey = process.env.OPENROUTER_API_KEY?.trim();
44
44
  const defaultOpenRouterBase = defaultOpenRouterBaseUrl();
45
45
  const knownModelConfig = isKnownModel(options.model) ? MODEL_CONFIGS[options.model] : undefined;
46
- const provider = knownModelConfig?.provider ?? 'other';
46
+ const provider = knownModelConfig?.provider ?? "other";
47
47
  const hasOpenAIKey = Boolean(optionsApiKey) ||
48
48
  Boolean(process.env.OPENAI_API_KEY) ||
49
49
  Boolean(process.env.AZURE_OPENAI_API_KEY && options.azure?.endpoint);
@@ -52,21 +52,21 @@ export async function runOracle(options, deps = {}) {
52
52
  const hasXaiKey = Boolean(optionsApiKey) || Boolean(process.env.XAI_API_KEY);
53
53
  let baseUrl = options.baseUrl?.trim();
54
54
  if (!baseUrl) {
55
- if (options.model.startsWith('grok')) {
55
+ if (options.model.startsWith("grok")) {
56
56
  baseUrl = resolvedXaiBaseUrl;
57
57
  }
58
- else if (provider === 'anthropic') {
58
+ else if (provider === "anthropic") {
59
59
  baseUrl = process.env.ANTHROPIC_BASE_URL?.trim();
60
60
  }
61
61
  else {
62
62
  baseUrl = process.env.OPENAI_BASE_URL?.trim();
63
63
  }
64
64
  }
65
- const providerKeyMissing = (provider === 'openai' && !hasOpenAIKey) ||
66
- (provider === 'anthropic' && !hasAnthropicKey) ||
67
- (provider === 'google' && !hasGeminiKey) ||
68
- (provider === 'xai' && !hasXaiKey) ||
69
- provider === 'other';
65
+ const providerKeyMissing = (provider === "openai" && !hasOpenAIKey) ||
66
+ (provider === "anthropic" && !hasAnthropicKey) ||
67
+ (provider === "google" && !hasGeminiKey) ||
68
+ (provider === "xai" && !hasXaiKey) ||
69
+ provider === "other";
70
70
  const openRouterFallback = providerKeyMissing && Boolean(openRouterApiKey);
71
71
  if (!baseUrl || openRouterFallback) {
72
72
  if (openRouterFallback) {
@@ -86,66 +86,76 @@ export async function runOracle(options, deps = {}) {
86
86
  const isAzureOpenAI = Boolean(options.azure?.endpoint);
87
87
  const getApiKeyForModel = (model) => {
88
88
  if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback) {
89
- return { key: optionsApiKey ?? openRouterApiKey, source: 'OPENROUTER_API_KEY' };
89
+ return { key: optionsApiKey ?? openRouterApiKey, source: "OPENROUTER_API_KEY" };
90
90
  }
91
- if (typeof model === 'string' && model.startsWith('gpt')) {
91
+ if (typeof model === "string" && model.startsWith("gpt")) {
92
92
  if (optionsApiKey)
93
- return { key: optionsApiKey, source: 'apiKey option' };
93
+ return { key: optionsApiKey, source: "apiKey option" };
94
94
  if (isAzureOpenAI) {
95
95
  const key = process.env.AZURE_OPENAI_API_KEY ?? process.env.OPENAI_API_KEY;
96
- return { key, source: 'AZURE_OPENAI_API_KEY|OPENAI_API_KEY' };
96
+ return { key, source: "AZURE_OPENAI_API_KEY|OPENAI_API_KEY" };
97
97
  }
98
- return { key: process.env.OPENAI_API_KEY, source: 'OPENAI_API_KEY' };
98
+ return { key: process.env.OPENAI_API_KEY, source: "OPENAI_API_KEY" };
99
99
  }
100
- if (typeof model === 'string' && model.startsWith('gemini')) {
101
- return { key: optionsApiKey ?? process.env.GEMINI_API_KEY, source: 'GEMINI_API_KEY' };
100
+ if (typeof model === "string" && model.startsWith("gemini")) {
101
+ return { key: optionsApiKey ?? process.env.GEMINI_API_KEY, source: "GEMINI_API_KEY" };
102
102
  }
103
- if (typeof model === 'string' && model.startsWith('claude')) {
104
- return { key: optionsApiKey ?? process.env.ANTHROPIC_API_KEY, source: 'ANTHROPIC_API_KEY' };
103
+ if (typeof model === "string" && model.startsWith("claude")) {
104
+ return { key: optionsApiKey ?? process.env.ANTHROPIC_API_KEY, source: "ANTHROPIC_API_KEY" };
105
105
  }
106
- if (typeof model === 'string' && model.startsWith('grok')) {
107
- return { key: optionsApiKey ?? process.env.XAI_API_KEY, source: 'XAI_API_KEY' };
106
+ if (typeof model === "string" && model.startsWith("grok")) {
107
+ return { key: optionsApiKey ?? process.env.XAI_API_KEY, source: "XAI_API_KEY" };
108
108
  }
109
- return { key: optionsApiKey ?? openRouterApiKey, source: optionsApiKey ? 'apiKey option' : 'OPENROUTER_API_KEY' };
109
+ return {
110
+ key: optionsApiKey ?? openRouterApiKey,
111
+ source: optionsApiKey ? "apiKey option" : "OPENROUTER_API_KEY",
112
+ };
110
113
  };
111
114
  const apiKeyResult = getApiKeyForModel(options.model);
112
115
  const apiKey = apiKeyResult.key;
113
116
  if (!apiKey) {
114
117
  const envVar = isOpenRouterBaseUrl(baseUrl) || openRouterFallback
115
- ? 'OPENROUTER_API_KEY'
116
- : options.model.startsWith('gpt')
118
+ ? "OPENROUTER_API_KEY"
119
+ : options.model.startsWith("gpt")
117
120
  ? isAzureOpenAI
118
- ? 'AZURE_OPENAI_API_KEY (or OPENAI_API_KEY)'
119
- : 'OPENAI_API_KEY'
120
- : options.model.startsWith('gemini')
121
- ? 'GEMINI_API_KEY'
122
- : options.model.startsWith('claude')
123
- ? 'ANTHROPIC_API_KEY'
124
- : options.model.startsWith('grok')
125
- ? 'XAI_API_KEY'
126
- : 'OPENROUTER_API_KEY';
127
- throw new PromptValidationError(`Missing ${envVar}. Set it via the environment or a .env file.`, {
121
+ ? "AZURE_OPENAI_API_KEY (or OPENAI_API_KEY)"
122
+ : "OPENAI_API_KEY"
123
+ : options.model.startsWith("gemini")
124
+ ? "GEMINI_API_KEY"
125
+ : options.model.startsWith("claude")
126
+ ? "ANTHROPIC_API_KEY"
127
+ : options.model.startsWith("grok")
128
+ ? "XAI_API_KEY"
129
+ : "OPENROUTER_API_KEY";
130
+ const browserModeHint = options.model.startsWith("gpt")
131
+ ? ' If you have a ChatGPT Pro subscription, retry with --engine browser (or MCP engine:"browser" / preset:"chatgpt-pro-heavy"); browser mode uses your signed-in ChatGPT session instead of an API key.'
132
+ : "";
133
+ throw new PromptValidationError(`Missing ${envVar}. Set it via the environment or a .env file.${browserModeHint}`, {
128
134
  env: envVar,
129
135
  });
130
136
  }
131
137
  const envVar = apiKeyResult.source;
132
- const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? '10', 10);
138
+ const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? "10", 10);
133
139
  const promptLength = options.prompt?.trim().length ?? 0;
134
140
  // Enforce the short-prompt guardrail on pro-tier models because they're costly; cheaper models can run short prompts without blocking.
135
141
  const isProTierModel = isProModel(options.model);
136
142
  if (isProTierModel && !Number.isNaN(minPromptLength) && promptLength < minPromptLength) {
137
143
  throw new PromptValidationError(`Prompt is too short (<${minPromptLength} chars). This was likely accidental; please provide more detail.`, { minPromptLength, promptLength });
138
144
  }
139
- const resolverOpenRouterApiKey = openRouterFallback || isOpenRouterBaseUrl(baseUrl) ? openRouterApiKey ?? apiKey : undefined;
145
+ const resolverOpenRouterApiKey = openRouterFallback || isOpenRouterBaseUrl(baseUrl) ? (openRouterApiKey ?? apiKey) : undefined;
140
146
  const modelConfig = await resolveModelConfig(options.model, {
141
147
  baseUrl,
142
148
  openRouterApiKey: resolverOpenRouterApiKey,
143
149
  });
144
150
  const isLongRunningModel = isProTierModel;
145
151
  const supportsBackground = modelConfig.supportsBackground !== false;
146
- const useBackground = supportsBackground ? options.background ?? isLongRunningModel : false;
152
+ const useBackground = supportsBackground ? (options.background ?? isLongRunningModel) : false;
147
153
  const inputTokenBudget = options.maxInput ?? modelConfig.inputLimit;
148
- const files = await readFiles(options.file ?? [], { cwd, fsModule, maxFileSizeBytes: options.maxFileSizeBytes });
154
+ const files = await readFiles(options.file ?? [], {
155
+ cwd,
156
+ fsModule,
157
+ maxFileSizeBytes: options.maxFileSizeBytes,
158
+ });
149
159
  const searchEnabled = options.search !== false;
150
160
  logVerbose(`cwd: ${cwd}`);
151
161
  let pendingNoFilesTip = null;
@@ -154,21 +164,21 @@ export async function runOracle(options, deps = {}) {
154
164
  const displayPaths = files
155
165
  .map((file) => path.relative(cwd, file.path) || file.path)
156
166
  .slice(0, 10)
157
- .join(', ');
158
- const extra = files.length > 10 ? ` (+${files.length - 10} more)` : '';
167
+ .join(", ");
168
+ const extra = files.length > 10 ? ` (+${files.length - 10} more)` : "";
159
169
  logVerbose(`Attached files (${files.length}): ${displayPaths}${extra}`);
160
170
  }
161
171
  else {
162
- logVerbose('No files attached.');
172
+ logVerbose("No files attached.");
163
173
  if (!isPreview) {
164
174
  pendingNoFilesTip =
165
- 'Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.';
175
+ "Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.";
166
176
  }
167
177
  }
168
178
  const shortPrompt = (options.prompt?.trim().length ?? 0) < 80;
169
179
  if (!isPreview && shortPrompt) {
170
180
  pendingShortPromptTip =
171
- 'Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.';
181
+ "Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.";
172
182
  }
173
183
  const fileTokenInfo = getFileTokenStats(files, {
174
184
  cwd,
@@ -183,7 +193,7 @@ export async function runOracle(options, deps = {}) {
183
193
  const fileCount = files.length;
184
194
  const richTty = allowStdout && process.stdout.isTTY && chalk.level > 0;
185
195
  const renderPlain = Boolean(options.renderPlain);
186
- const timeoutSeconds = options.timeoutSeconds === undefined || options.timeoutSeconds === 'auto'
196
+ const timeoutSeconds = options.timeoutSeconds === undefined || options.timeoutSeconds === "auto"
187
197
  ? isLongRunningModel
188
198
  ? DEFAULT_TIMEOUT_PRO_MS / 1000
189
199
  : DEFAULT_TIMEOUT_NON_PRO_MS / 1000
@@ -194,7 +204,7 @@ export async function runOracle(options, deps = {}) {
194
204
  const effectiveModelId = options.effectiveModelId ??
195
205
  (azureDeploymentName
196
206
  ? azureDeploymentName
197
- : options.model.startsWith('gemini')
207
+ : options.model.startsWith("gemini")
198
208
  ? resolveGeminiModelId(options.model)
199
209
  : (modelConfig.apiModel ?? modelConfig.model));
200
210
  if (!isPreview && options.previousResponseId) {
@@ -213,30 +223,31 @@ export async function runOracle(options, deps = {}) {
213
223
  });
214
224
  requestBody.model = effectiveModelId;
215
225
  const estimatedInputTokens = estimateRequestTokens(requestBody, modelConfig);
216
- const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => (richTty ? chalk.green(text) : text));
226
+ const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => richTty ? chalk.green(text) : text);
217
227
  const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
218
- const filesPhrase = fileCount === 0 ? 'no files' : `${fileLabel} files`;
228
+ const filesPhrase = fileCount === 0 ? "no files" : `${fileLabel} files`;
219
229
  const headerModelLabelBase = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
220
230
  const headerModelSuffix = effectiveModelId !== modelConfig.model
221
231
  ? richTty
222
232
  ? chalk.gray(` (API: ${effectiveModelId})`)
223
233
  : ` (API: ${effectiveModelId})`
224
- : '';
234
+ : "";
225
235
  const headerLine = `Calling ${headerModelLabelBase}${headerModelSuffix} — ${tokenLabel} tokens, ${filesPhrase}.`;
226
- const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
236
+ const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) &&
237
+ fileTokenInfo.stats.length > 0;
227
238
  if (!isPreview) {
228
239
  if (!options.suppressHeader) {
229
240
  log(headerLine);
230
241
  }
231
242
  const maskedKey = maskApiKey(apiKey);
232
243
  if (maskedKey && options.verbose) {
233
- const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (API: ${effectiveModelId})` : '';
244
+ const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (API: ${effectiveModelId})` : "";
234
245
  log(dim(`Using ${envVar}=${maskedKey} for model ${modelConfig.model}${resolvedSuffix}`));
235
246
  }
236
247
  if (!options.suppressHeader &&
237
- (modelConfig.model === 'gpt-5.1-pro' || modelConfig.model === 'gpt-5.2-pro') &&
238
- effectiveModelId === 'gpt-5.4-pro') {
239
- log(dim(`Note: \`${modelConfig.model}\` is a stable CLI alias; OpenAI API uses \`gpt-5.4-pro\`.`));
248
+ (modelConfig.model === "gpt-5.1-pro" || modelConfig.model === "gpt-5.2-pro") &&
249
+ effectiveModelId === "gpt-5.5-pro") {
250
+ log(dim(`Note: \`${modelConfig.model}\` is a stable CLI alias; OpenAI API uses \`gpt-5.5-pro\`.`));
240
251
  }
241
252
  if (baseUrl) {
242
253
  log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
@@ -245,7 +256,7 @@ export async function runOracle(options, deps = {}) {
245
256
  log(dim(`Resolved model: ${modelConfig.model} → ${effectiveModelId}`));
246
257
  }
247
258
  if (options.background && !supportsBackground) {
248
- log(dim('Background runs are not supported for this model; streaming in foreground instead.'));
259
+ log(dim("Background runs are not supported for this model; streaming in foreground instead."));
249
260
  }
250
261
  if (!options.suppressTips) {
251
262
  if (pendingNoFilesTip) {
@@ -256,10 +267,10 @@ export async function runOracle(options, deps = {}) {
256
267
  }
257
268
  }
258
269
  if (isLongRunningModel) {
259
- log(dim('This model can take up to 60 minutes (usually replies much faster).'));
270
+ log(dim("This model can take up to 60 minutes (usually replies much faster)."));
260
271
  }
261
272
  if (options.verbose || isLongRunningModel) {
262
- log(dim('Press Ctrl+C to cancel.'));
273
+ log(dim("Press Ctrl+C to cancel."));
263
274
  }
264
275
  }
265
276
  if (shouldReportFiles) {
@@ -270,19 +281,19 @@ export async function runOracle(options, deps = {}) {
270
281
  }
271
282
  logVerbose(`Estimated tokens (request body): ${estimatedInputTokens.toLocaleString()}`);
272
283
  if (isPreview && previewMode) {
273
- if (previewMode === 'json' || previewMode === 'full') {
274
- log('Request JSON');
284
+ if (previewMode === "json" || previewMode === "full") {
285
+ log("Request JSON");
275
286
  log(JSON.stringify(requestBody, null, 2));
276
- log('');
287
+ log("");
277
288
  }
278
- if (previewMode === 'full') {
279
- log('Assembled Prompt');
289
+ if (previewMode === "full") {
290
+ log("Assembled Prompt");
280
291
  log(promptWithFiles);
281
- log('');
292
+ log("");
282
293
  }
283
294
  log(`Estimated input tokens: ${estimatedInputTokens.toLocaleString()} / ${inputTokenBudget.toLocaleString()} (model: ${modelConfig.model})`);
284
295
  return {
285
- mode: 'preview',
296
+ mode: "preview",
286
297
  previewMode,
287
298
  requestBody,
288
299
  estimatedInputTokens,
@@ -290,31 +301,31 @@ export async function runOracle(options, deps = {}) {
290
301
  };
291
302
  }
292
303
  const proxyCompatibleBaseUrl = baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl)) ? baseUrl : undefined;
293
- const apiEndpoint = modelConfig.model.startsWith('gemini')
304
+ const apiEndpoint = modelConfig.model.startsWith("gemini")
294
305
  ? proxyCompatibleBaseUrl
295
306
  : proxyCompatibleBaseUrl
296
307
  ? proxyCompatibleBaseUrl
297
- : modelConfig.model.startsWith('claude')
298
- ? process.env.ANTHROPIC_BASE_URL ?? baseUrl
308
+ : modelConfig.model.startsWith("claude")
309
+ ? (process.env.ANTHROPIC_BASE_URL ?? baseUrl)
299
310
  : baseUrl;
300
311
  const clientInstance = client ??
301
312
  clientFactory(apiKey, {
302
313
  baseUrl: apiEndpoint,
303
314
  azure: options.azure,
304
315
  model: options.model,
305
- resolvedModelId: modelConfig.model.startsWith('claude')
316
+ resolvedModelId: modelConfig.model.startsWith("claude")
306
317
  ? resolveClaudeModelId(effectiveModelId)
307
- : modelConfig.model.startsWith('gemini')
318
+ : modelConfig.model.startsWith("gemini")
308
319
  ? resolveGeminiModelId(effectiveModelId)
309
320
  : effectiveModelId,
310
321
  httpTimeoutMs: options.httpTimeoutMs,
311
322
  });
312
- logVerbose('Dispatching request to API...');
323
+ logVerbose("Dispatching request to API...");
313
324
  if (options.verbose) {
314
- log(''); // ensure verbose section is separated from Answer stream
325
+ log(""); // ensure verbose section is separated from Answer stream
315
326
  }
316
327
  const stopOscProgress = startOscProgress({
317
- label: useBackground ? 'Waiting for API (background)' : 'Waiting for API',
328
+ label: useBackground ? "Waiting for API (background)" : "Waiting for API",
318
329
  targetMs: useBackground ? timeoutMs : Math.min(timeoutMs, 10 * 60_000),
319
330
  indeterminate: true,
320
331
  write: sinkWrite,
@@ -328,16 +339,16 @@ export async function runOracle(options, deps = {}) {
328
339
  const timeoutExceeded = () => now() - runStart >= timeoutMs;
329
340
  const throwIfTimedOut = () => {
330
341
  if (timeoutExceeded()) {
331
- throw new OracleTransportError('client-timeout', `Timed out waiting for API response after ${formatElapsed(timeoutMs)}.`);
342
+ throw new OracleTransportError("client-timeout", `Timed out waiting for API response after ${formatElapsed(timeoutMs)}.`);
332
343
  }
333
344
  };
334
345
  const ensureAnswerHeader = () => {
335
346
  if (options.silent || answerHeaderPrinted)
336
347
  return;
337
348
  // Always add a separating newline for readability; optionally include the label depending on caller needs.
338
- log('');
349
+ log("");
339
350
  if (allowAnswerHeader) {
340
- log(chalk.bold('Answer:'));
351
+ log(chalk.bold("Answer:"));
341
352
  }
342
353
  answerHeaderPrinted = true;
343
354
  };
@@ -405,20 +416,20 @@ export async function runOracle(options, deps = {}) {
405
416
  isTty && !renderPlain
406
417
  ? createMarkdownStreamer({
407
418
  render: renderMarkdownAnsi,
408
- spacing: 'single',
409
- mode: 'hybrid',
419
+ spacing: "single",
420
+ mode: "hybrid",
410
421
  })
411
422
  : null;
412
423
  for await (const event of stream) {
413
424
  throwIfTimedOut();
414
- const isTextDelta = event.type === 'chunk' || event.type === 'response.output_text.delta';
425
+ const isTextDelta = event.type === "chunk" || event.type === "response.output_text.delta";
415
426
  if (!isTextDelta)
416
427
  continue;
417
428
  stopOscProgress();
418
429
  stopHeartbeatNow();
419
430
  sawTextDelta = true;
420
431
  ensureAnswerHeader();
421
- if (options.silent || typeof event.delta !== 'string')
432
+ if (options.silent || typeof event.delta !== "string")
422
433
  continue;
423
434
  // Always keep the log/bookkeeping sink up to date.
424
435
  sinkWrite(event.delta);
@@ -458,41 +469,41 @@ export async function runOracle(options, deps = {}) {
458
469
  stopOscProgress();
459
470
  }
460
471
  if (!response) {
461
- throw new Error('API did not return a response.');
472
+ throw new Error("API did not return a response.");
462
473
  }
463
474
  // We only add spacing when streamed text was printed.
464
475
  if (sawTextDelta && !options.silent) {
465
476
  if (renderPlain) {
466
477
  // Plain streaming already wrote chunks; ensure clean separation.
467
- stdoutWrite('\n');
478
+ stdoutWrite("\n");
468
479
  }
469
480
  else {
470
481
  // Separate streamed output from logs.
471
- log('');
482
+ log("");
472
483
  }
473
484
  }
474
- logVerbose(`Response status: ${response.status ?? 'completed'}`);
475
- if (response.status && response.status !== 'completed') {
485
+ logVerbose(`Response status: ${response.status ?? "completed"}`);
486
+ if (response.status && response.status !== "completed") {
476
487
  // API can reply `in_progress` even after the stream closes; give it a brief grace poll.
477
- if (response.id && response.status === 'in_progress') {
488
+ if (response.id && response.status === "in_progress") {
478
489
  const polishingStart = now();
479
490
  const pollIntervalMs = 2_000;
480
491
  const maxWaitMs = 180_000;
481
- log(chalk.dim('Response still in_progress; polling until completion...'));
492
+ log(chalk.dim("Response still in_progress; polling until completion..."));
482
493
  // Short polling loop — we don't want to hang forever, just catch late finalization.
483
494
  while (now() - polishingStart < maxWaitMs) {
484
495
  throwIfTimedOut();
485
496
  await wait(pollIntervalMs);
486
497
  const refreshed = await clientInstance.responses.retrieve(response.id);
487
- if (refreshed.status === 'completed') {
498
+ if (refreshed.status === "completed") {
488
499
  response = refreshed;
489
500
  break;
490
501
  }
491
502
  }
492
503
  }
493
- if (response.status !== 'completed') {
504
+ if (response.status !== "completed") {
494
505
  const detail = response.error?.message || response.incomplete_details?.reason || response.status;
495
- log(chalk.yellow(`API ended the run early (status=${response.status}${response.incomplete_details?.reason ? `, reason=${response.incomplete_details.reason}` : ''}).`));
506
+ log(chalk.yellow(`API ended the run early (status=${response.status}${response.incomplete_details?.reason ? `, reason=${response.incomplete_details.reason}` : ""}).`));
496
507
  throw new OracleResponseError(`Response did not complete: ${detail}`, response);
497
508
  }
498
509
  }
@@ -509,16 +520,16 @@ export async function runOracle(options, deps = {}) {
509
520
  ? renderPlain || !richTty
510
521
  ? answerText
511
522
  : renderMarkdownAnsi(answerText)
512
- : chalk.dim('(no text output)');
523
+ : chalk.dim("(no text output)");
513
524
  sinkWrite(printable);
514
- if (!printable.endsWith('\n')) {
515
- sinkWrite('\n');
525
+ if (!printable.endsWith("\n")) {
526
+ sinkWrite("\n");
516
527
  }
517
528
  stdoutWrite(printable);
518
- if (!printable.endsWith('\n')) {
519
- stdoutWrite('\n');
529
+ if (!printable.endsWith("\n")) {
530
+ stdoutWrite("\n");
520
531
  }
521
- log('');
532
+ log("");
522
533
  }
523
534
  }
524
535
  const usage = response.usage ?? {};
@@ -530,17 +541,21 @@ export async function runOracle(options, deps = {}) {
530
541
  const cost = pricing
531
542
  ? estimateUsdCost({
532
543
  usage: { inputTokens, outputTokens, reasoningTokens, totalTokens },
533
- pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
544
+ pricing: {
545
+ inputUsdPerToken: pricing.inputPerToken,
546
+ outputUsdPerToken: pricing.outputPerToken,
547
+ },
534
548
  })?.totalUsd
535
549
  : undefined;
536
550
  const effortLabel = modelConfig.reasoning?.effort;
537
551
  const modelLabel = effortLabel ? `${modelConfig.model}[${effortLabel}]` : modelConfig.model;
538
- const sessionIdContainsModel = typeof options.sessionId === 'string' && options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
552
+ const sessionIdContainsModel = typeof options.sessionId === "string" &&
553
+ options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
539
554
  const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]
540
555
  .map((value, index) => formatTokenValue(value, usage, index))
541
- .join('/');
556
+ .join("/");
542
557
  const tokensPart = (() => {
543
- const parts = tokensDisplay.split('/');
558
+ const parts = tokensDisplay.split("/");
544
559
  if (parts.length !== 4)
545
560
  return tokensDisplay;
546
561
  return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
@@ -553,7 +568,11 @@ export async function runOracle(options, deps = {}) {
553
568
  if (actualInput === undefined)
554
569
  return null;
555
570
  const delta = actualInput - estimatedInputTokens;
556
- const deltaText = delta === 0 ? '' : delta > 0 ? ` (+${delta.toLocaleString()})` : ` (${delta.toLocaleString()})`;
571
+ const deltaText = delta === 0
572
+ ? ""
573
+ : delta > 0
574
+ ? ` (+${delta.toLocaleString()})`
575
+ : ` (${delta.toLocaleString()})`;
557
576
  return `est→actual=${estimatedInputTokens.toLocaleString()}→${actualInput.toLocaleString()}${deltaText}`;
558
577
  })();
559
578
  const { line1, line2 } = formatFinishLine({
@@ -564,43 +583,49 @@ export async function runOracle(options, deps = {}) {
564
583
  summaryExtraParts: options.sessionId ? [`sid=${options.sessionId}`] : null,
565
584
  detailParts: [
566
585
  estActualPart,
567
- !searchEnabled ? 'search=off' : null,
586
+ !searchEnabled ? "search=off" : null,
568
587
  files.length > 0 ? `files=${files.length}` : null,
569
588
  ],
570
589
  });
571
590
  if (!options.silent) {
572
- log('');
591
+ log("");
573
592
  }
574
593
  log(chalk.blue(line1));
575
594
  if (line2) {
576
595
  log(dim(line2));
577
596
  }
578
597
  return {
579
- mode: 'live',
598
+ mode: "live",
580
599
  response,
581
- usage: { inputTokens, outputTokens, reasoningTokens, totalTokens, ...(cost != null ? { cost } : {}) },
600
+ usage: {
601
+ inputTokens,
602
+ outputTokens,
603
+ reasoningTokens,
604
+ totalTokens,
605
+ ...(cost != null ? { cost } : {}),
606
+ },
582
607
  elapsedMs,
583
608
  };
584
609
  }
585
610
  export function extractTextOutput(response) {
586
611
  if (Array.isArray(response.output_text) && response.output_text.length > 0) {
587
- return response.output_text.join('\n');
612
+ return response.output_text.join("\n");
588
613
  }
589
614
  if (Array.isArray(response.output)) {
590
615
  const segments = [];
591
616
  for (const item of response.output) {
592
617
  if (Array.isArray(item.content)) {
593
618
  for (const chunk of item.content) {
594
- if (chunk && (chunk.type === 'output_text' || chunk.type === 'text') && chunk.text) {
619
+ if (chunk && (chunk.type === "output_text" || chunk.type === "text") && chunk.text) {
595
620
  segments.push(chunk.text);
596
621
  }
597
622
  }
598
623
  }
599
- else if (typeof item.text === 'string') {
624
+ else if (typeof item.text === "string") {
600
625
  segments.push(item.text);
601
626
  }
602
627
  }
603
- return segments.join('\n');
628
+ return segments.join("\n");
604
629
  }
605
- return '';
630
+ return "";
606
631
  }
@@ -1,10 +1,10 @@
1
1
  export function resolvePreviewMode(value) {
2
- const allowed = new Set(['summary', 'json', 'full']);
3
- if (typeof value === 'string' && value.length > 0) {
4
- return allowed.has(value) ? value : 'summary';
2
+ const allowed = new Set(["summary", "json", "full"]);
3
+ if (typeof value === "string" && value.length > 0) {
4
+ return allowed.has(value) ? value : "summary";
5
5
  }
6
6
  if (value) {
7
- return 'summary';
7
+ return "summary";
8
8
  }
9
9
  return undefined;
10
10
  }
@@ -13,7 +13,10 @@ export function resolvePreviewMode(value) {
13
13
  */
14
14
  export function formatTokenCount(value) {
15
15
  if (Math.abs(value) >= 1000) {
16
- const abbreviated = (value / 1000).toFixed(2).replace(/\.0+$/, '').replace(/\.([1-9]*)0$/, '.$1');
16
+ const abbreviated = (value / 1000)
17
+ .toFixed(2)
18
+ .replace(/\.0+$/, "")
19
+ .replace(/\.([1-9]*)0$/, ".$1");
17
20
  return `${abbreviated}k`;
18
21
  }
19
22
  return value.toLocaleString();