@steipete/oracle 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +53 -15
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +41 -7
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/sessionRunner.js +72 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -11
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +5 -1
- package/package.json +3 -1
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import "dotenv/config";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { once } from "node:events";
|
|
6
5
|
import { Command, Option } from "commander";
|
|
7
6
|
// Allow `npx @steipete/oracle oracle-mcp` to resolve the MCP server even though npx runs the default binary.
|
|
8
7
|
if (process.argv[2] === "oracle-mcp") {
|
|
@@ -15,41 +14,33 @@ import { shouldRequirePrompt } from "../src/cli/promptRequirement.js";
|
|
|
15
14
|
import { resolveDashPrompt } from "../src/cli/stdin.js";
|
|
16
15
|
import chalk from "chalk";
|
|
17
16
|
import { sessionStore, pruneOldSessions } from "../src/sessionStore.js";
|
|
18
|
-
import { DEFAULT_MODEL, MODEL_CONFIGS
|
|
17
|
+
import { DEFAULT_MODEL, MODEL_CONFIGS } from "../src/oracle/config.js";
|
|
19
18
|
import { isKnownModel } from "../src/oracle/modelResolver.js";
|
|
20
|
-
import { CHATGPT_URL } from "../src/
|
|
21
|
-
import { createRemoteBrowserExecutor } from "../src/remote/client.js";
|
|
22
|
-
import { createGeminiWebExecutor } from "../src/gemini-web/index.js";
|
|
19
|
+
import { CHATGPT_URL } from "../src/browser/constants.js";
|
|
23
20
|
import { applyHelpStyling } from "../src/cli/help.js";
|
|
24
21
|
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
25
22
|
import { copyToClipboard } from "../src/cli/clipboard.js";
|
|
26
23
|
import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
|
|
27
24
|
import { shouldDetachSession } from "../src/cli/detach.js";
|
|
28
25
|
import { applyHiddenAliases } from "../src/cli/hiddenAliases.js";
|
|
29
|
-
import { buildBrowserConfig, resolveBrowserModelLabel } from "../src/cli/browserConfig.js";
|
|
30
|
-
import { performSessionRun } from "../src/cli/sessionRunner.js";
|
|
31
26
|
import { isMediaFile } from "../src/browser/prompt.js";
|
|
32
|
-
import { attachSession, showStatus, formatCompletionSummary } from "../src/cli/sessionDisplay.js";
|
|
33
27
|
import { formatCompactNumber } from "../src/cli/format.js";
|
|
34
28
|
import { formatIntroLine } from "../src/cli/tagline.js";
|
|
35
29
|
import { warnIfOversizeBundle } from "../src/cli/bundleWarnings.js";
|
|
36
30
|
import { formatRenderedMarkdown } from "../src/cli/renderOutput.js";
|
|
37
31
|
import { resolveRenderFlag, resolveRenderPlain } from "../src/cli/renderFlags.js";
|
|
38
|
-
import { resolveGeminiModelId } from "../src/oracle/
|
|
39
|
-
import { handleSessionCommand, formatSessionCleanupMessage, } from "../src/cli/sessionCommand.js";
|
|
32
|
+
import { resolveGeminiModelId } from "../src/oracle/geminiModels.js";
|
|
40
33
|
import { isErrorLogged } from "../src/cli/errorUtils.js";
|
|
41
|
-
import { handleSessionAlias, handleStatusFlag } from "../src/cli/rootAlias.js";
|
|
42
34
|
import { resolveOutputPath } from "../src/cli/writeOutputPath.js";
|
|
43
|
-
import { showBrowserTabsStatus } from "../src/cli/browserTabs.js";
|
|
44
35
|
import { getCliVersion } from "../src/version.js";
|
|
45
|
-
import { runDryRunSummary, runBrowserPreview } from "../src/cli/dryRun.js";
|
|
46
|
-
import { launchTui } from "../src/cli/tui/index.js";
|
|
47
36
|
import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from "../src/cli/notifier.js";
|
|
48
37
|
import { loadUserConfig } from "../src/config.js";
|
|
49
|
-
import { applyBrowserDefaultsFromConfig } from "../src/cli/browserDefaults.js";
|
|
50
38
|
import { shouldBlockDuplicatePrompt } from "../src/cli/duplicatePromptGuard.js";
|
|
51
39
|
import { resolveRemoteServiceConfig } from "../src/remote/remoteServiceConfig.js";
|
|
52
40
|
import { resolveConfiguredMaxFileSizeBytes } from "../src/cli/fileSize.js";
|
|
41
|
+
import { isAzureOpenAICandidateModel, validateProviderRouting, } from "../src/oracle/providerRouting.js";
|
|
42
|
+
import { buildSessionLifecycle, formatSessionLifecycleBlock } from "../src/cli/sessionLifecycle.js";
|
|
43
|
+
import { buildDetachedPerfTraceEnv, createPerfTrace, isTraceValueFlag, } from "../src/cli/perfTrace.js";
|
|
53
44
|
const VERSION = getCliVersion();
|
|
54
45
|
const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
|
|
55
46
|
const LEGACY_FLAG_ALIASES = new Map([
|
|
@@ -57,19 +48,117 @@ const LEGACY_FLAG_ALIASES = new Map([
|
|
|
57
48
|
["--[no-]notify-sound", "--notify-sound"],
|
|
58
49
|
["--[no-]background", "--background"],
|
|
59
50
|
]);
|
|
60
|
-
const
|
|
51
|
+
const legacyNormalizedArgv = process.argv.map((arg, index) => {
|
|
61
52
|
if (index < 2)
|
|
62
53
|
return arg;
|
|
63
54
|
return LEGACY_FLAG_ALIASES.get(arg) ?? arg;
|
|
64
55
|
});
|
|
65
|
-
const rawCliArgs =
|
|
66
|
-
const
|
|
56
|
+
const rawCliArgs = legacyNormalizedArgv.slice(2);
|
|
57
|
+
const hasCliEntrypointArg = rawCliArgs[0] === CLI_ENTRYPOINT;
|
|
58
|
+
const originalUserCliArgs = hasCliEntrypointArg ? rawCliArgs.slice(1) : rawCliArgs;
|
|
59
|
+
const perfTraceArgs = normalizePerfTraceArgs(originalUserCliArgs);
|
|
60
|
+
const userCliArgs = perfTraceArgs.args;
|
|
61
|
+
const normalizedArgv = [
|
|
62
|
+
...legacyNormalizedArgv.slice(0, 2),
|
|
63
|
+
...(hasCliEntrypointArg ? [CLI_ENTRYPOINT] : []),
|
|
64
|
+
...userCliArgs,
|
|
65
|
+
];
|
|
66
|
+
const routingCliArgs = stripPerfTraceArgs(userCliArgs);
|
|
67
67
|
const isTty = process.stdout.isTTY;
|
|
68
|
-
const
|
|
69
|
-
|
|
68
|
+
const perfTrace = createPerfTrace({
|
|
69
|
+
value: perfTraceArgs.value,
|
|
70
|
+
argv: userCliArgs,
|
|
71
|
+
version: VERSION,
|
|
72
|
+
});
|
|
73
|
+
process.once("exit", (code) => {
|
|
74
|
+
try {
|
|
75
|
+
perfTrace.flush(code);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(`Failed to write perf trace: ${error instanceof Error ? error.message : error}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
function stripPerfTraceArgs(args) {
|
|
82
|
+
const stripped = [];
|
|
83
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
84
|
+
const arg = args[index];
|
|
85
|
+
if (arg === "--perf-trace")
|
|
86
|
+
continue;
|
|
87
|
+
if (arg === "--perf-trace-path") {
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith("--perf-trace-path="))
|
|
92
|
+
continue;
|
|
93
|
+
stripped.push(arg);
|
|
94
|
+
}
|
|
95
|
+
return stripped;
|
|
96
|
+
}
|
|
97
|
+
function normalizePerfTraceArgs(args) {
|
|
98
|
+
const normalized = [];
|
|
99
|
+
let skipNextValue = false;
|
|
100
|
+
let value;
|
|
101
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
102
|
+
const arg = args[index];
|
|
103
|
+
if (skipNextValue) {
|
|
104
|
+
normalized.push(arg);
|
|
105
|
+
skipNextValue = false;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--") {
|
|
109
|
+
normalized.push(...args.slice(index));
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (arg.startsWith("--perf-trace=")) {
|
|
113
|
+
const tracePath = arg.slice("--perf-trace=".length);
|
|
114
|
+
if (tracePath) {
|
|
115
|
+
normalized.push("--perf-trace", "--perf-trace-path", tracePath);
|
|
116
|
+
value = tracePath;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
normalized.push("--perf-trace");
|
|
120
|
+
value = true;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (arg === "--perf-trace-path") {
|
|
125
|
+
const tracePath = args[index + 1];
|
|
126
|
+
if (!tracePath || tracePath.startsWith("-")) {
|
|
127
|
+
return { args: normalized, error: "option '--perf-trace-path <path>' argument missing" };
|
|
128
|
+
}
|
|
129
|
+
normalized.push(arg, tracePath);
|
|
130
|
+
value = tracePath;
|
|
131
|
+
index += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (arg.startsWith("--perf-trace-path=") && !arg.slice("--perf-trace-path=".length)) {
|
|
135
|
+
return { args: normalized, error: "option '--perf-trace-path <path>' argument missing" };
|
|
136
|
+
}
|
|
137
|
+
if (arg.startsWith("--perf-trace-path=")) {
|
|
138
|
+
value = arg.slice("--perf-trace-path=".length);
|
|
139
|
+
}
|
|
140
|
+
else if (arg === "--perf-trace") {
|
|
141
|
+
value ??= true;
|
|
142
|
+
}
|
|
143
|
+
normalized.push(arg);
|
|
144
|
+
const equalsIndex = arg.indexOf("=");
|
|
145
|
+
const flag = equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
|
|
146
|
+
skipNextValue = equalsIndex < 0 && isTraceValueFlag(flag);
|
|
147
|
+
}
|
|
148
|
+
return { args: normalized, value };
|
|
149
|
+
}
|
|
150
|
+
const doctorArgIndex = routingCliArgs.indexOf("doctor");
|
|
151
|
+
const doctorJsonRequested = doctorArgIndex >= 0 && routingCliArgs.slice(doctorArgIndex).includes("--json");
|
|
152
|
+
const docsArgIndex = routingCliArgs.indexOf("docs");
|
|
153
|
+
const docsCheckRequested = docsArgIndex >= 0 && routingCliArgs[docsArgIndex + 1] === "check";
|
|
154
|
+
const suppressIntro = doctorJsonRequested ||
|
|
155
|
+
docsCheckRequested ||
|
|
156
|
+
(routingCliArgs[0] === "bridge" &&
|
|
157
|
+
(routingCliArgs[1] === "codex-config" || routingCliArgs[1] === "claude-config"));
|
|
70
158
|
const program = new Command();
|
|
71
159
|
let introPrinted = false;
|
|
72
|
-
program.hook("preAction", () => {
|
|
160
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
161
|
+
perfTrace.mark("pre-action", { command: actionCommand.name() || "root" });
|
|
73
162
|
if (suppressIntro)
|
|
74
163
|
return;
|
|
75
164
|
if (introPrinted)
|
|
@@ -82,10 +171,10 @@ program.hook("preAction", async (thisCommand) => {
|
|
|
82
171
|
if (thisCommand !== program) {
|
|
83
172
|
return;
|
|
84
173
|
}
|
|
85
|
-
if (
|
|
174
|
+
if (routingCliArgs.some((arg) => arg === "--help" || arg === "-h")) {
|
|
86
175
|
return;
|
|
87
176
|
}
|
|
88
|
-
if (
|
|
177
|
+
if (routingCliArgs.length === 0) {
|
|
89
178
|
// Let the root action handle zero-arg entry (help + hint to `oracle tui`).
|
|
90
179
|
return;
|
|
91
180
|
}
|
|
@@ -101,10 +190,9 @@ program.hook("preAction", async (thisCommand) => {
|
|
|
101
190
|
opts.prompt = resolvedPrompt;
|
|
102
191
|
thisCommand.setOptionValue("prompt", resolvedPrompt);
|
|
103
192
|
}
|
|
104
|
-
if (shouldRequirePrompt(
|
|
193
|
+
if (shouldRequirePrompt(routingCliArgs, opts)) {
|
|
105
194
|
console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
|
|
106
|
-
thisCommand.help({ error:
|
|
107
|
-
process.exitCode = 1;
|
|
195
|
+
thisCommand.help({ error: true });
|
|
108
196
|
return;
|
|
109
197
|
}
|
|
110
198
|
});
|
|
@@ -118,6 +206,7 @@ program
|
|
|
118
206
|
.option("--followup <sessionId|responseId>", "Continue an OpenAI/Azure Responses API run from a stored response id (resp_...) or from a stored oracle session id.")
|
|
119
207
|
.option("--followup-model <model>", "When following up a multi-model session, choose which model response to continue from.")
|
|
120
208
|
.option("-f, --file <paths...>", "Files/directories or glob patterns to attach (prefix with !pattern to exclude). Oversized files are rejected automatically (default cap: 1 MB; configurable via ORACLE_MAX_FILE_SIZE_BYTES or config.maxFileSizeBytes).", collectPaths, [])
|
|
209
|
+
.option("--max-file-size-bytes <bytes>", "Reject files larger than this many bytes.", parseIntOption)
|
|
121
210
|
.addOption(new Option("--include <paths...>", "Alias for --file.")
|
|
122
211
|
.argParser(collectPaths)
|
|
123
212
|
.default([])
|
|
@@ -151,7 +240,7 @@ program
|
|
|
151
240
|
.addOption(new Option("--no-notify", "Disable desktop notifications.").default(undefined))
|
|
152
241
|
.addOption(new Option("--notify-sound", "Play a notification sound on completion (default off).").default(undefined))
|
|
153
242
|
.addOption(new Option("--no-notify-sound", "Disable notification sounds.").default(undefined))
|
|
154
|
-
.addOption(new Option("--timeout <seconds|auto>", "Overall timeout before aborting the API call (auto = 60m for Pro models, 120s otherwise).")
|
|
243
|
+
.addOption(new Option("--timeout <seconds|duration|auto>", "Overall timeout before aborting the API call (auto = 60m for Pro models, 120s otherwise).")
|
|
155
244
|
.argParser(parseTimeoutOption)
|
|
156
245
|
.default("auto"))
|
|
157
246
|
.addOption(new Option("--background", "Use Responses API background mode (create + retrieve) for API runs.").default(undefined))
|
|
@@ -171,6 +260,10 @@ program
|
|
|
171
260
|
.choices(["summary", "json", "full"])
|
|
172
261
|
.preset("summary")
|
|
173
262
|
.default(false))
|
|
263
|
+
.option("--route", "Print API provider route plan and exit.", false)
|
|
264
|
+
.option("--preflight", "Check API provider readiness for the requested model(s) and exit.", false)
|
|
265
|
+
.addOption(new Option("--perf-trace", "Write CLI performance timing trace JSON (or set ORACLE_PERF_TRACE=1/path).").default(false))
|
|
266
|
+
.addOption(new Option("--perf-trace-path <path>", "Write CLI performance timing trace JSON to an explicit path.").default(undefined))
|
|
174
267
|
.addOption(new Option("--exec-session <id>").hideHelp())
|
|
175
268
|
.addOption(new Option("--session <id>").hideHelp())
|
|
176
269
|
.addOption(new Option("--status", "Show stored sessions (alias for `oracle status`).")
|
|
@@ -180,6 +273,10 @@ program
|
|
|
180
273
|
.option("--render", "Alias for --render-markdown.", false)
|
|
181
274
|
.option("--render-plain", "Render markdown without ANSI/highlighting (use plain text even in a TTY).", false)
|
|
182
275
|
.option("--write-output <path>", "Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).")
|
|
276
|
+
.option("--allow-partial", "Exit 0 for multi-model runs when at least one model succeeds.", false)
|
|
277
|
+
.addOption(new Option("--partial <mode>", "Multi-model failure policy (fail | ok).")
|
|
278
|
+
.choices(["fail", "ok"])
|
|
279
|
+
.default(undefined))
|
|
183
280
|
.option("--verbose-render", "Show render/TTY diagnostics when replaying sessions.", false)
|
|
184
281
|
.addOption(new Option("--search <mode>", "Set server-side search behavior (on/off).")
|
|
185
282
|
.argParser(parseSearchOption)
|
|
@@ -191,6 +288,10 @@ program
|
|
|
191
288
|
.argParser(parseIntOption)
|
|
192
289
|
.hideHelp())
|
|
193
290
|
.option("--base-url <url>", "Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).")
|
|
291
|
+
.addOption(new Option("--provider <provider>", "Choose API provider routing: auto, openai, or azure. Use openai to ignore Azure env/config.")
|
|
292
|
+
.choices(["auto", "openai", "azure"])
|
|
293
|
+
.default("auto"))
|
|
294
|
+
.option("--no-azure", "Disable Azure OpenAI routing for this run (same as --provider openai).")
|
|
194
295
|
.option("--azure-endpoint <url>", "Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).")
|
|
195
296
|
.option("--azure-deployment <name>", "Azure OpenAI Deployment Name.")
|
|
196
297
|
.option("--azure-api-version <version>", "Azure OpenAI API Version.")
|
|
@@ -221,6 +322,7 @@ program
|
|
|
221
322
|
.addOption(new Option("--browser-inline-cookies-file <path>", "Load inline cookies from file (JSON or base64 JSON).").hideHelp())
|
|
222
323
|
.addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
|
|
223
324
|
.addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
|
|
325
|
+
.addOption(new Option("--browser-manual-login-profile-dir <path>", "Persistent Chrome profile directory for manual-login browser runs.").hideHelp())
|
|
224
326
|
.addOption(new Option("--browser-headless", "Launch Chrome in headless mode.").hideHelp())
|
|
225
327
|
.addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
|
|
226
328
|
.addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
|
|
@@ -243,6 +345,9 @@ program
|
|
|
243
345
|
.addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
|
|
244
346
|
.addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
|
|
245
347
|
.addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
|
|
348
|
+
.addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled: text (default) or zip.")
|
|
349
|
+
.choices(["text", "zip"])
|
|
350
|
+
.default("text"))
|
|
246
351
|
.addOption(new Option("--youtube <url>", "YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com)."))
|
|
247
352
|
.addOption(new Option("--generate-image <file>", "Generate image and save to file (Gemini browser mode; ChatGPT browser mode saves downloadable image artifacts when present)."))
|
|
248
353
|
.addOption(new Option("--edit-image <file>", "Edit existing image (Gemini browser mode; for ChatGPT attach source images with --file and use --generate-image for output)."))
|
|
@@ -391,9 +496,46 @@ program
|
|
|
391
496
|
.command("tui")
|
|
392
497
|
.description("Launch the interactive terminal UI for humans (no automation).")
|
|
393
498
|
.action(async () => {
|
|
499
|
+
const { launchTui } = await import("../src/cli/tui/index.js");
|
|
394
500
|
await sessionStore.ensureStorage();
|
|
395
501
|
await launchTui({ version: VERSION, printIntro: false });
|
|
396
502
|
});
|
|
503
|
+
program
|
|
504
|
+
.command("doctor")
|
|
505
|
+
.description("Diagnose Oracle API provider readiness and routing.")
|
|
506
|
+
.option("--providers", "Inspect API provider keys and route choices.", false)
|
|
507
|
+
.option("--models <models>", "Comma-separated API model list to inspect.")
|
|
508
|
+
.option("-m, --model <model>", "Single API model to inspect.")
|
|
509
|
+
.addOption(new Option("--provider <provider>", "Choose API provider routing: auto, openai, or azure.")
|
|
510
|
+
.choices(["auto", "openai", "azure"])
|
|
511
|
+
.default("auto"))
|
|
512
|
+
.option("--no-azure", "Disable Azure OpenAI routing for this inspection.")
|
|
513
|
+
.option("--azure-endpoint <url>", "Azure OpenAI Endpoint.")
|
|
514
|
+
.option("--azure-deployment <name>", "Azure OpenAI Deployment Name.")
|
|
515
|
+
.option("--azure-api-version <version>", "Azure OpenAI API Version.")
|
|
516
|
+
.option("--base-url <url>", "Override OpenAI-compatible base URL.")
|
|
517
|
+
.option("--json", "Print structured JSON.", false)
|
|
518
|
+
.action(async function () {
|
|
519
|
+
const { runProviderDoctor } = await import("../src/cli/providerDoctor.js");
|
|
520
|
+
await runProviderDoctor(this.optsWithGlobals());
|
|
521
|
+
});
|
|
522
|
+
const docsCommand = program.command("docs").description("Documentation maintenance utilities.");
|
|
523
|
+
docsCommand
|
|
524
|
+
.command("check")
|
|
525
|
+
.description("Check documented CLI flags against Commander help metadata.")
|
|
526
|
+
.option("--docs-path <file...>", "Markdown files to check (default core shipped docs).")
|
|
527
|
+
.option("--json", "Print structured JSON.", false)
|
|
528
|
+
.action(async (options) => {
|
|
529
|
+
const { checkDocsFlags, printDocsCheckResult } = await import("../src/cli/docsCheck.js");
|
|
530
|
+
const result = await checkDocsFlags({ command: program, paths: options.docsPath });
|
|
531
|
+
if (options.json) {
|
|
532
|
+
console.log(JSON.stringify(result, null, 2));
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
printDocsCheckResult(result);
|
|
536
|
+
}
|
|
537
|
+
process.exitCode = result.issues.length > 0 ? 1 : 0;
|
|
538
|
+
});
|
|
397
539
|
program
|
|
398
540
|
.command("session [id]")
|
|
399
541
|
.description("Attach to a stored session or list recent sessions when no ID is provided.")
|
|
@@ -412,6 +554,7 @@ program
|
|
|
412
554
|
.option("--browser-tab <ref>", "Override the browser tab ref used for harvesting/live tail (current, target id, URL, or title substring).")
|
|
413
555
|
.addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
|
|
414
556
|
.action(async (sessionId, _options, cmd) => {
|
|
557
|
+
const { handleSessionCommand } = await import("../src/cli/sessionCommand.js");
|
|
415
558
|
await handleSessionCommand(sessionId, cmd);
|
|
416
559
|
});
|
|
417
560
|
program
|
|
@@ -435,6 +578,7 @@ program
|
|
|
435
578
|
process.exitCode = 1;
|
|
436
579
|
return;
|
|
437
580
|
}
|
|
581
|
+
const { showBrowserTabsStatus } = await import("../src/cli/browserTabs.js");
|
|
438
582
|
await showBrowserTabsStatus();
|
|
439
583
|
return;
|
|
440
584
|
}
|
|
@@ -449,6 +593,7 @@ program
|
|
|
449
593
|
const includeAll = statusOptions.all;
|
|
450
594
|
const result = await sessionStore.deleteOlderThan({ hours, includeAll });
|
|
451
595
|
const scope = includeAll ? "all stored sessions" : `sessions older than ${hours}h`;
|
|
596
|
+
const { formatSessionCleanupMessage } = await import("../src/cli/sessionCommand.js");
|
|
452
597
|
console.log(formatSessionCleanupMessage(result, scope));
|
|
453
598
|
return;
|
|
454
599
|
}
|
|
@@ -463,10 +608,12 @@ program
|
|
|
463
608
|
? process.stdout.isTTY
|
|
464
609
|
: false;
|
|
465
610
|
const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
|
|
611
|
+
const { attachSession } = await import("../src/cli/sessionDisplay.js");
|
|
466
612
|
await attachSession(sessionId, { renderMarkdown, renderPrompt: !statusOptions.hidePrompt });
|
|
467
613
|
return;
|
|
468
614
|
}
|
|
469
615
|
const showExamples = usesDefaultStatusFilters(command);
|
|
616
|
+
const { showStatus } = await import("../src/cli/sessionDisplay.js");
|
|
470
617
|
await showStatus({
|
|
471
618
|
hours: statusOptions.all ? Infinity : statusOptions.hours,
|
|
472
619
|
includeAll: statusOptions.all,
|
|
@@ -490,6 +637,13 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
490
637
|
throw new Error("Prompt is required.");
|
|
491
638
|
}
|
|
492
639
|
const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
|
|
640
|
+
const timeoutSeconds = overrides.timeoutSeconds ?? options.timeout;
|
|
641
|
+
const resolvedTimeoutMs = typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0
|
|
642
|
+
? timeoutSeconds * 1000
|
|
643
|
+
: undefined;
|
|
644
|
+
const httpTimeoutMs = overrides.httpTimeoutMs ?? options.httpTimeout ?? resolvedTimeoutMs;
|
|
645
|
+
const zombieTimeoutMs = overrides.zombieTimeoutMs ?? options.zombieTimeout ?? resolvedTimeoutMs;
|
|
646
|
+
const partialMode = options.allowPartial ? "ok" : options.partial;
|
|
493
647
|
const azure = options.azureEndpoint || overrides.azure?.endpoint
|
|
494
648
|
? {
|
|
495
649
|
endpoint: overrides.azure?.endpoint ?? options.azureEndpoint,
|
|
@@ -510,15 +664,17 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
510
664
|
maxInput: overrides.maxInput ?? options.maxInput,
|
|
511
665
|
maxOutput: overrides.maxOutput ?? options.maxOutput,
|
|
512
666
|
system: overrides.system ?? options.system,
|
|
513
|
-
timeoutSeconds
|
|
514
|
-
httpTimeoutMs
|
|
515
|
-
zombieTimeoutMs
|
|
667
|
+
timeoutSeconds,
|
|
668
|
+
httpTimeoutMs,
|
|
669
|
+
zombieTimeoutMs,
|
|
516
670
|
zombieUseLastActivity: overrides.zombieUseLastActivity ?? options.zombieLastActivity,
|
|
671
|
+
partialMode,
|
|
517
672
|
silent: overrides.silent ?? options.silent,
|
|
518
673
|
search: overrides.search ?? options.search,
|
|
519
674
|
preview: overrides.preview ?? undefined,
|
|
520
675
|
previewMode: overrides.previewMode ?? options.previewMode,
|
|
521
676
|
apiKey: overrides.apiKey ?? options.apiKey,
|
|
677
|
+
provider: overrides.provider ?? options.provider,
|
|
522
678
|
baseUrl: normalizedBaseUrl,
|
|
523
679
|
azure,
|
|
524
680
|
sessionId: overrides.sessionId ?? options.sessionId,
|
|
@@ -529,6 +685,7 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
529
685
|
"auto",
|
|
530
686
|
browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
|
|
531
687
|
browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
|
|
688
|
+
browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "text",
|
|
532
689
|
generateImage: overrides.generateImage ?? options.generateImage,
|
|
533
690
|
outputPath: overrides.outputPath ?? options.output,
|
|
534
691
|
browserFollowUps: overrides.browserFollowUps ?? options.browserFollowUp ?? [],
|
|
@@ -537,6 +694,59 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
537
694
|
writeOutputPath: overrides.writeOutputPath ?? options.writeOutputPath,
|
|
538
695
|
};
|
|
539
696
|
}
|
|
697
|
+
function resolveApiProviderMode(options) {
|
|
698
|
+
const provider = options.provider ?? "auto";
|
|
699
|
+
if (provider === "azure" && options.azure === false) {
|
|
700
|
+
throw new Error("--provider azure cannot be combined with --no-azure.");
|
|
701
|
+
}
|
|
702
|
+
if (options.azure === false) {
|
|
703
|
+
return "openai";
|
|
704
|
+
}
|
|
705
|
+
return provider;
|
|
706
|
+
}
|
|
707
|
+
function hasExplicitAzureOption(optionUsesDefault) {
|
|
708
|
+
return (!optionUsesDefault("azureEndpoint") ||
|
|
709
|
+
!optionUsesDefault("azureDeployment") ||
|
|
710
|
+
!optionUsesDefault("azureApiVersion"));
|
|
711
|
+
}
|
|
712
|
+
function firstNonEmpty(...values) {
|
|
713
|
+
return values.find((value) => value?.trim());
|
|
714
|
+
}
|
|
715
|
+
function formatRouteTargetForLog(raw) {
|
|
716
|
+
if (!raw)
|
|
717
|
+
return "";
|
|
718
|
+
try {
|
|
719
|
+
const parsed = new URL(raw);
|
|
720
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
721
|
+
let routePath = "";
|
|
722
|
+
if (segments.length > 0) {
|
|
723
|
+
routePath = `/${segments[0]}`;
|
|
724
|
+
if (segments.length > 1) {
|
|
725
|
+
routePath += "/...";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return `${parsed.host}${routePath}`;
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return raw.replace(/^https?:\/\//u, "").replace(/\/+$/u, "");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function validateApiProviderRoutingForCli(runOptions) {
|
|
735
|
+
const models = Array.isArray(runOptions.models) && runOptions.models.length > 0
|
|
736
|
+
? runOptions.models
|
|
737
|
+
: [runOptions.model];
|
|
738
|
+
for (const model of models) {
|
|
739
|
+
validateProviderRouting({
|
|
740
|
+
model,
|
|
741
|
+
providerMode: runOptions.provider,
|
|
742
|
+
azure: runOptions.azure,
|
|
743
|
+
}, {
|
|
744
|
+
onAzureDeploymentMissing: (state) => {
|
|
745
|
+
console.log(chalk.dim(`Provider: Azure OpenAI | endpoint: ${formatRouteTargetForLog(state.azureEndpoint)} | deployment: none | key: ${runOptions.apiKey ? "apiKey option" : "AZURE_OPENAI_API_KEY|OPENAI_API_KEY"}`));
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
540
750
|
export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
|
|
541
751
|
if (sessionMode === "browser" && runOptions.search === false) {
|
|
542
752
|
logFn(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
|
|
@@ -696,18 +906,21 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
696
906
|
preview: false,
|
|
697
907
|
previewMode: undefined,
|
|
698
908
|
apiKey: undefined,
|
|
909
|
+
provider: stored.provider,
|
|
699
910
|
baseUrl: normalizeBaseUrl(stored.baseUrl),
|
|
700
911
|
azure: stored.azure,
|
|
701
912
|
timeoutSeconds: stored.timeoutSeconds,
|
|
702
913
|
httpTimeoutMs: stored.httpTimeoutMs,
|
|
703
914
|
zombieTimeoutMs: stored.zombieTimeoutMs,
|
|
704
915
|
zombieUseLastActivity: stored.zombieUseLastActivity,
|
|
916
|
+
partialMode: stored.partialMode,
|
|
705
917
|
sessionId: metadata.id,
|
|
706
918
|
verbose: stored.verbose,
|
|
707
919
|
heartbeatIntervalMs: stored.heartbeatIntervalMs,
|
|
708
920
|
browserAttachments: stored.browserAttachments,
|
|
709
921
|
browserInlineFiles: stored.browserInlineFiles,
|
|
710
922
|
browserBundleFiles: stored.browserBundleFiles,
|
|
923
|
+
browserBundleFormat: stored.browserBundleFormat,
|
|
711
924
|
browserFollowUps: stored.browserFollowUps,
|
|
712
925
|
background: stored.background,
|
|
713
926
|
renderPlain: stored.renderPlain,
|
|
@@ -721,7 +934,9 @@ function getBrowserConfigFromMetadata(metadata) {
|
|
|
721
934
|
return metadata.options?.browserConfig ?? metadata.browser?.config;
|
|
722
935
|
}
|
|
723
936
|
async function runRootCommand(options) {
|
|
937
|
+
perfTrace.mark("root-command-start");
|
|
724
938
|
if (process.env.ORACLE_FORCE_TUI === "1") {
|
|
939
|
+
const { launchTui } = await import("../src/cli/tui/index.js");
|
|
725
940
|
await sessionStore.ensureStorage();
|
|
726
941
|
await launchTui({ version: VERSION, printIntro: false });
|
|
727
942
|
return;
|
|
@@ -729,17 +944,14 @@ async function runRootCommand(options) {
|
|
|
729
944
|
const userConfig = (await loadUserConfig()).config;
|
|
730
945
|
const helpRequested = rawCliArgs.some((arg) => arg === "--help" || arg === "-h");
|
|
731
946
|
const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
|
|
732
|
-
if (multiModelProvided) {
|
|
733
|
-
const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? "");
|
|
734
|
-
if (modelFromConfigOrCli) {
|
|
735
|
-
throw new Error("--models cannot be combined with --model.");
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
947
|
const optionUsesDefault = (name) => {
|
|
739
948
|
// Commander reports undefined for untouched options, so treat undefined/default the same
|
|
740
949
|
const source = program.getOptionValueSource?.(name);
|
|
741
950
|
return source == null || source === "default";
|
|
742
951
|
};
|
|
952
|
+
if (multiModelProvided && !optionUsesDefault("model") && normalizeModelOption(options.model)) {
|
|
953
|
+
throw new Error("--models cannot be combined with --model.");
|
|
954
|
+
}
|
|
743
955
|
if (helpRequested) {
|
|
744
956
|
if (options.verbose) {
|
|
745
957
|
console.log("");
|
|
@@ -787,31 +999,19 @@ async function runRootCommand(options) {
|
|
|
787
999
|
if (remoteHost) {
|
|
788
1000
|
console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
|
|
789
1001
|
}
|
|
790
|
-
if (
|
|
1002
|
+
if (routingCliArgs.length === 0) {
|
|
791
1003
|
console.log(chalk.yellow("No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI."));
|
|
792
1004
|
program.outputHelp();
|
|
793
1005
|
return;
|
|
794
1006
|
}
|
|
795
|
-
const retentionHours = typeof options.retainHours === "number" ? options.retainHours : undefined;
|
|
796
|
-
await sessionStore.ensureStorage();
|
|
797
|
-
await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
|
|
798
1007
|
if (options.debugHelp) {
|
|
799
1008
|
printDebugHelp(program.name());
|
|
800
1009
|
return;
|
|
801
1010
|
}
|
|
802
|
-
if (options.dryRun &&
|
|
1011
|
+
if (options.dryRun && renderMarkdown) {
|
|
803
1012
|
throw new Error("--dry-run cannot be combined with --render-markdown.");
|
|
804
1013
|
}
|
|
805
|
-
|
|
806
|
-
let engine = resolveEngine({
|
|
807
|
-
engine: preferredEngine,
|
|
808
|
-
browserFlag: options.browser,
|
|
809
|
-
env: process.env,
|
|
810
|
-
});
|
|
811
|
-
if (options.browser) {
|
|
812
|
-
console.log(chalk.yellow("`--browser` is deprecated; use `--engine browser` instead."));
|
|
813
|
-
}
|
|
814
|
-
if (optionUsesDefault("model") && userConfig.model) {
|
|
1014
|
+
if (!multiModelProvided && optionUsesDefault("model") && userConfig.model) {
|
|
815
1015
|
options.model = userConfig.model;
|
|
816
1016
|
}
|
|
817
1017
|
if (optionUsesDefault("search") && userConfig.search) {
|
|
@@ -826,6 +1026,95 @@ async function runRootCommand(options) {
|
|
|
826
1026
|
if (optionUsesDefault("baseUrl") && userConfig.apiBaseUrl) {
|
|
827
1027
|
options.baseUrl = userConfig.apiBaseUrl;
|
|
828
1028
|
}
|
|
1029
|
+
const providerMode = resolveApiProviderMode(options);
|
|
1030
|
+
const engineModels = multiModelProvided
|
|
1031
|
+
? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
|
|
1032
|
+
: [resolveApiModel(normalizeModelOption(options.model) || DEFAULT_MODEL)];
|
|
1033
|
+
if (options.route || options.preflight) {
|
|
1034
|
+
const routeAzureEndpoint = firstNonEmpty(options.azureEndpoint, process.env.AZURE_OPENAI_ENDPOINT, userConfig.azure?.endpoint);
|
|
1035
|
+
const configuredAzureForRoute = routeAzureEndpoint
|
|
1036
|
+
? {
|
|
1037
|
+
endpoint: routeAzureEndpoint,
|
|
1038
|
+
deployment: firstNonEmpty(options.azureDeployment, process.env.AZURE_OPENAI_DEPLOYMENT, userConfig.azure?.deployment),
|
|
1039
|
+
apiVersion: firstNonEmpty(options.azureApiVersion, process.env.AZURE_OPENAI_API_VERSION, userConfig.azure?.apiVersion),
|
|
1040
|
+
}
|
|
1041
|
+
: undefined;
|
|
1042
|
+
const { buildProviderRoutePlan } = await import("../src/oracle/providerRoutePlan.js");
|
|
1043
|
+
const plans = engineModels.map((model) => buildProviderRoutePlan({
|
|
1044
|
+
model,
|
|
1045
|
+
providerMode,
|
|
1046
|
+
azure: configuredAzureForRoute,
|
|
1047
|
+
baseUrl: options.baseUrl,
|
|
1048
|
+
env: process.env,
|
|
1049
|
+
}));
|
|
1050
|
+
const { printProviderPlans } = await import("../src/cli/providerDoctor.js");
|
|
1051
|
+
printProviderPlans(plans, { title: options.preflight ? "Provider preflight" : "Route plan" });
|
|
1052
|
+
process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const retentionHours = typeof options.retainHours === "number" ? options.retainHours : undefined;
|
|
1056
|
+
await sessionStore.ensureStorage();
|
|
1057
|
+
await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
|
|
1058
|
+
if (providerMode === "openai") {
|
|
1059
|
+
if (hasExplicitAzureOption(optionUsesDefault)) {
|
|
1060
|
+
throw new Error("--provider openai/--no-azure cannot be combined with Azure options.");
|
|
1061
|
+
}
|
|
1062
|
+
options.azureEndpoint = undefined;
|
|
1063
|
+
options.azureDeployment = undefined;
|
|
1064
|
+
options.azureApiVersion = undefined;
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
if (optionUsesDefault("azureEndpoint")) {
|
|
1068
|
+
if (process.env.AZURE_OPENAI_ENDPOINT) {
|
|
1069
|
+
options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
1070
|
+
}
|
|
1071
|
+
else if (userConfig.azure?.endpoint) {
|
|
1072
|
+
options.azureEndpoint = userConfig.azure.endpoint;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (optionUsesDefault("azureDeployment")) {
|
|
1076
|
+
if (process.env.AZURE_OPENAI_DEPLOYMENT) {
|
|
1077
|
+
options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
1078
|
+
}
|
|
1079
|
+
else if (userConfig.azure?.deployment) {
|
|
1080
|
+
options.azureDeployment = userConfig.azure.deployment;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (optionUsesDefault("azureApiVersion")) {
|
|
1084
|
+
if (process.env.AZURE_OPENAI_API_VERSION) {
|
|
1085
|
+
options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
1086
|
+
}
|
|
1087
|
+
else if (userConfig.azure?.apiVersion) {
|
|
1088
|
+
options.azureApiVersion = userConfig.azure.apiVersion;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (providerMode === "azure" && !options.azureEndpoint?.trim()) {
|
|
1092
|
+
throw new Error("--provider azure requires --azure-endpoint or AZURE_OPENAI_ENDPOINT.");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const azureAutoApiRequested = providerMode !== "openai" &&
|
|
1096
|
+
Boolean(options.azureEndpoint?.trim()) &&
|
|
1097
|
+
engineModels.some((model) => isAzureOpenAICandidateModel(model));
|
|
1098
|
+
const explicitApiProviderRequested = providerMode !== "auto" || hasExplicitAzureOption(optionUsesDefault);
|
|
1099
|
+
const preferredEngine = options.engine ?? (explicitApiProviderRequested ? undefined : userConfig.engine);
|
|
1100
|
+
let engine = resolveEngine({
|
|
1101
|
+
engine: preferredEngine,
|
|
1102
|
+
browserFlag: options.browser,
|
|
1103
|
+
apiProviderRequested: explicitApiProviderRequested,
|
|
1104
|
+
env: process.env,
|
|
1105
|
+
});
|
|
1106
|
+
const envEnginePreference = (process.env.ORACLE_ENGINE ?? "").trim().toLowerCase();
|
|
1107
|
+
const browserEngineRequested = options.browser ||
|
|
1108
|
+
options.engine === "browser" ||
|
|
1109
|
+
Boolean(remoteHost) ||
|
|
1110
|
+
(!explicitApiProviderRequested &&
|
|
1111
|
+
(userConfig.engine === "browser" || envEnginePreference === "browser"));
|
|
1112
|
+
if (azureAutoApiRequested && engine === "browser" && !browserEngineRequested) {
|
|
1113
|
+
engine = "api";
|
|
1114
|
+
}
|
|
1115
|
+
if (options.browser) {
|
|
1116
|
+
console.log(chalk.yellow("`--browser` is deprecated; use `--engine browser` instead."));
|
|
1117
|
+
}
|
|
829
1118
|
if (remoteHost && engine !== "browser") {
|
|
830
1119
|
throw new Error("--remote-host requires --engine browser.");
|
|
831
1120
|
}
|
|
@@ -835,30 +1124,6 @@ async function runRootCommand(options) {
|
|
|
835
1124
|
if (options.browserTab && engine !== "browser") {
|
|
836
1125
|
throw new Error("--browser-tab requires --engine browser.");
|
|
837
1126
|
}
|
|
838
|
-
if (optionUsesDefault("azureEndpoint")) {
|
|
839
|
-
if (process.env.AZURE_OPENAI_ENDPOINT) {
|
|
840
|
-
options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
841
|
-
}
|
|
842
|
-
else if (userConfig.azure?.endpoint) {
|
|
843
|
-
options.azureEndpoint = userConfig.azure.endpoint;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
if (optionUsesDefault("azureDeployment")) {
|
|
847
|
-
if (process.env.AZURE_OPENAI_DEPLOYMENT) {
|
|
848
|
-
options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
849
|
-
}
|
|
850
|
-
else if (userConfig.azure?.deployment) {
|
|
851
|
-
options.azureDeployment = userConfig.azure.deployment;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
if (optionUsesDefault("azureApiVersion")) {
|
|
855
|
-
if (process.env.AZURE_OPENAI_API_VERSION) {
|
|
856
|
-
options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
857
|
-
}
|
|
858
|
-
else if (userConfig.azure?.apiVersion) {
|
|
859
|
-
options.azureApiVersion = userConfig.azure.apiVersion;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
1127
|
const normalizedMultiModels = multiModelProvided
|
|
863
1128
|
? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
|
|
864
1129
|
: [];
|
|
@@ -873,14 +1138,17 @@ async function runRootCommand(options) {
|
|
|
873
1138
|
const isCodex = primaryModelCandidate.startsWith("gpt-5.1-codex");
|
|
874
1139
|
const isClaude = primaryModelCandidate.startsWith("claude");
|
|
875
1140
|
const userForcedBrowser = options.browser || options.engine === "browser";
|
|
1141
|
+
const browserExplicitlyRequested = browserEngineRequested;
|
|
876
1142
|
const isBrowserCompatible = (model) => model.startsWith("gpt-") || model.startsWith("gemini");
|
|
877
|
-
const hasNonBrowserCompatibleTarget =
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (hasNonBrowserCompatibleTarget) {
|
|
1143
|
+
const hasNonBrowserCompatibleTarget = normalizedMultiModels.length > 0
|
|
1144
|
+
? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
|
|
1145
|
+
: !isBrowserCompatible(resolvedModelCandidate);
|
|
1146
|
+
if (browserExplicitlyRequested && hasNonBrowserCompatibleTarget) {
|
|
882
1147
|
throw new Error("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.");
|
|
883
1148
|
}
|
|
1149
|
+
if (engine === "browser" && hasNonBrowserCompatibleTarget) {
|
|
1150
|
+
engine = "api";
|
|
1151
|
+
}
|
|
884
1152
|
if (isClaude && engine === "browser") {
|
|
885
1153
|
console.log(chalk.dim("Browser engine is not supported for Claude models; switching to API."));
|
|
886
1154
|
engine = "api";
|
|
@@ -916,12 +1184,14 @@ async function runRootCommand(options) {
|
|
|
916
1184
|
const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
|
|
917
1185
|
const { models: _rawModels, ...optionsWithoutModels } = options;
|
|
918
1186
|
const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
|
|
919
|
-
resolvedOptions.maxFileSizeBytes =
|
|
1187
|
+
resolvedOptions.maxFileSizeBytes =
|
|
1188
|
+
options.maxFileSizeBytes ?? resolveConfiguredMaxFileSizeBytes(userConfig, process.env);
|
|
920
1189
|
if (normalizedMultiModels.length > 0) {
|
|
921
1190
|
resolvedOptions.models = normalizedMultiModels;
|
|
922
1191
|
}
|
|
923
1192
|
resolvedOptions.baseUrl = resolvedBaseUrl;
|
|
924
1193
|
resolvedOptions.effectiveModelId = effectiveModelId;
|
|
1194
|
+
resolvedOptions.provider = providerMode;
|
|
925
1195
|
resolvedOptions.writeOutputPath = resolveOutputPath(options.writeOutput, process.cwd());
|
|
926
1196
|
// Decide whether to block until completion:
|
|
927
1197
|
// - explicit --wait / --no-wait wins
|
|
@@ -935,10 +1205,19 @@ async function runRootCommand(options) {
|
|
|
935
1205
|
console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
|
|
936
1206
|
waitPreference = true;
|
|
937
1207
|
}
|
|
938
|
-
if (
|
|
1208
|
+
if (options.status) {
|
|
1209
|
+
const { attachSession, showStatus } = await import("../src/cli/sessionDisplay.js");
|
|
1210
|
+
if (options.session) {
|
|
1211
|
+
await attachSession(options.session);
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
await showStatus({ hours: 24, includeAll: false, limit: 100, showExamples: true });
|
|
1215
|
+
}
|
|
939
1216
|
return;
|
|
940
1217
|
}
|
|
941
|
-
if (
|
|
1218
|
+
if (options.session) {
|
|
1219
|
+
const { attachSession } = await import("../src/cli/sessionDisplay.js");
|
|
1220
|
+
await attachSession(options.session);
|
|
942
1221
|
return;
|
|
943
1222
|
}
|
|
944
1223
|
if (options.execSession) {
|
|
@@ -953,6 +1232,8 @@ async function runRootCommand(options) {
|
|
|
953
1232
|
const modelConfig = isKnownModel(resolvedModel)
|
|
954
1233
|
? MODEL_CONFIGS[resolvedModel]
|
|
955
1234
|
: MODEL_CONFIGS["gpt-5.1"];
|
|
1235
|
+
const { buildRequestBody } = await import("../src/oracle/request.js");
|
|
1236
|
+
const { estimateRequestTokens } = await import("../src/oracle/tokenEstimate.js");
|
|
956
1237
|
const requestBody = buildRequestBody({
|
|
957
1238
|
modelConfig,
|
|
958
1239
|
systemPrompt: bundle.systemPrompt,
|
|
@@ -988,16 +1269,19 @@ async function runRootCommand(options) {
|
|
|
988
1269
|
return;
|
|
989
1270
|
}
|
|
990
1271
|
const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
|
|
1272
|
+
const { applyBrowserDefaultsFromConfig } = await import("../src/cli/browserDefaults.js");
|
|
991
1273
|
applyBrowserDefaultsFromConfig(options, userConfig, getSource);
|
|
992
1274
|
const sessionMode = engine === "browser" ? "browser" : "api";
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
1275
|
+
const browserConfig = await (async () => {
|
|
1276
|
+
if (sessionMode !== "browser")
|
|
1277
|
+
return undefined;
|
|
1278
|
+
const { buildBrowserConfig, resolveBrowserModelLabel } = await import("../src/cli/browserConfig.js");
|
|
1279
|
+
return buildBrowserConfig({
|
|
996
1280
|
...options,
|
|
997
1281
|
model: resolvedModel,
|
|
998
|
-
browserModelLabel:
|
|
999
|
-
})
|
|
1000
|
-
|
|
1282
|
+
browserModelLabel: resolveBrowserModelLabel(cliModelArg, resolvedModel),
|
|
1283
|
+
});
|
|
1284
|
+
})();
|
|
1001
1285
|
if (previewMode) {
|
|
1002
1286
|
if (!options.prompt) {
|
|
1003
1287
|
throw new Error("Prompt is required when using --dry-run/preview.");
|
|
@@ -1027,6 +1311,7 @@ async function runRootCommand(options) {
|
|
|
1027
1311
|
baseUrl: resolvedBaseUrl,
|
|
1028
1312
|
});
|
|
1029
1313
|
if (engine === "browser") {
|
|
1314
|
+
const { runBrowserPreview } = await import("../src/cli/dryRun.js");
|
|
1030
1315
|
await runBrowserPreview({
|
|
1031
1316
|
runOptions,
|
|
1032
1317
|
cwd: process.cwd(),
|
|
@@ -1038,6 +1323,8 @@ async function runRootCommand(options) {
|
|
|
1038
1323
|
return;
|
|
1039
1324
|
}
|
|
1040
1325
|
// API dry-run/preview path
|
|
1326
|
+
validateApiProviderRoutingForCli(runOptions);
|
|
1327
|
+
const { runDryRunSummary } = await import("../src/cli/dryRun.js");
|
|
1041
1328
|
if (previewMode === "summary") {
|
|
1042
1329
|
await runDryRunSummary({
|
|
1043
1330
|
engine,
|
|
@@ -1096,6 +1383,7 @@ async function runRootCommand(options) {
|
|
|
1096
1383
|
? options.file.filter((f) => !isMediaFile(f))
|
|
1097
1384
|
: options.file;
|
|
1098
1385
|
if (filesToValidate.length > 0) {
|
|
1386
|
+
const { readFiles } = await import("../src/oracle/files.js");
|
|
1099
1387
|
await readFiles(filesToValidate, {
|
|
1100
1388
|
cwd: process.cwd(),
|
|
1101
1389
|
maxFileSizeBytes: resolvedOptions.maxFileSizeBytes,
|
|
@@ -1110,12 +1398,14 @@ async function runRootCommand(options) {
|
|
|
1110
1398
|
});
|
|
1111
1399
|
let browserDeps;
|
|
1112
1400
|
if (browserConfig && remoteHost) {
|
|
1401
|
+
const { createRemoteBrowserExecutor } = await import("../src/remote/client.js");
|
|
1113
1402
|
browserDeps = {
|
|
1114
1403
|
executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
|
|
1115
1404
|
};
|
|
1116
1405
|
console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
|
|
1117
1406
|
}
|
|
1118
1407
|
else if (browserConfig && resolvedModel.startsWith("gemini")) {
|
|
1408
|
+
const { createGeminiWebExecutor } = await import("../src/gemini-web/index.js");
|
|
1119
1409
|
browserDeps = {
|
|
1120
1410
|
executeBrowser: createGeminiWebExecutor({
|
|
1121
1411
|
youtube: options.youtube,
|
|
@@ -1138,6 +1428,7 @@ async function runRootCommand(options) {
|
|
|
1138
1428
|
previewMode: undefined,
|
|
1139
1429
|
baseUrl: resolvedBaseUrl,
|
|
1140
1430
|
});
|
|
1431
|
+
const { runDryRunSummary } = await import("../src/cli/dryRun.js");
|
|
1141
1432
|
await runDryRunSummary({
|
|
1142
1433
|
engine,
|
|
1143
1434
|
runOptions: baseRunOptions,
|
|
@@ -1155,6 +1446,9 @@ async function runRootCommand(options) {
|
|
|
1155
1446
|
background: resolvedOptions.background ?? userConfig.background,
|
|
1156
1447
|
baseUrl: resolvedBaseUrl,
|
|
1157
1448
|
});
|
|
1449
|
+
if (sessionMode === "api") {
|
|
1450
|
+
validateApiProviderRoutingForCli(baseRunOptions);
|
|
1451
|
+
}
|
|
1158
1452
|
enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
|
|
1159
1453
|
if (sessionMode === "browser" && baseRunOptions.search === false) {
|
|
1160
1454
|
console.log(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
|
|
@@ -1195,22 +1489,32 @@ async function runRootCommand(options) {
|
|
|
1195
1489
|
console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
|
|
1196
1490
|
return false;
|
|
1197
1491
|
});
|
|
1492
|
+
const lifecycle = buildSessionLifecycle({
|
|
1493
|
+
engine,
|
|
1494
|
+
detached,
|
|
1495
|
+
reattachCommand: `oracle session ${sessionMeta.id}`,
|
|
1496
|
+
});
|
|
1497
|
+
await sessionStore.updateSession(sessionMeta.id, { lifecycle });
|
|
1498
|
+
const sessionWithLifecycle = { ...sessionMeta, lifecycle };
|
|
1198
1499
|
if (!waitPreference) {
|
|
1199
1500
|
if (!detached) {
|
|
1200
1501
|
console.log(chalk.red("Unable to start in background; use --wait to run inline."));
|
|
1201
1502
|
process.exitCode = 1;
|
|
1202
1503
|
return;
|
|
1203
1504
|
}
|
|
1204
|
-
|
|
1505
|
+
for (const line of formatSessionLifecycleBlock(sessionWithLifecycle)) {
|
|
1506
|
+
console.log(line);
|
|
1507
|
+
}
|
|
1205
1508
|
console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
|
|
1206
1509
|
return;
|
|
1207
1510
|
}
|
|
1208
1511
|
if (detached === false) {
|
|
1209
|
-
await runInteractiveSession(
|
|
1512
|
+
await runInteractiveSession(sessionWithLifecycle, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps);
|
|
1210
1513
|
return;
|
|
1211
1514
|
}
|
|
1212
1515
|
if (detached) {
|
|
1213
1516
|
console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
|
|
1517
|
+
const { attachSession } = await import("../src/cli/sessionDisplay.js");
|
|
1214
1518
|
await attachSession(sessionMeta.id, { suppressMetadata: true });
|
|
1215
1519
|
}
|
|
1216
1520
|
}
|
|
@@ -1237,7 +1541,12 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
|
|
|
1237
1541
|
writeChunk(chunk);
|
|
1238
1542
|
return true;
|
|
1239
1543
|
};
|
|
1544
|
+
for (const line of formatSessionLifecycleBlock(sessionMeta)) {
|
|
1545
|
+
console.log(line);
|
|
1546
|
+
logLine(line);
|
|
1547
|
+
}
|
|
1240
1548
|
try {
|
|
1549
|
+
const { performSessionRun } = await import("../src/cli/sessionRunner.js");
|
|
1241
1550
|
await performSessionRun({
|
|
1242
1551
|
sessionMeta,
|
|
1243
1552
|
runOptions,
|
|
@@ -1253,6 +1562,7 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
|
|
|
1253
1562
|
});
|
|
1254
1563
|
const latest = await sessionStore.readSession(sessionMeta.id);
|
|
1255
1564
|
if (!suppressSummary) {
|
|
1565
|
+
const { formatCompletionSummary } = await import("../src/cli/sessionDisplay.js");
|
|
1256
1566
|
const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
|
|
1257
1567
|
if (summary) {
|
|
1258
1568
|
console.log("\n" + chalk.green.bold(summary));
|
|
@@ -1268,10 +1578,11 @@ async function launchDetachedSession(sessionId) {
|
|
|
1268
1578
|
return new Promise((resolve, reject) => {
|
|
1269
1579
|
try {
|
|
1270
1580
|
const args = ["--", CLI_ENTRYPOINT, "--exec-session", sessionId];
|
|
1581
|
+
const env = buildDetachedPerfTraceEnv(process.env, perfTraceArgs.value, sessionId);
|
|
1271
1582
|
const child = spawn(process.execPath, args, {
|
|
1272
1583
|
detached: true,
|
|
1273
1584
|
stdio: "ignore",
|
|
1274
|
-
env
|
|
1585
|
+
env,
|
|
1275
1586
|
});
|
|
1276
1587
|
child.once("error", reject);
|
|
1277
1588
|
child.once("spawn", () => {
|
|
@@ -1314,6 +1625,7 @@ async function restartSession(sessionId, options) {
|
|
|
1314
1625
|
? runOptions.file.filter((f) => !isMediaFile(f))
|
|
1315
1626
|
: runOptions.file;
|
|
1316
1627
|
if (filesToValidate.length > 0) {
|
|
1628
|
+
const { readFiles } = await import("../src/oracle/files.js");
|
|
1317
1629
|
await readFiles(filesToValidate, {
|
|
1318
1630
|
cwd,
|
|
1319
1631
|
maxFileSizeBytes: runOptions.maxFileSizeBytes,
|
|
@@ -1347,12 +1659,14 @@ async function restartSession(sessionId, options) {
|
|
|
1347
1659
|
}
|
|
1348
1660
|
let browserDeps;
|
|
1349
1661
|
if (browserConfig && remoteHost) {
|
|
1662
|
+
const { createRemoteBrowserExecutor } = await import("../src/remote/client.js");
|
|
1350
1663
|
browserDeps = {
|
|
1351
1664
|
executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
|
|
1352
1665
|
};
|
|
1353
1666
|
console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
|
|
1354
1667
|
}
|
|
1355
1668
|
else if (browserConfig && runOptions.model.startsWith("gemini")) {
|
|
1669
|
+
const { createGeminiWebExecutor } = await import("../src/gemini-web/index.js");
|
|
1356
1670
|
browserDeps = {
|
|
1357
1671
|
executeBrowser: createGeminiWebExecutor({
|
|
1358
1672
|
youtube: storedOptions.youtube,
|
|
@@ -1369,6 +1683,9 @@ async function restartSession(sessionId, options) {
|
|
|
1369
1683
|
}
|
|
1370
1684
|
}
|
|
1371
1685
|
const remoteExecutionActive = Boolean(browserDeps);
|
|
1686
|
+
if (sessionMode === "api") {
|
|
1687
|
+
validateApiProviderRoutingForCli(runOptions);
|
|
1688
|
+
}
|
|
1372
1689
|
await sessionStore.ensureStorage();
|
|
1373
1690
|
const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
|
|
1374
1691
|
const sessionMeta = await sessionStore.createSession({
|
|
@@ -1406,22 +1723,32 @@ async function restartSession(sessionId, options) {
|
|
|
1406
1723
|
console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
|
|
1407
1724
|
return false;
|
|
1408
1725
|
});
|
|
1726
|
+
const lifecycle = buildSessionLifecycle({
|
|
1727
|
+
engine,
|
|
1728
|
+
detached,
|
|
1729
|
+
reattachCommand: `oracle session ${sessionMeta.id}`,
|
|
1730
|
+
});
|
|
1731
|
+
await sessionStore.updateSession(sessionMeta.id, { lifecycle });
|
|
1732
|
+
const sessionWithLifecycle = { ...sessionMeta, lifecycle };
|
|
1409
1733
|
if (!waitPreference) {
|
|
1410
1734
|
if (!detached) {
|
|
1411
1735
|
console.log(chalk.red("Unable to start in background; use --wait to run inline."));
|
|
1412
1736
|
process.exitCode = 1;
|
|
1413
1737
|
return;
|
|
1414
1738
|
}
|
|
1415
|
-
|
|
1739
|
+
for (const line of formatSessionLifecycleBlock(sessionWithLifecycle)) {
|
|
1740
|
+
console.log(line);
|
|
1741
|
+
}
|
|
1416
1742
|
console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
|
|
1417
1743
|
return;
|
|
1418
1744
|
}
|
|
1419
1745
|
if (detached === false) {
|
|
1420
|
-
await runInteractiveSession(
|
|
1746
|
+
await runInteractiveSession(sessionWithLifecycle, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps, cwd);
|
|
1421
1747
|
return;
|
|
1422
1748
|
}
|
|
1423
1749
|
if (detached) {
|
|
1424
1750
|
console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
|
|
1751
|
+
const { attachSession } = await import("../src/cli/sessionDisplay.js");
|
|
1425
1752
|
await attachSession(sessionMeta.id, { suppressMetadata: true });
|
|
1426
1753
|
}
|
|
1427
1754
|
}
|
|
@@ -1439,6 +1766,7 @@ async function executeSession(sessionId) {
|
|
|
1439
1766
|
const userConfig = (await loadUserConfig()).config;
|
|
1440
1767
|
const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
|
|
1441
1768
|
try {
|
|
1769
|
+
const { performSessionRun } = await import("../src/cli/sessionRunner.js");
|
|
1442
1770
|
await performSessionRun({
|
|
1443
1771
|
sessionMeta: metadata,
|
|
1444
1772
|
runOptions,
|
|
@@ -1553,12 +1881,26 @@ program.action(async function () {
|
|
|
1553
1881
|
await runRootCommand(options);
|
|
1554
1882
|
});
|
|
1555
1883
|
async function main() {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1884
|
+
if (perfTraceArgs.error) {
|
|
1885
|
+
console.error(`error: ${perfTraceArgs.error}`);
|
|
1886
|
+
console.error("(use --help for usage)");
|
|
1887
|
+
process.exitCode = 1;
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
const handleSigint = () => {
|
|
1560
1891
|
console.log(chalk.yellow("\nCancelled."));
|
|
1561
1892
|
process.exitCode = 130;
|
|
1893
|
+
// Browser/serve modes install their own SIGINT cleanup after this top-level handler.
|
|
1894
|
+
if (process.listenerCount("SIGINT") <= 1) {
|
|
1895
|
+
process.exit(130);
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
process.once("SIGINT", handleSigint);
|
|
1899
|
+
try {
|
|
1900
|
+
await program.parseAsync(normalizedArgv);
|
|
1901
|
+
}
|
|
1902
|
+
finally {
|
|
1903
|
+
process.off("SIGINT", handleSigint);
|
|
1562
1904
|
}
|
|
1563
1905
|
}
|
|
1564
1906
|
void main().catch((error) => {
|