@steipete/oracle 0.8.6 → 0.10.0

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