@andrewting19/oracle 0.9.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/LICENSE +21 -0
- package/README.md +300 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1480 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +127 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1386 -0
- package/dist/scripts/test-browser.js +106 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1085 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
- package/dist/src/browser/actions/attachments.js +1939 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +549 -0
- package/dist/src/browser/actions/navigation.js +451 -0
- package/dist/src/browser/actions/promptComposer.js +487 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +346 -0
- package/dist/src/browser/config.js +105 -0
- package/dist/src/browser/constants.js +75 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1826 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +46 -0
- package/dist/src/browser/profileState.js +285 -0
- package/dist/src/browser/prompt.js +182 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
- package/dist/src/browser/providers/index.js +2 -0
- package/dist/src/browser/reattach.js +189 -0
- package/dist/src/browser/reattachHelpers.js +387 -0
- package/dist/src/browser/sessionRunner.js +131 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +81 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +115 -0
- package/dist/src/cli/bridge/host.js +261 -0
- package/dist/src/cli/browserConfig.js +293 -0
- package/dist/src/cli/browserDefaults.js +82 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +14 -0
- package/dist/src/cli/dryRun.js +109 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/fileSize.js +11 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +21 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +316 -0
- package/dist/src/cli/options.js +305 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +90 -0
- package/dist/src/cli/sessionCommand.js +121 -0
- package/dist/src/cli/sessionDisplay.js +670 -0
- package/dist/src/cli/sessionLineage.js +60 -0
- package/dist/src/cli/sessionRunner.js +630 -0
- package/dist/src/cli/sessionTable.js +96 -0
- package/dist/src/cli/tagline.js +255 -0
- package/dist/src/cli/tui/index.js +499 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/browserSessionManager.js +81 -0
- package/dist/src/gemini-web/client.js +339 -0
- package/dist/src/gemini-web/executionClients.js +1 -0
- package/dist/src/gemini-web/executionMode.js +16 -0
- package/dist/src/gemini-web/executor.js +443 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +307 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +114 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +45 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +107 -0
- package/dist/src/oracle/client.js +235 -0
- package/dist/src/oracle/config.js +192 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +402 -0
- package/dist/src/oracle/finishLine.js +34 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +194 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +16 -0
- package/dist/src/oracle/request.js +58 -0
- package/dist/src/oracle/run.js +628 -0
- package/dist/src/oracle/runUtils.js +34 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +544 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +643 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +26 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +120 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +26 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, isTemporaryChatUrl, normalizeChatgptUrl, parseDuration, } from "../browserMode.js";
|
|
4
|
+
import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
|
|
5
|
+
import { getOracleHomeDir } from "../oracleHome.js";
|
|
6
|
+
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
7
|
+
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 60_000;
|
|
8
|
+
const DEFAULT_BROWSER_RECHECK_TIMEOUT_MS = 120_000;
|
|
9
|
+
const DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS = 120_000;
|
|
10
|
+
const DEFAULT_CHROME_PROFILE = "Default";
|
|
11
|
+
// Ordered array: most specific models first to ensure correct selection.
|
|
12
|
+
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
13
|
+
const BROWSER_MODEL_LABELS = [
|
|
14
|
+
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
15
|
+
["gpt-5.4-pro", "GPT-5.4 Pro"],
|
|
16
|
+
["gpt-5.2-thinking", "GPT-5.2 Thinking"],
|
|
17
|
+
["gpt-5.2-instant", "GPT-5.2 Instant"],
|
|
18
|
+
["gpt-5.2-pro", "GPT-5.4 Pro"],
|
|
19
|
+
["gpt-5.1-pro", "GPT-5.4 Pro"],
|
|
20
|
+
["gpt-5-pro", "GPT-5.4 Pro"],
|
|
21
|
+
// Base models last (least specific)
|
|
22
|
+
["gpt-5.4", "Thinking 5.4"],
|
|
23
|
+
["gpt-5.2", "GPT-5.2"], // Selects "Auto" in ChatGPT UI
|
|
24
|
+
["gpt-5.1", "GPT-5.2"], // Legacy alias → Auto
|
|
25
|
+
["gemini-3-pro", "Gemini 3 Pro"],
|
|
26
|
+
["gemini-3-pro-deep-think", "gemini-3-deep-think"],
|
|
27
|
+
];
|
|
28
|
+
export function normalizeChatGptModelForBrowser(model) {
|
|
29
|
+
const normalized = model.toLowerCase();
|
|
30
|
+
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
31
|
+
return model;
|
|
32
|
+
}
|
|
33
|
+
if (normalized === "gpt-5.4-pro" || normalized === "gpt-5.4") {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
// Pro variants: resolve to the latest Pro model in ChatGPT.
|
|
37
|
+
if (normalized === "gpt-5-pro" || normalized === "gpt-5.1-pro" || normalized === "gpt-5.2-pro") {
|
|
38
|
+
return "gpt-5.4-pro";
|
|
39
|
+
}
|
|
40
|
+
// Explicit model variants: keep as-is (they have their own browser labels)
|
|
41
|
+
if (normalized === "gpt-5.2-thinking" || normalized === "gpt-5.2-instant") {
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
// Legacy aliases: map to base GPT-5.2 (Auto)
|
|
45
|
+
if (normalized === "gpt-5.1") {
|
|
46
|
+
return "gpt-5.2";
|
|
47
|
+
}
|
|
48
|
+
return model;
|
|
49
|
+
}
|
|
50
|
+
export async function buildBrowserConfig(options) {
|
|
51
|
+
const desiredModelOverride = options.browserModelLabel?.trim();
|
|
52
|
+
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
|
|
53
|
+
const baseModel = options.model.toLowerCase();
|
|
54
|
+
const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
|
|
55
|
+
const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
56
|
+
const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
|
|
57
|
+
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
58
|
+
let inline = await resolveInlineCookies({
|
|
59
|
+
inlineArg: options.browserInlineCookies,
|
|
60
|
+
inlineFileArg: options.browserInlineCookiesFile,
|
|
61
|
+
envPayload: process.env.ORACLE_BROWSER_COOKIES_JSON,
|
|
62
|
+
envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
|
|
63
|
+
cwd: process.cwd(),
|
|
64
|
+
});
|
|
65
|
+
if (inline?.source?.startsWith("home:") && options.browserNoCookieSync !== true) {
|
|
66
|
+
inline = undefined;
|
|
67
|
+
}
|
|
68
|
+
let remoteChrome;
|
|
69
|
+
if (options.remoteChrome) {
|
|
70
|
+
remoteChrome = parseRemoteChromeTarget(options.remoteChrome);
|
|
71
|
+
}
|
|
72
|
+
const rawUrl = options.chatgptUrl ?? options.browserUrl;
|
|
73
|
+
const url = rawUrl ? normalizeChatgptUrl(rawUrl, CHATGPT_URL) : undefined;
|
|
74
|
+
const desiredModel = isChatGptModel
|
|
75
|
+
? mapModelToBrowserLabel(options.model)
|
|
76
|
+
: shouldUseOverride
|
|
77
|
+
? desiredModelOverride
|
|
78
|
+
: mapModelToBrowserLabel(options.model);
|
|
79
|
+
if (modelStrategy === "select" &&
|
|
80
|
+
url &&
|
|
81
|
+
isTemporaryChatUrl(url) &&
|
|
82
|
+
/\bpro\b/i.test(desiredModel ?? "")) {
|
|
83
|
+
throw new Error("Temporary Chat mode does not expose Pro models in the ChatGPT model picker. " +
|
|
84
|
+
'Remove "temporary-chat=true" from --chatgpt-url (or omit --chatgpt-url), or use a non-Pro model (e.g. --model gpt-5.2).');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
|
|
88
|
+
chromePath: options.browserChromePath ?? null,
|
|
89
|
+
chromeCookiePath: options.browserCookiePath ?? null,
|
|
90
|
+
url,
|
|
91
|
+
debugPort: selectBrowserPort(options),
|
|
92
|
+
timeoutMs: options.browserTimeout
|
|
93
|
+
? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS)
|
|
94
|
+
: undefined,
|
|
95
|
+
inputTimeoutMs: options.browserInputTimeout
|
|
96
|
+
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
97
|
+
: undefined,
|
|
98
|
+
assistantRecheckDelayMs: options.browserRecheckDelay
|
|
99
|
+
? parseDuration(options.browserRecheckDelay, 0)
|
|
100
|
+
: undefined,
|
|
101
|
+
assistantRecheckTimeoutMs: options.browserRecheckTimeout
|
|
102
|
+
? parseDuration(options.browserRecheckTimeout, DEFAULT_BROWSER_RECHECK_TIMEOUT_MS)
|
|
103
|
+
: undefined,
|
|
104
|
+
reuseChromeWaitMs: options.browserReuseWait
|
|
105
|
+
? parseDuration(options.browserReuseWait, 0)
|
|
106
|
+
: undefined,
|
|
107
|
+
profileLockTimeoutMs: options.browserProfileLockTimeout
|
|
108
|
+
? parseDuration(options.browserProfileLockTimeout, 0)
|
|
109
|
+
: undefined,
|
|
110
|
+
autoReattachDelayMs: options.browserAutoReattachDelay
|
|
111
|
+
? parseDuration(options.browserAutoReattachDelay, 0)
|
|
112
|
+
: undefined,
|
|
113
|
+
autoReattachIntervalMs: options.browserAutoReattachInterval
|
|
114
|
+
? parseDuration(options.browserAutoReattachInterval, 0)
|
|
115
|
+
: undefined,
|
|
116
|
+
autoReattachTimeoutMs: options.browserAutoReattachTimeout
|
|
117
|
+
? parseDuration(options.browserAutoReattachTimeout, DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS)
|
|
118
|
+
: undefined,
|
|
119
|
+
cookieSyncWaitMs: options.browserCookieWait
|
|
120
|
+
? parseDuration(options.browserCookieWait, 0)
|
|
121
|
+
: undefined,
|
|
122
|
+
cookieSync: options.browserNoCookieSync ? false : undefined,
|
|
123
|
+
cookieNames,
|
|
124
|
+
inlineCookies: inline?.cookies,
|
|
125
|
+
inlineCookiesSource: inline?.source ?? null,
|
|
126
|
+
headless: undefined, // disable headless; Cloudflare blocks it
|
|
127
|
+
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
128
|
+
manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
|
|
129
|
+
manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
|
|
130
|
+
hideWindow: options.browserHideWindow ? true : undefined,
|
|
131
|
+
desiredModel,
|
|
132
|
+
modelStrategy,
|
|
133
|
+
debug: options.verbose ? true : undefined,
|
|
134
|
+
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
135
|
+
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
136
|
+
remoteChrome,
|
|
137
|
+
thinkingTime: options.browserThinkingTime,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function selectBrowserPort(options) {
|
|
141
|
+
const candidate = options.browserPort ?? options.browserDebugPort;
|
|
142
|
+
if (candidate === undefined || candidate === null)
|
|
143
|
+
return null;
|
|
144
|
+
if (!Number.isFinite(candidate) || candidate <= 0 || candidate > 65_535) {
|
|
145
|
+
throw new Error(`Invalid browser port: ${candidate}. Expected a number between 1 and 65535.`);
|
|
146
|
+
}
|
|
147
|
+
return candidate;
|
|
148
|
+
}
|
|
149
|
+
export function mapModelToBrowserLabel(model) {
|
|
150
|
+
const normalized = normalizeChatGptModelForBrowser(model);
|
|
151
|
+
// Iterate ordered array to find first match (most specific first)
|
|
152
|
+
for (const [key, label] of BROWSER_MODEL_LABELS) {
|
|
153
|
+
if (key === normalized) {
|
|
154
|
+
return label;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return DEFAULT_MODEL_TARGET;
|
|
158
|
+
}
|
|
159
|
+
export function resolveBrowserModelLabel(input, model) {
|
|
160
|
+
const trimmed = input?.trim?.() ?? "";
|
|
161
|
+
if (!trimmed) {
|
|
162
|
+
return mapModelToBrowserLabel(model);
|
|
163
|
+
}
|
|
164
|
+
const normalizedInput = trimmed.toLowerCase();
|
|
165
|
+
if (normalizedInput === model.toLowerCase()) {
|
|
166
|
+
return mapModelToBrowserLabel(model);
|
|
167
|
+
}
|
|
168
|
+
return trimmed;
|
|
169
|
+
}
|
|
170
|
+
function parseRemoteChromeTarget(raw) {
|
|
171
|
+
const target = raw.trim();
|
|
172
|
+
if (!target) {
|
|
173
|
+
throw new Error("Invalid remote-chrome value: expected host:port but received an empty string.");
|
|
174
|
+
}
|
|
175
|
+
const ipv6Match = target.match(/^\[(.+)]:(\d+)$/);
|
|
176
|
+
let host;
|
|
177
|
+
let portSegment;
|
|
178
|
+
if (ipv6Match) {
|
|
179
|
+
host = ipv6Match[1]?.trim();
|
|
180
|
+
portSegment = ipv6Match[2]?.trim();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const lastColon = target.lastIndexOf(":");
|
|
184
|
+
if (lastColon === -1) {
|
|
185
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Expected host:port (IPv6 must use [host]:port notation).`);
|
|
186
|
+
}
|
|
187
|
+
host = target.slice(0, lastColon).trim();
|
|
188
|
+
portSegment = target.slice(lastColon + 1).trim();
|
|
189
|
+
if (host.includes(":")) {
|
|
190
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Wrap IPv6 addresses in brackets, e.g. --remote-chrome "[2001:db8::1]:9222".`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!host) {
|
|
194
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Host portion is missing; expected host:port.`);
|
|
195
|
+
}
|
|
196
|
+
const port = Number.parseInt(portSegment ?? "", 10);
|
|
197
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
|
|
198
|
+
throw new Error(`Invalid remote-chrome port: "${portSegment ?? ""}". Expected a number between 1 and 65535.`);
|
|
199
|
+
}
|
|
200
|
+
return { host, port };
|
|
201
|
+
}
|
|
202
|
+
function parseCookieNames(raw) {
|
|
203
|
+
if (!raw)
|
|
204
|
+
return undefined;
|
|
205
|
+
const names = raw
|
|
206
|
+
.split(",")
|
|
207
|
+
.map((entry) => entry.trim())
|
|
208
|
+
.filter(Boolean);
|
|
209
|
+
return names.length ? names : undefined;
|
|
210
|
+
}
|
|
211
|
+
async function resolveInlineCookies({ inlineArg, inlineFileArg, envPayload, envFile, cwd, }) {
|
|
212
|
+
const tryLoad = async (source, allowPathResolution) => {
|
|
213
|
+
if (!source)
|
|
214
|
+
return undefined;
|
|
215
|
+
const trimmed = source.trim();
|
|
216
|
+
if (!trimmed)
|
|
217
|
+
return undefined;
|
|
218
|
+
if (allowPathResolution) {
|
|
219
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.join(cwd, trimmed);
|
|
220
|
+
try {
|
|
221
|
+
const stat = await fs.stat(resolved);
|
|
222
|
+
if (stat.isFile()) {
|
|
223
|
+
const fileContent = await fs.readFile(resolved, "utf8");
|
|
224
|
+
const parsed = parseInlineCookiesPayload(fileContent);
|
|
225
|
+
if (parsed)
|
|
226
|
+
return parsed;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// not a file; treat as payload below
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return parseInlineCookiesPayload(trimmed);
|
|
234
|
+
};
|
|
235
|
+
const sources = [
|
|
236
|
+
{ value: inlineFileArg, allowPath: true, source: "inline-file" },
|
|
237
|
+
{ value: inlineArg, allowPath: true, source: "inline-arg" },
|
|
238
|
+
{ value: envFile, allowPath: true, source: "env-file" },
|
|
239
|
+
{ value: envPayload, allowPath: false, source: "env-payload" },
|
|
240
|
+
];
|
|
241
|
+
for (const { value, allowPath, source } of sources) {
|
|
242
|
+
const parsed = await tryLoad(value, allowPath);
|
|
243
|
+
if (parsed)
|
|
244
|
+
return { cookies: parsed, source };
|
|
245
|
+
}
|
|
246
|
+
// fallback: ~/.oracle/cookies.{json,base64}
|
|
247
|
+
const oracleHome = getOracleHomeDir();
|
|
248
|
+
const candidates = ["cookies.json", "cookies.base64"];
|
|
249
|
+
for (const file of candidates) {
|
|
250
|
+
const fullPath = path.join(oracleHome, file);
|
|
251
|
+
try {
|
|
252
|
+
const stat = await fs.stat(fullPath);
|
|
253
|
+
if (!stat.isFile())
|
|
254
|
+
continue;
|
|
255
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
256
|
+
const parsed = parseInlineCookiesPayload(content);
|
|
257
|
+
if (parsed)
|
|
258
|
+
return { cookies: parsed, source: `home:${file}` };
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// ignore missing/invalid
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
function parseInlineCookiesPayload(raw) {
|
|
267
|
+
if (!raw)
|
|
268
|
+
return undefined;
|
|
269
|
+
const text = raw.trim();
|
|
270
|
+
if (!text)
|
|
271
|
+
return undefined;
|
|
272
|
+
let jsonPayload = text;
|
|
273
|
+
// Attempt base64 decode first; fall back to raw text on failure.
|
|
274
|
+
try {
|
|
275
|
+
const decoded = Buffer.from(text, "base64").toString("utf8");
|
|
276
|
+
if (decoded.trim().startsWith("[")) {
|
|
277
|
+
jsonPayload = decoded;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// not base64; continue with raw text
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(jsonPayload);
|
|
285
|
+
if (Array.isArray(parsed)) {
|
|
286
|
+
return parsed;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// invalid json; skip silently to keep this hidden flag non-fatal
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { normalizeChatgptUrl, CHATGPT_URL } from "../browserMode.js";
|
|
2
|
+
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
3
|
+
const browser = config.browser;
|
|
4
|
+
if (!browser)
|
|
5
|
+
return;
|
|
6
|
+
const isUnset = (key) => {
|
|
7
|
+
const source = getSource(key);
|
|
8
|
+
return source === undefined || source === "default";
|
|
9
|
+
};
|
|
10
|
+
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
11
|
+
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
12
|
+
if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
13
|
+
options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? "", CHATGPT_URL);
|
|
14
|
+
}
|
|
15
|
+
if (isUnset("browserChromeProfile") && browser.chromeProfile !== undefined) {
|
|
16
|
+
options.browserChromeProfile = browser.chromeProfile ?? undefined;
|
|
17
|
+
}
|
|
18
|
+
if (isUnset("browserChromePath") && browser.chromePath !== undefined) {
|
|
19
|
+
options.browserChromePath = browser.chromePath ?? undefined;
|
|
20
|
+
}
|
|
21
|
+
if (isUnset("browserCookiePath") && browser.chromeCookiePath !== undefined) {
|
|
22
|
+
options.browserCookiePath = browser.chromeCookiePath ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
if (isUnset("browserUrl") && options.browserUrl === undefined && browser.url !== undefined) {
|
|
25
|
+
options.browserUrl = browser.url;
|
|
26
|
+
}
|
|
27
|
+
if (isUnset("browserTimeout") && typeof browser.timeoutMs === "number") {
|
|
28
|
+
options.browserTimeout = String(browser.timeoutMs);
|
|
29
|
+
}
|
|
30
|
+
if (isUnset("browserPort") && typeof browser.debugPort === "number") {
|
|
31
|
+
options.browserPort = browser.debugPort;
|
|
32
|
+
}
|
|
33
|
+
if (isUnset("browserInputTimeout") && typeof browser.inputTimeoutMs === "number") {
|
|
34
|
+
options.browserInputTimeout = String(browser.inputTimeoutMs);
|
|
35
|
+
}
|
|
36
|
+
if (isUnset("browserRecheckDelay") && typeof browser.assistantRecheckDelayMs === "number") {
|
|
37
|
+
options.browserRecheckDelay = String(browser.assistantRecheckDelayMs);
|
|
38
|
+
}
|
|
39
|
+
if (isUnset("browserRecheckTimeout") && typeof browser.assistantRecheckTimeoutMs === "number") {
|
|
40
|
+
options.browserRecheckTimeout = String(browser.assistantRecheckTimeoutMs);
|
|
41
|
+
}
|
|
42
|
+
if (isUnset("browserReuseWait") && typeof browser.reuseChromeWaitMs === "number") {
|
|
43
|
+
options.browserReuseWait = String(browser.reuseChromeWaitMs);
|
|
44
|
+
}
|
|
45
|
+
if (isUnset("browserProfileLockTimeout") && typeof browser.profileLockTimeoutMs === "number") {
|
|
46
|
+
options.browserProfileLockTimeout = String(browser.profileLockTimeoutMs);
|
|
47
|
+
}
|
|
48
|
+
if (isUnset("browserAutoReattachDelay") && typeof browser.autoReattachDelayMs === "number") {
|
|
49
|
+
options.browserAutoReattachDelay = String(browser.autoReattachDelayMs);
|
|
50
|
+
}
|
|
51
|
+
if (isUnset("browserAutoReattachInterval") &&
|
|
52
|
+
typeof browser.autoReattachIntervalMs === "number") {
|
|
53
|
+
options.browserAutoReattachInterval = String(browser.autoReattachIntervalMs);
|
|
54
|
+
}
|
|
55
|
+
if (isUnset("browserAutoReattachTimeout") && typeof browser.autoReattachTimeoutMs === "number") {
|
|
56
|
+
options.browserAutoReattachTimeout = String(browser.autoReattachTimeoutMs);
|
|
57
|
+
}
|
|
58
|
+
if (isUnset("browserCookieWait") && typeof browser.cookieSyncWaitMs === "number") {
|
|
59
|
+
options.browserCookieWait = String(browser.cookieSyncWaitMs);
|
|
60
|
+
}
|
|
61
|
+
if (isUnset("browserHeadless") && browser.headless !== undefined) {
|
|
62
|
+
options.browserHeadless = browser.headless;
|
|
63
|
+
}
|
|
64
|
+
if (isUnset("browserHideWindow") && browser.hideWindow !== undefined) {
|
|
65
|
+
options.browserHideWindow = browser.hideWindow;
|
|
66
|
+
}
|
|
67
|
+
if (isUnset("browserKeepBrowser") && browser.keepBrowser !== undefined) {
|
|
68
|
+
options.browserKeepBrowser = browser.keepBrowser;
|
|
69
|
+
}
|
|
70
|
+
if (isUnset("browserModelStrategy") && browser.modelStrategy !== undefined) {
|
|
71
|
+
options.browserModelStrategy = browser.modelStrategy;
|
|
72
|
+
}
|
|
73
|
+
if (isUnset("browserThinkingTime") && browser.thinkingTime !== undefined) {
|
|
74
|
+
options.browserThinkingTime = browser.thinkingTime;
|
|
75
|
+
}
|
|
76
|
+
if (isUnset("browserManualLogin") && browser.manualLogin !== undefined) {
|
|
77
|
+
options.browserManualLogin = browser.manualLogin;
|
|
78
|
+
}
|
|
79
|
+
if (isUnset("browserManualLoginProfileDir") && browser.manualLoginProfileDir !== undefined) {
|
|
80
|
+
options.browserManualLoginProfileDir = browser.manualLoginProfileDir;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export function warnIfOversizeBundle(estimatedTokens, threshold = 196_000, log = console.log) {
|
|
3
|
+
if (Number.isNaN(estimatedTokens) || estimatedTokens <= threshold) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const msg = `Warning: bundle is ~${estimatedTokens.toLocaleString()} tokens (>${threshold.toLocaleString()}); may exceed model limits.`;
|
|
7
|
+
log(chalk.red(msg));
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isProModel } from "../oracle/modelResolver.js";
|
|
2
|
+
export function shouldDetachSession({
|
|
3
|
+
// Params kept for policy tweaks.
|
|
4
|
+
engine, model, waitPreference, disableDetachEnv, }) {
|
|
5
|
+
if (disableDetachEnv)
|
|
6
|
+
return false;
|
|
7
|
+
// Explicit --wait means "stay attached", regardless of model defaults.
|
|
8
|
+
if (waitPreference)
|
|
9
|
+
return false;
|
|
10
|
+
// Only Pro-tier API runs should start detached by default; browser runs stay inline so failures surface.
|
|
11
|
+
if (isProModel(model) && engine === "api")
|
|
12
|
+
return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from "../oracle.js";
|
|
3
|
+
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
4
|
+
import { assembleBrowserPrompt } from "../browser/prompt.js";
|
|
5
|
+
import { buildTokenEstimateSuffix, formatAttachmentLabel } from "../browser/promptSummary.js";
|
|
6
|
+
import { buildCookiePlan } from "../browser/policies.js";
|
|
7
|
+
export async function runDryRunSummary({ engine, runOptions, cwd, version, log, browserConfig, }, deps = {}) {
|
|
8
|
+
if (engine === "browser") {
|
|
9
|
+
await runBrowserDryRun({ runOptions, cwd, version, log, browserConfig }, deps);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
await runApiDryRun({ runOptions, cwd, version, log }, deps);
|
|
13
|
+
}
|
|
14
|
+
async function runApiDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
15
|
+
const readFilesImpl = deps.readFilesImpl ?? readFiles;
|
|
16
|
+
const files = await readFilesImpl(runOptions.file ?? [], { cwd });
|
|
17
|
+
const systemPrompt = runOptions.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
18
|
+
const combinedPrompt = buildPrompt(runOptions.prompt ?? "", files, cwd);
|
|
19
|
+
const modelConfig = isKnownModel(runOptions.model)
|
|
20
|
+
? MODEL_CONFIGS[runOptions.model]
|
|
21
|
+
: MODEL_CONFIGS["gpt-5.1"];
|
|
22
|
+
const tokenizer = modelConfig.tokenizer;
|
|
23
|
+
const estimatedInputTokens = tokenizer([
|
|
24
|
+
{ role: "system", content: systemPrompt },
|
|
25
|
+
{ role: "user", content: combinedPrompt },
|
|
26
|
+
], TOKENIZER_OPTIONS);
|
|
27
|
+
const headerLine = `[dry-run] Oracle (${version}) would call ${runOptions.model} with ~${estimatedInputTokens.toLocaleString()} tokens and ${files.length} files.`;
|
|
28
|
+
log(chalk.cyan(headerLine));
|
|
29
|
+
if (files.length === 0) {
|
|
30
|
+
log(chalk.dim("[dry-run] No files matched the provided --file patterns."));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const inputBudget = runOptions.maxInput ?? modelConfig.inputLimit;
|
|
34
|
+
const stats = getFileTokenStats(files, {
|
|
35
|
+
cwd,
|
|
36
|
+
tokenizer,
|
|
37
|
+
tokenizerOptions: TOKENIZER_OPTIONS,
|
|
38
|
+
inputTokenBudget: inputBudget,
|
|
39
|
+
});
|
|
40
|
+
printFileTokenStats(stats, { inputTokenBudget: inputBudget, log });
|
|
41
|
+
}
|
|
42
|
+
async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig, }, deps) {
|
|
43
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
44
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
45
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
46
|
+
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
47
|
+
log(chalk.cyan(headerLine));
|
|
48
|
+
logBrowserCookieStrategy(browserConfig, log, "dry-run");
|
|
49
|
+
logBrowserFileSummary(artifacts, log, "dry-run");
|
|
50
|
+
}
|
|
51
|
+
function logBrowserCookieStrategy(browserConfig, log, label) {
|
|
52
|
+
if (!browserConfig)
|
|
53
|
+
return;
|
|
54
|
+
const plan = buildCookiePlan(browserConfig);
|
|
55
|
+
log(chalk.bold(`[${label}] ${plan.description}`));
|
|
56
|
+
}
|
|
57
|
+
function logBrowserFileSummary(artifacts, log, label) {
|
|
58
|
+
if (artifacts.attachments.length > 0) {
|
|
59
|
+
const prefix = artifacts.bundled
|
|
60
|
+
? `[${label}] Bundled upload:`
|
|
61
|
+
: `[${label}] Attachments to upload:`;
|
|
62
|
+
log(chalk.bold(prefix));
|
|
63
|
+
artifacts.attachments.forEach((attachment) => {
|
|
64
|
+
log(` • ${formatAttachmentLabel(attachment)}`);
|
|
65
|
+
});
|
|
66
|
+
if (artifacts.bundled) {
|
|
67
|
+
log(chalk.dim(` (bundled ${artifacts.bundled.originalCount} files into ${artifacts.bundled.bundlePath})`));
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (artifacts.inlineFileCount > 0) {
|
|
72
|
+
log(chalk.bold(`[${label}] Inline file content:`));
|
|
73
|
+
log(` • ${artifacts.inlineFileCount} file${artifacts.inlineFileCount === 1 ? "" : "s"} pasted directly into the composer.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
log(chalk.dim(`[${label}] No files attached.`));
|
|
77
|
+
}
|
|
78
|
+
export async function runBrowserPreview({ runOptions, cwd, version, previewMode, log, }, deps = {}) {
|
|
79
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
80
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
81
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
82
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
83
|
+
log(chalk.cyan(headerLine));
|
|
84
|
+
logBrowserFileSummary(artifacts, log, "preview");
|
|
85
|
+
if (previewMode === "json" || previewMode === "full") {
|
|
86
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
87
|
+
path: attachment.path,
|
|
88
|
+
displayPath: attachment.displayPath,
|
|
89
|
+
sizeBytes: attachment.sizeBytes,
|
|
90
|
+
}));
|
|
91
|
+
const previewPayload = {
|
|
92
|
+
model: runOptions.model,
|
|
93
|
+
engine: "browser",
|
|
94
|
+
composerText: artifacts.composerText,
|
|
95
|
+
attachments: attachmentSummary,
|
|
96
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
97
|
+
bundled: artifacts.bundled,
|
|
98
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
99
|
+
};
|
|
100
|
+
log("");
|
|
101
|
+
log(chalk.bold("Preview JSON"));
|
|
102
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
103
|
+
}
|
|
104
|
+
if (previewMode === "full") {
|
|
105
|
+
log("");
|
|
106
|
+
log(chalk.bold("Composer Text"));
|
|
107
|
+
log(artifacts.composerText || chalk.dim("(empty prompt)"));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export async function shouldBlockDuplicatePrompt({ prompt, force, sessionStore, log = console.log, }) {
|
|
3
|
+
if (force)
|
|
4
|
+
return false;
|
|
5
|
+
const normalized = prompt?.trim();
|
|
6
|
+
if (!normalized)
|
|
7
|
+
return false;
|
|
8
|
+
const running = (await sessionStore.listSessions()).filter((entry) => entry.status === "running");
|
|
9
|
+
const duplicate = running.find((entry) => (entry.options?.prompt?.trim?.() ?? "") === normalized);
|
|
10
|
+
if (!duplicate)
|
|
11
|
+
return false;
|
|
12
|
+
log(chalk.yellow(`A session with the same prompt is already running (${duplicate.id}). Reattach with "oracle session ${duplicate.id}" or rerun with --force to start another run.`));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isProModel } from "../oracle/modelResolver.js";
|
|
2
|
+
export function defaultWaitPreference(model, engine) {
|
|
3
|
+
// Pro-class API runs can take a long time; prefer non-blocking unless explicitly overridden.
|
|
4
|
+
if (engine === "api" && isProModel(model)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return true; // browser or non-pro models are fast enough to block by default
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Determine which engine to use based on CLI flags and the environment.
|
|
11
|
+
*
|
|
12
|
+
* Precedence:
|
|
13
|
+
* 1) Legacy --browser flag forces browser.
|
|
14
|
+
* 2) Explicit --engine value.
|
|
15
|
+
* 3) ORACLE_ENGINE environment override (api|browser).
|
|
16
|
+
* 4) OPENAI_API_KEY decides: api when set, otherwise browser.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveEngine({ engine, browserFlag, env, }) {
|
|
19
|
+
if (browserFlag) {
|
|
20
|
+
return "browser";
|
|
21
|
+
}
|
|
22
|
+
if (engine) {
|
|
23
|
+
return engine;
|
|
24
|
+
}
|
|
25
|
+
const envEngine = normalizeEngineMode(env.ORACLE_ENGINE);
|
|
26
|
+
if (envEngine) {
|
|
27
|
+
return envEngine;
|
|
28
|
+
}
|
|
29
|
+
return env.OPENAI_API_KEY ? "api" : "browser";
|
|
30
|
+
}
|
|
31
|
+
function normalizeEngineMode(raw) {
|
|
32
|
+
if (typeof raw !== "string") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const normalized = raw.trim().toLowerCase();
|
|
36
|
+
if (normalized === "api")
|
|
37
|
+
return "api";
|
|
38
|
+
if (normalized === "browser")
|
|
39
|
+
return "browser";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const LOGGED_SYMBOL = Symbol("oracle.alreadyLogged");
|
|
2
|
+
export function markErrorLogged(error) {
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
error[LOGGED_SYMBOL] = true;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function isErrorLogged(error) {
|
|
8
|
+
return Boolean(error instanceof Error && error[LOGGED_SYMBOL]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { normalizeMaxFileSizeBytes } from "../oracle/files.js";
|
|
2
|
+
export function resolveConfiguredMaxFileSizeBytes(userConfig, env = process.env) {
|
|
3
|
+
const envValue = env.ORACLE_MAX_FILE_SIZE_BYTES?.trim();
|
|
4
|
+
if (envValue) {
|
|
5
|
+
return normalizeMaxFileSizeBytes(envValue, "ORACLE_MAX_FILE_SIZE_BYTES");
|
|
6
|
+
}
|
|
7
|
+
if (userConfig?.maxFileSizeBytes !== undefined) {
|
|
8
|
+
return normalizeMaxFileSizeBytes(userConfig.maxFileSizeBytes, "config.maxFileSizeBytes");
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatCompactNumber(value) {
|
|
2
|
+
if (Number.isNaN(value) || !Number.isFinite(value))
|
|
3
|
+
return "0";
|
|
4
|
+
const abs = Math.abs(value);
|
|
5
|
+
const stripTrailingZero = (text) => text.replace(/\.0$/, "");
|
|
6
|
+
if (abs >= 1_000_000) {
|
|
7
|
+
return `${stripTrailingZero((value / 1_000_000).toFixed(1))}m`;
|
|
8
|
+
}
|
|
9
|
+
if (abs >= 1_000) {
|
|
10
|
+
return `${stripTrailingZero((value / 1_000).toFixed(1))}k`;
|
|
11
|
+
}
|
|
12
|
+
return value.toLocaleString();
|
|
13
|
+
}
|