@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.
Files changed (47) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/modelSelection.js +53 -15
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +75 -18
  6. package/dist/src/browser/actions/thinkingTime.js +23 -8
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +41 -7
  9. package/dist/src/browser/manualLoginProfile.js +54 -0
  10. package/dist/src/browser/projectSourcesRunner.js +16 -5
  11. package/dist/src/browser/prompt.js +56 -37
  12. package/dist/src/browser/sessionRunner.js +72 -1
  13. package/dist/src/browser/utils.js +1 -47
  14. package/dist/src/browser/zipBundle.js +152 -0
  15. package/dist/src/cli/browserConfig.js +13 -11
  16. package/dist/src/cli/browserDefaults.js +2 -1
  17. package/dist/src/cli/docsCheck.js +186 -0
  18. package/dist/src/cli/engine.js +11 -4
  19. package/dist/src/cli/options.js +12 -6
  20. package/dist/src/cli/perfTrace.js +242 -0
  21. package/dist/src/cli/promptRequirement.js +2 -0
  22. package/dist/src/cli/providerDoctor.js +85 -0
  23. package/dist/src/cli/runOptions.js +46 -16
  24. package/dist/src/cli/sessionDisplay.js +39 -4
  25. package/dist/src/cli/sessionLifecycle.js +38 -0
  26. package/dist/src/cli/sessionRunner.js +228 -3
  27. package/dist/src/cli/sessionTable.js +2 -1
  28. package/dist/src/duration.js +47 -0
  29. package/dist/src/mcp/tools/consult.js +19 -3
  30. package/dist/src/mcp/types.js +1 -0
  31. package/dist/src/mcp/utils.js +4 -1
  32. package/dist/src/oracle/baseUrl.js +17 -0
  33. package/dist/src/oracle/client.js +1 -22
  34. package/dist/src/oracle/config.js +17 -4
  35. package/dist/src/oracle/gemini.js +2 -22
  36. package/dist/src/oracle/geminiModels.js +21 -0
  37. package/dist/src/oracle/modelResolver.js +7 -1
  38. package/dist/src/oracle/multiModelRunner.js +20 -2
  39. package/dist/src/oracle/providerFailures.js +204 -0
  40. package/dist/src/oracle/providerRoutePlan.js +281 -0
  41. package/dist/src/oracle/providerRouting.js +92 -0
  42. package/dist/src/oracle/run.js +157 -54
  43. package/dist/src/oracle.js +1 -0
  44. package/dist/src/remote/client.js +8 -0
  45. package/dist/src/remote/server.js +26 -0
  46. package/dist/src/sessionManager.js +5 -1
  47. package/package.json +3 -1
@@ -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, readFiles, estimateRequestTokens, buildRequestBody, } from "../src/oracle.js";
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/browserMode.js";
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/gemini.js";
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 normalizedArgv = process.argv.map((arg, index) => {
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 = normalizedArgv.slice(2);
66
- const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
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 suppressIntro = userCliArgs[0] === "bridge" &&
69
- (userCliArgs[1] === "codex-config" || userCliArgs[1] === "claude-config");
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 (userCliArgs.some((arg) => arg === "--help" || arg === "-h")) {
174
+ if (routingCliArgs.some((arg) => arg === "--help" || arg === "-h")) {
86
175
  return;
87
176
  }
88
- if (userCliArgs.length === 0) {
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(userCliArgs, opts)) {
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: false });
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: overrides.timeoutSeconds ?? options.timeout,
514
- httpTimeoutMs: overrides.httpTimeoutMs ?? options.httpTimeout,
515
- zombieTimeoutMs: overrides.zombieTimeoutMs ?? options.zombieTimeout,
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 (userCliArgs.length === 0) {
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 && options.renderMarkdown) {
1011
+ if (options.dryRun && renderMarkdown) {
803
1012
  throw new Error("--dry-run cannot be combined with --render-markdown.");
804
1013
  }
805
- const preferredEngine = options.engine ?? userConfig.engine;
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 = (engine === "browser" || userForcedBrowser) &&
878
- (normalizedMultiModels.length > 0
879
- ? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
880
- : !isBrowserCompatible(resolvedModelCandidate));
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 = resolveConfiguredMaxFileSizeBytes(userConfig, process.env);
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 (await handleStatusFlag(options, { attachSession, showStatus })) {
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 (await handleSessionAlias(options, { attachSession })) {
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 browserModelLabelOverride = sessionMode === "browser" ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
994
- const browserConfig = sessionMode === "browser"
995
- ? await buildBrowserConfig({
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: browserModelLabelOverride,
999
- })
1000
- : undefined;
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
- console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
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(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps);
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: process.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
- console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
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(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps, cwd);
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
- const parsePromise = program.parseAsync(normalizedArgv);
1557
- const sigintPromise = once(process, "SIGINT").then(() => "sigint");
1558
- const result = await Promise.race([parsePromise.then(() => "parsed"), sigintPromise]);
1559
- if (result === "sigint") {
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) => {