@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.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +53 -15
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +41 -7
- 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/sessionRunner.js +72 -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 +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -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 +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- 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 +5 -1
- package/package.json +3 -1
|
@@ -5,41 +5,59 @@ import { resolveGeminiModelId } from "../oracle/gemini.js";
|
|
|
5
5
|
import { PromptValidationError } from "../oracle/errors.js";
|
|
6
6
|
import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
|
|
7
7
|
import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
|
|
8
|
+
import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
|
|
8
9
|
export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
|
|
9
|
-
const resolvedEngine = resolveEngineWithConfig({
|
|
10
|
+
const resolvedEngine = resolveEngineWithConfig({
|
|
11
|
+
engine,
|
|
12
|
+
configEngine: userConfig?.engine,
|
|
13
|
+
env,
|
|
14
|
+
});
|
|
10
15
|
const browserRequested = engine === "browser";
|
|
11
16
|
const browserConfigured = userConfig?.engine === "browser";
|
|
17
|
+
const envBrowserConfigured = (env.ORACLE_ENGINE ?? "").trim().toLowerCase() === "browser";
|
|
12
18
|
const requestedModelList = Array.isArray(models) ? models : [];
|
|
13
19
|
const normalizedRequestedModels = requestedModelList
|
|
14
20
|
.map((entry) => normalizeModelOption(entry))
|
|
15
21
|
.filter(Boolean);
|
|
16
22
|
const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const isClaude = resolvedModel.startsWith("claude");
|
|
24
|
-
const isGrok = resolvedModel.startsWith("grok");
|
|
25
|
-
const isGeminiApiOnly = resolvedModel === "gemini-3.1-pro";
|
|
23
|
+
const apiModel = resolveApiModel(cliModelArg);
|
|
24
|
+
const browserModel = normalizeChatGptModelForBrowser(inferModelFromLabel(cliModelArg));
|
|
25
|
+
const isCodex = apiModel.startsWith("gpt-5.1-codex");
|
|
26
|
+
const isClaude = apiModel.startsWith("claude");
|
|
27
|
+
const isGrok = apiModel.startsWith("grok");
|
|
28
|
+
const isGeminiApiOnly = apiModel === "gemini-3.1-pro";
|
|
26
29
|
const engineWasBrowser = resolvedEngine === "browser";
|
|
27
30
|
const allModels = normalizedRequestedModels.length > 0
|
|
28
31
|
? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
|
|
29
|
-
: [
|
|
32
|
+
: [apiModel];
|
|
33
|
+
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
|
|
30
34
|
const includesGeminiApiOnly = allModels.some((m) => m === "gemini-3.1-pro");
|
|
31
35
|
if ((browserRequested || browserConfigured) && includesGeminiApiOnly) {
|
|
32
36
|
throw new PromptValidationError("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.", { engine: "browser", models: allModels });
|
|
33
37
|
}
|
|
34
38
|
const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
|
|
35
|
-
const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) &&
|
|
39
|
+
const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) &&
|
|
40
|
+
browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
|
|
36
41
|
if (hasNonBrowserCompatibleTarget) {
|
|
37
42
|
throw new PromptValidationError("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.", { engine: "browser", models: allModels });
|
|
38
43
|
}
|
|
39
|
-
const
|
|
40
|
-
const
|
|
44
|
+
const azure = resolveAzureOptions(userConfig, env);
|
|
45
|
+
const azureAutoApi = Boolean(azure?.endpoint) &&
|
|
46
|
+
!browserRequested &&
|
|
47
|
+
!browserConfigured &&
|
|
48
|
+
!envBrowserConfigured &&
|
|
49
|
+
allModels.some(isAzureOpenAICandidateModel);
|
|
50
|
+
const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || isGeminiApiOnly || azureAutoApi);
|
|
51
|
+
const fixedEngine = isCodex ||
|
|
52
|
+
isClaude ||
|
|
53
|
+
isGrok ||
|
|
54
|
+
isGeminiApiOnly ||
|
|
55
|
+
azureAutoApi ||
|
|
56
|
+
normalizedRequestedModels.length > 0
|
|
41
57
|
? "api"
|
|
42
58
|
: resolvedEngine;
|
|
59
|
+
// Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
|
|
60
|
+
const resolvedModel = fixedEngine === "browser" ? browserModel : apiModel;
|
|
43
61
|
const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
|
|
44
62
|
? `${prompt.trim()}\n${userConfig.promptSuffix}`
|
|
45
63
|
: prompt;
|
|
@@ -66,11 +84,12 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
66
84
|
filesReport: userConfig?.filesReport,
|
|
67
85
|
background: userConfig?.background,
|
|
68
86
|
baseUrl,
|
|
87
|
+
azure,
|
|
69
88
|
effectiveModelId,
|
|
70
89
|
};
|
|
71
90
|
return { runOptions, resolvedEngine: fixedEngine, engineCoercedToApi };
|
|
72
91
|
}
|
|
73
|
-
function resolveEngineWithConfig({ engine, configEngine, env, }) {
|
|
92
|
+
function resolveEngineWithConfig({ engine, configEngine, apiProviderRequested, env, }) {
|
|
74
93
|
if (engine)
|
|
75
94
|
return engine;
|
|
76
95
|
const envOverride = (env.ORACLE_ENGINE ?? "").trim().toLowerCase();
|
|
@@ -79,7 +98,18 @@ function resolveEngineWithConfig({ engine, configEngine, env, }) {
|
|
|
79
98
|
}
|
|
80
99
|
if (configEngine)
|
|
81
100
|
return configEngine;
|
|
82
|
-
return resolveEngine({ engine: undefined, env });
|
|
101
|
+
return resolveEngine({ engine: undefined, apiProviderRequested, env });
|
|
102
|
+
}
|
|
103
|
+
function resolveAzureOptions(userConfig, env) {
|
|
104
|
+
const endpoint = env.AZURE_OPENAI_ENDPOINT ?? userConfig?.azure?.endpoint;
|
|
105
|
+
if (!endpoint?.trim()) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
endpoint,
|
|
110
|
+
deployment: env.AZURE_OPENAI_DEPLOYMENT ?? userConfig?.azure?.deployment,
|
|
111
|
+
apiVersion: env.AZURE_OPENAI_API_VERSION ?? userConfig?.azure?.apiVersion,
|
|
112
|
+
};
|
|
83
113
|
}
|
|
84
114
|
function resolveEffectiveModelId(model) {
|
|
85
115
|
if (typeof model === "string" && model.startsWith("gemini")) {
|
|
@@ -10,6 +10,7 @@ import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportA
|
|
|
10
10
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
11
11
|
import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
|
|
12
12
|
import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
|
|
13
|
+
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
|
|
13
14
|
const isTty = () => Boolean(process.stdout.isTTY);
|
|
14
15
|
const dim = (text) => (isTty() ? kleur.dim(text) : text);
|
|
15
16
|
export const MAX_RENDER_BYTES = 200_000;
|
|
@@ -219,6 +220,8 @@ export async function attachSession(sessionId, options) {
|
|
|
219
220
|
browser: {
|
|
220
221
|
config: metadata.browser?.config,
|
|
221
222
|
runtime,
|
|
223
|
+
modelSelection: metadata.browser?.modelSelection,
|
|
224
|
+
warnings: metadata.browser?.warnings,
|
|
222
225
|
},
|
|
223
226
|
artifacts,
|
|
224
227
|
response: { status: "completed" },
|
|
@@ -266,6 +269,11 @@ export async function attachSession(sessionId, options) {
|
|
|
266
269
|
}
|
|
267
270
|
console.log(`Created: ${metadata.createdAt}`);
|
|
268
271
|
console.log(`Status: ${metadata.status}`);
|
|
272
|
+
if (metadata.lifecycle) {
|
|
273
|
+
const attached = metadata.lifecycle.attached ? "attached" : "detached";
|
|
274
|
+
console.log(`Execution: ${formatSessionExecutionLabel(metadata)} (${attached})`);
|
|
275
|
+
console.log(`Reattach: ${metadata.lifecycle.reattachCommand}`);
|
|
276
|
+
}
|
|
269
277
|
if (metadata.models && metadata.models.length > 0) {
|
|
270
278
|
console.log("Models:");
|
|
271
279
|
for (const run of metadata.models) {
|
|
@@ -278,6 +286,13 @@ export async function attachSession(sessionId, options) {
|
|
|
278
286
|
else if (metadata.model) {
|
|
279
287
|
console.log(`Model: ${metadata.model}`);
|
|
280
288
|
}
|
|
289
|
+
const browserEvidence = formatBrowserEvidence(metadata);
|
|
290
|
+
if (browserEvidence) {
|
|
291
|
+
console.log("Browser evidence:");
|
|
292
|
+
for (const line of browserEvidence) {
|
|
293
|
+
console.log(dim(`- ${line}`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
281
296
|
if (metadata.artifacts && metadata.artifacts.length > 0) {
|
|
282
297
|
console.log("Artifacts:");
|
|
283
298
|
for (const artifact of metadata.artifacts) {
|
|
@@ -299,7 +314,7 @@ export async function attachSession(sessionId, options) {
|
|
|
299
314
|
console.log(dim(`User error: ${userErrorSummary}`));
|
|
300
315
|
}
|
|
301
316
|
}
|
|
302
|
-
const shouldTrimIntro = initialStatus === "completed" || initialStatus === "error";
|
|
317
|
+
const shouldTrimIntro = initialStatus === "completed" || initialStatus === "partial" || initialStatus === "error";
|
|
303
318
|
if (options?.renderPrompt !== false) {
|
|
304
319
|
const prompt = await readStoredPrompt(sessionId);
|
|
305
320
|
if (prompt) {
|
|
@@ -422,7 +437,7 @@ export async function attachSession(sessionId, options) {
|
|
|
422
437
|
if (!latest) {
|
|
423
438
|
break;
|
|
424
439
|
}
|
|
425
|
-
if (latest.status === "completed" || latest.status === "error") {
|
|
440
|
+
if (latest.status === "completed" || latest.status === "partial" || latest.status === "error") {
|
|
426
441
|
await printNew();
|
|
427
442
|
flushRemainder();
|
|
428
443
|
if (!options?.suppressMetadata) {
|
|
@@ -430,10 +445,11 @@ export async function attachSession(sessionId, options) {
|
|
|
430
445
|
console.log("\nResult:");
|
|
431
446
|
console.log(`Session failed: ${latest.errorMessage}`);
|
|
432
447
|
}
|
|
433
|
-
if (latest.status === "completed" && latest.usage) {
|
|
448
|
+
if ((latest.status === "completed" || latest.status === "partial") && latest.usage) {
|
|
434
449
|
const summary = formatCompletionSummary(latest, { includeSlug: true });
|
|
435
450
|
if (summary) {
|
|
436
|
-
|
|
451
|
+
const color = latest.status === "partial" ? chalk.yellow.bold : chalk.green.bold;
|
|
452
|
+
console.log(`\n${color(summary)}`);
|
|
437
453
|
}
|
|
438
454
|
else {
|
|
439
455
|
const usage = latest.usage;
|
|
@@ -495,6 +511,25 @@ export function formatUserErrorMetadata(metadata) {
|
|
|
495
511
|
}
|
|
496
512
|
return parts.length > 0 ? parts.join(" | ") : null;
|
|
497
513
|
}
|
|
514
|
+
export function formatBrowserEvidence(metadata) {
|
|
515
|
+
const browser = metadata.browser;
|
|
516
|
+
if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const lines = [];
|
|
520
|
+
const evidence = browser.modelSelection;
|
|
521
|
+
if (evidence) {
|
|
522
|
+
const requested = evidence.requestedModel ?? "(none)";
|
|
523
|
+
const resolved = evidence.resolvedLabel ?? "(unavailable)";
|
|
524
|
+
const strategy = evidence.strategy ?? "(default)";
|
|
525
|
+
const verified = evidence.verified ? "yes" : "no";
|
|
526
|
+
lines.push(`model requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}`);
|
|
527
|
+
}
|
|
528
|
+
for (const warning of browser.warnings ?? []) {
|
|
529
|
+
lines.push(`warning ${warning.code}: ${warning.message}`);
|
|
530
|
+
}
|
|
531
|
+
return lines.length > 0 ? lines : null;
|
|
532
|
+
}
|
|
498
533
|
export function buildReattachLine(metadata) {
|
|
499
534
|
if (!metadata.id) {
|
|
500
535
|
return null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function buildSessionLifecycle({ engine, detached, reattachCommand, }) {
|
|
2
|
+
return {
|
|
3
|
+
engine,
|
|
4
|
+
execution: detached ? "background" : "foreground",
|
|
5
|
+
attached: !detached,
|
|
6
|
+
detached,
|
|
7
|
+
reattachCommand,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function formatSessionLifecycleBlock(meta) {
|
|
11
|
+
const lifecycle = meta.lifecycle;
|
|
12
|
+
if (!lifecycle) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const modelCount = meta.models?.length ?? (meta.model ? 1 : 0);
|
|
16
|
+
const detachValue = lifecycle.detached
|
|
17
|
+
? lifecycle.execution === "background"
|
|
18
|
+
? "yes, polling"
|
|
19
|
+
: "yes"
|
|
20
|
+
: "no";
|
|
21
|
+
const lines = [
|
|
22
|
+
`Session: ${meta.id}`,
|
|
23
|
+
`Mode: ${lifecycle.engine} ${lifecycle.execution}`,
|
|
24
|
+
`Models: ${modelCount > 1 ? `${modelCount} parallel` : String(modelCount || 1)}`,
|
|
25
|
+
`Detach: ${detachValue}`,
|
|
26
|
+
`Reattach: ${lifecycle.reattachCommand}`,
|
|
27
|
+
];
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
export function formatSessionExecutionLabel(meta) {
|
|
31
|
+
const lifecycle = meta.lifecycle;
|
|
32
|
+
if (!lifecycle) {
|
|
33
|
+
return meta.mode ?? meta.options?.mode ?? "api";
|
|
34
|
+
}
|
|
35
|
+
const engine = lifecycle.engine === "browser" ? "br" : lifecycle.engine;
|
|
36
|
+
const execution = lifecycle.execution === "background" ? "bg" : "fg";
|
|
37
|
+
return `${engine}/${execution}`;
|
|
38
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import kleur from "kleur";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from "../oracle.js";
|
|
4
|
+
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
|
|
5
5
|
import { ensureSessionArtifacts, runBrowserSessionExecution, } from "../browser/sessionRunner.js";
|
|
6
6
|
import { renderMarkdownAnsi } from "./markdownRenderer.js";
|
|
7
7
|
import { formatResponseMetadata, formatTransportMetadata } from "./sessionDisplay.js";
|
|
@@ -82,6 +82,8 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
82
82
|
config: browserConfig,
|
|
83
83
|
runtime: result.runtime,
|
|
84
84
|
archive: result.archive,
|
|
85
|
+
modelSelection: result.modelSelection,
|
|
86
|
+
warnings: result.warnings,
|
|
85
87
|
},
|
|
86
88
|
artifacts: mergeArtifacts(sessionMeta.artifacts, result.artifacts),
|
|
87
89
|
response: undefined,
|
|
@@ -245,8 +247,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
245
247
|
});
|
|
246
248
|
log(statusColor(line1));
|
|
247
249
|
const hasFailure = summary.rejected.length > 0;
|
|
250
|
+
const allowPartial = runOptions.partialMode === "ok" && summary.fulfilled.length > 0;
|
|
251
|
+
if (hasFailure) {
|
|
252
|
+
const resultLabel = summary.fulfilled.length > 0 ? "partial success" : "failed";
|
|
253
|
+
log(statusColor(`Multi-model result: ${resultLabel}, ${summary.fulfilled.length}/${multiModels.length} succeeded`));
|
|
254
|
+
}
|
|
248
255
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
249
|
-
status: hasFailure ? "error" : "completed",
|
|
256
|
+
status: hasFailure ? (allowPartial ? "partial" : "error") : "completed",
|
|
250
257
|
completedAt: new Date().toISOString(),
|
|
251
258
|
usage: aggregateUsage,
|
|
252
259
|
elapsedMs: summary.elapsedMs,
|
|
@@ -273,15 +280,52 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
273
280
|
savedOutputs.push({ model: entry.model, path: savedPath });
|
|
274
281
|
}
|
|
275
282
|
}
|
|
283
|
+
const sessionWithRuns = (await readSessionForManifest(sessionMeta.id)) ?? {
|
|
284
|
+
...sessionMeta,
|
|
285
|
+
models: sessionMeta.models,
|
|
286
|
+
};
|
|
287
|
+
const runLogs = await collectMultiModelRunLogs(sessionMeta.id, sessionWithRuns.models, summary);
|
|
288
|
+
const manifestPath = await writeMultiModelOutputManifest({
|
|
289
|
+
baseOutputPath: runOptions.writeOutputPath,
|
|
290
|
+
sessionId: sessionMeta.id,
|
|
291
|
+
status: hasFailure ? (allowPartial ? "partial" : "error") : "completed",
|
|
292
|
+
summary,
|
|
293
|
+
savedOutputs,
|
|
294
|
+
modelRuns: sessionWithRuns.models,
|
|
295
|
+
runLogs,
|
|
296
|
+
runOptions,
|
|
297
|
+
log,
|
|
298
|
+
});
|
|
276
299
|
if (savedOutputs.length > 0) {
|
|
277
300
|
log(dim("Saved outputs:"));
|
|
278
301
|
for (const item of savedOutputs) {
|
|
279
302
|
log(dim(`- ${item.model} -> ${item.path}`));
|
|
280
303
|
}
|
|
281
304
|
}
|
|
305
|
+
if (manifestPath) {
|
|
306
|
+
log(dim(`Output manifest: ${manifestPath}`));
|
|
307
|
+
}
|
|
308
|
+
if (runLogs.length > 0) {
|
|
309
|
+
log(dim(""));
|
|
310
|
+
log(dim("Run logs:"));
|
|
311
|
+
for (const item of runLogs) {
|
|
312
|
+
log(dim(`- ${item.model} -> ${item.path}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
282
315
|
}
|
|
283
316
|
if (hasFailure) {
|
|
284
|
-
|
|
317
|
+
log(dim("Failures:"));
|
|
318
|
+
for (const item of summary.rejected) {
|
|
319
|
+
const providerContext = providerFailureContextForModel(item.model, runOptions);
|
|
320
|
+
log(dim(`- ${item.model}: ${formatMultiModelFailure(item.reason, providerContext)}`));
|
|
321
|
+
for (const line of formatMultiModelFailureDetails(item.reason, providerContext)) {
|
|
322
|
+
log(dim(line));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (hasFailure && !allowPartial) {
|
|
327
|
+
const firstFailure = summary.rejected[0];
|
|
328
|
+
throw sanitizeMultiModelFailureForThrow(firstFailure.reason, providerFailureContextForModel(firstFailure.model, runOptions));
|
|
285
329
|
}
|
|
286
330
|
return;
|
|
287
331
|
}
|
|
@@ -483,6 +527,187 @@ function mergeArtifacts(existing, additions) {
|
|
|
483
527
|
function formatError(error) {
|
|
484
528
|
return error instanceof Error ? error.message : String(error);
|
|
485
529
|
}
|
|
530
|
+
function providerFailureContextForModel(model, runOptions) {
|
|
531
|
+
return {
|
|
532
|
+
model,
|
|
533
|
+
providerMode: runOptions.provider,
|
|
534
|
+
azure: runOptions.azure,
|
|
535
|
+
baseUrl: runOptions.baseUrl,
|
|
536
|
+
apiKey: runOptions.apiKey,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function formatMultiModelFailure(error, context) {
|
|
540
|
+
const userError = asOracleUserError(error);
|
|
541
|
+
if (userError) {
|
|
542
|
+
return `${userError.category}, ${userError.message}`;
|
|
543
|
+
}
|
|
544
|
+
const providerFailure = classifyProviderFailure(error, context);
|
|
545
|
+
if (providerFailure) {
|
|
546
|
+
return providerFailure.label;
|
|
547
|
+
}
|
|
548
|
+
if (error instanceof OracleTransportError) {
|
|
549
|
+
return `${error.reason}, ${error.message}`;
|
|
550
|
+
}
|
|
551
|
+
if (error instanceof OracleResponseError) {
|
|
552
|
+
return error.message;
|
|
553
|
+
}
|
|
554
|
+
return formatError(error);
|
|
555
|
+
}
|
|
556
|
+
function formatMultiModelFailureDetails(error, context) {
|
|
557
|
+
const providerFailure = classifyProviderFailure(error, context);
|
|
558
|
+
if (!providerFailure) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
const lines = [];
|
|
562
|
+
if (providerFailure.keyEnv) {
|
|
563
|
+
lines.push(` key: ${providerFailure.keyEnv}`);
|
|
564
|
+
}
|
|
565
|
+
lines.push(` provider said: ${providerFailure.providerMessage}`);
|
|
566
|
+
lines.push(` fix: ${providerFailure.fix}`);
|
|
567
|
+
return lines;
|
|
568
|
+
}
|
|
569
|
+
function sanitizeMultiModelFailureForThrow(error, context) {
|
|
570
|
+
const providerFailure = classifyProviderFailure(error, context);
|
|
571
|
+
if (!providerFailure) {
|
|
572
|
+
return error;
|
|
573
|
+
}
|
|
574
|
+
const modelPrefix = typeof context === "object" && context?.model ? `${context.model}: ` : "";
|
|
575
|
+
const message = `${modelPrefix}${providerFailure.label}: ${providerFailure.providerMessage}`;
|
|
576
|
+
if (!(error instanceof Error)) {
|
|
577
|
+
return new Error(message);
|
|
578
|
+
}
|
|
579
|
+
error.message = message;
|
|
580
|
+
if (error.stack) {
|
|
581
|
+
const [firstLine, ...rest] = error.stack.split("\n");
|
|
582
|
+
const prefix = firstLine.includes(":") ? firstLine.split(":", 1)[0] : error.name;
|
|
583
|
+
error.stack = [prefix ? `${prefix}: ${message}` : message, ...rest].join("\n");
|
|
584
|
+
}
|
|
585
|
+
return error;
|
|
586
|
+
}
|
|
587
|
+
export function deriveOutputManifestPath(basePath) {
|
|
588
|
+
const ext = path.extname(basePath);
|
|
589
|
+
const stem = path.basename(basePath, ext);
|
|
590
|
+
const dir = path.dirname(basePath);
|
|
591
|
+
return path.join(dir, `${stem}.oracle.json`);
|
|
592
|
+
}
|
|
593
|
+
async function collectMultiModelRunLogs(sessionId, modelRuns, summary) {
|
|
594
|
+
const sessionDir = await resolveSessionDir(sessionId);
|
|
595
|
+
const logsByModel = new Map();
|
|
596
|
+
for (const run of modelRuns ?? []) {
|
|
597
|
+
if (run.log?.path) {
|
|
598
|
+
logsByModel.set(run.model, resolveSessionPath(sessionDir, run.log.path));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const entry of summary.fulfilled) {
|
|
602
|
+
if (!logsByModel.has(entry.model)) {
|
|
603
|
+
logsByModel.set(entry.model, entry.logPath);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return [...logsByModel.entries()].map(([model, logPath]) => ({ model, path: logPath }));
|
|
607
|
+
}
|
|
608
|
+
async function writeMultiModelOutputManifest({ baseOutputPath, sessionId, status, summary, savedOutputs, modelRuns, runLogs, runOptions, log, }) {
|
|
609
|
+
const manifestPath = deriveOutputManifestPath(baseOutputPath);
|
|
610
|
+
const normalizedTarget = path.resolve(manifestPath);
|
|
611
|
+
const normalizedSessionsDir = path.resolve(sessionStore.sessionsDir());
|
|
612
|
+
if (normalizedTarget === normalizedSessionsDir ||
|
|
613
|
+
normalizedTarget.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
614
|
+
log(dim(`output manifest skipped: refusing to write inside session storage (${normalizedSessionsDir}).`));
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
const manifest = buildMultiModelOutputManifest({
|
|
618
|
+
baseOutputPath,
|
|
619
|
+
sessionId,
|
|
620
|
+
status,
|
|
621
|
+
summary,
|
|
622
|
+
savedOutputs,
|
|
623
|
+
modelRuns,
|
|
624
|
+
runLogs,
|
|
625
|
+
runOptions,
|
|
626
|
+
});
|
|
627
|
+
try {
|
|
628
|
+
await fs.mkdir(path.dirname(normalizedTarget), { recursive: true });
|
|
629
|
+
await fs.writeFile(normalizedTarget, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
630
|
+
return normalizedTarget;
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
634
|
+
log(dim(`output manifest failed (${reason}); session completed anyway.`));
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function buildMultiModelOutputManifest({ baseOutputPath, sessionId, status, summary, savedOutputs, modelRuns, runLogs, runOptions, }) {
|
|
639
|
+
const outputByModel = new Map(savedOutputs.map((entry) => [entry.model, entry.path]));
|
|
640
|
+
const logsByModel = new Map(runLogs.map((entry) => [entry.model, entry.path]));
|
|
641
|
+
const runsByModel = new Map((modelRuns ?? []).map((run) => [run.model, run]));
|
|
642
|
+
const fulfilledByModel = new Map(summary.fulfilled.map((entry) => [entry.model, entry]));
|
|
643
|
+
const rejectedByModel = new Map(summary.rejected.map((entry) => [entry.model, entry.reason]));
|
|
644
|
+
const orderedModels = [
|
|
645
|
+
...summary.fulfilled.map((entry) => entry.model),
|
|
646
|
+
...summary.rejected.map((entry) => entry.model),
|
|
647
|
+
];
|
|
648
|
+
return {
|
|
649
|
+
version: 1,
|
|
650
|
+
sessionId,
|
|
651
|
+
status,
|
|
652
|
+
outputBasePath: path.resolve(baseOutputPath),
|
|
653
|
+
createdAt: new Date().toISOString(),
|
|
654
|
+
models: orderedModels.map((model) => {
|
|
655
|
+
const run = runsByModel.get(model);
|
|
656
|
+
const fulfilled = fulfilledByModel.get(model);
|
|
657
|
+
const reason = rejectedByModel.get(model);
|
|
658
|
+
const userError = reason ? asOracleUserError(reason) : undefined;
|
|
659
|
+
const providerFailure = reason
|
|
660
|
+
? classifyProviderFailure(reason, providerFailureContextForModel(model, runOptions))
|
|
661
|
+
: undefined;
|
|
662
|
+
return {
|
|
663
|
+
model,
|
|
664
|
+
status: fulfilled ? "completed" : reason ? "error" : (run?.status ?? "error"),
|
|
665
|
+
outputPath: outputByModel.get(model),
|
|
666
|
+
logPath: logsByModel.get(model),
|
|
667
|
+
errorCategory: run?.error?.category ?? userError?.category ?? providerFailure?.category,
|
|
668
|
+
errorMessage: run?.error?.message ??
|
|
669
|
+
userError?.message ??
|
|
670
|
+
providerFailure?.label ??
|
|
671
|
+
(reason ? formatError(reason) : undefined),
|
|
672
|
+
elapsedMs: calculateModelElapsedMs(run),
|
|
673
|
+
usage: run?.usage ?? fulfilled?.usage,
|
|
674
|
+
};
|
|
675
|
+
}),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function calculateModelElapsedMs(run) {
|
|
679
|
+
if (!run?.startedAt || !run.completedAt) {
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
const startedMs = Date.parse(run.startedAt);
|
|
683
|
+
const completedMs = Date.parse(run.completedAt);
|
|
684
|
+
if (!Number.isFinite(startedMs) || !Number.isFinite(completedMs) || completedMs < startedMs) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
return completedMs - startedMs;
|
|
688
|
+
}
|
|
689
|
+
async function readSessionForManifest(sessionId) {
|
|
690
|
+
try {
|
|
691
|
+
return (await sessionStore.readSession(sessionId)) ?? null;
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function resolveSessionDir(sessionId) {
|
|
698
|
+
try {
|
|
699
|
+
return (await sessionStore.getPaths(sessionId)).dir;
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function resolveSessionPath(sessionDir, targetPath) {
|
|
706
|
+
if (path.isAbsolute(targetPath) || !sessionDir) {
|
|
707
|
+
return targetPath;
|
|
708
|
+
}
|
|
709
|
+
return path.join(sessionDir, targetPath);
|
|
710
|
+
}
|
|
486
711
|
async function writeAssistantOutput(targetPath, content, log) {
|
|
487
712
|
if (!targetPath)
|
|
488
713
|
return;
|
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import kleur from "kleur";
|
|
3
3
|
import { MODEL_CONFIGS } from "../oracle.js";
|
|
4
4
|
import { estimateUsdCost } from "tokentally";
|
|
5
|
+
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
|
|
5
6
|
const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
6
7
|
const dim = (text, rich) => (rich ? kleur.dim(text) : text);
|
|
7
8
|
export const STATUS_PAD = 9;
|
|
@@ -19,7 +20,7 @@ export function formatSessionTableRow(meta, options) {
|
|
|
19
20
|
const status = colorStatus(meta.status ?? "unknown", rich);
|
|
20
21
|
const modelLabel = (meta.model ?? "n/a").padEnd(MODEL_PAD);
|
|
21
22
|
const model = rich ? chalk.white(modelLabel) : modelLabel;
|
|
22
|
-
const modeLabel = (meta
|
|
23
|
+
const modeLabel = formatSessionExecutionLabel(meta).padEnd(MODE_PAD);
|
|
23
24
|
const mode = rich ? chalk.gray(modeLabel) : modeLabel;
|
|
24
25
|
const timestampLabel = formatTimestampAligned(meta.createdAt).padEnd(TIMESTAMP_PAD);
|
|
25
26
|
const timestamp = rich ? chalk.gray(timestampLabel) : timestampLabel;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function parseDuration(input, fallback) {
|
|
2
|
+
if (!input) {
|
|
3
|
+
return fallback;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
const lowercase = trimmed.toLowerCase();
|
|
10
|
+
if (/^[0-9]+$/.test(lowercase)) {
|
|
11
|
+
return Number(lowercase);
|
|
12
|
+
}
|
|
13
|
+
const normalized = lowercase.replace(/\s+/g, "");
|
|
14
|
+
const singleMatch = /^([0-9]+)(ms|s|m|h)$/i.exec(normalized);
|
|
15
|
+
if (singleMatch && singleMatch[0].length === normalized.length) {
|
|
16
|
+
const value = Number(singleMatch[1]);
|
|
17
|
+
return convertUnit(value, singleMatch[2]);
|
|
18
|
+
}
|
|
19
|
+
const multiDuration = /([0-9]+)(ms|h|m|s)/g;
|
|
20
|
+
let total = 0;
|
|
21
|
+
let lastIndex = 0;
|
|
22
|
+
let match = multiDuration.exec(normalized);
|
|
23
|
+
while (match !== null) {
|
|
24
|
+
total += convertUnit(Number(match[1]), match[2]);
|
|
25
|
+
lastIndex = multiDuration.lastIndex;
|
|
26
|
+
match = multiDuration.exec(normalized);
|
|
27
|
+
}
|
|
28
|
+
if (total > 0 && lastIndex === normalized.length) {
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
function convertUnit(value, unitRaw) {
|
|
34
|
+
const unit = unitRaw?.toLowerCase();
|
|
35
|
+
switch (unit) {
|
|
36
|
+
case "ms":
|
|
37
|
+
return value;
|
|
38
|
+
case "s":
|
|
39
|
+
return value * 1000;
|
|
40
|
+
case "m":
|
|
41
|
+
return value * 60_000;
|
|
42
|
+
case "h":
|
|
43
|
+
return value * 3_600_000;
|
|
44
|
+
default:
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -60,6 +60,10 @@ const consultInputShape = {
|
|
|
60
60
|
.boolean()
|
|
61
61
|
.optional()
|
|
62
62
|
.describe("Browser-only: bundle many files into a single upload (helps with upload limits)."),
|
|
63
|
+
browserBundleFormat: z
|
|
64
|
+
.enum(["text", "zip"])
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Browser-only: bundle upload format when browserBundleFiles is true or auto-bundling is needed. Defaults to "text"; "zip" preserves individual file names in one uploaded archive.'),
|
|
63
67
|
browserThinkingTime: z
|
|
64
68
|
.enum(["light", "standard", "extended", "heavy"])
|
|
65
69
|
.optional()
|
|
@@ -140,6 +144,7 @@ const consultDryRunResolvedShape = z.object({
|
|
|
140
144
|
researchMode: z.string().nullable().optional(),
|
|
141
145
|
attachments: z.string().optional(),
|
|
142
146
|
bundleFiles: z.boolean().optional(),
|
|
147
|
+
bundleFormat: z.enum(["text", "zip"]).optional(),
|
|
143
148
|
keepBrowser: z.boolean().optional(),
|
|
144
149
|
manualLogin: z.boolean().optional(),
|
|
145
150
|
profileDir: z.string().nullable().optional(),
|
|
@@ -196,7 +201,9 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
|
|
|
196
201
|
? mapModelToBrowserLabel(runModel)
|
|
197
202
|
: resolveBrowserModelLabel(preferredLabel, runModel);
|
|
198
203
|
const configuredUrl = configuredBrowser.chatgptUrl ?? configuredBrowser.url ?? CHATGPT_URL;
|
|
199
|
-
const manualLogin = hasProfileDir
|
|
204
|
+
const manualLogin = hasProfileDir
|
|
205
|
+
? true
|
|
206
|
+
: (configuredBrowser.manualLogin ?? process.platform === "win32");
|
|
200
207
|
return {
|
|
201
208
|
...configuredBrowser,
|
|
202
209
|
url: configuredUrl,
|
|
@@ -224,6 +231,12 @@ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browser
|
|
|
224
231
|
}
|
|
225
232
|
if (resolvedEngine === "browser") {
|
|
226
233
|
guidance.push("Browser engine uses the signed-in ChatGPT profile; run dryRun:true before live use.");
|
|
234
|
+
if (browserConfig?.manualLogin) {
|
|
235
|
+
const profile = browserConfig.manualLoginProfileDir ?? "~/.oracle/browser-profile";
|
|
236
|
+
guidance.push(`Manual-login browser mode uses Oracle's private Chrome profile at ${profile}, separate from your normal Chrome profile.`);
|
|
237
|
+
guidance.push(`First-time setup: run oracle --engine browser --browser-manual-login --browser-keep-browser --browser-manual-login-profile-dir ${JSON.stringify(profile)} -p "HI", sign into ChatGPT in that window, then retry the consult.`);
|
|
238
|
+
guidance.push("If this profile is not signed in, non-setup MCP/browser runs fail fast instead of waiting for the full browser timeout.");
|
|
239
|
+
}
|
|
227
240
|
}
|
|
228
241
|
const desiredModel = browserConfig?.desiredModel ?? null;
|
|
229
242
|
const thinkingTime = browserConfig?.thinkingTime ?? null;
|
|
@@ -251,6 +264,7 @@ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browser
|
|
|
251
264
|
researchMode: browserConfig?.researchMode ?? null,
|
|
252
265
|
attachments: runOptions.browserAttachments,
|
|
253
266
|
bundleFiles: runOptions.browserBundleFiles,
|
|
267
|
+
bundleFormat: runOptions.browserBundleFormat,
|
|
254
268
|
keepBrowser: browserConfig?.keepBrowser,
|
|
255
269
|
manualLogin: browserConfig?.manualLogin,
|
|
256
270
|
profileDir: browserConfig?.manualLoginProfileDir ?? null,
|
|
@@ -277,6 +291,7 @@ export function formatConsultDryRunResolved(details) {
|
|
|
277
291
|
lines.push(` browser research mode: ${details.browser.researchMode ?? "off"}`);
|
|
278
292
|
lines.push(` browser attachments: ${details.browser.attachments ?? "auto"}`);
|
|
279
293
|
lines.push(` browser bundle files: ${details.browser.bundleFiles ? "yes" : "no"}`);
|
|
294
|
+
lines.push(` browser bundle format: ${details.browser.bundleFormat ?? "text"}`);
|
|
280
295
|
lines.push(` browser keep browser: ${details.browser.keepBrowser ? "yes" : "no"}`);
|
|
281
296
|
lines.push(` browser manual login: ${details.browser.manualLogin ? "yes" : "no"}`);
|
|
282
297
|
if (details.browser.profileDir) {
|
|
@@ -295,7 +310,7 @@ export function formatConsultDryRunResolved(details) {
|
|
|
295
310
|
export function registerConsultTool(server) {
|
|
296
311
|
server.registerTool("consult", {
|
|
297
312
|
title: "Run an oracle session",
|
|
298
|
-
description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. For browser-based image/file uploads, set `browserAttachments:"always"`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
|
|
313
|
+
description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. Browser manual-login uses a private Oracle Chrome profile separate from the user\'s normal Chrome; dry-run output includes first-time setup guidance when that path is active. For browser-based image/file uploads, set `browserAttachments:"always"`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
|
|
299
314
|
// Cast to any to satisfy SDK typings across differing Zod versions.
|
|
300
315
|
inputSchema: consultInputShape,
|
|
301
316
|
outputSchema: consultOutputShape,
|
|
@@ -311,7 +326,7 @@ export function registerConsultTool(server) {
|
|
|
311
326
|
content: textContent(error instanceof Error ? error.message : String(error)),
|
|
312
327
|
};
|
|
313
328
|
}
|
|
314
|
-
const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, dryRun, slug, } = parsedInput;
|
|
329
|
+
const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, dryRun, slug, } = parsedInput;
|
|
315
330
|
const { config: userConfig } = await loadUserConfig();
|
|
316
331
|
const { runOptions, resolvedEngine } = mapConsultToRunOptions({
|
|
317
332
|
prompt,
|
|
@@ -322,6 +337,7 @@ export function registerConsultTool(server) {
|
|
|
322
337
|
search,
|
|
323
338
|
browserAttachments,
|
|
324
339
|
browserBundleFiles,
|
|
340
|
+
browserBundleFormat,
|
|
325
341
|
browserFollowUps,
|
|
326
342
|
userConfig,
|
|
327
343
|
env: process.env,
|