@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
|
@@ -5,6 +5,61 @@ import { runBrowserMode } from "../browserMode.js";
|
|
|
5
5
|
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
6
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
7
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
8
|
+
const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
|
|
9
|
+
const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
|
|
10
|
+
function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
11
|
+
if (!browserConfig.desiredModel) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
requestedModel: browserConfig.desiredModel,
|
|
16
|
+
resolvedLabel: null,
|
|
17
|
+
strategy: browserConfig.modelStrategy,
|
|
18
|
+
status: "unavailable",
|
|
19
|
+
verified: false,
|
|
20
|
+
source: "config",
|
|
21
|
+
capturedAt: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function formatModelSelectionEvidence(evidence) {
|
|
25
|
+
const requested = evidence.requestedModel ?? "(none)";
|
|
26
|
+
const resolved = evidence.resolvedLabel ?? "(unavailable)";
|
|
27
|
+
const strategy = evidence.strategy ?? "(default)";
|
|
28
|
+
const verified = evidence.verified ? "yes" : "no";
|
|
29
|
+
return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
|
|
30
|
+
}
|
|
31
|
+
function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
|
|
32
|
+
const candidates = [
|
|
33
|
+
runOptions.model,
|
|
34
|
+
browserConfig.desiredModel,
|
|
35
|
+
evidence?.requestedModel,
|
|
36
|
+
evidence?.resolvedLabel,
|
|
37
|
+
];
|
|
38
|
+
return candidates.some((value) => typeof value === "string" && /\bpro\b/i.test(value));
|
|
39
|
+
}
|
|
40
|
+
export function buildBrowserRunWarningsForTest(args) {
|
|
41
|
+
return buildBrowserRunWarnings(args);
|
|
42
|
+
}
|
|
43
|
+
function buildBrowserRunWarnings(args) {
|
|
44
|
+
if (!isRequestedProBrowserRun(args.runOptions, args.browserConfig, args.modelSelection) ||
|
|
45
|
+
args.inputTokens < LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD ||
|
|
46
|
+
args.elapsedMs >= LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
code: "browser-pro-fast-large-run",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `Large browser Pro run completed quickly (${(args.elapsedMs / 1000).toFixed(0)}s for ~${args.inputTokens.toLocaleString()} input tokens); verify the stored model selection evidence before claiming Pro Extended output.`,
|
|
54
|
+
details: {
|
|
55
|
+
inputTokens: args.inputTokens,
|
|
56
|
+
elapsedMs: args.elapsedMs,
|
|
57
|
+
requestedModel: args.modelSelection?.requestedModel ?? args.browserConfig.desiredModel,
|
|
58
|
+
resolvedLabel: args.modelSelection?.resolvedLabel ?? null,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
8
63
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
9
64
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
10
65
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
@@ -37,7 +92,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
37
92
|
if (typeof message !== "string")
|
|
38
93
|
return;
|
|
39
94
|
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
40
|
-
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance/i.test(message);
|
|
95
|
+
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
41
96
|
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
42
97
|
return;
|
|
43
98
|
log(message);
|
|
@@ -84,6 +139,20 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
84
139
|
const message = error instanceof Error ? error.message : "Browser automation failed.";
|
|
85
140
|
throw new BrowserAutomationError(message, { stage: "execute-browser" }, error);
|
|
86
141
|
}
|
|
142
|
+
const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
|
|
143
|
+
if (modelSelection) {
|
|
144
|
+
log(formatModelSelectionEvidence(modelSelection));
|
|
145
|
+
}
|
|
146
|
+
const warnings = buildBrowserRunWarnings({
|
|
147
|
+
runOptions,
|
|
148
|
+
browserConfig,
|
|
149
|
+
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
150
|
+
elapsedMs: browserResult.tookMs,
|
|
151
|
+
modelSelection,
|
|
152
|
+
});
|
|
153
|
+
for (const warning of warnings) {
|
|
154
|
+
log(chalk.yellow(`[browser] ${warning.message}`));
|
|
155
|
+
}
|
|
87
156
|
if (!runOptions.silent) {
|
|
88
157
|
log(chalk.bold("Answer:"));
|
|
89
158
|
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim("(no text output)"));
|
|
@@ -145,9 +214,12 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
145
214
|
chromeTargetId: browserResult.chromeTargetId,
|
|
146
215
|
tabUrl: browserResult.tabUrl,
|
|
147
216
|
conversationId: browserResult.conversationId,
|
|
217
|
+
promptSubmitted: browserResult.promptSubmitted,
|
|
148
218
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
149
219
|
},
|
|
150
220
|
archive: browserResult.archive,
|
|
221
|
+
modelSelection,
|
|
222
|
+
warnings,
|
|
151
223
|
answerText,
|
|
152
224
|
artifacts: savedArtifacts,
|
|
153
225
|
};
|
|
@@ -1,50 +1,4 @@
|
|
|
1
|
-
export
|
|
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
|
-
}
|
|
1
|
+
export { parseDuration } from "../duration.js";
|
|
48
2
|
export function delay(ms) {
|
|
49
3
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
4
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const ZIP_UTF8_FLAG = 0x0800;
|
|
2
|
+
const ZIP_STORE_METHOD = 0;
|
|
3
|
+
const ZIP_VERSION_NEEDED = 20;
|
|
4
|
+
const ZIP_DOS_TIME = 0x0000;
|
|
5
|
+
const ZIP_DOS_DATE = 0x0021;
|
|
6
|
+
const CRC32_TABLE = (() => {
|
|
7
|
+
const table = new Uint32Array(256);
|
|
8
|
+
for (let i = 0; i < table.length; i += 1) {
|
|
9
|
+
let value = i;
|
|
10
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
11
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
12
|
+
}
|
|
13
|
+
table[i] = value >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return table;
|
|
16
|
+
})();
|
|
17
|
+
function crc32(buffer) {
|
|
18
|
+
let crc = 0xffffffff;
|
|
19
|
+
for (const byte of buffer) {
|
|
20
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
21
|
+
}
|
|
22
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
function normalizeZipPath(inputPath, fallback) {
|
|
25
|
+
const normalized = inputPath
|
|
26
|
+
.replace(/\\/g, "/")
|
|
27
|
+
.replace(/^[a-zA-Z]:\//, "")
|
|
28
|
+
.replace(/^\/+/, "")
|
|
29
|
+
.split("/")
|
|
30
|
+
.filter((segment) => segment && segment !== "." && segment !== "..")
|
|
31
|
+
.join("/");
|
|
32
|
+
return normalized || fallback;
|
|
33
|
+
}
|
|
34
|
+
function uniqueZipPath(inputPath, index, seen) {
|
|
35
|
+
const normalized = normalizeZipPath(inputPath, `file-${index + 1}.txt`);
|
|
36
|
+
const extIndex = normalized.lastIndexOf(".");
|
|
37
|
+
const base = extIndex > 0 ? normalized.slice(0, extIndex) : normalized;
|
|
38
|
+
const ext = extIndex > 0 ? normalized.slice(extIndex) : "";
|
|
39
|
+
let candidate = normalized;
|
|
40
|
+
let suffix = 2;
|
|
41
|
+
while (seen.has(candidate)) {
|
|
42
|
+
candidate = `${base}-${suffix}${ext}`;
|
|
43
|
+
suffix += 1;
|
|
44
|
+
}
|
|
45
|
+
seen.add(candidate);
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
function assertZip32(value, label) {
|
|
49
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
50
|
+
throw new Error(`${label} exceeds ZIP32 limits.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function assertZip16(value, label) {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff) {
|
|
55
|
+
throw new Error(`${label} exceeds ZIP16 limits.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function localFileHeader(entry) {
|
|
59
|
+
const header = Buffer.alloc(30);
|
|
60
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
61
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
|
|
62
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 6);
|
|
63
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 8);
|
|
64
|
+
header.writeUInt16LE(ZIP_DOS_TIME, 10);
|
|
65
|
+
header.writeUInt16LE(ZIP_DOS_DATE, 12);
|
|
66
|
+
header.writeUInt32LE(entry.crc32, 14);
|
|
67
|
+
header.writeUInt32LE(entry.content.length, 18);
|
|
68
|
+
header.writeUInt32LE(entry.content.length, 22);
|
|
69
|
+
header.writeUInt16LE(entry.name.length, 26);
|
|
70
|
+
header.writeUInt16LE(0, 28);
|
|
71
|
+
return header;
|
|
72
|
+
}
|
|
73
|
+
function centralDirectoryHeader(entry) {
|
|
74
|
+
const header = Buffer.alloc(46);
|
|
75
|
+
header.writeUInt32LE(0x02014b50, 0);
|
|
76
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
|
|
77
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 6);
|
|
78
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 8);
|
|
79
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 10);
|
|
80
|
+
header.writeUInt16LE(ZIP_DOS_TIME, 12);
|
|
81
|
+
header.writeUInt16LE(ZIP_DOS_DATE, 14);
|
|
82
|
+
header.writeUInt32LE(entry.crc32, 16);
|
|
83
|
+
header.writeUInt32LE(entry.content.length, 20);
|
|
84
|
+
header.writeUInt32LE(entry.content.length, 24);
|
|
85
|
+
header.writeUInt16LE(entry.name.length, 28);
|
|
86
|
+
header.writeUInt16LE(0, 30);
|
|
87
|
+
header.writeUInt16LE(0, 32);
|
|
88
|
+
header.writeUInt16LE(0, 34);
|
|
89
|
+
header.writeUInt16LE(0, 36);
|
|
90
|
+
header.writeUInt32LE(0, 38);
|
|
91
|
+
header.writeUInt32LE(entry.localHeaderOffset, 42);
|
|
92
|
+
return header;
|
|
93
|
+
}
|
|
94
|
+
function endOfCentralDirectory(entryCount, centralSize, centralOffset) {
|
|
95
|
+
const footer = Buffer.alloc(22);
|
|
96
|
+
footer.writeUInt32LE(0x06054b50, 0);
|
|
97
|
+
footer.writeUInt16LE(0, 4);
|
|
98
|
+
footer.writeUInt16LE(0, 6);
|
|
99
|
+
footer.writeUInt16LE(entryCount, 8);
|
|
100
|
+
footer.writeUInt16LE(entryCount, 10);
|
|
101
|
+
footer.writeUInt32LE(centralSize, 12);
|
|
102
|
+
footer.writeUInt32LE(centralOffset, 16);
|
|
103
|
+
footer.writeUInt16LE(0, 20);
|
|
104
|
+
return footer;
|
|
105
|
+
}
|
|
106
|
+
export function createStoredZip(entries) {
|
|
107
|
+
if (entries.length > 0xffff) {
|
|
108
|
+
throw new Error("Too many files for a ZIP32 browser bundle.");
|
|
109
|
+
}
|
|
110
|
+
assertZip16(entries.length, "ZIP entry count");
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const prepared = [];
|
|
113
|
+
const localParts = [];
|
|
114
|
+
let offset = 0;
|
|
115
|
+
entries.forEach((entry, index) => {
|
|
116
|
+
const name = Buffer.from(uniqueZipPath(entry.path, index, seen), "utf8");
|
|
117
|
+
const content = Buffer.isBuffer(entry.content)
|
|
118
|
+
? entry.content
|
|
119
|
+
: Buffer.from(entry.content, "utf8");
|
|
120
|
+
assertZip16(name.length, "ZIP file name");
|
|
121
|
+
assertZip32(content.length, "ZIP entry size");
|
|
122
|
+
assertZip32(offset, "ZIP local header offset");
|
|
123
|
+
const preparedEntry = {
|
|
124
|
+
name,
|
|
125
|
+
content,
|
|
126
|
+
crc32: crc32(content),
|
|
127
|
+
localHeaderOffset: offset,
|
|
128
|
+
};
|
|
129
|
+
prepared.push(preparedEntry);
|
|
130
|
+
const header = localFileHeader(preparedEntry);
|
|
131
|
+
localParts.push(header, name, content);
|
|
132
|
+
offset += header.length + name.length + content.length;
|
|
133
|
+
assertZip32(offset, "ZIP local data size");
|
|
134
|
+
});
|
|
135
|
+
const centralOffset = offset;
|
|
136
|
+
const centralParts = [];
|
|
137
|
+
for (const entry of prepared) {
|
|
138
|
+
const header = centralDirectoryHeader(entry);
|
|
139
|
+
centralParts.push(header, entry.name);
|
|
140
|
+
offset += header.length + entry.name.length;
|
|
141
|
+
assertZip32(offset, "ZIP central directory size");
|
|
142
|
+
}
|
|
143
|
+
const centralSize = offset - centralOffset;
|
|
144
|
+
assertZip32(centralOffset, "ZIP central directory offset");
|
|
145
|
+
assertZip32(centralSize, "ZIP central directory size");
|
|
146
|
+
const footer = endOfCentralDirectory(prepared.length, centralSize, centralOffset);
|
|
147
|
+
return Buffer.concat([...localParts, ...centralParts, footer]);
|
|
148
|
+
}
|
|
149
|
+
export const __test__ = {
|
|
150
|
+
crc32,
|
|
151
|
+
normalizeZipPath,
|
|
152
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET
|
|
3
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
|
|
4
|
+
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
5
|
+
import { parseDuration } from "../duration.js";
|
|
4
6
|
import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
|
|
5
7
|
import { getOracleHomeDir } from "../oracleHome.js";
|
|
6
8
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
@@ -12,14 +14,14 @@ const DEFAULT_CHROME_PROFILE = "Default";
|
|
|
12
14
|
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
13
15
|
const BROWSER_MODEL_LABELS = [
|
|
14
16
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
15
|
-
["gpt-5.5-pro", "
|
|
17
|
+
["gpt-5.5-pro", "Pro"],
|
|
16
18
|
["gpt-5.5", "Thinking 5.5"],
|
|
17
|
-
["gpt-5.4-pro", "
|
|
19
|
+
["gpt-5.4-pro", "Pro"],
|
|
18
20
|
["gpt-5.2-thinking", "GPT-5.2 Thinking"],
|
|
19
21
|
["gpt-5.2-instant", "GPT-5.2 Instant"],
|
|
20
|
-
["gpt-5.2-pro", "
|
|
21
|
-
["gpt-5.1-pro", "
|
|
22
|
-
["gpt-5-pro", "
|
|
22
|
+
["gpt-5.2-pro", "Pro"],
|
|
23
|
+
["gpt-5.1-pro", "Pro"],
|
|
24
|
+
["gpt-5-pro", "Pro"],
|
|
23
25
|
// Base models last (least specific)
|
|
24
26
|
["gpt-5.4", "Thinking 5.4"],
|
|
25
27
|
["gpt-5.2", "GPT-5.2"], // Selects "Auto" in ChatGPT UI
|
|
@@ -32,14 +34,14 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
32
34
|
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
33
35
|
return model;
|
|
34
36
|
}
|
|
35
|
-
if (normalized === "gpt-5.5-pro" ||
|
|
36
|
-
normalized === "gpt-5.5" ||
|
|
37
|
-
normalized === "gpt-5.4-pro" ||
|
|
38
|
-
normalized === "gpt-5.4") {
|
|
37
|
+
if (normalized === "gpt-5.5-pro" || normalized === "gpt-5.5" || normalized === "gpt-5.4") {
|
|
39
38
|
return normalized;
|
|
40
39
|
}
|
|
41
40
|
// Pro variants: resolve to the latest Pro model in ChatGPT.
|
|
42
|
-
if (normalized === "gpt-5-pro" ||
|
|
41
|
+
if (normalized === "gpt-5-pro" ||
|
|
42
|
+
normalized === "gpt-5.1-pro" ||
|
|
43
|
+
normalized === "gpt-5.2-pro" ||
|
|
44
|
+
normalized === "gpt-5.4-pro") {
|
|
43
45
|
return "gpt-5.5-pro";
|
|
44
46
|
}
|
|
45
47
|
// Explicit model variants: keep as-is (they have their own browser labels)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CHATGPT_URL } from "../browser/constants.js";
|
|
2
|
+
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
2
3
|
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
3
4
|
const browser = config.browser;
|
|
4
5
|
if (!browser)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_DOC_PATHS = [
|
|
4
|
+
"README.md",
|
|
5
|
+
"docs/index.md",
|
|
6
|
+
"docs/agents.md",
|
|
7
|
+
"docs/sessions.md",
|
|
8
|
+
"docs/spec.md",
|
|
9
|
+
"docs/cli-reference.md",
|
|
10
|
+
];
|
|
11
|
+
const FLAG_RE = /(^|[\s`([{|,])(--[a-z][a-z0-9-]*)(?=$|[\s`)[\].,;:|=<>}])/g;
|
|
12
|
+
const SLASH_FLAG_RE = /--[a-z][a-z0-9-]*(?:\/(?:--[a-z][a-z0-9-]*|-[a-z][a-z0-9-]*))+/g;
|
|
13
|
+
const ROOT_ONLY_SECTIONS = new Set(["Core consult flags"]);
|
|
14
|
+
export async function checkDocsFlags({ command, cwd = process.cwd(), paths, }) {
|
|
15
|
+
const availableFlags = collectCommanderFlags(command);
|
|
16
|
+
const rootFlags = collectCommanderFlags(command, { recursive: false });
|
|
17
|
+
const commandFlags = collectCommandFlags(command);
|
|
18
|
+
const docPaths = await resolveDocPaths(cwd, paths);
|
|
19
|
+
const issues = [];
|
|
20
|
+
const checkedFlags = new Set();
|
|
21
|
+
for (const docPath of docPaths) {
|
|
22
|
+
const body = await fs.readFile(path.resolve(cwd, docPath), "utf8");
|
|
23
|
+
for (const reference of extractMarkdownFlagReferences(body)) {
|
|
24
|
+
const { command: commandPath, flag, section } = reference;
|
|
25
|
+
checkedFlags.add(flag);
|
|
26
|
+
const scopedFlags = commandPath
|
|
27
|
+
? (commandFlags.get(commandPath) ?? availableFlags)
|
|
28
|
+
: section && ROOT_ONLY_SECTIONS.has(section)
|
|
29
|
+
? rootFlags
|
|
30
|
+
: availableFlags;
|
|
31
|
+
if (!scopedFlags.has(flag)) {
|
|
32
|
+
issues.push({ file: docPath, flag, section, command: commandPath });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
checkedFiles: docPaths,
|
|
38
|
+
checkedFlags: [...checkedFlags].sort(),
|
|
39
|
+
issues: issues.sort((a, b) => a.file.localeCompare(b.file) || a.flag.localeCompare(b.flag)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function printDocsCheckResult(result, log = console.log) {
|
|
43
|
+
if (result.issues.length === 0) {
|
|
44
|
+
log(`Docs/help check: ok (${result.checkedFlags.length} flags, ${result.checkedFiles.length} files)`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
log("Docs/help drift:");
|
|
48
|
+
for (const issue of result.issues) {
|
|
49
|
+
const scopes = [issue.section, issue.command].filter(Boolean);
|
|
50
|
+
const scope = scopes.length > 0 ? ` (${scopes.join(", ")})` : "";
|
|
51
|
+
log(`- ${issue.file}${scope} mentions ${issue.flag}, but CLI help does not expose ${issue.flag}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function collectCommanderFlags(command, options) {
|
|
55
|
+
const flags = new Set(["--help", "--version"]);
|
|
56
|
+
for (const option of command.options) {
|
|
57
|
+
for (const flag of extractOptionFlags(option.flags)) {
|
|
58
|
+
flags.add(flag);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (options?.recursive === false) {
|
|
62
|
+
return flags;
|
|
63
|
+
}
|
|
64
|
+
for (const subcommand of command.commands) {
|
|
65
|
+
for (const flag of collectCommanderFlags(subcommand)) {
|
|
66
|
+
flags.add(flag);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return flags;
|
|
70
|
+
}
|
|
71
|
+
function collectCommandFlags(command, pathParts = ["oracle"]) {
|
|
72
|
+
const flags = new Map();
|
|
73
|
+
flags.set(pathParts.join(" "), collectCommanderFlags(command, { recursive: false }));
|
|
74
|
+
for (const subcommand of command.commands) {
|
|
75
|
+
for (const [path, subcommandFlags] of collectCommandFlags(subcommand, [
|
|
76
|
+
...pathParts,
|
|
77
|
+
subcommand.name(),
|
|
78
|
+
])) {
|
|
79
|
+
flags.set(path, subcommandFlags);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return flags;
|
|
83
|
+
}
|
|
84
|
+
export function extractMarkdownFlags(markdown) {
|
|
85
|
+
return [
|
|
86
|
+
...new Set(extractMarkdownFlagReferences(markdown).map((reference) => reference.flag)),
|
|
87
|
+
].sort();
|
|
88
|
+
}
|
|
89
|
+
function extractMarkdownFlagReferences(markdown) {
|
|
90
|
+
const references = [];
|
|
91
|
+
let section;
|
|
92
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
93
|
+
const heading = line.match(/^##+\s+(.+?)\s*$/);
|
|
94
|
+
if (heading) {
|
|
95
|
+
section = heading[1];
|
|
96
|
+
}
|
|
97
|
+
const commandPath = extractOracleCommandPath(line);
|
|
98
|
+
const lineFlags = new Set();
|
|
99
|
+
for (const match of line.matchAll(FLAG_RE)) {
|
|
100
|
+
const flag = match[2];
|
|
101
|
+
if (!flag || flag.endsWith("-")) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
lineFlags.add(flag);
|
|
105
|
+
}
|
|
106
|
+
for (const flag of expandSlashFlagReferences(line)) {
|
|
107
|
+
lineFlags.add(flag);
|
|
108
|
+
}
|
|
109
|
+
for (const flag of lineFlags) {
|
|
110
|
+
references.push({ flag, section, command: commandPath });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return references.sort((a, b) => a.flag.localeCompare(b.flag) || (a.section ?? "").localeCompare(b.section ?? ""));
|
|
114
|
+
}
|
|
115
|
+
function extractOracleCommandPath(line) {
|
|
116
|
+
const trimmed = line.trim().replace(/^[$>]\s+/, "");
|
|
117
|
+
if (!trimmed.startsWith("oracle ") && !trimmed.startsWith("npx ")) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const tokens = trimmed.split(/\s+/);
|
|
121
|
+
let oracleIndex = tokens[0] === "oracle" ? 0 : -1;
|
|
122
|
+
if (oracleIndex === -1) {
|
|
123
|
+
oracleIndex = tokens.findIndex((token) => token === "@steipete/oracle");
|
|
124
|
+
}
|
|
125
|
+
if (oracleIndex === -1) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const pathParts = ["oracle"];
|
|
129
|
+
for (const token of tokens.slice(oracleIndex + 1)) {
|
|
130
|
+
if (token.startsWith("-") || !/^[a-z][a-z0-9-]*$/.test(token)) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
pathParts.push(token);
|
|
134
|
+
}
|
|
135
|
+
return pathParts.join(" ");
|
|
136
|
+
}
|
|
137
|
+
function extractOptionFlags(flagsText) {
|
|
138
|
+
const flags = new Set();
|
|
139
|
+
for (const match of flagsText.matchAll(/--\[no-\]([a-z][a-z0-9-]*)/g)) {
|
|
140
|
+
flags.add(`--${match[1]}`);
|
|
141
|
+
flags.add(`--no-${match[1]}`);
|
|
142
|
+
}
|
|
143
|
+
for (const match of flagsText.matchAll(/--[a-z][a-z0-9-]*/g)) {
|
|
144
|
+
flags.add(match[0]);
|
|
145
|
+
}
|
|
146
|
+
return [...flags];
|
|
147
|
+
}
|
|
148
|
+
function expandSlashFlagReferences(line) {
|
|
149
|
+
const flags = [];
|
|
150
|
+
for (const match of line.matchAll(SLASH_FLAG_RE)) {
|
|
151
|
+
const parts = match[0].split("/");
|
|
152
|
+
const base = parts[0];
|
|
153
|
+
const basePrefix = base.slice(0, base.lastIndexOf("-") + 1);
|
|
154
|
+
flags.push(base);
|
|
155
|
+
for (const part of parts.slice(1)) {
|
|
156
|
+
if (part.startsWith("--")) {
|
|
157
|
+
flags.push(part);
|
|
158
|
+
}
|
|
159
|
+
else if (part.startsWith("-") && basePrefix) {
|
|
160
|
+
flags.push(`${basePrefix}${part.slice(1)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return flags;
|
|
165
|
+
}
|
|
166
|
+
async function resolveDocPaths(cwd, paths) {
|
|
167
|
+
const candidates = paths && paths.length > 0 ? paths : DEFAULT_DOC_PATHS;
|
|
168
|
+
const existing = [];
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
try {
|
|
171
|
+
const stat = await fs.stat(path.resolve(cwd, candidate));
|
|
172
|
+
if (stat.isFile()) {
|
|
173
|
+
existing.push(candidate);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
if (paths && paths.length > 0) {
|
|
178
|
+
throw new Error(`Docs check path not found: ${candidate}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (existing.length === 0) {
|
|
183
|
+
throw new Error("No docs found to check. Run from the repo root or pass --docs-path <file>.");
|
|
184
|
+
}
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -12,21 +12,28 @@ export function defaultWaitPreference(model, engine) {
|
|
|
12
12
|
* Precedence:
|
|
13
13
|
* 1) Legacy --browser flag forces browser.
|
|
14
14
|
* 2) Explicit --engine value.
|
|
15
|
-
* 3)
|
|
16
|
-
* 4)
|
|
15
|
+
* 3) Explicit API provider routing flags force API.
|
|
16
|
+
* 4) ORACLE_ENGINE environment override (api|browser).
|
|
17
|
+
* 5) API environment decides: api when set, otherwise browser.
|
|
17
18
|
*/
|
|
18
|
-
export function resolveEngine({ engine, browserFlag, env, }) {
|
|
19
|
+
export function resolveEngine({ engine, browserFlag, apiProviderRequested, env, }) {
|
|
19
20
|
if (browserFlag) {
|
|
20
21
|
return "browser";
|
|
21
22
|
}
|
|
22
23
|
if (engine) {
|
|
23
24
|
return engine;
|
|
24
25
|
}
|
|
26
|
+
if (apiProviderRequested) {
|
|
27
|
+
return "api";
|
|
28
|
+
}
|
|
25
29
|
const envEngine = normalizeEngineMode(env.ORACLE_ENGINE);
|
|
26
30
|
if (envEngine) {
|
|
27
31
|
return envEngine;
|
|
28
32
|
}
|
|
29
|
-
return env
|
|
33
|
+
return hasApiEnvironment(env) ? "api" : "browser";
|
|
34
|
+
}
|
|
35
|
+
function hasApiEnvironment(env) {
|
|
36
|
+
return Boolean(env.OPENAI_API_KEY || env.OPENROUTER_API_KEY);
|
|
30
37
|
}
|
|
31
38
|
function normalizeEngineMode(raw) {
|
|
32
39
|
if (typeof raw !== "string") {
|
package/dist/src/cli/options.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { InvalidArgumentError } from "commander";
|
|
2
|
-
import { parseDuration } from "../
|
|
2
|
+
import { parseDuration } from "../duration.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fg from "fast-glob";
|
|
5
|
-
import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle.js";
|
|
5
|
+
import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle/config.js";
|
|
6
6
|
export function collectPaths(value, previous = []) {
|
|
7
7
|
if (!value) {
|
|
8
8
|
return previous;
|
|
@@ -140,11 +140,17 @@ export function parseTimeoutOption(value) {
|
|
|
140
140
|
const normalized = value.trim().toLowerCase();
|
|
141
141
|
if (normalized === "auto")
|
|
142
142
|
return "auto";
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
if (/^[0-9]+(?:\.[0-9]+)?$/.test(normalized)) {
|
|
144
|
+
const parsed = Number.parseFloat(normalized);
|
|
145
|
+
if (parsed > 0) {
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
146
148
|
}
|
|
147
|
-
|
|
149
|
+
const parsedMs = parseDuration(normalized, Number.NaN);
|
|
150
|
+
if (!Number.isFinite(parsedMs) || parsedMs <= 0) {
|
|
151
|
+
throw new InvalidArgumentError('Timeout must be a positive number of seconds, a duration like "10m", or "auto".');
|
|
152
|
+
}
|
|
153
|
+
return parsedMs / 1000;
|
|
148
154
|
}
|
|
149
155
|
export function parseDurationOption(value, label) {
|
|
150
156
|
if (value == null)
|