@steipete/oracle 0.11.1 → 0.12.1
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.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +74 -20
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +76 -18
- package/dist/src/browser/actions/thinkingTime.js +133 -19
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +78 -9
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattachability.js +22 -0
- package/dist/src/browser/sessionRunner.js +73 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -11
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +47 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +272 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +308 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +104 -107
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +43 -23
- package/package.json +15 -12
package/dist/src/oracle/run.js
CHANGED
|
@@ -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,
|
|
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,8 +12,9 @@ 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 {
|
|
16
|
-
import {
|
|
15
|
+
import { isCustomBaseUrl } from "./baseUrl.js";
|
|
16
|
+
import { createDefaultClientFactory } from "./client.js";
|
|
17
|
+
import { maskApiKey } from "./logging.js";
|
|
17
18
|
import { startHeartbeat } from "../heartbeat.js";
|
|
18
19
|
import { startOscProgress } from "./oscProgress.js";
|
|
19
20
|
import { createFsAdapter } from "./fsAdapter.js";
|
|
@@ -24,7 +25,9 @@ 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 {
|
|
28
|
+
import { isOpenRouterBaseUrl, isProModel, resolveModelConfig } from "./modelResolver.js";
|
|
29
|
+
import { validateProviderRouting } from "./providerRouting.js";
|
|
30
|
+
import { formatRouteTargetForLog, resolveProviderRoute, } from "./providerRoutePlan.js";
|
|
28
31
|
const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
|
|
29
32
|
const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
|
|
30
33
|
// Default timeout for non-pro API runs (fast models) — give them up to 120s.
|
|
@@ -33,100 +36,84 @@ const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
|
|
|
33
36
|
const defaultWait = (ms) => new Promise((resolve) => {
|
|
34
37
|
setTimeout(resolve, ms);
|
|
35
38
|
});
|
|
39
|
+
function formatProviderRouteLogLine(route, keySource) {
|
|
40
|
+
if (route.isAzureOpenAI) {
|
|
41
|
+
return `Provider: Azure OpenAI | endpoint: ${formatRouteTargetForLog(route.azureEndpoint)} | deployment: ${route.azureDeploymentName || "none"} | key: ${keySource}`;
|
|
42
|
+
}
|
|
43
|
+
return `Provider: ${route.providerLabel} | base: ${route.base} | key: ${keySource}`;
|
|
44
|
+
}
|
|
45
|
+
function runtimeKeySource({ route, providerMode, optionsApiKey, }) {
|
|
46
|
+
if (optionsApiKey &&
|
|
47
|
+
(route.isAzureOpenAI ||
|
|
48
|
+
providerMode === "openai" ||
|
|
49
|
+
route.provider === "openai" ||
|
|
50
|
+
route.providerLabel === "OpenAI-compatible")) {
|
|
51
|
+
return "apiKey option";
|
|
52
|
+
}
|
|
53
|
+
if (route.isAzureOpenAI) {
|
|
54
|
+
return "AZURE_OPENAI_API_KEY|OPENAI_API_KEY";
|
|
55
|
+
}
|
|
56
|
+
if (providerMode === "openai") {
|
|
57
|
+
return "OPENAI_API_KEY";
|
|
58
|
+
}
|
|
59
|
+
if (isOpenRouterBaseUrl(route.baseUrl) || route.openRouterFallback || route.model.includes("/")) {
|
|
60
|
+
return "OPENROUTER_API_KEY";
|
|
61
|
+
}
|
|
62
|
+
if (route.model.startsWith("gpt"))
|
|
63
|
+
return "OPENAI_API_KEY";
|
|
64
|
+
if (route.model.startsWith("gemini"))
|
|
65
|
+
return "GEMINI_API_KEY";
|
|
66
|
+
if (route.model.startsWith("claude"))
|
|
67
|
+
return "ANTHROPIC_API_KEY";
|
|
68
|
+
if (route.model.startsWith("grok"))
|
|
69
|
+
return "XAI_API_KEY";
|
|
70
|
+
return optionsApiKey ? "apiKey option" : route.keySource;
|
|
71
|
+
}
|
|
36
72
|
export async function runOracle(options, deps = {}) {
|
|
37
73
|
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
74
|
const stdoutWrite = allowStdout
|
|
39
75
|
? (stdoutWriteDep ?? process.stdout.write.bind(process.stdout))
|
|
40
76
|
: () => true;
|
|
41
77
|
const isTty = allowStdout && isStdoutTty;
|
|
42
|
-
const resolvedXaiBaseUrl = process.env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
|
|
43
|
-
const openRouterApiKey = process.env.OPENROUTER_API_KEY?.trim();
|
|
44
|
-
const defaultOpenRouterBase = defaultOpenRouterBaseUrl();
|
|
45
|
-
const knownModelConfig = isKnownModel(options.model) ? MODEL_CONFIGS[options.model] : undefined;
|
|
46
|
-
const provider = knownModelConfig?.provider ?? "other";
|
|
47
|
-
const hasOpenAIKey = Boolean(optionsApiKey) ||
|
|
48
|
-
Boolean(process.env.OPENAI_API_KEY) ||
|
|
49
|
-
Boolean(process.env.AZURE_OPENAI_API_KEY && options.azure?.endpoint);
|
|
50
|
-
const hasAnthropicKey = Boolean(optionsApiKey) || Boolean(process.env.ANTHROPIC_API_KEY);
|
|
51
|
-
const hasGeminiKey = Boolean(optionsApiKey) || Boolean(process.env.GEMINI_API_KEY);
|
|
52
|
-
const hasXaiKey = Boolean(optionsApiKey) || Boolean(process.env.XAI_API_KEY);
|
|
53
|
-
let baseUrl = options.baseUrl?.trim();
|
|
54
|
-
if (!baseUrl) {
|
|
55
|
-
if (options.model.startsWith("grok")) {
|
|
56
|
-
baseUrl = resolvedXaiBaseUrl;
|
|
57
|
-
}
|
|
58
|
-
else if (provider === "anthropic") {
|
|
59
|
-
baseUrl = process.env.ANTHROPIC_BASE_URL?.trim();
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
baseUrl = process.env.OPENAI_BASE_URL?.trim();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
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);
|
|
71
|
-
if (!baseUrl || openRouterFallback) {
|
|
72
|
-
if (openRouterFallback) {
|
|
73
|
-
baseUrl = defaultOpenRouterBase;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (baseUrl && isOpenRouterBaseUrl(baseUrl)) {
|
|
77
|
-
baseUrl = normalizeOpenRouterBaseUrl(baseUrl);
|
|
78
|
-
}
|
|
79
|
-
const logVerbose = (message) => {
|
|
80
|
-
if (options.verbose) {
|
|
81
|
-
log(dim(`[verbose] ${message}`));
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
78
|
const previewMode = resolvePreviewMode(options.previewMode ?? options.preview);
|
|
85
79
|
const isPreview = Boolean(previewMode);
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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" };
|
|
80
|
+
const providerMode = options.provider ?? "auto";
|
|
81
|
+
validateProviderRouting({
|
|
82
|
+
model: options.model,
|
|
83
|
+
providerMode,
|
|
84
|
+
azure: options.azure,
|
|
85
|
+
}, {
|
|
86
|
+
onAzureDeploymentMissing: (state) => {
|
|
87
|
+
if (!isPreview && !options.suppressHeader) {
|
|
88
|
+
log(dim(`Provider: Azure OpenAI | endpoint: ${formatRouteTargetForLog(state.azureEndpoint)} | deployment: none | key: ${optionsApiKey ? "apiKey option" : "AZURE_OPENAI_API_KEY|OPENAI_API_KEY"}`));
|
|
97
89
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const route = resolveProviderRoute({
|
|
93
|
+
model: options.model,
|
|
94
|
+
providerMode,
|
|
95
|
+
azure: options.azure,
|
|
96
|
+
baseUrl: options.baseUrl,
|
|
97
|
+
apiKey: optionsApiKey,
|
|
98
|
+
env: process.env,
|
|
99
|
+
});
|
|
100
|
+
const { isAzureOpenAI, azureDeploymentName } = route;
|
|
101
|
+
const baseUrl = route.baseUrl;
|
|
102
|
+
const openRouterFallback = route.openRouterFallback;
|
|
103
|
+
const logVerbose = (message) => {
|
|
104
|
+
if (options.verbose) {
|
|
105
|
+
log(dim(`[verbose] ${message}`));
|
|
108
106
|
}
|
|
109
|
-
return {
|
|
110
|
-
key: optionsApiKey ?? openRouterApiKey,
|
|
111
|
-
source: optionsApiKey ? "apiKey option" : "OPENROUTER_API_KEY",
|
|
112
|
-
};
|
|
113
107
|
};
|
|
114
|
-
const
|
|
115
|
-
const apiKey = apiKeyResult.key;
|
|
108
|
+
const apiKey = route.apiKey;
|
|
116
109
|
if (!apiKey) {
|
|
117
|
-
const envVar =
|
|
118
|
-
? "
|
|
119
|
-
:
|
|
120
|
-
?
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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";
|
|
110
|
+
const envVar = isAzureOpenAI
|
|
111
|
+
? "AZURE_OPENAI_API_KEY (or OPENAI_API_KEY)"
|
|
112
|
+
: providerMode === "openai"
|
|
113
|
+
? "OPENAI_API_KEY"
|
|
114
|
+
: isOpenRouterBaseUrl(baseUrl) || openRouterFallback
|
|
115
|
+
? "OPENROUTER_API_KEY"
|
|
116
|
+
: route.keySource;
|
|
130
117
|
const browserModeHint = options.model.startsWith("gpt")
|
|
131
118
|
? ' 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
119
|
: "";
|
|
@@ -134,7 +121,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
134
121
|
env: envVar,
|
|
135
122
|
});
|
|
136
123
|
}
|
|
137
|
-
const envVar =
|
|
124
|
+
const envVar = runtimeKeySource({ route, providerMode, optionsApiKey });
|
|
138
125
|
const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? "10", 10);
|
|
139
126
|
const promptLength = options.prompt?.trim().length ?? 0;
|
|
140
127
|
// Enforce the short-prompt guardrail on pro-tier models because they're costly; cheaper models can run short prompts without blocking.
|
|
@@ -142,7 +129,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
142
129
|
if (isProTierModel && !Number.isNaN(minPromptLength) && promptLength < minPromptLength) {
|
|
143
130
|
throw new PromptValidationError(`Prompt is too short (<${minPromptLength} chars). This was likely accidental; please provide more detail.`, { minPromptLength, promptLength });
|
|
144
131
|
}
|
|
145
|
-
const resolverOpenRouterApiKey = openRouterFallback || isOpenRouterBaseUrl(baseUrl) ?
|
|
132
|
+
const resolverOpenRouterApiKey = openRouterFallback || isOpenRouterBaseUrl(baseUrl) ? apiKey : undefined;
|
|
146
133
|
const modelConfig = await resolveModelConfig(options.model, {
|
|
147
134
|
baseUrl,
|
|
148
135
|
openRouterApiKey: resolverOpenRouterApiKey,
|
|
@@ -199,14 +186,17 @@ export async function runOracle(options, deps = {}) {
|
|
|
199
186
|
: DEFAULT_TIMEOUT_NON_PRO_MS / 1000
|
|
200
187
|
: options.timeoutSeconds;
|
|
201
188
|
const timeoutMs = timeoutSeconds * 1000;
|
|
202
|
-
const
|
|
189
|
+
const httpTimeoutMs = typeof options.httpTimeoutMs === "number" &&
|
|
190
|
+
Number.isFinite(options.httpTimeoutMs) &&
|
|
191
|
+
options.httpTimeoutMs > 0
|
|
192
|
+
? options.httpTimeoutMs
|
|
193
|
+
: timeoutMs;
|
|
203
194
|
// Track the concrete model id we dispatch to (especially for Gemini preview aliases)
|
|
204
|
-
const effectiveModelId =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
: (modelConfig.apiModel ?? modelConfig.model));
|
|
195
|
+
const effectiveModelId = azureDeploymentName ??
|
|
196
|
+
options.effectiveModelId ??
|
|
197
|
+
(options.model.startsWith("gemini")
|
|
198
|
+
? resolveGeminiModelId(options.model)
|
|
199
|
+
: (modelConfig.apiModel ?? modelConfig.model));
|
|
210
200
|
if (!isPreview && options.previousResponseId) {
|
|
211
201
|
log(dim(`Continuing from response ${options.previousResponseId}`));
|
|
212
202
|
}
|
|
@@ -238,6 +228,7 @@ export async function runOracle(options, deps = {}) {
|
|
|
238
228
|
if (!isPreview) {
|
|
239
229
|
if (!options.suppressHeader) {
|
|
240
230
|
log(headerLine);
|
|
231
|
+
log(dim(formatProviderRouteLogLine(route, envVar)));
|
|
241
232
|
}
|
|
242
233
|
const maskedKey = maskApiKey(apiKey);
|
|
243
234
|
if (maskedKey && options.verbose) {
|
|
@@ -249,9 +240,6 @@ export async function runOracle(options, deps = {}) {
|
|
|
249
240
|
effectiveModelId === "gpt-5.5-pro") {
|
|
250
241
|
log(dim(`Note: \`${modelConfig.model}\` is a stable CLI alias; OpenAI API uses \`gpt-5.5-pro\`.`));
|
|
251
242
|
}
|
|
252
|
-
if (baseUrl) {
|
|
253
|
-
log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
|
|
254
|
-
}
|
|
255
243
|
if (effectiveModelId !== modelConfig.model) {
|
|
256
244
|
log(dim(`Resolved model: ${modelConfig.model} → ${effectiveModelId}`));
|
|
257
245
|
}
|
|
@@ -269,6 +257,11 @@ export async function runOracle(options, deps = {}) {
|
|
|
269
257
|
if (isLongRunningModel) {
|
|
270
258
|
log(dim("This model can take up to 60 minutes (usually replies much faster)."));
|
|
271
259
|
}
|
|
260
|
+
if (options.verbose || isLongRunningModel || httpTimeoutMs < timeoutMs) {
|
|
261
|
+
const timeoutLine = `Timeouts | overall: ${formatElapsed(timeoutMs)} | transport: ${formatElapsed(httpTimeoutMs)}`;
|
|
262
|
+
const timeoutNote = httpTimeoutMs < timeoutMs ? " | note: transport can fail before overall timeout" : "";
|
|
263
|
+
log(dim(`${timeoutLine}${timeoutNote}`));
|
|
264
|
+
}
|
|
272
265
|
if (options.verbose || isLongRunningModel) {
|
|
273
266
|
log(dim("Press Ctrl+C to cancel."));
|
|
274
267
|
}
|
|
@@ -300,25 +293,29 @@ export async function runOracle(options, deps = {}) {
|
|
|
300
293
|
inputTokenBudget,
|
|
301
294
|
};
|
|
302
295
|
}
|
|
303
|
-
const proxyCompatibleBaseUrl = baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl))
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
296
|
+
const proxyCompatibleBaseUrl = !isAzureOpenAI && baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl))
|
|
297
|
+
? baseUrl
|
|
298
|
+
: undefined;
|
|
299
|
+
const apiEndpoint = isAzureOpenAI
|
|
300
|
+
? undefined
|
|
301
|
+
: modelConfig.model.startsWith("gemini")
|
|
307
302
|
? proxyCompatibleBaseUrl
|
|
308
|
-
:
|
|
309
|
-
?
|
|
310
|
-
:
|
|
303
|
+
: proxyCompatibleBaseUrl
|
|
304
|
+
? proxyCompatibleBaseUrl
|
|
305
|
+
: modelConfig.model.startsWith("claude")
|
|
306
|
+
? baseUrl
|
|
307
|
+
: baseUrl;
|
|
311
308
|
const clientInstance = client ??
|
|
312
309
|
clientFactory(apiKey, {
|
|
313
310
|
baseUrl: apiEndpoint,
|
|
314
|
-
azure: options.azure,
|
|
311
|
+
azure: isAzureOpenAI ? options.azure : undefined,
|
|
315
312
|
model: options.model,
|
|
316
313
|
resolvedModelId: modelConfig.model.startsWith("claude")
|
|
317
314
|
? resolveClaudeModelId(effectiveModelId)
|
|
318
315
|
: modelConfig.model.startsWith("gemini")
|
|
319
316
|
? resolveGeminiModelId(effectiveModelId)
|
|
320
317
|
: effectiveModelId,
|
|
321
|
-
httpTimeoutMs
|
|
318
|
+
httpTimeoutMs,
|
|
322
319
|
});
|
|
323
320
|
logVerbose("Dispatching request to API...");
|
|
324
321
|
if (options.verbose) {
|
package/dist/src/oracle.js
CHANGED
|
@@ -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`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { createWriteStream, mkdirSync } from "node:fs";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { DEFAULT_MODEL
|
|
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() {
|
|
@@ -85,14 +86,26 @@ async function fileExists(targetPath) {
|
|
|
85
86
|
return false;
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
+
function isFileExistsError(error) {
|
|
90
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
91
|
+
}
|
|
92
|
+
async function reserveUniqueSessionDir(baseSlug) {
|
|
89
93
|
let candidate = baseSlug;
|
|
90
94
|
let suffix = 2;
|
|
91
|
-
|
|
95
|
+
for (;;) {
|
|
96
|
+
const dir = sessionDir(candidate);
|
|
97
|
+
try {
|
|
98
|
+
await fs.mkdir(dir, { recursive: false });
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!isFileExistsError(error)) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
92
106
|
candidate = `${baseSlug}-${suffix}`;
|
|
93
107
|
suffix += 1;
|
|
94
108
|
}
|
|
95
|
-
return candidate;
|
|
96
109
|
}
|
|
97
110
|
async function listModelRunFiles(sessionId) {
|
|
98
111
|
const dir = modelsDir(sessionId);
|
|
@@ -152,9 +165,7 @@ export async function readModelRunMetadata(sessionId, model) {
|
|
|
152
165
|
export async function initializeSession(options, cwd, notifications, baseSlugOverride) {
|
|
153
166
|
await ensureSessionStorage();
|
|
154
167
|
const baseSlug = baseSlugOverride || createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
|
|
155
|
-
const sessionId = await
|
|
156
|
-
const dir = sessionDir(sessionId);
|
|
157
|
-
await ensureDir(dir);
|
|
168
|
+
const sessionId = await reserveUniqueSessionDir(baseSlug);
|
|
158
169
|
const mode = options.mode ?? "api";
|
|
159
170
|
const browserConfig = options.browserConfig;
|
|
160
171
|
const modelList = Array.isArray(options.models) && options.models.length > 0
|
|
@@ -199,8 +210,10 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
199
210
|
browserAttachments: options.browserAttachments,
|
|
200
211
|
browserInlineFiles: options.browserInlineFiles,
|
|
201
212
|
browserBundleFiles: options.browserBundleFiles,
|
|
213
|
+
browserBundleFormat: options.browserBundleFormat,
|
|
202
214
|
background: options.background,
|
|
203
215
|
search: options.search,
|
|
216
|
+
provider: options.provider,
|
|
204
217
|
baseUrl: options.baseUrl,
|
|
205
218
|
azure: options.azure,
|
|
206
219
|
timeoutSeconds: options.timeoutSeconds,
|
|
@@ -208,6 +221,7 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
208
221
|
zombieTimeoutMs: options.zombieTimeoutMs,
|
|
209
222
|
zombieUseLastActivity: options.zombieUseLastActivity,
|
|
210
223
|
writeOutputPath: options.writeOutputPath,
|
|
224
|
+
partialMode: options.partialMode,
|
|
211
225
|
waitPreference: options.waitPreference,
|
|
212
226
|
youtube: options.youtube,
|
|
213
227
|
generateImage: options.generateImage,
|
|
@@ -235,25 +249,25 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
235
249
|
return metadata;
|
|
236
250
|
}
|
|
237
251
|
export async function readSessionMetadata(sessionId) {
|
|
238
|
-
const modern = await readModernSessionMetadata(sessionId);
|
|
252
|
+
const modern = await readModernSessionMetadata(sessionId, { reconcile: true, persist: false });
|
|
239
253
|
if (modern) {
|
|
240
254
|
return modern;
|
|
241
255
|
}
|
|
242
|
-
const legacy = await readLegacySessionMetadata(sessionId);
|
|
256
|
+
const legacy = await readLegacySessionMetadata(sessionId, { reconcile: true, persist: false });
|
|
243
257
|
if (legacy) {
|
|
244
258
|
return legacy;
|
|
245
259
|
}
|
|
246
260
|
return null;
|
|
247
261
|
}
|
|
248
262
|
export async function updateSessionMetadata(sessionId, updates) {
|
|
249
|
-
const existing = (await readModernSessionMetadata(sessionId)) ??
|
|
250
|
-
(await readLegacySessionMetadata(sessionId)) ??
|
|
263
|
+
const existing = (await readModernSessionMetadata(sessionId, { reconcile: false, persist: false })) ??
|
|
264
|
+
(await readLegacySessionMetadata(sessionId, { reconcile: false, persist: false })) ??
|
|
251
265
|
{ id: sessionId };
|
|
252
266
|
const next = { ...existing, ...updates };
|
|
253
267
|
await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), "utf8");
|
|
254
268
|
return next;
|
|
255
269
|
}
|
|
256
|
-
async function readModernSessionMetadata(sessionId) {
|
|
270
|
+
async function readModernSessionMetadata(sessionId, options) {
|
|
257
271
|
try {
|
|
258
272
|
const raw = await fs.readFile(metaPath(sessionId), "utf8");
|
|
259
273
|
const parsed = JSON.parse(raw);
|
|
@@ -261,25 +275,31 @@ async function readModernSessionMetadata(sessionId) {
|
|
|
261
275
|
return null;
|
|
262
276
|
}
|
|
263
277
|
const enriched = await attachModelRuns(parsed, sessionId);
|
|
264
|
-
|
|
265
|
-
return await markZombie(runtimeChecked, { persist: false });
|
|
278
|
+
return options.reconcile ? reconcileSessionMetadata(enriched, options) : enriched;
|
|
266
279
|
}
|
|
267
280
|
catch {
|
|
268
281
|
return null;
|
|
269
282
|
}
|
|
270
283
|
}
|
|
271
|
-
async function readLegacySessionMetadata(sessionId) {
|
|
284
|
+
async function readLegacySessionMetadata(sessionId, options) {
|
|
272
285
|
try {
|
|
273
286
|
const raw = await fs.readFile(legacySessionPath(sessionId), "utf8");
|
|
274
287
|
const parsed = JSON.parse(raw);
|
|
275
288
|
const enriched = await attachModelRuns(parsed, sessionId);
|
|
276
|
-
|
|
277
|
-
return await markZombie(runtimeChecked, { persist: false });
|
|
289
|
+
return options.reconcile ? reconcileSessionMetadata(enriched, options) : enriched;
|
|
278
290
|
}
|
|
279
291
|
catch {
|
|
280
292
|
return null;
|
|
281
293
|
}
|
|
282
294
|
}
|
|
295
|
+
async function readRawSessionMetadata(sessionId) {
|
|
296
|
+
return ((await readModernSessionMetadata(sessionId, { reconcile: false, persist: false })) ??
|
|
297
|
+
(await readLegacySessionMetadata(sessionId, { reconcile: false, persist: false })));
|
|
298
|
+
}
|
|
299
|
+
async function reconcileSessionMetadata(meta, { persist }) {
|
|
300
|
+
const runtimeChecked = await markDeadBrowser(meta, { persist });
|
|
301
|
+
return await markZombie(runtimeChecked, { persist });
|
|
302
|
+
}
|
|
283
303
|
function isSessionMetadataRecord(value) {
|
|
284
304
|
return Boolean(value && typeof value.id === "string" && value.status);
|
|
285
305
|
}
|
|
@@ -293,7 +313,7 @@ async function attachModelRuns(meta, sessionId) {
|
|
|
293
313
|
export function createSessionLogWriter(sessionId, model) {
|
|
294
314
|
const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
|
|
295
315
|
if (model) {
|
|
296
|
-
|
|
316
|
+
mkdirSync(modelsDir(sessionId), { recursive: true });
|
|
297
317
|
}
|
|
298
318
|
const stream = createWriteStream(targetPath, { flags: "a" });
|
|
299
319
|
const logLine = (line = "") => {
|
|
@@ -310,10 +330,10 @@ export async function listSessionsMetadata() {
|
|
|
310
330
|
const entries = await fs.readdir(getSessionsDir()).catch(() => []);
|
|
311
331
|
const metas = [];
|
|
312
332
|
for (const entry of entries) {
|
|
313
|
-
let meta = await
|
|
333
|
+
let meta = await readRawSessionMetadata(entry);
|
|
314
334
|
if (meta) {
|
|
315
|
-
|
|
316
|
-
meta = await
|
|
335
|
+
// Keep stored metadata consistent with status reconciliation done by `oracle status`.
|
|
336
|
+
meta = await reconcileSessionMetadata(meta, { persist: true });
|
|
317
337
|
metas.push(meta);
|
|
318
338
|
}
|
|
319
339
|
}
|
|
@@ -384,7 +404,7 @@ export async function readModelLog(sessionId, model) {
|
|
|
384
404
|
}
|
|
385
405
|
}
|
|
386
406
|
export async function readSessionRequest(sessionId) {
|
|
387
|
-
const modern = await readModernSessionMetadata(sessionId);
|
|
407
|
+
const modern = await readModernSessionMetadata(sessionId, { reconcile: false, persist: false });
|
|
388
408
|
if (modern?.options) {
|
|
389
409
|
return modern.options;
|
|
390
410
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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});}});\"",
|
|
@@ -45,8 +46,9 @@
|
|
|
45
46
|
"test": "vitest run",
|
|
46
47
|
"test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
|
|
47
48
|
"test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
|
|
48
|
-
"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
|
+
"test:mcp:mcporter": "CHROME_DEVTOOLS_URL=http://127.0.0.1:0 pnpm dlx mcporter list oracle-local --schema --config config/mcporter.json && CHROME_DEVTOOLS_URL=http://127.0.0.1:0 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",
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"@google/genai": "^2.0.1",
|
|
60
62
|
"@google/generative-ai": "^0.24.1",
|
|
61
63
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
|
-
"@steipete/sweet-cookie": "^0.
|
|
64
|
+
"@steipete/sweet-cookie": "^0.3.0",
|
|
63
65
|
"chalk": "^5.6.2",
|
|
64
66
|
"chrome-launcher": "^1.2.1",
|
|
65
67
|
"chrome-remote-interface": "^0.34.0",
|
|
@@ -68,11 +70,11 @@
|
|
|
68
70
|
"dotenv": "^17.4.2",
|
|
69
71
|
"fast-glob": "^3.3.3",
|
|
70
72
|
"gpt-tokenizer": "^3.4.0",
|
|
71
|
-
"inquirer": "13.4.
|
|
73
|
+
"inquirer": "13.4.3",
|
|
72
74
|
"json5": "^2.2.3",
|
|
73
75
|
"kleur": "^4.1.5",
|
|
74
76
|
"markdansi": "0.2.1",
|
|
75
|
-
"openai": "^6.
|
|
77
|
+
"openai": "^6.38.0",
|
|
76
78
|
"osc-progress": "^0.3.0",
|
|
77
79
|
"qs": "^6.15.1",
|
|
78
80
|
"shiki": "^4.0.2",
|
|
@@ -82,17 +84,17 @@
|
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {
|
|
84
86
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
85
|
-
"@types/chrome-remote-interface": "^0.
|
|
87
|
+
"@types/chrome-remote-interface": "^0.34.0",
|
|
86
88
|
"@types/inquirer": "^9.0.9",
|
|
87
89
|
"@types/node": "^25.6.0",
|
|
88
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
89
|
-
"@vitest/coverage-v8": "4.1.
|
|
90
|
-
"devtools-protocol": "0.0.
|
|
90
|
+
"@typescript/native-preview": "7.0.0-dev.20260516.1",
|
|
91
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
92
|
+
"devtools-protocol": "0.0.1629771",
|
|
91
93
|
"es-toolkit": "^1.46.1",
|
|
92
94
|
"esbuild": "^0.28.0",
|
|
93
|
-
"oxfmt": "0.
|
|
95
|
+
"oxfmt": "0.50.0",
|
|
94
96
|
"oxlint": "^1.62.0",
|
|
95
|
-
"puppeteer-core": "^
|
|
97
|
+
"puppeteer-core": "^25.0.2",
|
|
96
98
|
"tsx": "^4.21.0",
|
|
97
99
|
"typescript": "^6.0.3",
|
|
98
100
|
"vitest": "^4.1.5"
|
|
@@ -111,7 +113,8 @@
|
|
|
111
113
|
"packageManager": "pnpm@10.33.2",
|
|
112
114
|
"pnpm": {
|
|
113
115
|
"overrides": {
|
|
114
|
-
"devtools-protocol": "0.0.
|
|
116
|
+
"devtools-protocol": "0.0.1629771",
|
|
117
|
+
"vite": "7.3.2"
|
|
115
118
|
},
|
|
116
119
|
"onlyBuiltDependencies": [
|
|
117
120
|
"esbuild"
|