@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,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,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, maxFileSizeBytes: options.maxFileSizeBytes });
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,7 +190,7 @@ 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
@@ -194,7 +201,7 @@ export async function runOracle(options, deps = {}) {
194
201
  const effectiveModelId = options.effectiveModelId ??
195
202
  (azureDeploymentName
196
203
  ? azureDeploymentName
197
- : options.model.startsWith('gemini')
204
+ : options.model.startsWith("gemini")
198
205
  ? resolveGeminiModelId(options.model)
199
206
  : (modelConfig.apiModel ?? modelConfig.model));
200
207
  if (!isPreview && options.previousResponseId) {
@@ -213,30 +220,31 @@ export async function runOracle(options, deps = {}) {
213
220
  });
214
221
  requestBody.model = effectiveModelId;
215
222
  const estimatedInputTokens = estimateRequestTokens(requestBody, modelConfig);
216
- const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => (richTty ? chalk.green(text) : text));
223
+ const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => richTty ? chalk.green(text) : text);
217
224
  const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
218
- const filesPhrase = fileCount === 0 ? 'no files' : `${fileLabel} files`;
225
+ const filesPhrase = fileCount === 0 ? "no files" : `${fileLabel} files`;
219
226
  const headerModelLabelBase = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
220
227
  const headerModelSuffix = effectiveModelId !== modelConfig.model
221
228
  ? richTty
222
229
  ? chalk.gray(` (API: ${effectiveModelId})`)
223
230
  : ` (API: ${effectiveModelId})`
224
- : '';
231
+ : "";
225
232
  const headerLine = `Calling ${headerModelLabelBase}${headerModelSuffix} — ${tokenLabel} tokens, ${filesPhrase}.`;
226
- const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
233
+ const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) &&
234
+ fileTokenInfo.stats.length > 0;
227
235
  if (!isPreview) {
228
236
  if (!options.suppressHeader) {
229
237
  log(headerLine);
230
238
  }
231
239
  const maskedKey = maskApiKey(apiKey);
232
240
  if (maskedKey && options.verbose) {
233
- const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (API: ${effectiveModelId})` : '';
241
+ const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (API: ${effectiveModelId})` : "";
234
242
  log(dim(`Using ${envVar}=${maskedKey} for model ${modelConfig.model}${resolvedSuffix}`));
235
243
  }
236
244
  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\`.`));
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\`.`));
240
248
  }
241
249
  if (baseUrl) {
242
250
  log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
@@ -245,7 +253,7 @@ export async function runOracle(options, deps = {}) {
245
253
  log(dim(`Resolved model: ${modelConfig.model} → ${effectiveModelId}`));
246
254
  }
247
255
  if (options.background && !supportsBackground) {
248
- 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."));
249
257
  }
250
258
  if (!options.suppressTips) {
251
259
  if (pendingNoFilesTip) {
@@ -256,10 +264,10 @@ export async function runOracle(options, deps = {}) {
256
264
  }
257
265
  }
258
266
  if (isLongRunningModel) {
259
- 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)."));
260
268
  }
261
269
  if (options.verbose || isLongRunningModel) {
262
- log(dim('Press Ctrl+C to cancel.'));
270
+ log(dim("Press Ctrl+C to cancel."));
263
271
  }
264
272
  }
265
273
  if (shouldReportFiles) {
@@ -270,19 +278,19 @@ export async function runOracle(options, deps = {}) {
270
278
  }
271
279
  logVerbose(`Estimated tokens (request body): ${estimatedInputTokens.toLocaleString()}`);
272
280
  if (isPreview && previewMode) {
273
- if (previewMode === 'json' || previewMode === 'full') {
274
- log('Request JSON');
281
+ if (previewMode === "json" || previewMode === "full") {
282
+ log("Request JSON");
275
283
  log(JSON.stringify(requestBody, null, 2));
276
- log('');
284
+ log("");
277
285
  }
278
- if (previewMode === 'full') {
279
- log('Assembled Prompt');
286
+ if (previewMode === "full") {
287
+ log("Assembled Prompt");
280
288
  log(promptWithFiles);
281
- log('');
289
+ log("");
282
290
  }
283
291
  log(`Estimated input tokens: ${estimatedInputTokens.toLocaleString()} / ${inputTokenBudget.toLocaleString()} (model: ${modelConfig.model})`);
284
292
  return {
285
- mode: 'preview',
293
+ mode: "preview",
286
294
  previewMode,
287
295
  requestBody,
288
296
  estimatedInputTokens,
@@ -290,31 +298,31 @@ export async function runOracle(options, deps = {}) {
290
298
  };
291
299
  }
292
300
  const proxyCompatibleBaseUrl = baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl)) ? baseUrl : undefined;
293
- const apiEndpoint = modelConfig.model.startsWith('gemini')
301
+ const apiEndpoint = modelConfig.model.startsWith("gemini")
294
302
  ? proxyCompatibleBaseUrl
295
303
  : proxyCompatibleBaseUrl
296
304
  ? proxyCompatibleBaseUrl
297
- : modelConfig.model.startsWith('claude')
298
- ? process.env.ANTHROPIC_BASE_URL ?? baseUrl
305
+ : modelConfig.model.startsWith("claude")
306
+ ? (process.env.ANTHROPIC_BASE_URL ?? baseUrl)
299
307
  : baseUrl;
300
308
  const clientInstance = client ??
301
309
  clientFactory(apiKey, {
302
310
  baseUrl: apiEndpoint,
303
311
  azure: options.azure,
304
312
  model: options.model,
305
- resolvedModelId: modelConfig.model.startsWith('claude')
313
+ resolvedModelId: modelConfig.model.startsWith("claude")
306
314
  ? resolveClaudeModelId(effectiveModelId)
307
- : modelConfig.model.startsWith('gemini')
315
+ : modelConfig.model.startsWith("gemini")
308
316
  ? resolveGeminiModelId(effectiveModelId)
309
317
  : effectiveModelId,
310
318
  httpTimeoutMs: options.httpTimeoutMs,
311
319
  });
312
- logVerbose('Dispatching request to API...');
320
+ logVerbose("Dispatching request to API...");
313
321
  if (options.verbose) {
314
- log(''); // ensure verbose section is separated from Answer stream
322
+ log(""); // ensure verbose section is separated from Answer stream
315
323
  }
316
324
  const stopOscProgress = startOscProgress({
317
- label: useBackground ? 'Waiting for API (background)' : 'Waiting for API',
325
+ label: useBackground ? "Waiting for API (background)" : "Waiting for API",
318
326
  targetMs: useBackground ? timeoutMs : Math.min(timeoutMs, 10 * 60_000),
319
327
  indeterminate: true,
320
328
  write: sinkWrite,
@@ -328,16 +336,16 @@ export async function runOracle(options, deps = {}) {
328
336
  const timeoutExceeded = () => now() - runStart >= timeoutMs;
329
337
  const throwIfTimedOut = () => {
330
338
  if (timeoutExceeded()) {
331
- 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)}.`);
332
340
  }
333
341
  };
334
342
  const ensureAnswerHeader = () => {
335
343
  if (options.silent || answerHeaderPrinted)
336
344
  return;
337
345
  // Always add a separating newline for readability; optionally include the label depending on caller needs.
338
- log('');
346
+ log("");
339
347
  if (allowAnswerHeader) {
340
- log(chalk.bold('Answer:'));
348
+ log(chalk.bold("Answer:"));
341
349
  }
342
350
  answerHeaderPrinted = true;
343
351
  };
@@ -405,20 +413,20 @@ export async function runOracle(options, deps = {}) {
405
413
  isTty && !renderPlain
406
414
  ? createMarkdownStreamer({
407
415
  render: renderMarkdownAnsi,
408
- spacing: 'single',
409
- mode: 'hybrid',
416
+ spacing: "single",
417
+ mode: "hybrid",
410
418
  })
411
419
  : null;
412
420
  for await (const event of stream) {
413
421
  throwIfTimedOut();
414
- const isTextDelta = event.type === 'chunk' || event.type === 'response.output_text.delta';
422
+ const isTextDelta = event.type === "chunk" || event.type === "response.output_text.delta";
415
423
  if (!isTextDelta)
416
424
  continue;
417
425
  stopOscProgress();
418
426
  stopHeartbeatNow();
419
427
  sawTextDelta = true;
420
428
  ensureAnswerHeader();
421
- if (options.silent || typeof event.delta !== 'string')
429
+ if (options.silent || typeof event.delta !== "string")
422
430
  continue;
423
431
  // Always keep the log/bookkeeping sink up to date.
424
432
  sinkWrite(event.delta);
@@ -458,41 +466,41 @@ export async function runOracle(options, deps = {}) {
458
466
  stopOscProgress();
459
467
  }
460
468
  if (!response) {
461
- throw new Error('API did not return a response.');
469
+ throw new Error("API did not return a response.");
462
470
  }
463
471
  // We only add spacing when streamed text was printed.
464
472
  if (sawTextDelta && !options.silent) {
465
473
  if (renderPlain) {
466
474
  // Plain streaming already wrote chunks; ensure clean separation.
467
- stdoutWrite('\n');
475
+ stdoutWrite("\n");
468
476
  }
469
477
  else {
470
478
  // Separate streamed output from logs.
471
- log('');
479
+ log("");
472
480
  }
473
481
  }
474
- logVerbose(`Response status: ${response.status ?? 'completed'}`);
475
- if (response.status && response.status !== 'completed') {
482
+ logVerbose(`Response status: ${response.status ?? "completed"}`);
483
+ if (response.status && response.status !== "completed") {
476
484
  // API can reply `in_progress` even after the stream closes; give it a brief grace poll.
477
- if (response.id && response.status === 'in_progress') {
485
+ if (response.id && response.status === "in_progress") {
478
486
  const polishingStart = now();
479
487
  const pollIntervalMs = 2_000;
480
488
  const maxWaitMs = 180_000;
481
- log(chalk.dim('Response still in_progress; polling until completion...'));
489
+ log(chalk.dim("Response still in_progress; polling until completion..."));
482
490
  // Short polling loop — we don't want to hang forever, just catch late finalization.
483
491
  while (now() - polishingStart < maxWaitMs) {
484
492
  throwIfTimedOut();
485
493
  await wait(pollIntervalMs);
486
494
  const refreshed = await clientInstance.responses.retrieve(response.id);
487
- if (refreshed.status === 'completed') {
495
+ if (refreshed.status === "completed") {
488
496
  response = refreshed;
489
497
  break;
490
498
  }
491
499
  }
492
500
  }
493
- if (response.status !== 'completed') {
501
+ if (response.status !== "completed") {
494
502
  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}` : ''}).`));
503
+ log(chalk.yellow(`API ended the run early (status=${response.status}${response.incomplete_details?.reason ? `, reason=${response.incomplete_details.reason}` : ""}).`));
496
504
  throw new OracleResponseError(`Response did not complete: ${detail}`, response);
497
505
  }
498
506
  }
@@ -509,16 +517,16 @@ export async function runOracle(options, deps = {}) {
509
517
  ? renderPlain || !richTty
510
518
  ? answerText
511
519
  : renderMarkdownAnsi(answerText)
512
- : chalk.dim('(no text output)');
520
+ : chalk.dim("(no text output)");
513
521
  sinkWrite(printable);
514
- if (!printable.endsWith('\n')) {
515
- sinkWrite('\n');
522
+ if (!printable.endsWith("\n")) {
523
+ sinkWrite("\n");
516
524
  }
517
525
  stdoutWrite(printable);
518
- if (!printable.endsWith('\n')) {
519
- stdoutWrite('\n');
526
+ if (!printable.endsWith("\n")) {
527
+ stdoutWrite("\n");
520
528
  }
521
- log('');
529
+ log("");
522
530
  }
523
531
  }
524
532
  const usage = response.usage ?? {};
@@ -530,17 +538,21 @@ export async function runOracle(options, deps = {}) {
530
538
  const cost = pricing
531
539
  ? estimateUsdCost({
532
540
  usage: { inputTokens, outputTokens, reasoningTokens, totalTokens },
533
- pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
541
+ pricing: {
542
+ inputUsdPerToken: pricing.inputPerToken,
543
+ outputUsdPerToken: pricing.outputPerToken,
544
+ },
534
545
  })?.totalUsd
535
546
  : undefined;
536
547
  const effortLabel = modelConfig.reasoning?.effort;
537
548
  const modelLabel = effortLabel ? `${modelConfig.model}[${effortLabel}]` : modelConfig.model;
538
- 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());
539
551
  const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]
540
552
  .map((value, index) => formatTokenValue(value, usage, index))
541
- .join('/');
553
+ .join("/");
542
554
  const tokensPart = (() => {
543
- const parts = tokensDisplay.split('/');
555
+ const parts = tokensDisplay.split("/");
544
556
  if (parts.length !== 4)
545
557
  return tokensDisplay;
546
558
  return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
@@ -553,7 +565,11 @@ export async function runOracle(options, deps = {}) {
553
565
  if (actualInput === undefined)
554
566
  return null;
555
567
  const delta = actualInput - estimatedInputTokens;
556
- 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()})`;
557
573
  return `est→actual=${estimatedInputTokens.toLocaleString()}→${actualInput.toLocaleString()}${deltaText}`;
558
574
  })();
559
575
  const { line1, line2 } = formatFinishLine({
@@ -564,43 +580,49 @@ export async function runOracle(options, deps = {}) {
564
580
  summaryExtraParts: options.sessionId ? [`sid=${options.sessionId}`] : null,
565
581
  detailParts: [
566
582
  estActualPart,
567
- !searchEnabled ? 'search=off' : null,
583
+ !searchEnabled ? "search=off" : null,
568
584
  files.length > 0 ? `files=${files.length}` : null,
569
585
  ],
570
586
  });
571
587
  if (!options.silent) {
572
- log('');
588
+ log("");
573
589
  }
574
590
  log(chalk.blue(line1));
575
591
  if (line2) {
576
592
  log(dim(line2));
577
593
  }
578
594
  return {
579
- mode: 'live',
595
+ mode: "live",
580
596
  response,
581
- usage: { inputTokens, outputTokens, reasoningTokens, totalTokens, ...(cost != null ? { cost } : {}) },
597
+ usage: {
598
+ inputTokens,
599
+ outputTokens,
600
+ reasoningTokens,
601
+ totalTokens,
602
+ ...(cost != null ? { cost } : {}),
603
+ },
582
604
  elapsedMs,
583
605
  };
584
606
  }
585
607
  export function extractTextOutput(response) {
586
608
  if (Array.isArray(response.output_text) && response.output_text.length > 0) {
587
- return response.output_text.join('\n');
609
+ return response.output_text.join("\n");
588
610
  }
589
611
  if (Array.isArray(response.output)) {
590
612
  const segments = [];
591
613
  for (const item of response.output) {
592
614
  if (Array.isArray(item.content)) {
593
615
  for (const chunk of item.content) {
594
- if (chunk && (chunk.type === 'output_text' || chunk.type === 'text') && chunk.text) {
616
+ if (chunk && (chunk.type === "output_text" || chunk.type === "text") && chunk.text) {
595
617
  segments.push(chunk.text);
596
618
  }
597
619
  }
598
620
  }
599
- else if (typeof item.text === 'string') {
621
+ else if (typeof item.text === "string") {
600
622
  segments.push(item.text);
601
623
  }
602
624
  }
603
- return segments.join('\n');
625
+ return segments.join("\n");
604
626
  }
605
- return '';
627
+ return "";
606
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();