@steipete/oracle 0.11.1 → 0.12.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 (47) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/modelSelection.js +53 -15
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +75 -18
  6. package/dist/src/browser/actions/thinkingTime.js +23 -8
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +41 -7
  9. package/dist/src/browser/manualLoginProfile.js +54 -0
  10. package/dist/src/browser/projectSourcesRunner.js +16 -5
  11. package/dist/src/browser/prompt.js +56 -37
  12. package/dist/src/browser/sessionRunner.js +72 -1
  13. package/dist/src/browser/utils.js +1 -47
  14. package/dist/src/browser/zipBundle.js +152 -0
  15. package/dist/src/cli/browserConfig.js +13 -11
  16. package/dist/src/cli/browserDefaults.js +2 -1
  17. package/dist/src/cli/docsCheck.js +186 -0
  18. package/dist/src/cli/engine.js +11 -4
  19. package/dist/src/cli/options.js +12 -6
  20. package/dist/src/cli/perfTrace.js +242 -0
  21. package/dist/src/cli/promptRequirement.js +2 -0
  22. package/dist/src/cli/providerDoctor.js +85 -0
  23. package/dist/src/cli/runOptions.js +46 -16
  24. package/dist/src/cli/sessionDisplay.js +39 -4
  25. package/dist/src/cli/sessionLifecycle.js +38 -0
  26. package/dist/src/cli/sessionRunner.js +228 -3
  27. package/dist/src/cli/sessionTable.js +2 -1
  28. package/dist/src/duration.js +47 -0
  29. package/dist/src/mcp/tools/consult.js +19 -3
  30. package/dist/src/mcp/types.js +1 -0
  31. package/dist/src/mcp/utils.js +4 -1
  32. package/dist/src/oracle/baseUrl.js +17 -0
  33. package/dist/src/oracle/client.js +1 -22
  34. package/dist/src/oracle/config.js +17 -4
  35. package/dist/src/oracle/gemini.js +2 -22
  36. package/dist/src/oracle/geminiModels.js +21 -0
  37. package/dist/src/oracle/modelResolver.js +7 -1
  38. package/dist/src/oracle/multiModelRunner.js +20 -2
  39. package/dist/src/oracle/providerFailures.js +204 -0
  40. package/dist/src/oracle/providerRoutePlan.js +281 -0
  41. package/dist/src/oracle/providerRouting.js +92 -0
  42. package/dist/src/oracle/run.js +157 -54
  43. package/dist/src/oracle.js +1 -0
  44. package/dist/src/remote/client.js +8 -0
  45. package/dist/src/remote/server.js +26 -0
  46. package/dist/src/sessionManager.js +5 -1
  47. package/package.json +3 -1
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { performance } from "node:perf_hooks";
7
- import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from "./config.js";
7
+ import { DEFAULT_SYSTEM_PROMPT, TOKENIZER_OPTIONS } from "./config.js";
8
8
  import { readFiles } from "./files.js";
9
9
  import { buildPrompt, buildRequestBody } from "./request.js";
10
10
  import { estimateRequestTokens } from "./tokenEstimate.js";
@@ -12,7 +12,8 @@ import { formatElapsed } from "./format.js";
12
12
  import { formatFinishLine } from "./finishLine.js";
13
13
  import { getFileTokenStats, printFileTokenStats } from "./tokenStats.js";
14
14
  import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from "./errors.js";
15
- import { createDefaultClientFactory, isCustomBaseUrl } from "./client.js";
15
+ import { isCustomBaseUrl } from "./baseUrl.js";
16
+ import { createDefaultClientFactory } from "./client.js";
16
17
  import { formatBaseUrlForLog, maskApiKey } from "./logging.js";
17
18
  import { startHeartbeat } from "../heartbeat.js";
18
19
  import { startOscProgress } from "./oscProgress.js";
@@ -24,15 +25,61 @@ import { createMarkdownStreamer } from "markdansi";
24
25
  import { executeBackgroundResponse } from "./background.js";
25
26
  import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from "./runUtils.js";
26
27
  import { estimateUsdCost } from "tokentally";
27
- import { defaultOpenRouterBaseUrl, isKnownModel, isOpenRouterBaseUrl, isProModel, resolveModelConfig, normalizeOpenRouterBaseUrl, } from "./modelResolver.js";
28
+ import { defaultOpenRouterBaseUrl, isOpenRouterBaseUrl, isProModel, resolveModelConfig, normalizeOpenRouterBaseUrl, } from "./modelResolver.js";
29
+ import { validateProviderRouting } from "./providerRouting.js";
28
30
  const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
29
31
  const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
30
32
  // Default timeout for non-pro API runs (fast models) — give them up to 120s.
31
33
  const DEFAULT_TIMEOUT_NON_PRO_MS = 120_000;
32
34
  const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
35
+ const DEFAULT_PROVIDER_HOSTS = {
36
+ anthropic: "api.anthropic.com",
37
+ google: "generativelanguage.googleapis.com",
38
+ openai: "api.openai.com",
39
+ xai: "api.x.ai",
40
+ };
33
41
  const defaultWait = (ms) => new Promise((resolve) => {
34
42
  setTimeout(resolve, ms);
35
43
  });
44
+ function formatRouteTargetForLog(raw, fallbackHost = "") {
45
+ if (!raw)
46
+ return fallbackHost;
47
+ try {
48
+ const parsed = new URL(raw);
49
+ const segments = parsed.pathname.split("/").filter(Boolean);
50
+ let path = "";
51
+ if (segments.length > 0) {
52
+ path = `/${segments[0]}`;
53
+ if (segments.length > 1) {
54
+ path += "/...";
55
+ }
56
+ }
57
+ return `${parsed.host}${path}`;
58
+ }
59
+ catch {
60
+ const formatted = formatBaseUrlForLog(raw).replace(/^https?:\/\//u, "");
61
+ return formatted || fallbackHost;
62
+ }
63
+ }
64
+ function formatProviderRouteLogLine({ provider, baseUrl, openRouterFallback, isAzureOpenAI, azureEndpoint, azureDeploymentName, envVar, }) {
65
+ if (isAzureOpenAI) {
66
+ return `Provider: Azure OpenAI | endpoint: ${formatRouteTargetForLog(azureEndpoint)} | deployment: ${azureDeploymentName || "none"} | key: ${envVar}`;
67
+ }
68
+ const isOpenRouter = isOpenRouterBaseUrl(baseUrl) || openRouterFallback;
69
+ const routeProvider = isOpenRouter
70
+ ? "OpenRouter"
71
+ : baseUrl && isCustomBaseUrl(baseUrl)
72
+ ? "OpenAI-compatible"
73
+ : provider === "anthropic"
74
+ ? "Anthropic"
75
+ : provider === "google"
76
+ ? "Google Gemini"
77
+ : provider === "xai"
78
+ ? "xAI"
79
+ : "OpenAI";
80
+ const fallbackHost = DEFAULT_PROVIDER_HOSTS[provider] ?? DEFAULT_PROVIDER_HOSTS.openai;
81
+ return `Provider: ${routeProvider} | base: ${formatRouteTargetForLog(baseUrl, fallbackHost)} | key: ${envVar}`;
82
+ }
36
83
  export async function runOracle(options, deps = {}) {
37
84
  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
85
  const stdoutWrite = allowStdout
@@ -42,32 +89,64 @@ export async function runOracle(options, deps = {}) {
42
89
  const resolvedXaiBaseUrl = process.env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
43
90
  const openRouterApiKey = process.env.OPENROUTER_API_KEY?.trim();
44
91
  const defaultOpenRouterBase = defaultOpenRouterBaseUrl();
45
- const knownModelConfig = isKnownModel(options.model) ? MODEL_CONFIGS[options.model] : undefined;
46
- const provider = knownModelConfig?.provider ?? "other";
92
+ const previewMode = resolvePreviewMode(options.previewMode ?? options.preview);
93
+ const isPreview = Boolean(previewMode);
94
+ const providerMode = options.provider ?? "auto";
95
+ const routing = validateProviderRouting({
96
+ model: options.model,
97
+ providerMode,
98
+ azure: options.azure,
99
+ }, {
100
+ onAzureDeploymentMissing: (state) => {
101
+ if (!isPreview && !options.suppressHeader) {
102
+ log(dim(`Provider: Azure OpenAI | endpoint: ${formatRouteTargetForLog(state.azureEndpoint)} | deployment: none | key: ${optionsApiKey ? "apiKey option" : "AZURE_OPENAI_API_KEY|OPENAI_API_KEY"}`));
103
+ }
104
+ },
105
+ });
106
+ const { provider, isAzureOpenAI, azureEndpoint, azureDeploymentName } = routing;
47
107
  const hasOpenAIKey = Boolean(optionsApiKey) ||
48
108
  Boolean(process.env.OPENAI_API_KEY) ||
49
- Boolean(process.env.AZURE_OPENAI_API_KEY && options.azure?.endpoint);
109
+ Boolean(providerMode !== "openai" && process.env.AZURE_OPENAI_API_KEY && options.azure?.endpoint);
50
110
  const hasAnthropicKey = Boolean(optionsApiKey) || Boolean(process.env.ANTHROPIC_API_KEY);
51
111
  const hasGeminiKey = Boolean(optionsApiKey) || Boolean(process.env.GEMINI_API_KEY);
52
112
  const hasXaiKey = Boolean(optionsApiKey) || Boolean(process.env.XAI_API_KEY);
53
113
  let baseUrl = options.baseUrl?.trim();
114
+ const providerQualifiedOpenRouterCandidate = !isAzureOpenAI && providerMode !== "openai" && options.model.includes("/");
115
+ if (baseUrl &&
116
+ providerQualifiedOpenRouterCandidate &&
117
+ !isOpenRouterBaseUrl(baseUrl) &&
118
+ !isCustomBaseUrl(baseUrl)) {
119
+ baseUrl = undefined;
120
+ }
54
121
  if (!baseUrl) {
122
+ let envBaseUrl;
55
123
  if (options.model.startsWith("grok")) {
56
- baseUrl = resolvedXaiBaseUrl;
124
+ envBaseUrl = resolvedXaiBaseUrl;
57
125
  }
58
126
  else if (provider === "anthropic") {
59
- baseUrl = process.env.ANTHROPIC_BASE_URL?.trim();
127
+ envBaseUrl = process.env.ANTHROPIC_BASE_URL?.trim();
60
128
  }
61
129
  else {
62
- baseUrl = process.env.OPENAI_BASE_URL?.trim();
130
+ envBaseUrl = process.env.OPENAI_BASE_URL?.trim();
131
+ }
132
+ if (!providerQualifiedOpenRouterCandidate || (envBaseUrl && isCustomBaseUrl(envBaseUrl))) {
133
+ baseUrl = envBaseUrl;
63
134
  }
64
135
  }
65
- const providerKeyMissing = (provider === "openai" && !hasOpenAIKey) ||
66
- (provider === "anthropic" && !hasAnthropicKey) ||
67
- (provider === "google" && !hasGeminiKey) ||
68
- (provider === "xai" && !hasXaiKey) ||
69
- provider === "other";
70
- const openRouterFallback = providerKeyMissing && Boolean(openRouterApiKey);
136
+ const providerKeyMissing = !isAzureOpenAI &&
137
+ (providerMode === "openai"
138
+ ? !hasOpenAIKey
139
+ : (provider === "openai" && !hasOpenAIKey) ||
140
+ (provider === "anthropic" && !hasAnthropicKey) ||
141
+ (provider === "google" && !hasGeminiKey) ||
142
+ (provider === "xai" && !hasXaiKey) ||
143
+ provider === "other");
144
+ const providerQualifiedOpenRouterRoute = providerQualifiedOpenRouterCandidate && !baseUrl;
145
+ const openRouterFallback = !baseUrl &&
146
+ (providerQualifiedOpenRouterRoute ||
147
+ (providerMode !== "openai" &&
148
+ providerKeyMissing &&
149
+ (provider === "other" || Boolean(openRouterApiKey))));
71
150
  if (!baseUrl || openRouterFallback) {
72
151
  if (openRouterFallback) {
73
152
  baseUrl = defaultOpenRouterBase;
@@ -81,20 +160,24 @@ export async function runOracle(options, deps = {}) {
81
160
  log(dim(`[verbose] ${message}`));
82
161
  }
83
162
  };
84
- const previewMode = resolvePreviewMode(options.previewMode ?? options.preview);
85
- const isPreview = Boolean(previewMode);
86
- const isAzureOpenAI = Boolean(options.azure?.endpoint);
87
163
  const getApiKeyForModel = (model) => {
164
+ if (isAzureOpenAI) {
165
+ if (optionsApiKey)
166
+ return { key: optionsApiKey, source: "apiKey option" };
167
+ const key = process.env.AZURE_OPENAI_API_KEY ?? process.env.OPENAI_API_KEY;
168
+ return { key, source: "AZURE_OPENAI_API_KEY|OPENAI_API_KEY" };
169
+ }
170
+ if (providerMode === "openai") {
171
+ if (optionsApiKey)
172
+ return { key: optionsApiKey, source: "apiKey option" };
173
+ return { key: process.env.OPENAI_API_KEY, source: "OPENAI_API_KEY" };
174
+ }
88
175
  if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback) {
89
176
  return { key: optionsApiKey ?? openRouterApiKey, source: "OPENROUTER_API_KEY" };
90
177
  }
91
178
  if (typeof model === "string" && model.startsWith("gpt")) {
92
179
  if (optionsApiKey)
93
180
  return { key: optionsApiKey, source: "apiKey option" };
94
- if (isAzureOpenAI) {
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" };
97
- }
98
181
  return { key: process.env.OPENAI_API_KEY, source: "OPENAI_API_KEY" };
99
182
  }
100
183
  if (typeof model === "string" && model.startsWith("gemini")) {
@@ -114,19 +197,21 @@ export async function runOracle(options, deps = {}) {
114
197
  const apiKeyResult = getApiKeyForModel(options.model);
115
198
  const apiKey = apiKeyResult.key;
116
199
  if (!apiKey) {
117
- const envVar = isOpenRouterBaseUrl(baseUrl) || openRouterFallback
118
- ? "OPENROUTER_API_KEY"
119
- : options.model.startsWith("gpt")
120
- ? isAzureOpenAI
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";
200
+ const envVar = isAzureOpenAI
201
+ ? "AZURE_OPENAI_API_KEY (or OPENAI_API_KEY)"
202
+ : providerMode === "openai"
203
+ ? "OPENAI_API_KEY"
204
+ : isOpenRouterBaseUrl(baseUrl) || openRouterFallback
205
+ ? "OPENROUTER_API_KEY"
206
+ : options.model.startsWith("gpt")
207
+ ? "OPENAI_API_KEY"
208
+ : options.model.startsWith("gemini")
209
+ ? "GEMINI_API_KEY"
210
+ : options.model.startsWith("claude")
211
+ ? "ANTHROPIC_API_KEY"
212
+ : options.model.startsWith("grok")
213
+ ? "XAI_API_KEY"
214
+ : "OPENROUTER_API_KEY";
130
215
  const browserModeHint = options.model.startsWith("gpt")
131
216
  ? ' 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
217
  : "";
@@ -199,14 +284,17 @@ export async function runOracle(options, deps = {}) {
199
284
  : DEFAULT_TIMEOUT_NON_PRO_MS / 1000
200
285
  : options.timeoutSeconds;
201
286
  const timeoutMs = timeoutSeconds * 1000;
202
- const azureDeploymentName = isAzureOpenAI ? options.azure?.deployment?.trim() : undefined;
287
+ const httpTimeoutMs = typeof options.httpTimeoutMs === "number" &&
288
+ Number.isFinite(options.httpTimeoutMs) &&
289
+ options.httpTimeoutMs > 0
290
+ ? options.httpTimeoutMs
291
+ : timeoutMs;
203
292
  // Track the concrete model id we dispatch to (especially for Gemini preview aliases)
204
- const effectiveModelId = options.effectiveModelId ??
205
- (azureDeploymentName
206
- ? azureDeploymentName
207
- : options.model.startsWith("gemini")
208
- ? resolveGeminiModelId(options.model)
209
- : (modelConfig.apiModel ?? modelConfig.model));
293
+ const effectiveModelId = azureDeploymentName ??
294
+ options.effectiveModelId ??
295
+ (options.model.startsWith("gemini")
296
+ ? resolveGeminiModelId(options.model)
297
+ : (modelConfig.apiModel ?? modelConfig.model));
210
298
  if (!isPreview && options.previousResponseId) {
211
299
  log(dim(`Continuing from response ${options.previousResponseId}`));
212
300
  }
@@ -238,6 +326,15 @@ export async function runOracle(options, deps = {}) {
238
326
  if (!isPreview) {
239
327
  if (!options.suppressHeader) {
240
328
  log(headerLine);
329
+ log(dim(formatProviderRouteLogLine({
330
+ provider,
331
+ baseUrl,
332
+ openRouterFallback,
333
+ isAzureOpenAI,
334
+ azureEndpoint,
335
+ azureDeploymentName,
336
+ envVar,
337
+ })));
241
338
  }
242
339
  const maskedKey = maskApiKey(apiKey);
243
340
  if (maskedKey && options.verbose) {
@@ -249,9 +346,6 @@ export async function runOracle(options, deps = {}) {
249
346
  effectiveModelId === "gpt-5.5-pro") {
250
347
  log(dim(`Note: \`${modelConfig.model}\` is a stable CLI alias; OpenAI API uses \`gpt-5.5-pro\`.`));
251
348
  }
252
- if (baseUrl) {
253
- log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
254
- }
255
349
  if (effectiveModelId !== modelConfig.model) {
256
350
  log(dim(`Resolved model: ${modelConfig.model} → ${effectiveModelId}`));
257
351
  }
@@ -269,6 +363,11 @@ export async function runOracle(options, deps = {}) {
269
363
  if (isLongRunningModel) {
270
364
  log(dim("This model can take up to 60 minutes (usually replies much faster)."));
271
365
  }
366
+ if (options.verbose || isLongRunningModel || httpTimeoutMs < timeoutMs) {
367
+ const timeoutLine = `Timeouts | overall: ${formatElapsed(timeoutMs)} | transport: ${formatElapsed(httpTimeoutMs)}`;
368
+ const timeoutNote = httpTimeoutMs < timeoutMs ? " | note: transport can fail before overall timeout" : "";
369
+ log(dim(`${timeoutLine}${timeoutNote}`));
370
+ }
272
371
  if (options.verbose || isLongRunningModel) {
273
372
  log(dim("Press Ctrl+C to cancel."));
274
373
  }
@@ -300,25 +399,29 @@ export async function runOracle(options, deps = {}) {
300
399
  inputTokenBudget,
301
400
  };
302
401
  }
303
- const proxyCompatibleBaseUrl = baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl)) ? baseUrl : undefined;
304
- const apiEndpoint = modelConfig.model.startsWith("gemini")
305
- ? proxyCompatibleBaseUrl
306
- : proxyCompatibleBaseUrl
402
+ const proxyCompatibleBaseUrl = !isAzureOpenAI && baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl))
403
+ ? baseUrl
404
+ : undefined;
405
+ const apiEndpoint = isAzureOpenAI
406
+ ? undefined
407
+ : modelConfig.model.startsWith("gemini")
307
408
  ? proxyCompatibleBaseUrl
308
- : modelConfig.model.startsWith("claude")
309
- ? (process.env.ANTHROPIC_BASE_URL ?? baseUrl)
310
- : baseUrl;
409
+ : proxyCompatibleBaseUrl
410
+ ? proxyCompatibleBaseUrl
411
+ : modelConfig.model.startsWith("claude")
412
+ ? (process.env.ANTHROPIC_BASE_URL ?? baseUrl)
413
+ : baseUrl;
311
414
  const clientInstance = client ??
312
415
  clientFactory(apiKey, {
313
416
  baseUrl: apiEndpoint,
314
- azure: options.azure,
417
+ azure: isAzureOpenAI ? options.azure : undefined,
315
418
  model: options.model,
316
419
  resolvedModelId: modelConfig.model.startsWith("claude")
317
420
  ? resolveClaudeModelId(effectiveModelId)
318
421
  : modelConfig.model.startsWith("gemini")
319
422
  ? resolveGeminiModelId(effectiveModelId)
320
423
  : effectiveModelId,
321
- httpTimeoutMs: options.httpTimeoutMs,
424
+ httpTimeoutMs,
322
425
  });
323
426
  logVerbose("Dispatching request to API...");
324
427
  if (options.verbose) {
@@ -10,3 +10,4 @@ export { OracleResponseError, OracleTransportError, OracleUserError, FileValidat
10
10
  export { createDefaultClientFactory } from "./oracle/client.js";
11
11
  export { runOracle, extractTextOutput } from "./oracle/run.js";
12
12
  export { resolveGeminiModelId } from "./oracle/gemini.js";
13
+ export { classifyProviderFailure } from "./oracle/providerFailures.js";
@@ -8,10 +8,18 @@ export function createRemoteBrowserExecutor({ host, token }) {
8
8
  const payload = {
9
9
  prompt: options.prompt,
10
10
  attachments: await serializeAttachments(options.attachments ?? []),
11
+ fallbackSubmission: options.fallbackSubmission
12
+ ? {
13
+ prompt: options.fallbackSubmission.prompt,
14
+ attachments: await serializeAttachments(options.fallbackSubmission.attachments ?? []),
15
+ }
16
+ : undefined,
11
17
  browserConfig: options.config ?? {},
12
18
  options: {
13
19
  heartbeatIntervalMs: options.heartbeatIntervalMs,
14
20
  verbose: options.verbose,
21
+ sessionId: options.sessionId,
22
+ followUpPrompts: options.followUpPrompts,
15
23
  },
16
24
  };
17
25
  const body = Buffer.from(JSON.stringify(payload));
@@ -119,6 +119,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
119
119
  res.write(`${JSON.stringify(event)}\n`);
120
120
  };
121
121
  const attachments = [];
122
+ let fallbackSubmission;
122
123
  try {
123
124
  const attachmentsPayload = Array.isArray(payload.attachments) ? payload.attachments : [];
124
125
  for (const [index, attachment] of attachmentsPayload.entries()) {
@@ -131,6 +132,28 @@ export async function createRemoteServer(options = {}, deps = {}) {
131
132
  sizeBytes: attachment.sizeBytes,
132
133
  });
133
134
  }
135
+ if (payload.fallbackSubmission) {
136
+ const fallbackAttachmentDir = path.join(runDir, "fallback-attachments");
137
+ await mkdir(fallbackAttachmentDir, { recursive: true });
138
+ const fallbackAttachments = [];
139
+ const fallbackPayload = Array.isArray(payload.fallbackSubmission.attachments)
140
+ ? payload.fallbackSubmission.attachments
141
+ : [];
142
+ for (const [index, attachment] of fallbackPayload.entries()) {
143
+ const safeName = sanitizeName(attachment.fileName ?? `fallback-attachment-${index + 1}`);
144
+ const filePath = path.join(fallbackAttachmentDir, safeName);
145
+ await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
146
+ fallbackAttachments.push({
147
+ path: filePath,
148
+ displayPath: attachment.displayPath,
149
+ sizeBytes: attachment.sizeBytes,
150
+ });
151
+ }
152
+ fallbackSubmission = {
153
+ prompt: payload.fallbackSubmission.prompt,
154
+ attachments: fallbackAttachments,
155
+ };
156
+ }
134
157
  // Reuse the existing browser logger surface so clients see the same log stream.
135
158
  const automationLogger = ((message) => {
136
159
  if (typeof message === "string") {
@@ -159,10 +182,13 @@ export async function createRemoteServer(options = {}, deps = {}) {
159
182
  const result = await runBrowser({
160
183
  prompt: payload.prompt,
161
184
  attachments,
185
+ fallbackSubmission,
162
186
  config: payload.browserConfig,
163
187
  log: automationLogger,
164
188
  heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
165
189
  verbose: payload.options.verbose,
190
+ sessionId: payload.options.sessionId,
191
+ followUpPrompts: payload.options.followUpPrompts,
166
192
  });
167
193
  sendEvent({ type: "result", result: sanitizeResult(result) });
168
194
  logger(`[serve] Run ${runId} completed in ${Date.now() - runStartedAt}ms`);
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import fs from "node:fs/promises";
3
3
  import { createWriteStream } from "node:fs";
4
4
  import net from "node:net";
5
- import { DEFAULT_MODEL, formatElapsed } from "./oracle.js";
5
+ import { DEFAULT_MODEL } from "./oracle/config.js";
6
+ import { formatElapsed } from "./oracle/format.js";
6
7
  import { safeModelSlug } from "./oracle/modelResolver.js";
7
8
  import { getOracleHomeDir } from "./oracleHome.js";
8
9
  export function getSessionsDir() {
@@ -199,8 +200,10 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
199
200
  browserAttachments: options.browserAttachments,
200
201
  browserInlineFiles: options.browserInlineFiles,
201
202
  browserBundleFiles: options.browserBundleFiles,
203
+ browserBundleFormat: options.browserBundleFormat,
202
204
  background: options.background,
203
205
  search: options.search,
206
+ provider: options.provider,
204
207
  baseUrl: options.baseUrl,
205
208
  azure: options.azure,
206
209
  timeoutSeconds: options.timeoutSeconds,
@@ -208,6 +211,7 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
208
211
  zombieTimeoutMs: options.zombieTimeoutMs,
209
212
  zombieUseLastActivity: options.zombieUseLastActivity,
210
213
  writeOutputPath: options.writeOutputPath,
214
+ partialMode: options.partialMode,
211
215
  waitPreference: options.waitPreference,
212
216
  youtube: options.youtube,
213
217
  generateImage: options.generateImage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/steipete/oracle#readme",
@@ -30,6 +30,7 @@
30
30
  "main": "dist/bin/oracle-cli.js",
31
31
  "scripts": {
32
32
  "docs:list": "tsx scripts/docs-list.ts",
33
+ "docs:check": "node --no-deprecation --import tsx bin/oracle-cli.ts docs check",
33
34
  "docs:site": "node scripts/build-docs-site.mjs",
34
35
  "build": "tsgo -p tsconfig.build.json && pnpm run build:vendor",
35
36
  "build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
@@ -47,6 +48,7 @@
47
48
  "test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
48
49
  "test:mcp:mcporter": "pnpm dlx mcporter list oracle-local --schema --config config/mcporter.json && pnpm dlx mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
49
50
  "test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
51
+ "test:packed-cli": "node scripts/packed-cli-smoke.mjs",
50
52
  "test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
51
53
  "test:live:fast": "ORACLE_LIVE_TEST=1 ORACLE_LIVE_TEST_FAST=1 vitest run tests/live/browser-fast-live.test.ts",
52
54
  "test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",