@steipete/oracle 0.11.0 → 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 +56 -11
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/archiveConversation.js +12 -0
- package/dist/src/browser/actions/modelSelection.js +61 -18
- 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/config.js +1 -7
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +65 -48
- 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 -18
- 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 +5 -2
- 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 +8 -6
|
@@ -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,
|
package/dist/src/mcp/types.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const CONSULT_PRESETS = ["chatgpt-pro-heavy"];
|
|
3
|
-
export const consultInputSchema = z
|
|
3
|
+
export const consultInputSchema = z
|
|
4
|
+
.object({
|
|
4
5
|
preset: z.enum(CONSULT_PRESETS).optional(),
|
|
5
6
|
prompt: z.string().min(1, "Prompt is required."),
|
|
6
7
|
files: z.array(z.string()).default([]),
|
|
@@ -10,6 +11,7 @@ export const consultInputSchema = z.object({
|
|
|
10
11
|
browserModelLabel: z.string().optional(),
|
|
11
12
|
browserAttachments: z.enum(["auto", "never", "always"]).optional(),
|
|
12
13
|
browserBundleFiles: z.boolean().optional(),
|
|
14
|
+
browserBundleFormat: z.enum(["text", "zip"]).optional(),
|
|
13
15
|
browserThinkingTime: z.enum(["light", "standard", "extended", "heavy"]).optional(),
|
|
14
16
|
browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
|
|
15
17
|
browserResearchMode: z.enum(["deep"]).optional(),
|
|
@@ -19,7 +21,8 @@ export const consultInputSchema = z.object({
|
|
|
19
21
|
dryRun: z.boolean().optional(),
|
|
20
22
|
search: z.boolean().optional(),
|
|
21
23
|
slug: z.string().optional(),
|
|
22
|
-
})
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
23
26
|
export const sessionsInputSchema = z.object({
|
|
24
27
|
id: z.string().optional(),
|
|
25
28
|
hours: z.number().optional(),
|
package/dist/src/mcp/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveRunOptionsFromConfig } from "../cli/runOptions.js";
|
|
2
2
|
import { Launcher } from "chrome-launcher";
|
|
3
|
-
export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserFollowUps, userConfig, env = process.env, }) {
|
|
3
|
+
export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserBundleFormat, browserFollowUps, userConfig, env = process.env, }) {
|
|
4
4
|
// Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
|
|
5
5
|
// then overlay MCP-only overrides such as explicit search toggles.
|
|
6
6
|
const mergedModels = Array.isArray(models) && models.length > 0
|
|
@@ -24,6 +24,9 @@ export function mapConsultToRunOptions({ prompt, files, model, models, engine, s
|
|
|
24
24
|
if (typeof browserBundleFiles === "boolean") {
|
|
25
25
|
result.runOptions.browserBundleFiles = browserBundleFiles;
|
|
26
26
|
}
|
|
27
|
+
if (browserBundleFormat) {
|
|
28
|
+
result.runOptions.browserBundleFormat = browserBundleFormat;
|
|
29
|
+
}
|
|
27
30
|
if (Array.isArray(browserFollowUps)) {
|
|
28
31
|
result.runOptions.browserFollowUps = browserFollowUps
|
|
29
32
|
.map((entry) => entry.trim())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const NATIVE_API_HOSTS = [
|
|
2
|
+
"api.openai.com",
|
|
3
|
+
"api.anthropic.com",
|
|
4
|
+
"generativelanguage.googleapis.com",
|
|
5
|
+
"api.x.ai",
|
|
6
|
+
];
|
|
7
|
+
export function isCustomBaseUrl(baseUrl) {
|
|
8
|
+
if (!baseUrl)
|
|
9
|
+
return false;
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(baseUrl);
|
|
12
|
+
return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -4,28 +4,7 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { createGeminiClient } from "./gemini.js";
|
|
5
5
|
import { createClaudeClient } from "./claude.js";
|
|
6
6
|
import { isOpenRouterBaseUrl } from "./modelResolver.js";
|
|
7
|
-
|
|
8
|
-
* Known native API base URLs that should still use their dedicated SDKs.
|
|
9
|
-
* Any other custom base URL is treated as an OpenAI-compatible proxy and
|
|
10
|
-
* all models are routed through the chat/completions adapter.
|
|
11
|
-
*/
|
|
12
|
-
const NATIVE_API_HOSTS = [
|
|
13
|
-
"api.openai.com",
|
|
14
|
-
"api.anthropic.com",
|
|
15
|
-
"generativelanguage.googleapis.com",
|
|
16
|
-
"api.x.ai",
|
|
17
|
-
];
|
|
18
|
-
export function isCustomBaseUrl(baseUrl) {
|
|
19
|
-
if (!baseUrl)
|
|
20
|
-
return false;
|
|
21
|
-
try {
|
|
22
|
-
const url = new URL(baseUrl);
|
|
23
|
-
return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
7
|
+
import { isCustomBaseUrl } from "./baseUrl.js";
|
|
29
8
|
export function buildAzureResponsesBaseUrl(endpoint) {
|
|
30
9
|
return `${endpoint.replace(/\/+$/, "")}/openai/v1`;
|
|
31
10
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { countTokens as countTokensGpt5Pro } from "gpt-tokenizer/model/gpt-5-pro";
|
|
3
|
-
import { countTokens as countTokensAnthropicRaw } from "@anthropic-ai/tokenizer";
|
|
1
|
+
import { createRequire } from "node:module";
|
|
4
2
|
import { stringifyTokenizerInput } from "./tokenStringifier.js";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
let countTokensGpt5Impl;
|
|
5
|
+
let countTokensGpt5ProImpl;
|
|
6
|
+
let countTokensAnthropicImpl;
|
|
5
7
|
export const DEFAULT_MODEL = "gpt-5.5-pro";
|
|
6
8
|
export const PRO_MODELS = new Set([
|
|
7
9
|
"gpt-5.5-pro",
|
|
@@ -12,7 +14,18 @@ export const PRO_MODELS = new Set([
|
|
|
12
14
|
"claude-4.6-sonnet",
|
|
13
15
|
"claude-4.1-opus",
|
|
14
16
|
]);
|
|
15
|
-
const
|
|
17
|
+
const countTokensGpt5 = (input, options) => {
|
|
18
|
+
countTokensGpt5Impl ??= require("gpt-tokenizer/model/gpt-5").countTokens;
|
|
19
|
+
return countTokensGpt5Impl(input, options);
|
|
20
|
+
};
|
|
21
|
+
const countTokensGpt5Pro = (input, options) => {
|
|
22
|
+
countTokensGpt5ProImpl ??= require("gpt-tokenizer/model/gpt-5-pro").countTokens;
|
|
23
|
+
return countTokensGpt5ProImpl(input, options);
|
|
24
|
+
};
|
|
25
|
+
const countTokensAnthropic = (input) => {
|
|
26
|
+
countTokensAnthropicImpl ??= require("@anthropic-ai/tokenizer").countTokens;
|
|
27
|
+
return countTokensAnthropicImpl(stringifyTokenizerInput(input));
|
|
28
|
+
};
|
|
16
29
|
export const MODEL_CONFIGS = {
|
|
17
30
|
"gpt-5.5-pro": {
|
|
18
31
|
model: "gpt-5.5-pro",
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
import { GoogleGenAI, HarmCategory, HarmBlockThreshold, } from "@google/genai";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
"gemini-3-pro": "gemini-3-pro-preview",
|
|
5
|
-
"gpt-5.5": "gpt-5.5",
|
|
6
|
-
"gpt-5.5-pro": "gpt-5.5-pro",
|
|
7
|
-
"gpt-5.4": "gpt-5.4",
|
|
8
|
-
"gpt-5.4-pro": "gpt-5.4-pro",
|
|
9
|
-
"gpt-5.1-pro": "gpt-5.1-pro",
|
|
10
|
-
"gpt-5-pro": "gpt-5-pro",
|
|
11
|
-
"gpt-5.1": "gpt-5.1",
|
|
12
|
-
"gpt-5.1-codex": "gpt-5.1-codex",
|
|
13
|
-
"gpt-5.2": "gpt-5.2",
|
|
14
|
-
"gpt-5.2-instant": "gpt-5.2-instant",
|
|
15
|
-
"gpt-5.2-pro": "gpt-5.2-pro",
|
|
16
|
-
"claude-4.6-sonnet": "claude-4.6-sonnet",
|
|
17
|
-
"claude-4.1-opus": "claude-4.1-opus",
|
|
18
|
-
"grok-4.1": "grok-4.1",
|
|
19
|
-
};
|
|
20
|
-
export function resolveGeminiModelId(modelName) {
|
|
21
|
-
// Map our logical Gemini names to the exact model ids expected by the SDK.
|
|
22
|
-
return MODEL_ID_MAP[modelName] ?? modelName;
|
|
23
|
-
}
|
|
2
|
+
import { resolveGeminiModelId } from "./geminiModels.js";
|
|
3
|
+
export { resolveGeminiModelId } from "./geminiModels.js";
|
|
24
4
|
export function createGeminiClient(apiKey, modelName = "gemini-3-pro", resolvedModelId) {
|
|
25
5
|
const modelId = resolvedModelId ?? resolveGeminiModelId(modelName);
|
|
26
6
|
const genAI = new GoogleGenAI({ apiKey });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const MODEL_ID_MAP = {
|
|
2
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview",
|
|
3
|
+
"gemini-3-pro": "gemini-3-pro-preview",
|
|
4
|
+
"gpt-5.5": "gpt-5.5",
|
|
5
|
+
"gpt-5.5-pro": "gpt-5.5-pro",
|
|
6
|
+
"gpt-5.4": "gpt-5.4",
|
|
7
|
+
"gpt-5.4-pro": "gpt-5.4-pro",
|
|
8
|
+
"gpt-5.1-pro": "gpt-5.1-pro",
|
|
9
|
+
"gpt-5-pro": "gpt-5-pro",
|
|
10
|
+
"gpt-5.1": "gpt-5.1",
|
|
11
|
+
"gpt-5.1-codex": "gpt-5.1-codex",
|
|
12
|
+
"gpt-5.2": "gpt-5.2",
|
|
13
|
+
"gpt-5.2-instant": "gpt-5.2-instant",
|
|
14
|
+
"gpt-5.2-pro": "gpt-5.2-pro",
|
|
15
|
+
"claude-4.6-sonnet": "claude-4.6-sonnet",
|
|
16
|
+
"claude-4.1-opus": "claude-4.1-opus",
|
|
17
|
+
"grok-4.1": "grok-4.1",
|
|
18
|
+
};
|
|
19
|
+
export function resolveGeminiModelId(modelName) {
|
|
20
|
+
return MODEL_ID_MAP[modelName] ?? modelName;
|
|
21
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { MODEL_CONFIGS, PRO_MODELS } from "./config.js";
|
|
2
|
-
import { countTokens as countTokensGpt5Pro } from "gpt-tokenizer/model/gpt-5-pro";
|
|
3
3
|
import { pricingFromUsdPerMillion } from "tokentally";
|
|
4
4
|
const OPENROUTER_DEFAULT_BASE = "https://openrouter.ai/api/v1";
|
|
5
5
|
const OPENROUTER_MODELS_ENDPOINT = "https://openrouter.ai/api/v1/models";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
let countTokensGpt5ProImpl;
|
|
8
|
+
const countTokensGpt5Pro = (input, options) => {
|
|
9
|
+
countTokensGpt5ProImpl ??= require("gpt-tokenizer/model/gpt-5-pro").countTokens;
|
|
10
|
+
return countTokensGpt5ProImpl(input, options);
|
|
11
|
+
};
|
|
6
12
|
export function isKnownModel(model) {
|
|
7
13
|
return Object.hasOwn(MODEL_CONFIGS, model);
|
|
8
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from "../oracle.js";
|
|
3
|
+
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
|
|
4
4
|
import { sessionStore } from "../sessionStore.js";
|
|
5
5
|
import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from "osc-progress";
|
|
6
6
|
function forwardOscProgress(chunk, shouldForward) {
|
|
@@ -113,6 +113,13 @@ function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOr
|
|
|
113
113
|
})()
|
|
114
114
|
.catch(async (error) => {
|
|
115
115
|
const userError = asOracleUserError(error);
|
|
116
|
+
const providerFailure = classifyProviderFailure(error, {
|
|
117
|
+
model,
|
|
118
|
+
providerMode: runOptions.provider,
|
|
119
|
+
azure: runOptions.azure,
|
|
120
|
+
baseUrl: runOptions.baseUrl,
|
|
121
|
+
apiKey: runOptions.apiKey,
|
|
122
|
+
});
|
|
116
123
|
const responseMetadata = error instanceof OracleResponseError ? error.metadata : undefined;
|
|
117
124
|
const transportMetadata = error instanceof OracleTransportError ? { reason: error.reason } : undefined;
|
|
118
125
|
await store.updateModelRun(sessionMeta.id, model, {
|
|
@@ -126,7 +133,18 @@ function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOr
|
|
|
126
133
|
message: userError.message,
|
|
127
134
|
details: userError.details,
|
|
128
135
|
}
|
|
129
|
-
:
|
|
136
|
+
: providerFailure
|
|
137
|
+
? {
|
|
138
|
+
category: providerFailure.category,
|
|
139
|
+
message: providerFailure.label,
|
|
140
|
+
details: {
|
|
141
|
+
provider: providerFailure.provider,
|
|
142
|
+
keyEnv: providerFailure.keyEnv,
|
|
143
|
+
providerMessage: providerFailure.providerMessage,
|
|
144
|
+
fix: providerFailure.fix,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
: undefined,
|
|
130
148
|
log: await describeLog(sessionMeta.id, logWriter.logPath, store),
|
|
131
149
|
});
|
|
132
150
|
throw error;
|