@steipete/oracle 0.9.0 → 0.11.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/LICENSE +1 -1
- package/README.md +107 -49
- package/dist/bin/oracle-cli.js +551 -410
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +175 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +342 -119
- package/dist/src/browser/actions/navigation.js +183 -137
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +152 -91
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +276 -63
- package/dist/src/browser/config.js +59 -16
- package/dist/src/browser/constants.js +25 -12
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +250 -77
- package/dist/src/browser/domDebug.js +50 -1
- package/dist/src/browser/index.js +1559 -692
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +127 -42
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +178 -73
- package/dist/src/browser/reattachHelpers.js +32 -27
- package/dist/src/browser/sessionRunner.js +89 -25
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +24 -20
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +102 -48
- package/dist/src/cli/browserDefaults.js +51 -26
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +62 -26
- package/dist/src/cli/duplicatePromptGuard.js +12 -4
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +131 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +82 -21
- package/dist/src/cli/sessionDisplay.js +213 -87
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +149 -95
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +18 -12
- package/dist/src/mcp/tools/consult.js +246 -67
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +12 -5
- package/dist/src/mcp/utils.js +21 -8
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +160 -135
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +78 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +67 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
- /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -1,59 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import { spawn } from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { once } from
|
|
6
|
-
import { Command, Option } from
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { once } from "node:events";
|
|
6
|
+
import { Command, Option } from "commander";
|
|
7
7
|
// Allow `npx @steipete/oracle oracle-mcp` to resolve the MCP server even though npx runs the default binary.
|
|
8
|
-
if (process.argv[2] ===
|
|
9
|
-
const { startMcpServer } = await import(
|
|
8
|
+
if (process.argv[2] === "oracle-mcp") {
|
|
9
|
+
const { startMcpServer } = await import("../src/mcp/server.js");
|
|
10
10
|
await startMcpServer();
|
|
11
11
|
process.exit(0);
|
|
12
12
|
}
|
|
13
|
-
import { resolveEngine, defaultWaitPreference } from
|
|
14
|
-
import { shouldRequirePrompt } from
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
13
|
+
import { resolveEngine, defaultWaitPreference } from "../src/cli/engine.js";
|
|
14
|
+
import { shouldRequirePrompt } from "../src/cli/promptRequirement.js";
|
|
15
|
+
import { resolveDashPrompt } from "../src/cli/stdin.js";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import { sessionStore, pruneOldSessions } from "../src/sessionStore.js";
|
|
18
|
+
import { DEFAULT_MODEL, MODEL_CONFIGS, readFiles, estimateRequestTokens, buildRequestBody, } from "../src/oracle.js";
|
|
19
|
+
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";
|
|
23
|
+
import { applyHelpStyling } from "../src/cli/help.js";
|
|
24
|
+
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
25
|
+
import { copyToClipboard } from "../src/cli/clipboard.js";
|
|
26
|
+
import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
|
|
27
|
+
import { shouldDetachSession } from "../src/cli/detach.js";
|
|
28
|
+
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
|
+
import { isMediaFile } from "../src/browser/prompt.js";
|
|
32
|
+
import { attachSession, showStatus, formatCompletionSummary } from "../src/cli/sessionDisplay.js";
|
|
33
|
+
import { formatCompactNumber } from "../src/cli/format.js";
|
|
34
|
+
import { formatIntroLine } from "../src/cli/tagline.js";
|
|
35
|
+
import { warnIfOversizeBundle } from "../src/cli/bundleWarnings.js";
|
|
36
|
+
import { formatRenderedMarkdown } from "../src/cli/renderOutput.js";
|
|
37
|
+
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";
|
|
40
|
+
import { isErrorLogged } from "../src/cli/errorUtils.js";
|
|
41
|
+
import { handleSessionAlias, handleStatusFlag } from "../src/cli/rootAlias.js";
|
|
42
|
+
import { resolveOutputPath } from "../src/cli/writeOutputPath.js";
|
|
43
|
+
import { showBrowserTabsStatus } from "../src/cli/browserTabs.js";
|
|
44
|
+
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
|
+
import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from "../src/cli/notifier.js";
|
|
48
|
+
import { loadUserConfig } from "../src/config.js";
|
|
49
|
+
import { applyBrowserDefaultsFromConfig } from "../src/cli/browserDefaults.js";
|
|
50
|
+
import { shouldBlockDuplicatePrompt } from "../src/cli/duplicatePromptGuard.js";
|
|
51
|
+
import { resolveRemoteServiceConfig } from "../src/remote/remoteServiceConfig.js";
|
|
52
|
+
import { resolveConfiguredMaxFileSizeBytes } from "../src/cli/fileSize.js";
|
|
51
53
|
const VERSION = getCliVersion();
|
|
52
54
|
const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
|
|
53
55
|
const LEGACY_FLAG_ALIASES = new Map([
|
|
54
|
-
[
|
|
55
|
-
[
|
|
56
|
-
[
|
|
56
|
+
["--[no-]notify", "--notify"],
|
|
57
|
+
["--[no-]notify-sound", "--notify-sound"],
|
|
58
|
+
["--[no-]background", "--background"],
|
|
57
59
|
]);
|
|
58
60
|
const normalizedArgv = process.argv.map((arg, index) => {
|
|
59
61
|
if (index < 2)
|
|
@@ -63,20 +65,24 @@ const normalizedArgv = process.argv.map((arg, index) => {
|
|
|
63
65
|
const rawCliArgs = normalizedArgv.slice(2);
|
|
64
66
|
const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
|
|
65
67
|
const isTty = process.stdout.isTTY;
|
|
68
|
+
const suppressIntro = userCliArgs[0] === "bridge" &&
|
|
69
|
+
(userCliArgs[1] === "codex-config" || userCliArgs[1] === "claude-config");
|
|
66
70
|
const program = new Command();
|
|
67
71
|
let introPrinted = false;
|
|
68
|
-
program.hook(
|
|
72
|
+
program.hook("preAction", () => {
|
|
73
|
+
if (suppressIntro)
|
|
74
|
+
return;
|
|
69
75
|
if (introPrinted)
|
|
70
76
|
return;
|
|
71
77
|
console.log(formatIntroLine(VERSION, { env: process.env, richTty: isTty }));
|
|
72
78
|
introPrinted = true;
|
|
73
79
|
});
|
|
74
80
|
applyHelpStyling(program, VERSION, isTty);
|
|
75
|
-
program.hook(
|
|
81
|
+
program.hook("preAction", async (thisCommand) => {
|
|
76
82
|
if (thisCommand !== program) {
|
|
77
83
|
return;
|
|
78
84
|
}
|
|
79
|
-
if (userCliArgs.some((arg) => arg ===
|
|
85
|
+
if (userCliArgs.some((arg) => arg === "--help" || arg === "-h")) {
|
|
80
86
|
return;
|
|
81
87
|
}
|
|
82
88
|
if (userCliArgs.length === 0) {
|
|
@@ -88,7 +94,12 @@ program.hook('preAction', (thisCommand) => {
|
|
|
88
94
|
const positional = thisCommand.args?.[0];
|
|
89
95
|
if (!opts.prompt && positional) {
|
|
90
96
|
opts.prompt = positional;
|
|
91
|
-
thisCommand.setOptionValue(
|
|
97
|
+
thisCommand.setOptionValue("prompt", positional);
|
|
98
|
+
}
|
|
99
|
+
const resolvedPrompt = await resolveDashPrompt(opts.prompt);
|
|
100
|
+
if (resolvedPrompt !== opts.prompt) {
|
|
101
|
+
opts.prompt = resolvedPrompt;
|
|
102
|
+
thisCommand.setOptionValue("prompt", resolvedPrompt);
|
|
92
103
|
}
|
|
93
104
|
if (shouldRequirePrompt(userCliArgs, opts)) {
|
|
94
105
|
console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
|
|
@@ -98,141 +109,154 @@ program.hook('preAction', (thisCommand) => {
|
|
|
98
109
|
}
|
|
99
110
|
});
|
|
100
111
|
program
|
|
101
|
-
.name(
|
|
102
|
-
.description(
|
|
112
|
+
.name("oracle")
|
|
113
|
+
.description("One-shot GPT-5.5 Pro / GPT-5.5 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.")
|
|
103
114
|
.version(VERSION)
|
|
104
|
-
.argument(
|
|
105
|
-
.option(
|
|
106
|
-
.addOption(new Option(
|
|
107
|
-
.option(
|
|
108
|
-
.option(
|
|
109
|
-
.option(
|
|
110
|
-
.addOption(new Option(
|
|
115
|
+
.argument("[prompt]", "Prompt text (shorthand for --prompt).")
|
|
116
|
+
.option("-p, --prompt <text>", "User prompt to send to the model.")
|
|
117
|
+
.addOption(new Option("--message <text>", "Alias for --prompt.").hideHelp())
|
|
118
|
+
.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
|
+
.option("--followup-model <model>", "When following up a multi-model session, choose which model response to continue from.")
|
|
120
|
+
.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, [])
|
|
121
|
+
.addOption(new Option("--include <paths...>", "Alias for --file.")
|
|
111
122
|
.argParser(collectPaths)
|
|
112
123
|
.default([])
|
|
113
124
|
.hideHelp())
|
|
114
|
-
.addOption(new Option(
|
|
125
|
+
.addOption(new Option("--files <paths...>", "Alias for --file.")
|
|
115
126
|
.argParser(collectPaths)
|
|
116
127
|
.default([])
|
|
117
128
|
.hideHelp())
|
|
118
|
-
.addOption(new Option(
|
|
129
|
+
.addOption(new Option("--path <paths...>", "Alias for --file.")
|
|
119
130
|
.argParser(collectPaths)
|
|
120
131
|
.default([])
|
|
121
132
|
.hideHelp())
|
|
122
|
-
.addOption(new Option(
|
|
133
|
+
.addOption(new Option("--paths <paths...>", "Alias for --file.")
|
|
123
134
|
.argParser(collectPaths)
|
|
124
135
|
.default([])
|
|
125
136
|
.hideHelp())
|
|
126
|
-
.addOption(new Option(
|
|
127
|
-
.addOption(new Option(
|
|
128
|
-
.option(
|
|
129
|
-
.option(
|
|
130
|
-
.addOption(new Option(
|
|
137
|
+
.addOption(new Option("--copy-markdown", "Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.").default(false))
|
|
138
|
+
.addOption(new Option("--copy").hideHelp().default(false))
|
|
139
|
+
.option("-s, --slug <words>", "Custom session slug (3-5 words).")
|
|
140
|
+
.option("-m, --model <model>", 'Model to target (gpt-5.5-pro default). Also gpt-5.5, gpt-5.4-pro, gpt-5.4, gpt-5.1-pro, gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3.1-pro API-only, gemini-3-pro, claude-4.6-sonnet, claude-4.1-opus, or ChatGPT labels like "5.5 Pro" / "5.2 Thinking" for browser runs).', normalizeModelOption)
|
|
141
|
+
.addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
|
|
131
142
|
.argParser(collectModelList)
|
|
132
143
|
.default([]))
|
|
133
|
-
.addOption(new Option(
|
|
134
|
-
.addOption(new Option(
|
|
135
|
-
.
|
|
136
|
-
.
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
.addOption(new Option(
|
|
140
|
-
.addOption(new Option(
|
|
141
|
-
.addOption(new Option(
|
|
144
|
+
.addOption(new Option("-e, --engine <mode>", "Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.").choices(["api", "browser"]))
|
|
145
|
+
.addOption(new Option("--mode <mode>", "Alias for --engine (api | browser).")
|
|
146
|
+
.choices(["api", "browser"])
|
|
147
|
+
.hideHelp())
|
|
148
|
+
.option("--files-report", "Show token usage per attached file (also prints automatically when files exceed the token budget).", false)
|
|
149
|
+
.option("-v, --verbose", "Enable verbose logging for all operations.", false)
|
|
150
|
+
.addOption(new Option("--notify", "Desktop notification when a session finishes (default on unless CI/SSH).").default(undefined))
|
|
151
|
+
.addOption(new Option("--no-notify", "Disable desktop notifications.").default(undefined))
|
|
152
|
+
.addOption(new Option("--notify-sound", "Play a notification sound on completion (default off).").default(undefined))
|
|
153
|
+
.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).")
|
|
142
155
|
.argParser(parseTimeoutOption)
|
|
143
|
-
.default(
|
|
144
|
-
.addOption(new Option(
|
|
145
|
-
.addOption(new Option(
|
|
146
|
-
.addOption(new Option(
|
|
147
|
-
.argParser((value) => parseDurationOption(value,
|
|
156
|
+
.default("auto"))
|
|
157
|
+
.addOption(new Option("--background", "Use Responses API background mode (create + retrieve) for API runs.").default(undefined))
|
|
158
|
+
.addOption(new Option("--no-background", "Disable Responses API background mode.").default(undefined))
|
|
159
|
+
.addOption(new Option("--http-timeout <ms|s|m|h>", "HTTP client timeout for API requests (default 20m).")
|
|
160
|
+
.argParser((value) => parseDurationOption(value, "HTTP timeout"))
|
|
148
161
|
.default(undefined))
|
|
149
|
-
.addOption(new Option(
|
|
150
|
-
.argParser((value) => parseDurationOption(value,
|
|
162
|
+
.addOption(new Option("--zombie-timeout <ms|s|m|h>", "Override stale-session cutoff used by `oracle status` (default 60m).")
|
|
163
|
+
.argParser((value) => parseDurationOption(value, "Zombie timeout"))
|
|
151
164
|
.default(undefined))
|
|
152
|
-
.option(
|
|
153
|
-
.addOption(new Option(
|
|
165
|
+
.option("--zombie-last-activity", "Base stale-session detection on last log activity instead of start time.", false)
|
|
166
|
+
.addOption(new Option("--preview [mode]", "(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.")
|
|
154
167
|
.hideHelp()
|
|
155
|
-
.choices([
|
|
156
|
-
.preset(
|
|
157
|
-
.addOption(new Option(
|
|
158
|
-
.choices([
|
|
159
|
-
.preset(
|
|
168
|
+
.choices(["summary", "json", "full"])
|
|
169
|
+
.preset("summary"))
|
|
170
|
+
.addOption(new Option("--dry-run [mode]", "Preview without calling the model (summary | json | full).")
|
|
171
|
+
.choices(["summary", "json", "full"])
|
|
172
|
+
.preset("summary")
|
|
160
173
|
.default(false))
|
|
161
|
-
.addOption(new Option(
|
|
162
|
-
.addOption(new Option(
|
|
163
|
-
.addOption(new Option(
|
|
164
|
-
.
|
|
165
|
-
.
|
|
166
|
-
.option(
|
|
167
|
-
.option(
|
|
168
|
-
.option(
|
|
169
|
-
.
|
|
174
|
+
.addOption(new Option("--exec-session <id>").hideHelp())
|
|
175
|
+
.addOption(new Option("--session <id>").hideHelp())
|
|
176
|
+
.addOption(new Option("--status", "Show stored sessions (alias for `oracle status`).")
|
|
177
|
+
.default(false)
|
|
178
|
+
.hideHelp())
|
|
179
|
+
.option("--render-markdown", "Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.", false)
|
|
180
|
+
.option("--render", "Alias for --render-markdown.", false)
|
|
181
|
+
.option("--render-plain", "Render markdown without ANSI/highlighting (use plain text even in a TTY).", false)
|
|
182
|
+
.option("--write-output <path>", "Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).")
|
|
183
|
+
.option("--verbose-render", "Show render/TTY diagnostics when replaying sessions.", false)
|
|
184
|
+
.addOption(new Option("--search <mode>", "Set server-side search behavior (on/off).")
|
|
170
185
|
.argParser(parseSearchOption)
|
|
171
186
|
.hideHelp())
|
|
172
|
-
.addOption(new Option(
|
|
187
|
+
.addOption(new Option("--max-input <tokens>", "Override the input token budget for the selected model.")
|
|
188
|
+
.argParser(parseIntOption)
|
|
189
|
+
.hideHelp())
|
|
190
|
+
.addOption(new Option("--max-output <tokens>", "Override the max output tokens for the selected model.")
|
|
173
191
|
.argParser(parseIntOption)
|
|
174
192
|
.hideHelp())
|
|
175
|
-
.
|
|
193
|
+
.option("--base-url <url>", "Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).")
|
|
194
|
+
.option("--azure-endpoint <url>", "Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).")
|
|
195
|
+
.option("--azure-deployment <name>", "Azure OpenAI Deployment Name.")
|
|
196
|
+
.option("--azure-api-version <version>", "Azure OpenAI API Version.")
|
|
197
|
+
.addOption(new Option("--browser", "(deprecated) Use --engine browser instead.").default(false).hideHelp())
|
|
198
|
+
.addOption(new Option("--browser-chrome-profile <name>", "Chrome profile name/path for cookie reuse.").hideHelp())
|
|
199
|
+
.addOption(new Option("--browser-chrome-path <path>", "Explicit Chrome or Chromium executable path.").hideHelp())
|
|
200
|
+
.addOption(new Option("--browser-cookie-path <path>", "Explicit Chrome/Chromium cookie DB path for session reuse."))
|
|
201
|
+
.addOption(new Option("--browser-attach-running", "Attach to a running local browser session instead of launching Chrome (defaults to 127.0.0.1:9222; combine with --remote-chrome to hint a different host:port)."))
|
|
202
|
+
.addOption(new Option("--chatgpt-url <url>", `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
|
|
203
|
+
.addOption(new Option("--browser-url <url>", `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
|
|
204
|
+
.addOption(new Option("--browser-timeout <ms|s|m>", "Maximum time to wait for an answer (default 1200s / 20m).").hideHelp())
|
|
205
|
+
.addOption(new Option("--browser-input-timeout <ms|s|m>", "Maximum time to wait for the prompt textarea (default 60s).").hideHelp())
|
|
206
|
+
.addOption(new Option("--browser-recheck-delay <ms|s|m|h>", "After an assistant timeout, wait this long then revisit the conversation to retry capture.").hideHelp())
|
|
207
|
+
.addOption(new Option("--browser-recheck-timeout <ms|s|m|h>", "Time budget for the delayed recheck attempt (default 120s).").hideHelp())
|
|
208
|
+
.addOption(new Option("--browser-reuse-wait <ms|s|m|h>", "Wait for a shared Chrome profile to appear before launching a new one (helps parallel runs).").hideHelp())
|
|
209
|
+
.addOption(new Option("--browser-profile-lock-timeout <ms|s|m|h>", "Wait for the shared manual-login profile lock before sending (serializes parallel runs).").hideHelp())
|
|
210
|
+
.addOption(new Option("--browser-max-concurrent-tabs <n>", "Soft limit for concurrent ChatGPT tabs sharing one manual-login profile (default 3).").hideHelp())
|
|
211
|
+
.addOption(new Option("--browser-auto-reattach-delay <ms|s|m|h>", "Delay before starting periodic auto-reattach attempts after a timeout.").hideHelp())
|
|
212
|
+
.addOption(new Option("--browser-auto-reattach-interval <ms|s|m|h>", "Interval between auto-reattach attempts (0 disables).").hideHelp())
|
|
213
|
+
.addOption(new Option("--browser-auto-reattach-timeout <ms|s|m|h>", "Time budget for each auto-reattach attempt (default 120s).").hideHelp())
|
|
214
|
+
.addOption(new Option("--browser-cookie-wait <ms|s|m>", "Wait before retrying cookie sync when Chrome cookies are empty or locked.").hideHelp())
|
|
215
|
+
.addOption(new Option("--browser-port <port>", "Use a fixed Chrome DevTools port (helpful on WSL firewalls).").argParser(parseIntOption))
|
|
216
|
+
.addOption(new Option("--browser-debug-port <port>", "(alias) Use a fixed Chrome DevTools port.")
|
|
176
217
|
.argParser(parseIntOption)
|
|
177
218
|
.hideHelp())
|
|
178
|
-
.
|
|
179
|
-
.
|
|
180
|
-
.
|
|
181
|
-
.
|
|
182
|
-
.addOption(new Option(
|
|
183
|
-
.addOption(new Option(
|
|
184
|
-
.addOption(new Option(
|
|
185
|
-
.addOption(new Option(
|
|
186
|
-
.addOption(new Option(
|
|
187
|
-
.addOption(new Option(
|
|
188
|
-
.
|
|
189
|
-
.addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 60s).').hideHelp())
|
|
190
|
-
.addOption(new Option('--browser-recheck-delay <ms|s|m|h>', 'After an assistant timeout, wait this long then revisit the conversation to retry capture.').hideHelp())
|
|
191
|
-
.addOption(new Option('--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt (default 120s).').hideHelp())
|
|
192
|
-
.addOption(new Option('--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile to appear before launching a new one (helps parallel runs).').hideHelp())
|
|
193
|
-
.addOption(new Option('--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the shared manual-login profile lock before sending (serializes parallel runs).').hideHelp())
|
|
194
|
-
.addOption(new Option('--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before starting periodic auto-reattach attempts after a timeout.').hideHelp())
|
|
195
|
-
.addOption(new Option('--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).').hideHelp())
|
|
196
|
-
.addOption(new Option('--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt (default 120s).').hideHelp())
|
|
197
|
-
.addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
|
|
198
|
-
.addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
|
|
199
|
-
.argParser(parseIntOption))
|
|
200
|
-
.addOption(new Option('--browser-debug-port <port>', '(alias) Use a fixed Chrome DevTools port.').argParser(parseIntOption).hideHelp())
|
|
201
|
-
.addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
|
|
202
|
-
.addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
|
|
203
|
-
.addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
|
|
204
|
-
.addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
|
|
205
|
-
.addOption(new Option('--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.').hideHelp())
|
|
206
|
-
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
207
|
-
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
208
|
-
.addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
|
|
209
|
-
.addOption(new Option('--browser-model-strategy <mode>', 'ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.').choices(['select', 'current', 'ignore']))
|
|
210
|
-
.addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
|
|
211
|
-
.choices(['light', 'standard', 'extended', 'heavy'])
|
|
219
|
+
.addOption(new Option("--browser-cookie-names <names>", "Comma-separated cookie allowlist for sync.").hideHelp())
|
|
220
|
+
.addOption(new Option("--browser-inline-cookies <jsonOrBase64>", "Inline cookies payload (JSON array or base64-encoded JSON).").hideHelp())
|
|
221
|
+
.addOption(new Option("--browser-inline-cookies-file <path>", "Load inline cookies from file (JSON or base64 JSON).").hideHelp())
|
|
222
|
+
.addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
|
|
223
|
+
.addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
|
|
224
|
+
.addOption(new Option("--browser-headless", "Launch Chrome in headless mode.").hideHelp())
|
|
225
|
+
.addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
|
|
226
|
+
.addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
|
|
227
|
+
.addOption(new Option("--browser-model-strategy <mode>", "ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.").choices(["select", "current", "ignore"]))
|
|
228
|
+
.addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.")
|
|
229
|
+
.choices(["light", "standard", "extended", "heavy"])
|
|
212
230
|
.hideHelp())
|
|
213
|
-
.addOption(new Option(
|
|
214
|
-
.addOption(new Option(
|
|
215
|
-
.
|
|
216
|
-
.
|
|
217
|
-
.
|
|
218
|
-
.addOption(new Option(
|
|
219
|
-
.addOption(new Option(
|
|
220
|
-
.
|
|
221
|
-
.
|
|
222
|
-
.addOption(new Option(
|
|
223
|
-
.
|
|
224
|
-
.addOption(new Option(
|
|
225
|
-
.addOption(new Option(
|
|
226
|
-
.addOption(new Option(
|
|
227
|
-
.addOption(new Option(
|
|
228
|
-
.
|
|
229
|
-
.
|
|
230
|
-
.
|
|
231
|
-
.
|
|
232
|
-
.addOption(new Option(
|
|
233
|
-
.addOption(new Option(
|
|
234
|
-
.
|
|
235
|
-
|
|
231
|
+
.addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates ChatGPT Deep Research.").choices(["off", "deep"]))
|
|
232
|
+
.addOption(new Option("--browser-archive <mode>", "Archive completed ChatGPT browser conversations after local artifacts are saved (auto archives successful non-project one-shots only).").choices(["auto", "always", "never"]))
|
|
233
|
+
.addOption(new Option("--browser-follow-up <prompt>", "Submit an additional prompt in the same ChatGPT browser conversation after the initial answer; repeat for multi-turn consults.")
|
|
234
|
+
.argParser(collectTextValues)
|
|
235
|
+
.default([]))
|
|
236
|
+
.addOption(new Option("--browser-allow-cookie-errors", "Continue even if Chrome cookies cannot be copied.").hideHelp())
|
|
237
|
+
.addOption(new Option("--browser-attachments <mode>", "How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.")
|
|
238
|
+
.choices(["auto", "never", "always"])
|
|
239
|
+
.default("auto"))
|
|
240
|
+
.addOption(new Option("--remote-chrome <host:port>", "Connect to remote Chrome DevTools Protocol, or when combined with --browser-attach-running use this host:port as the local attach hint."))
|
|
241
|
+
.option("--browser-tab <ref>", "Reuse an existing ChatGPT tab by ref (current, target id, full URL, or title substring) instead of opening a new tab.")
|
|
242
|
+
.addOption(new Option("--remote-host <host:port>", "Delegate browser runs to a remote `oracle serve` instance."))
|
|
243
|
+
.addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
|
|
244
|
+
.addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
|
|
245
|
+
.addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
|
|
246
|
+
.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
|
+
.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
|
+
.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)."))
|
|
249
|
+
.addOption(new Option("--output <file>", "Output file path for image operations."))
|
|
250
|
+
.addOption(new Option("--aspect <ratio>", "Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only)."))
|
|
251
|
+
.addOption(new Option("--gemini-show-thoughts", "Display Gemini thinking process (Gemini web/cookie mode only).").default(false))
|
|
252
|
+
.option("--retain-hours <hours>", "Prune stored sessions older than this many hours before running (set 0 to disable).", parseFloatOption)
|
|
253
|
+
.option("--force", "Force start a new session even if an identical prompt is already running.", false)
|
|
254
|
+
.option("--debug-help", "Show the advanced/debug option set and exit.", false)
|
|
255
|
+
.option("--heartbeat <seconds>", "Emit periodic in-progress updates (0 to disable).", parseHeartbeatOption, 30)
|
|
256
|
+
.addOption(new Option("--wait").default(undefined))
|
|
257
|
+
.addOption(new Option("--no-wait").default(undefined).hideHelp())
|
|
258
|
+
.showHelpAfterError("(use --help for usage)");
|
|
259
|
+
program.addHelpText("after", `
|
|
236
260
|
Examples:
|
|
237
261
|
# Quick API run with two files
|
|
238
262
|
oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
|
|
@@ -245,15 +269,15 @@ Examples:
|
|
|
245
269
|
oracle --render --copy -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
|
|
246
270
|
`);
|
|
247
271
|
program
|
|
248
|
-
.command(
|
|
249
|
-
.description(
|
|
250
|
-
.option(
|
|
251
|
-
.option(
|
|
252
|
-
.option(
|
|
253
|
-
.option(
|
|
254
|
-
.option(
|
|
272
|
+
.command("serve")
|
|
273
|
+
.description("Run Oracle browser automation as a remote service for other machines.")
|
|
274
|
+
.option("--host <address>", "Interface to bind (default 0.0.0.0).")
|
|
275
|
+
.option("--port <number>", "Port to listen on (default random).", parseIntOption)
|
|
276
|
+
.option("--token <value>", "Access token clients must provide (random if omitted).")
|
|
277
|
+
.option("--manual-login", "Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).", false)
|
|
278
|
+
.option("--manual-login-profile-dir <path>", "Chrome profile directory for manual login (default ~/.oracle/browser-profile).")
|
|
255
279
|
.action(async (commandOptions) => {
|
|
256
|
-
const { serveRemote } = await import(
|
|
280
|
+
const { serveRemote } = await import("../src/remote/server.js");
|
|
257
281
|
await serveRemote({
|
|
258
282
|
host: commandOptions.host,
|
|
259
283
|
port: commandOptions.port,
|
|
@@ -262,119 +286,180 @@ program
|
|
|
262
286
|
manualLoginProfileDir: commandOptions.manualLoginProfileDir,
|
|
263
287
|
});
|
|
264
288
|
});
|
|
265
|
-
const
|
|
289
|
+
const projectSourcesCommand = program
|
|
290
|
+
.command("project-sources")
|
|
291
|
+
.description("Manage ChatGPT Project Sources as explicit shared project context.");
|
|
292
|
+
function addProjectSourcesCommonOptions(command) {
|
|
293
|
+
return command
|
|
294
|
+
.option("--chatgpt-url <url>", "ChatGPT project URL ending in /project (or browser.chatgptUrl config).")
|
|
295
|
+
.addOption(new Option("--browser-manual-login", "Reuse a persistent signed-in Chrome profile.").default(undefined))
|
|
296
|
+
.option("--browser-manual-login-profile-dir <path>", "Persistent Chrome profile directory.")
|
|
297
|
+
.option("--browser-timeout <duration>", "Overall browser timeout (e.g. 10m, 1h).")
|
|
298
|
+
.option("--browser-input-timeout <duration>", "Timeout waiting for the Project Sources UI.")
|
|
299
|
+
.option("--browser-profile-lock-timeout <duration>", "Timeout waiting for profile launch lock.")
|
|
300
|
+
.option("--browser-reuse-wait <duration>", "Wait for an existing shared Chrome to appear.")
|
|
301
|
+
.option("--browser-max-concurrent-tabs <n>", "Concurrent tabs allowed for the shared profile.")
|
|
302
|
+
.option("--browser-cookie-wait <duration>", "Wait before retrying cookie sync.")
|
|
303
|
+
.option("--browser-chrome-profile <profile>", "Chrome profile name for cookie sync.")
|
|
304
|
+
.option("--browser-chrome-path <path>", "Chrome/Chromium executable path.")
|
|
305
|
+
.option("--browser-cookie-path <path>", "Explicit Chrome cookie DB path.")
|
|
306
|
+
.option("--browser-inline-cookies <json>", "Inline ChatGPT cookies JSON.")
|
|
307
|
+
.option("--browser-inline-cookies-file <path>", "File containing ChatGPT cookies JSON.")
|
|
308
|
+
.option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.")
|
|
309
|
+
.option("--browser-keep-browser", "Keep Chrome running after completion.", false)
|
|
310
|
+
.option("--browser-hide-window", "Hide Chrome window after launch on macOS.", false)
|
|
311
|
+
.option("--browser-allow-cookie-errors", "Continue when cookie sync fails.", false)
|
|
312
|
+
.option("--max-file-size-bytes <bytes>", "Reject uploads larger than this many bytes.", parseIntOption)
|
|
313
|
+
.option("--json", "Print structured JSON.", false)
|
|
314
|
+
.option("-v, --verbose", "Enable verbose browser logging.", false);
|
|
315
|
+
}
|
|
316
|
+
addProjectSourcesCommonOptions(projectSourcesCommand
|
|
317
|
+
.command("list")
|
|
318
|
+
.description("List sources already attached to a ChatGPT Project.")).action(async function () {
|
|
319
|
+
const { runProjectSourcesCliCommand } = await import("../src/cli/projectSources.js");
|
|
320
|
+
await runProjectSourcesCliCommand("list", this.optsWithGlobals());
|
|
321
|
+
});
|
|
322
|
+
addProjectSourcesCommonOptions(projectSourcesCommand
|
|
323
|
+
.command("add")
|
|
324
|
+
.description("Upload files into a ChatGPT Project's persistent Sources tab.")
|
|
325
|
+
.option("-f, --file <paths...>", "Files/directories or globs to add as project sources.", collectPaths, [])
|
|
326
|
+
.option("--dry-run", "Validate files and show the upload plan without touching the browser.", false)).action(async function () {
|
|
327
|
+
const { runProjectSourcesCliCommand } = await import("../src/cli/projectSources.js");
|
|
328
|
+
await runProjectSourcesCliCommand("add", this.optsWithGlobals());
|
|
329
|
+
});
|
|
330
|
+
const bridgeCommand = program
|
|
331
|
+
.command("bridge")
|
|
332
|
+
.description("Bridge a Windows-hosted ChatGPT session to Linux clients.");
|
|
266
333
|
bridgeCommand
|
|
267
|
-
.command(
|
|
268
|
-
.description(
|
|
269
|
-
.option(
|
|
270
|
-
.option(
|
|
271
|
-
.option(
|
|
272
|
-
.option(
|
|
273
|
-
.option(
|
|
274
|
-
.option(
|
|
275
|
-
.option(
|
|
276
|
-
.option(
|
|
277
|
-
.option(
|
|
278
|
-
.option(
|
|
279
|
-
.option(
|
|
334
|
+
.command("host")
|
|
335
|
+
.description("Start a secure oracle serve host (optionally with an SSH reverse tunnel).")
|
|
336
|
+
.option("--bind <host:port>", "Local bind address for the host service (default 127.0.0.1:9473).")
|
|
337
|
+
.option("--token <token|auto>", "Service access token (default auto).", "auto")
|
|
338
|
+
.option("--write-connection <path>", "Write a connection artifact JSON (default ~/.oracle/bridge-connection.json).")
|
|
339
|
+
.option("--ssh <user@host>", "Maintain an SSH reverse tunnel to the Linux host (ssh -N -R ...).")
|
|
340
|
+
.option("--ssh-remote-port <port>", "Remote port to bind on the Linux host (default matches --bind port).", parseIntOption)
|
|
341
|
+
.option("--ssh-identity <path>", "SSH identity file (ssh -i).")
|
|
342
|
+
.option("--ssh-extra-args <args>", "Extra args passed to ssh (quoted string).")
|
|
343
|
+
.option("--background", "Run the host in the background and write pid/log files.", false)
|
|
344
|
+
.option("--foreground", "Run the host in the foreground (default).", false)
|
|
345
|
+
.option("--print", "Print the client connection string (includes token).", false)
|
|
346
|
+
.option("--print-token", "Print only the token.", false)
|
|
280
347
|
.action(async (commandOptions) => {
|
|
281
|
-
const { runBridgeHost } = await import(
|
|
348
|
+
const { runBridgeHost } = await import("../src/cli/bridge/host.js");
|
|
282
349
|
await runBridgeHost(commandOptions);
|
|
283
350
|
});
|
|
284
351
|
bridgeCommand
|
|
285
|
-
.command(
|
|
286
|
-
.description(
|
|
287
|
-
.requiredOption(
|
|
288
|
-
.option(
|
|
289
|
-
.option(
|
|
290
|
-
.option(
|
|
291
|
-
.option(
|
|
352
|
+
.command("client")
|
|
353
|
+
.description("Configure this machine to use a remote oracle serve host.")
|
|
354
|
+
.requiredOption("--connect <connection>", "Connection string or path to bridge-connection.json.")
|
|
355
|
+
.option("--config <path>", "Override the oracle config file location (default ~/.oracle/config.json).")
|
|
356
|
+
.option("--no-write-config", "Do not write ~/.oracle/config.json (just validate).")
|
|
357
|
+
.option("--no-test", "Skip remote /health check.")
|
|
358
|
+
.option("--print-env", "Print env var exports (includes token).", false)
|
|
292
359
|
.action(async (commandOptions) => {
|
|
293
|
-
const { runBridgeClient } = await import(
|
|
360
|
+
const { runBridgeClient } = await import("../src/cli/bridge/client.js");
|
|
294
361
|
await runBridgeClient(commandOptions);
|
|
295
362
|
});
|
|
296
363
|
bridgeCommand
|
|
297
|
-
.command(
|
|
298
|
-
.description(
|
|
299
|
-
.option(
|
|
364
|
+
.command("doctor")
|
|
365
|
+
.description("Diagnose bridge connectivity and browser engine prerequisites.")
|
|
366
|
+
.option("--verbose", "Show extra diagnostics.", false)
|
|
300
367
|
.action(async (commandOptions) => {
|
|
301
|
-
const { runBridgeDoctor } = await import(
|
|
368
|
+
const { runBridgeDoctor } = await import("../src/cli/bridge/doctor.js");
|
|
302
369
|
await runBridgeDoctor(commandOptions);
|
|
303
370
|
});
|
|
304
371
|
bridgeCommand
|
|
305
|
-
.command(
|
|
306
|
-
.description(
|
|
307
|
-
.option(
|
|
372
|
+
.command("codex-config")
|
|
373
|
+
.description("Print a Codex CLI MCP server config snippet for oracle-mcp.")
|
|
374
|
+
.option("--print-token", "Include ORACLE_REMOTE_TOKEN in the snippet.", false)
|
|
308
375
|
.action(async (commandOptions) => {
|
|
309
|
-
const { runBridgeCodexConfig } = await import(
|
|
376
|
+
const { runBridgeCodexConfig } = await import("../src/cli/bridge/codexConfig.js");
|
|
310
377
|
await runBridgeCodexConfig(commandOptions);
|
|
311
378
|
});
|
|
312
379
|
bridgeCommand
|
|
313
|
-
.command(
|
|
314
|
-
.description(
|
|
315
|
-
.option(
|
|
380
|
+
.command("claude-config")
|
|
381
|
+
.description("Print a Claude Code MCP config snippet (.mcp.json) for oracle-mcp.")
|
|
382
|
+
.option("--print-token", "Include ORACLE_REMOTE_TOKEN in the snippet.", false)
|
|
383
|
+
.option("--local-browser", "Use a local signed-in Chrome profile instead of a remote bridge.", false)
|
|
384
|
+
.option("--oracle-home-dir <path>", "Override ORACLE_HOME_DIR in the generated snippet.")
|
|
385
|
+
.option("--browser-profile-dir <path>", "Override ORACLE_BROWSER_PROFILE_DIR in the generated snippet.")
|
|
316
386
|
.action(async (commandOptions) => {
|
|
317
|
-
const { runBridgeClaudeConfig } = await import(
|
|
387
|
+
const { runBridgeClaudeConfig } = await import("../src/cli/bridge/claudeConfig.js");
|
|
318
388
|
await runBridgeClaudeConfig(commandOptions);
|
|
319
389
|
});
|
|
320
390
|
program
|
|
321
|
-
.command(
|
|
322
|
-
.description(
|
|
391
|
+
.command("tui")
|
|
392
|
+
.description("Launch the interactive terminal UI for humans (no automation).")
|
|
323
393
|
.action(async () => {
|
|
324
394
|
await sessionStore.ensureStorage();
|
|
325
395
|
await launchTui({ version: VERSION, printIntro: false });
|
|
326
396
|
});
|
|
327
|
-
|
|
328
|
-
.command(
|
|
329
|
-
.description(
|
|
330
|
-
.option(
|
|
331
|
-
.option(
|
|
332
|
-
.option(
|
|
333
|
-
.option(
|
|
334
|
-
.option(
|
|
335
|
-
.option(
|
|
336
|
-
.option(
|
|
337
|
-
.option(
|
|
338
|
-
.option(
|
|
339
|
-
.
|
|
397
|
+
program
|
|
398
|
+
.command("session [id]")
|
|
399
|
+
.description("Attach to a stored session or list recent sessions when no ID is provided.")
|
|
400
|
+
.option("--hours <hours>", "Look back this many hours when listing sessions (default 24).", parseFloatOption, 24)
|
|
401
|
+
.option("--limit <count>", "Maximum sessions to show when listing (max 1000).", parseIntOption, 100)
|
|
402
|
+
.option("--all", "Include all stored sessions regardless of age.", false)
|
|
403
|
+
.option("--clear", "Delete stored sessions older than the provided window (24h default).", false)
|
|
404
|
+
.option("--hide-prompt", "Hide stored prompt when displaying a session.", false)
|
|
405
|
+
.option("--render", "Render completed session output as markdown (rich TTY only).", false)
|
|
406
|
+
.option("--render-markdown", "Alias for --render.", false)
|
|
407
|
+
.option("--model <name>", "Filter sessions/output for a specific model.", "")
|
|
408
|
+
.option("--path", "Print the stored session paths instead of attaching.", false)
|
|
409
|
+
.option("--harvest", "Re-read the bound browser tab and print/save the latest assistant output.", false)
|
|
410
|
+
.option("--live", "Tail the live browser tab for this session until it completes, stalls, or detaches.", false)
|
|
411
|
+
.option("--write-output <path>", "Write harvested browser output to this file (requires --harvest or --live).")
|
|
412
|
+
.option("--browser-tab <ref>", "Override the browser tab ref used for harvesting/live tail (current, target id, URL, or title substring).")
|
|
413
|
+
.addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
|
|
340
414
|
.action(async (sessionId, _options, cmd) => {
|
|
341
415
|
await handleSessionCommand(sessionId, cmd);
|
|
342
416
|
});
|
|
343
|
-
|
|
344
|
-
.command(
|
|
345
|
-
.description(
|
|
346
|
-
.option(
|
|
347
|
-
.option(
|
|
348
|
-
.option(
|
|
349
|
-
.option(
|
|
350
|
-
.option(
|
|
351
|
-
.option(
|
|
352
|
-
.option(
|
|
353
|
-
.option(
|
|
354
|
-
.
|
|
417
|
+
program
|
|
418
|
+
.command("status [id]")
|
|
419
|
+
.description("List recent sessions (24h window by default) or attach to a session when an ID is provided.")
|
|
420
|
+
.option("--hours <hours>", "Look back this many hours (default 24).", parseFloatOption, 24)
|
|
421
|
+
.option("--limit <count>", "Maximum sessions to show (max 1000).", parseIntOption, 100)
|
|
422
|
+
.option("--all", "Include all stored sessions regardless of age.", false)
|
|
423
|
+
.option("--clear", "Delete stored sessions older than the provided window (24h default).", false)
|
|
424
|
+
.option("--render", "Render completed session output as markdown (rich TTY only).", false)
|
|
425
|
+
.option("--render-markdown", "Alias for --render.", false)
|
|
426
|
+
.option("--model <name>", "Filter sessions/output for a specific model.", "")
|
|
427
|
+
.option("--hide-prompt", "Hide stored prompt when displaying a session.", false)
|
|
428
|
+
.option("--browser-tabs", "List live ChatGPT browser tabs and known Oracle session linkage.", false)
|
|
429
|
+
.addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
|
|
355
430
|
.action(async (sessionId, _options, command) => {
|
|
356
431
|
const statusOptions = command.opts();
|
|
432
|
+
if (statusOptions.browserTabs) {
|
|
433
|
+
if (sessionId) {
|
|
434
|
+
console.error("Cannot combine a session ID with --browser-tabs. Remove the ID to inspect live browser tabs.");
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await showBrowserTabsStatus();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
357
441
|
const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
|
|
358
442
|
if (clearRequested) {
|
|
359
443
|
if (sessionId) {
|
|
360
|
-
console.error(
|
|
444
|
+
console.error("Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.");
|
|
361
445
|
process.exitCode = 1;
|
|
362
446
|
return;
|
|
363
447
|
}
|
|
364
448
|
const hours = statusOptions.hours;
|
|
365
449
|
const includeAll = statusOptions.all;
|
|
366
450
|
const result = await sessionStore.deleteOlderThan({ hours, includeAll });
|
|
367
|
-
const scope = includeAll ?
|
|
451
|
+
const scope = includeAll ? "all stored sessions" : `sessions older than ${hours}h`;
|
|
368
452
|
console.log(formatSessionCleanupMessage(result, scope));
|
|
369
453
|
return;
|
|
370
454
|
}
|
|
371
|
-
if (sessionId ===
|
|
455
|
+
if (sessionId === "clear" || sessionId === "clean") {
|
|
372
456
|
console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
|
|
373
457
|
process.exitCode = 1;
|
|
374
458
|
return;
|
|
375
459
|
}
|
|
376
460
|
if (sessionId) {
|
|
377
|
-
const autoRender = !command.getOptionValueSource?.(
|
|
461
|
+
const autoRender = !command.getOptionValueSource?.("render") &&
|
|
462
|
+
!command.getOptionValueSource?.("renderMarkdown")
|
|
378
463
|
? process.stdout.isTTY
|
|
379
464
|
: false;
|
|
380
465
|
const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
|
|
@@ -390,19 +475,19 @@ const statusCommand = program
|
|
|
390
475
|
});
|
|
391
476
|
});
|
|
392
477
|
program
|
|
393
|
-
.command(
|
|
394
|
-
.description(
|
|
395
|
-
.addOption(new Option(
|
|
396
|
-
.addOption(new Option(
|
|
397
|
-
.option(
|
|
398
|
-
.option(
|
|
478
|
+
.command("restart <id>")
|
|
479
|
+
.description("Re-run a stored session as a new session (clones options).")
|
|
480
|
+
.addOption(new Option("--wait").default(undefined))
|
|
481
|
+
.addOption(new Option("--no-wait").default(undefined).hideHelp())
|
|
482
|
+
.option("--remote-host <host:port>", "Delegate browser runs to a remote `oracle serve` instance.")
|
|
483
|
+
.option("--remote-token <token>", "Access token for the remote `oracle serve` instance.")
|
|
399
484
|
.action(async (sessionId, _options, cmd) => {
|
|
400
485
|
const restartOptions = cmd.opts();
|
|
401
486
|
await restartSession(sessionId, restartOptions);
|
|
402
487
|
});
|
|
403
488
|
function buildRunOptions(options, overrides = {}) {
|
|
404
489
|
if (!options.prompt) {
|
|
405
|
-
throw new Error(
|
|
490
|
+
throw new Error("Prompt is required.");
|
|
406
491
|
}
|
|
407
492
|
const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
|
|
408
493
|
const azure = options.azureEndpoint || overrides.azure?.endpoint
|
|
@@ -439,35 +524,40 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
439
524
|
sessionId: overrides.sessionId ?? options.sessionId,
|
|
440
525
|
verbose: overrides.verbose ?? options.verbose,
|
|
441
526
|
heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
|
|
442
|
-
browserAttachments: overrides.browserAttachments ??
|
|
527
|
+
browserAttachments: overrides.browserAttachments ??
|
|
528
|
+
options.browserAttachments ??
|
|
529
|
+
"auto",
|
|
443
530
|
browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
|
|
444
531
|
browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
|
|
532
|
+
generateImage: overrides.generateImage ?? options.generateImage,
|
|
533
|
+
outputPath: overrides.outputPath ?? options.output,
|
|
534
|
+
browserFollowUps: overrides.browserFollowUps ?? options.browserFollowUp ?? [],
|
|
445
535
|
background: overrides.background ?? undefined,
|
|
446
536
|
renderPlain: overrides.renderPlain ?? options.renderPlain ?? false,
|
|
447
537
|
writeOutputPath: overrides.writeOutputPath ?? options.writeOutputPath,
|
|
448
538
|
};
|
|
449
539
|
}
|
|
450
540
|
export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
|
|
451
|
-
if (sessionMode ===
|
|
452
|
-
logFn(chalk.dim(
|
|
541
|
+
if (sessionMode === "browser" && runOptions.search === false) {
|
|
542
|
+
logFn(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
|
|
453
543
|
runOptions.search = undefined;
|
|
454
544
|
}
|
|
455
545
|
}
|
|
456
546
|
function resolveHeartbeatIntervalMs(seconds) {
|
|
457
|
-
if (typeof seconds !==
|
|
547
|
+
if (typeof seconds !== "number" || seconds <= 0) {
|
|
458
548
|
return undefined;
|
|
459
549
|
}
|
|
460
550
|
return Math.round(seconds * 1000);
|
|
461
551
|
}
|
|
462
552
|
function assertFollowupSupported({ engine, model, baseUrl, azureEndpoint, }) {
|
|
463
|
-
if (engine !==
|
|
464
|
-
throw new Error(
|
|
553
|
+
if (engine !== "api") {
|
|
554
|
+
throw new Error("--followup requires --engine api.");
|
|
465
555
|
}
|
|
466
|
-
if (model.startsWith(
|
|
556
|
+
if (model.startsWith("gemini") || model.startsWith("claude")) {
|
|
467
557
|
throw new Error(`--followup is only supported for OpenAI Responses API runs. Model ${model} uses a provider client without previous_response_id support.`);
|
|
468
558
|
}
|
|
469
559
|
if (baseUrl && !azureEndpoint) {
|
|
470
|
-
throw new Error(
|
|
560
|
+
throw new Error("--followup is only supported for the default OpenAI Responses API or Azure OpenAI Responses. Custom --base-url providers are not supported.");
|
|
471
561
|
}
|
|
472
562
|
}
|
|
473
563
|
function levenshteinDistance(a, b) {
|
|
@@ -477,8 +567,8 @@ function levenshteinDistance(a, b) {
|
|
|
477
567
|
return b.length;
|
|
478
568
|
if (b.length === 0)
|
|
479
569
|
return a.length;
|
|
480
|
-
const previous =
|
|
481
|
-
const current =
|
|
570
|
+
const previous = Array.from({ length: b.length + 1 });
|
|
571
|
+
const current = Array.from({ length: b.length + 1 });
|
|
482
572
|
for (let j = 0; j <= b.length; j += 1) {
|
|
483
573
|
previous[j] = j;
|
|
484
574
|
}
|
|
@@ -515,7 +605,7 @@ async function suggestFollowupSessionIds(input, limit = 3) {
|
|
|
515
605
|
const seen = new Set();
|
|
516
606
|
const ranked = sessions
|
|
517
607
|
.map((meta) => meta.id)
|
|
518
|
-
.filter((id) => typeof id ===
|
|
608
|
+
.filter((id) => typeof id === "string" && id.length > 0)
|
|
519
609
|
.filter((id) => {
|
|
520
610
|
if (seen.has(id))
|
|
521
611
|
return false;
|
|
@@ -531,16 +621,18 @@ async function suggestFollowupSessionIds(input, limit = 3) {
|
|
|
531
621
|
async function resolveFollowupReference(value, followupModel) {
|
|
532
622
|
const trimmed = value.trim();
|
|
533
623
|
if (trimmed.length === 0) {
|
|
534
|
-
throw new Error(
|
|
624
|
+
throw new Error("--followup requires a session id or response id.");
|
|
535
625
|
}
|
|
536
|
-
if (trimmed.startsWith(
|
|
626
|
+
if (trimmed.startsWith("resp_")) {
|
|
537
627
|
return { responseId: trimmed };
|
|
538
628
|
}
|
|
539
629
|
// Treat as oracle session id (slug).
|
|
540
630
|
const meta = await sessionStore.readSession(trimmed);
|
|
541
631
|
if (!meta) {
|
|
542
632
|
const suggestions = await suggestFollowupSessionIds(trimmed);
|
|
543
|
-
const suggestionText = suggestions.length > 0
|
|
633
|
+
const suggestionText = suggestions.length > 0
|
|
634
|
+
? ` Did you mean: ${suggestions.map((id) => `"${id}"`).join(", ")}?`
|
|
635
|
+
: "";
|
|
544
636
|
throw new Error(`No session found with ID ${trimmed}.${suggestionText} Run "oracle status --hours 72 --limit 20" to list recent sessions.`);
|
|
545
637
|
}
|
|
546
638
|
const fromMetadata = extractResponseIdFromSession(meta, followupModel);
|
|
@@ -548,7 +640,7 @@ async function resolveFollowupReference(value, followupModel) {
|
|
|
548
640
|
return { responseId: fromMetadata, sessionId: meta.id };
|
|
549
641
|
}
|
|
550
642
|
// Fallback: scrape the log for a response id (covers older sessions / edge cases).
|
|
551
|
-
const logText = await sessionStore.readLog(trimmed).catch(() =>
|
|
643
|
+
const logText = await sessionStore.readLog(trimmed).catch(() => "");
|
|
552
644
|
const matches = logText.match(/resp_[A-Za-z0-9]+/g) ?? [];
|
|
553
645
|
const last = matches.length > 0 ? matches[matches.length - 1] : null;
|
|
554
646
|
if (last) {
|
|
@@ -560,7 +652,7 @@ function extractResponseIdFromSession(meta, followupModel) {
|
|
|
560
652
|
// Single-model sessions store response metadata at the session root.
|
|
561
653
|
const rootResponse = meta.response ?? null;
|
|
562
654
|
const rootResponseId = rootResponse?.responseId ?? rootResponse?.id;
|
|
563
|
-
if (rootResponseId && rootResponseId.startsWith(
|
|
655
|
+
if (rootResponseId && rootResponseId.startsWith("resp_")) {
|
|
564
656
|
return rootResponseId;
|
|
565
657
|
}
|
|
566
658
|
const runs = Array.isArray(meta.models) ? meta.models : [];
|
|
@@ -575,19 +667,19 @@ function extractResponseIdFromSession(meta, followupModel) {
|
|
|
575
667
|
};
|
|
576
668
|
const chosen = pickRun();
|
|
577
669
|
if (!chosen) {
|
|
578
|
-
const models = runs.map((r) => r.model).join(
|
|
670
|
+
const models = runs.map((r) => r.model).join(", ");
|
|
579
671
|
throw new Error(followupModel
|
|
580
672
|
? `Session ${meta.id} has no model named ${followupModel}. Available: ${models}`
|
|
581
673
|
: `Session ${meta.id} has multiple model runs. Re-run with --followup-model. Available: ${models}`);
|
|
582
674
|
}
|
|
583
675
|
const runResponse = chosen.response ?? null;
|
|
584
676
|
const runResponseId = runResponse?.responseId ?? runResponse?.id;
|
|
585
|
-
return runResponseId && runResponseId.startsWith(
|
|
677
|
+
return runResponseId && runResponseId.startsWith("resp_") ? runResponseId : null;
|
|
586
678
|
}
|
|
587
679
|
function buildRunOptionsFromMetadata(metadata) {
|
|
588
680
|
const stored = metadata.options ?? {};
|
|
589
681
|
return {
|
|
590
|
-
prompt: stored.prompt ??
|
|
682
|
+
prompt: stored.prompt ?? "",
|
|
591
683
|
model: stored.model ?? DEFAULT_MODEL,
|
|
592
684
|
models: stored.models,
|
|
593
685
|
previousResponseId: stored.previousResponseId,
|
|
@@ -616,42 +708,43 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
616
708
|
browserAttachments: stored.browserAttachments,
|
|
617
709
|
browserInlineFiles: stored.browserInlineFiles,
|
|
618
710
|
browserBundleFiles: stored.browserBundleFiles,
|
|
711
|
+
browserFollowUps: stored.browserFollowUps,
|
|
619
712
|
background: stored.background,
|
|
620
713
|
renderPlain: stored.renderPlain,
|
|
621
714
|
writeOutputPath: stored.writeOutputPath,
|
|
622
715
|
};
|
|
623
716
|
}
|
|
624
717
|
function getSessionMode(metadata) {
|
|
625
|
-
return metadata.mode ?? metadata.options?.mode ??
|
|
718
|
+
return metadata.mode ?? metadata.options?.mode ?? "api";
|
|
626
719
|
}
|
|
627
720
|
function getBrowserConfigFromMetadata(metadata) {
|
|
628
721
|
return metadata.options?.browserConfig ?? metadata.browser?.config;
|
|
629
722
|
}
|
|
630
723
|
async function runRootCommand(options) {
|
|
631
|
-
if (process.env.ORACLE_FORCE_TUI ===
|
|
724
|
+
if (process.env.ORACLE_FORCE_TUI === "1") {
|
|
632
725
|
await sessionStore.ensureStorage();
|
|
633
726
|
await launchTui({ version: VERSION, printIntro: false });
|
|
634
727
|
return;
|
|
635
728
|
}
|
|
636
729
|
const userConfig = (await loadUserConfig()).config;
|
|
637
|
-
const helpRequested = rawCliArgs.some((arg) => arg ===
|
|
730
|
+
const helpRequested = rawCliArgs.some((arg) => arg === "--help" || arg === "-h");
|
|
638
731
|
const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
|
|
639
732
|
if (multiModelProvided) {
|
|
640
|
-
const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ??
|
|
733
|
+
const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? "");
|
|
641
734
|
if (modelFromConfigOrCli) {
|
|
642
|
-
throw new Error(
|
|
735
|
+
throw new Error("--models cannot be combined with --model.");
|
|
643
736
|
}
|
|
644
737
|
}
|
|
645
738
|
const optionUsesDefault = (name) => {
|
|
646
739
|
// Commander reports undefined for untouched options, so treat undefined/default the same
|
|
647
740
|
const source = program.getOptionValueSource?.(name);
|
|
648
|
-
return source == null || source ===
|
|
741
|
+
return source == null || source === "default";
|
|
649
742
|
};
|
|
650
743
|
if (helpRequested) {
|
|
651
744
|
if (options.verbose) {
|
|
652
|
-
console.log(
|
|
745
|
+
console.log("");
|
|
653
746
|
printDebugHelp(program.name());
|
|
654
|
-
console.log(
|
|
747
|
+
console.log("");
|
|
655
748
|
}
|
|
656
749
|
program.help({ error: false });
|
|
657
750
|
return;
|
|
@@ -661,8 +754,8 @@ async function runRootCommand(options) {
|
|
|
661
754
|
if (mergedFileInputs.length > 0) {
|
|
662
755
|
const { deduped, duplicates } = dedupePathInputs(mergedFileInputs, { cwd: process.cwd() });
|
|
663
756
|
if (duplicates.length > 0) {
|
|
664
|
-
const preview = duplicates.slice(0, 8).join(
|
|
665
|
-
const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` :
|
|
757
|
+
const preview = duplicates.slice(0, 8).join(", ");
|
|
758
|
+
const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : "";
|
|
666
759
|
console.log(chalk.dim(`Ignoring duplicate --file inputs: ${preview}${suffix}`));
|
|
667
760
|
}
|
|
668
761
|
options.file = deduped;
|
|
@@ -671,11 +764,11 @@ async function runRootCommand(options) {
|
|
|
671
764
|
const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
|
|
672
765
|
const renderPlain = resolveRenderPlain(options.renderPlain, options.render, options.renderMarkdown);
|
|
673
766
|
const applyRetentionOption = () => {
|
|
674
|
-
if (optionUsesDefault(
|
|
767
|
+
if (optionUsesDefault("retainHours") && typeof userConfig.sessionRetentionHours === "number") {
|
|
675
768
|
options.retainHours = userConfig.sessionRetentionHours;
|
|
676
769
|
}
|
|
677
770
|
const envRetention = process.env.ORACLE_RETAIN_HOURS;
|
|
678
|
-
if (optionUsesDefault(
|
|
771
|
+
if (optionUsesDefault("retainHours") && envRetention) {
|
|
679
772
|
const parsed = Number.parseFloat(envRetention);
|
|
680
773
|
if (!Number.isNaN(parsed)) {
|
|
681
774
|
options.retainHours = parsed;
|
|
@@ -695,11 +788,11 @@ async function runRootCommand(options) {
|
|
|
695
788
|
console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
|
|
696
789
|
}
|
|
697
790
|
if (userCliArgs.length === 0) {
|
|
698
|
-
console.log(chalk.yellow(
|
|
791
|
+
console.log(chalk.yellow("No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI."));
|
|
699
792
|
program.outputHelp();
|
|
700
793
|
return;
|
|
701
794
|
}
|
|
702
|
-
const retentionHours = typeof options.retainHours ===
|
|
795
|
+
const retentionHours = typeof options.retainHours === "number" ? options.retainHours : undefined;
|
|
703
796
|
await sessionStore.ensureStorage();
|
|
704
797
|
await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
|
|
705
798
|
if (options.debugHelp) {
|
|
@@ -707,35 +800,42 @@ async function runRootCommand(options) {
|
|
|
707
800
|
return;
|
|
708
801
|
}
|
|
709
802
|
if (options.dryRun && options.renderMarkdown) {
|
|
710
|
-
throw new Error(
|
|
803
|
+
throw new Error("--dry-run cannot be combined with --render-markdown.");
|
|
711
804
|
}
|
|
712
805
|
const preferredEngine = options.engine ?? userConfig.engine;
|
|
713
|
-
let engine = resolveEngine({
|
|
806
|
+
let engine = resolveEngine({
|
|
807
|
+
engine: preferredEngine,
|
|
808
|
+
browserFlag: options.browser,
|
|
809
|
+
env: process.env,
|
|
810
|
+
});
|
|
714
811
|
if (options.browser) {
|
|
715
|
-
console.log(chalk.yellow(
|
|
812
|
+
console.log(chalk.yellow("`--browser` is deprecated; use `--engine browser` instead."));
|
|
716
813
|
}
|
|
717
|
-
if (optionUsesDefault(
|
|
814
|
+
if (optionUsesDefault("model") && userConfig.model) {
|
|
718
815
|
options.model = userConfig.model;
|
|
719
816
|
}
|
|
720
|
-
if (optionUsesDefault(
|
|
721
|
-
options.search = userConfig.search ===
|
|
817
|
+
if (optionUsesDefault("search") && userConfig.search) {
|
|
818
|
+
options.search = userConfig.search === "on";
|
|
722
819
|
}
|
|
723
|
-
if (optionUsesDefault(
|
|
820
|
+
if (optionUsesDefault("filesReport") && userConfig.filesReport != null) {
|
|
724
821
|
options.filesReport = Boolean(userConfig.filesReport);
|
|
725
822
|
}
|
|
726
|
-
if (optionUsesDefault(
|
|
823
|
+
if (optionUsesDefault("heartbeat") && typeof userConfig.heartbeatSeconds === "number") {
|
|
727
824
|
options.heartbeat = userConfig.heartbeatSeconds;
|
|
728
825
|
}
|
|
729
|
-
if (optionUsesDefault(
|
|
826
|
+
if (optionUsesDefault("baseUrl") && userConfig.apiBaseUrl) {
|
|
730
827
|
options.baseUrl = userConfig.apiBaseUrl;
|
|
731
828
|
}
|
|
732
|
-
if (remoteHost && engine !==
|
|
733
|
-
throw new Error(
|
|
829
|
+
if (remoteHost && engine !== "browser") {
|
|
830
|
+
throw new Error("--remote-host requires --engine browser.");
|
|
734
831
|
}
|
|
735
832
|
if (remoteHost && options.remoteChrome) {
|
|
736
|
-
throw new Error(
|
|
833
|
+
throw new Error("--remote-host cannot be combined with --remote-chrome.");
|
|
834
|
+
}
|
|
835
|
+
if (options.browserTab && engine !== "browser") {
|
|
836
|
+
throw new Error("--browser-tab requires --engine browser.");
|
|
737
837
|
}
|
|
738
|
-
if (optionUsesDefault(
|
|
838
|
+
if (optionUsesDefault("azureEndpoint")) {
|
|
739
839
|
if (process.env.AZURE_OPENAI_ENDPOINT) {
|
|
740
840
|
options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
741
841
|
}
|
|
@@ -743,7 +843,7 @@ async function runRootCommand(options) {
|
|
|
743
843
|
options.azureEndpoint = userConfig.azure.endpoint;
|
|
744
844
|
}
|
|
745
845
|
}
|
|
746
|
-
if (optionUsesDefault(
|
|
846
|
+
if (optionUsesDefault("azureDeployment")) {
|
|
747
847
|
if (process.env.AZURE_OPENAI_DEPLOYMENT) {
|
|
748
848
|
options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
749
849
|
}
|
|
@@ -751,7 +851,7 @@ async function runRootCommand(options) {
|
|
|
751
851
|
options.azureDeployment = userConfig.azure.deployment;
|
|
752
852
|
}
|
|
753
853
|
}
|
|
754
|
-
if (optionUsesDefault(
|
|
854
|
+
if (optionUsesDefault("azureApiVersion")) {
|
|
755
855
|
if (process.env.AZURE_OPENAI_API_VERSION) {
|
|
756
856
|
options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
757
857
|
}
|
|
@@ -762,52 +862,56 @@ async function runRootCommand(options) {
|
|
|
762
862
|
const normalizedMultiModels = multiModelProvided
|
|
763
863
|
? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
|
|
764
864
|
: [];
|
|
765
|
-
const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ?
|
|
865
|
+
const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? "" : DEFAULT_MODEL);
|
|
766
866
|
const resolvedModelCandidate = multiModelProvided
|
|
767
867
|
? normalizedMultiModels[0]
|
|
768
|
-
: engine ===
|
|
868
|
+
: engine === "browser"
|
|
769
869
|
? inferModelFromLabel(cliModelArg || DEFAULT_MODEL)
|
|
770
870
|
: resolveApiModel(cliModelArg || DEFAULT_MODEL);
|
|
771
871
|
const primaryModelCandidate = normalizedMultiModels[0] ?? resolvedModelCandidate;
|
|
772
|
-
const isGemini = primaryModelCandidate.startsWith(
|
|
773
|
-
const isCodex = primaryModelCandidate.startsWith(
|
|
774
|
-
const isClaude = primaryModelCandidate.startsWith(
|
|
775
|
-
const userForcedBrowser = options.browser || options.engine ===
|
|
776
|
-
const isBrowserCompatible = (model) => model.startsWith(
|
|
777
|
-
const hasNonBrowserCompatibleTarget = (engine ===
|
|
872
|
+
const isGemini = primaryModelCandidate.startsWith("gemini");
|
|
873
|
+
const isCodex = primaryModelCandidate.startsWith("gpt-5.1-codex");
|
|
874
|
+
const isClaude = primaryModelCandidate.startsWith("claude");
|
|
875
|
+
const userForcedBrowser = options.browser || options.engine === "browser";
|
|
876
|
+
const isBrowserCompatible = (model) => model.startsWith("gpt-") || model.startsWith("gemini");
|
|
877
|
+
const hasNonBrowserCompatibleTarget = (engine === "browser" || userForcedBrowser) &&
|
|
778
878
|
(normalizedMultiModels.length > 0
|
|
779
879
|
? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
|
|
780
880
|
: !isBrowserCompatible(resolvedModelCandidate));
|
|
781
881
|
if (hasNonBrowserCompatibleTarget) {
|
|
782
|
-
throw new Error(
|
|
882
|
+
throw new Error("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.");
|
|
783
883
|
}
|
|
784
|
-
if (isClaude && engine ===
|
|
785
|
-
console.log(chalk.dim(
|
|
786
|
-
engine =
|
|
884
|
+
if (isClaude && engine === "browser") {
|
|
885
|
+
console.log(chalk.dim("Browser engine is not supported for Claude models; switching to API."));
|
|
886
|
+
engine = "api";
|
|
787
887
|
}
|
|
788
|
-
if (isCodex && engine ===
|
|
789
|
-
console.log(chalk.dim(
|
|
790
|
-
engine =
|
|
888
|
+
if (isCodex && engine === "browser") {
|
|
889
|
+
console.log(chalk.dim("Browser engine is not supported for gpt-5.1-codex; switching to API."));
|
|
890
|
+
engine = "api";
|
|
791
891
|
}
|
|
792
892
|
if (normalizedMultiModels.length > 0) {
|
|
793
|
-
engine =
|
|
893
|
+
engine = "api";
|
|
794
894
|
}
|
|
795
895
|
if (remoteHost && normalizedMultiModels.length > 0) {
|
|
796
|
-
throw new Error(
|
|
896
|
+
throw new Error("--remote-host does not support --models yet. Use API engine locally instead.");
|
|
797
897
|
}
|
|
798
898
|
const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
|
|
799
|
-
const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model ===
|
|
800
|
-
if ((userForcedBrowser || userConfig.engine ===
|
|
801
|
-
throw new Error(
|
|
899
|
+
const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model === "gemini-3.1-pro");
|
|
900
|
+
if ((userForcedBrowser || userConfig.engine === "browser") && includesGeminiApiOnly) {
|
|
901
|
+
throw new Error("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.");
|
|
802
902
|
}
|
|
803
|
-
if (engine ===
|
|
804
|
-
console.log(chalk.dim(
|
|
805
|
-
engine =
|
|
903
|
+
if (engine === "browser" && includesGeminiApiOnly) {
|
|
904
|
+
console.log(chalk.dim("gemini-3.1-pro is API-only today; switching to API."));
|
|
905
|
+
engine = "api";
|
|
806
906
|
}
|
|
807
|
-
const
|
|
907
|
+
const browserFollowUpCount = options.browserFollowUp?.filter((entry) => entry.trim().length > 0).length ?? 0;
|
|
908
|
+
if (engine !== "browser" && browserFollowUpCount > 0) {
|
|
909
|
+
throw new Error("--browser-follow-up requires --engine browser.");
|
|
910
|
+
}
|
|
911
|
+
const effectiveModelId = resolvedModel.startsWith("gemini")
|
|
808
912
|
? resolveGeminiModelId(resolvedModel)
|
|
809
913
|
: isKnownModel(resolvedModel)
|
|
810
|
-
? MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel
|
|
914
|
+
? (MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel)
|
|
811
915
|
: resolvedModel;
|
|
812
916
|
const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
|
|
813
917
|
const { models: _rawModels, ...optionsWithoutModels } = options;
|
|
@@ -828,7 +932,7 @@ async function runRootCommand(options) {
|
|
|
828
932
|
engine,
|
|
829
933
|
});
|
|
830
934
|
if (remoteHost && waitPreference === false) {
|
|
831
|
-
console.log(chalk.dim(
|
|
935
|
+
console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
|
|
832
936
|
waitPreference = true;
|
|
833
937
|
}
|
|
834
938
|
if (await handleStatusFlag(options, { attachSession, showStatus })) {
|
|
@@ -843,10 +947,12 @@ async function runRootCommand(options) {
|
|
|
843
947
|
}
|
|
844
948
|
if (renderMarkdown || copyMarkdown) {
|
|
845
949
|
if (!options.prompt) {
|
|
846
|
-
throw new Error(
|
|
950
|
+
throw new Error("Prompt is required when using --render-markdown or --copy-markdown.");
|
|
847
951
|
}
|
|
848
952
|
const bundle = await buildMarkdownBundle({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
|
|
849
|
-
const modelConfig = isKnownModel(resolvedModel)
|
|
953
|
+
const modelConfig = isKnownModel(resolvedModel)
|
|
954
|
+
? MODEL_CONFIGS[resolvedModel]
|
|
955
|
+
: MODEL_CONFIGS["gpt-5.1"];
|
|
850
956
|
const requestBody = buildRequestBody({
|
|
851
957
|
modelConfig,
|
|
852
958
|
systemPrompt: bundle.systemPrompt,
|
|
@@ -863,25 +969,38 @@ async function runRootCommand(options) {
|
|
|
863
969
|
? bundle.markdown
|
|
864
970
|
: await formatRenderedMarkdown(bundle.markdown, { richTty: isTty });
|
|
865
971
|
// Trim trailing newlines from the rendered bundle so we print exactly one blank before the summary line.
|
|
866
|
-
console.log(output.replace(/\n+$/u,
|
|
972
|
+
console.log(output.replace(/\n+$/u, ""));
|
|
867
973
|
}
|
|
868
974
|
if (copyMarkdown) {
|
|
869
975
|
const result = await copyToClipboard(bundle.markdown);
|
|
870
976
|
if (result.success) {
|
|
871
|
-
const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` :
|
|
977
|
+
const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : "";
|
|
872
978
|
const summary = `Copied markdown to clipboard (~${formatCompactNumber(estimatedTokens)} tokens${filesPart}).`;
|
|
873
979
|
console.log(chalk.green(summary));
|
|
874
980
|
}
|
|
875
981
|
else {
|
|
876
|
-
const reason = result.error instanceof Error
|
|
982
|
+
const reason = result.error instanceof Error
|
|
983
|
+
? result.error.message
|
|
984
|
+
: String(result.error ?? "unknown error");
|
|
877
985
|
console.log(chalk.dim(`Copy failed (${reason}); markdown not printed. Re-run with --render-markdown if you need the content.`));
|
|
878
986
|
}
|
|
879
987
|
}
|
|
880
988
|
return;
|
|
881
989
|
}
|
|
990
|
+
const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
|
|
991
|
+
applyBrowserDefaultsFromConfig(options, userConfig, getSource);
|
|
992
|
+
const sessionMode = engine === "browser" ? "browser" : "api";
|
|
993
|
+
const browserModelLabelOverride = sessionMode === "browser" ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
|
|
994
|
+
const browserConfig = sessionMode === "browser"
|
|
995
|
+
? await buildBrowserConfig({
|
|
996
|
+
...options,
|
|
997
|
+
model: resolvedModel,
|
|
998
|
+
browserModelLabel: browserModelLabelOverride,
|
|
999
|
+
})
|
|
1000
|
+
: undefined;
|
|
882
1001
|
if (previewMode) {
|
|
883
1002
|
if (!options.prompt) {
|
|
884
|
-
throw new Error(
|
|
1003
|
+
throw new Error("Prompt is required when using --dry-run/preview.");
|
|
885
1004
|
}
|
|
886
1005
|
if (userConfig.promptSuffix) {
|
|
887
1006
|
options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
|
|
@@ -895,26 +1014,31 @@ async function runRootCommand(options) {
|
|
|
895
1014
|
azureEndpoint: resolvedOptions.azure?.endpoint,
|
|
896
1015
|
});
|
|
897
1016
|
if (normalizedMultiModels.length > 0) {
|
|
898
|
-
throw new Error(
|
|
1017
|
+
throw new Error("--followup cannot be combined with --models.");
|
|
899
1018
|
}
|
|
900
1019
|
const followup = await resolveFollowupReference(options.followup, options.followupModel);
|
|
901
1020
|
resolvedOptions.previousResponseId = followup.responseId;
|
|
902
1021
|
resolvedOptions.followupSessionId = followup.sessionId;
|
|
903
1022
|
resolvedOptions.followupModel = options.followupModel;
|
|
904
1023
|
}
|
|
905
|
-
const runOptions = buildRunOptions(resolvedOptions, {
|
|
906
|
-
|
|
1024
|
+
const runOptions = buildRunOptions(resolvedOptions, {
|
|
1025
|
+
preview: true,
|
|
1026
|
+
previewMode,
|
|
1027
|
+
baseUrl: resolvedBaseUrl,
|
|
1028
|
+
});
|
|
1029
|
+
if (engine === "browser") {
|
|
907
1030
|
await runBrowserPreview({
|
|
908
1031
|
runOptions,
|
|
909
1032
|
cwd: process.cwd(),
|
|
910
1033
|
version: VERSION,
|
|
911
1034
|
previewMode,
|
|
912
1035
|
log: console.log,
|
|
1036
|
+
browserConfig,
|
|
913
1037
|
}, {});
|
|
914
1038
|
return;
|
|
915
1039
|
}
|
|
916
1040
|
// API dry-run/preview path
|
|
917
|
-
if (previewMode ===
|
|
1041
|
+
if (previewMode === "summary") {
|
|
918
1042
|
await runDryRunSummary({
|
|
919
1043
|
engine,
|
|
920
1044
|
runOptions,
|
|
@@ -934,7 +1058,7 @@ async function runRootCommand(options) {
|
|
|
934
1058
|
return;
|
|
935
1059
|
}
|
|
936
1060
|
if (!options.prompt) {
|
|
937
|
-
throw new Error(
|
|
1061
|
+
throw new Error("Prompt is required when starting a new session.");
|
|
938
1062
|
}
|
|
939
1063
|
if (userConfig.promptSuffix) {
|
|
940
1064
|
options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
|
|
@@ -948,7 +1072,7 @@ async function runRootCommand(options) {
|
|
|
948
1072
|
azureEndpoint: resolvedOptions.azure?.endpoint,
|
|
949
1073
|
});
|
|
950
1074
|
if (normalizedMultiModels.length > 0) {
|
|
951
|
-
throw new Error(
|
|
1075
|
+
throw new Error("--followup cannot be combined with --models.");
|
|
952
1076
|
}
|
|
953
1077
|
const followup = await resolveFollowupReference(options.followup, options.followupModel);
|
|
954
1078
|
resolvedOptions.previousResponseId = followup.responseId;
|
|
@@ -957,6 +1081,7 @@ async function runRootCommand(options) {
|
|
|
957
1081
|
}
|
|
958
1082
|
const duplicateBlocked = await shouldBlockDuplicatePrompt({
|
|
959
1083
|
prompt: resolvedOptions.prompt,
|
|
1084
|
+
browserFollowUps: resolvedOptions.browserFollowUp,
|
|
960
1085
|
force: options.force,
|
|
961
1086
|
sessionStore,
|
|
962
1087
|
log: console.log,
|
|
@@ -966,8 +1091,10 @@ async function runRootCommand(options) {
|
|
|
966
1091
|
return;
|
|
967
1092
|
}
|
|
968
1093
|
if (options.file && options.file.length > 0) {
|
|
969
|
-
const isBrowserMode = engine ===
|
|
970
|
-
const filesToValidate = isBrowserMode
|
|
1094
|
+
const isBrowserMode = engine === "browser" || userForcedBrowser;
|
|
1095
|
+
const filesToValidate = isBrowserMode
|
|
1096
|
+
? options.file.filter((f) => !isMediaFile(f))
|
|
1097
|
+
: options.file;
|
|
971
1098
|
if (filesToValidate.length > 0) {
|
|
972
1099
|
await readFiles(filesToValidate, {
|
|
973
1100
|
cwd: process.cwd(),
|
|
@@ -975,23 +1102,12 @@ async function runRootCommand(options) {
|
|
|
975
1102
|
});
|
|
976
1103
|
}
|
|
977
1104
|
}
|
|
978
|
-
const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
|
|
979
|
-
applyBrowserDefaultsFromConfig(options, userConfig, getSource);
|
|
980
1105
|
const notifications = resolveNotificationSettings({
|
|
981
1106
|
cliNotify: options.notify,
|
|
982
1107
|
cliNotifySound: options.notifySound,
|
|
983
1108
|
env: process.env,
|
|
984
1109
|
config: userConfig.notify,
|
|
985
1110
|
});
|
|
986
|
-
const sessionMode = engine === 'browser' ? 'browser' : 'api';
|
|
987
|
-
const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
|
|
988
|
-
const browserConfig = sessionMode === 'browser'
|
|
989
|
-
? await buildBrowserConfig({
|
|
990
|
-
...options,
|
|
991
|
-
model: resolvedModel,
|
|
992
|
-
browserModelLabel: browserModelLabelOverride,
|
|
993
|
-
})
|
|
994
|
-
: undefined;
|
|
995
1111
|
let browserDeps;
|
|
996
1112
|
if (browserConfig && remoteHost) {
|
|
997
1113
|
browserDeps = {
|
|
@@ -999,7 +1115,7 @@ async function runRootCommand(options) {
|
|
|
999
1115
|
};
|
|
1000
1116
|
console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
|
|
1001
1117
|
}
|
|
1002
|
-
else if (browserConfig && resolvedModel.startsWith(
|
|
1118
|
+
else if (browserConfig && resolvedModel.startsWith("gemini")) {
|
|
1003
1119
|
browserDeps = {
|
|
1004
1120
|
executeBrowser: createGeminiWebExecutor({
|
|
1005
1121
|
youtube: options.youtube,
|
|
@@ -1010,9 +1126,9 @@ async function runRootCommand(options) {
|
|
|
1010
1126
|
showThoughts: options.geminiShowThoughts,
|
|
1011
1127
|
}),
|
|
1012
1128
|
};
|
|
1013
|
-
console.log(chalk.dim(
|
|
1014
|
-
if (browserConfig.modelStrategy && browserConfig.modelStrategy !==
|
|
1015
|
-
console.log(chalk.dim(
|
|
1129
|
+
console.log(chalk.dim("Using Gemini web client for browser automation"));
|
|
1130
|
+
if (browserConfig.modelStrategy && browserConfig.modelStrategy !== "select") {
|
|
1131
|
+
console.log(chalk.dim("Browser model strategy is ignored for Gemini web runs."));
|
|
1016
1132
|
}
|
|
1017
1133
|
}
|
|
1018
1134
|
const remoteExecutionActive = Boolean(browserDeps);
|
|
@@ -1040,8 +1156,8 @@ async function runRootCommand(options) {
|
|
|
1040
1156
|
baseUrl: resolvedBaseUrl,
|
|
1041
1157
|
});
|
|
1042
1158
|
enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
|
|
1043
|
-
if (sessionMode ===
|
|
1044
|
-
console.log(chalk.dim(
|
|
1159
|
+
if (sessionMode === "browser" && baseRunOptions.search === false) {
|
|
1160
|
+
console.log(chalk.dim("Note: search is not available in browser engine; ignoring search=false."));
|
|
1045
1161
|
baseRunOptions.search = undefined;
|
|
1046
1162
|
}
|
|
1047
1163
|
const sessionMeta = await sessionStore.createSession({
|
|
@@ -1063,7 +1179,7 @@ async function runRootCommand(options) {
|
|
|
1063
1179
|
sessionId: sessionMeta.id,
|
|
1064
1180
|
effectiveModelId,
|
|
1065
1181
|
};
|
|
1066
|
-
const disableDetachEnv = process.env.ORACLE_NO_DETACH ===
|
|
1182
|
+
const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
|
|
1067
1183
|
const detachAllowed = remoteExecutionActive
|
|
1068
1184
|
? false
|
|
1069
1185
|
: shouldDetachSession({
|
|
@@ -1081,12 +1197,12 @@ async function runRootCommand(options) {
|
|
|
1081
1197
|
});
|
|
1082
1198
|
if (!waitPreference) {
|
|
1083
1199
|
if (!detached) {
|
|
1084
|
-
console.log(chalk.red(
|
|
1200
|
+
console.log(chalk.red("Unable to start in background; use --wait to run inline."));
|
|
1085
1201
|
process.exitCode = 1;
|
|
1086
1202
|
return;
|
|
1087
1203
|
}
|
|
1088
1204
|
console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
|
|
1089
|
-
console.log(chalk.dim(
|
|
1205
|
+
console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
|
|
1090
1206
|
return;
|
|
1091
1207
|
}
|
|
1092
1208
|
if (detached === false) {
|
|
@@ -1101,8 +1217,8 @@ async function runRootCommand(options) {
|
|
|
1101
1217
|
async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
|
|
1102
1218
|
const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
|
|
1103
1219
|
let headerAugmented = false;
|
|
1104
|
-
const combinedLog = (message =
|
|
1105
|
-
if (!headerAugmented && message.startsWith(
|
|
1220
|
+
const combinedLog = (message = "") => {
|
|
1221
|
+
if (!headerAugmented && message.startsWith("oracle (")) {
|
|
1106
1222
|
headerAugmented = true;
|
|
1107
1223
|
if (showReattachHint) {
|
|
1108
1224
|
console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
|
|
@@ -1131,21 +1247,19 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
|
|
|
1131
1247
|
log: combinedLog,
|
|
1132
1248
|
write: combinedWrite,
|
|
1133
1249
|
version: VERSION,
|
|
1134
|
-
notifications: notifications ??
|
|
1250
|
+
notifications: notifications ??
|
|
1251
|
+
deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
|
|
1135
1252
|
browserDeps,
|
|
1136
1253
|
});
|
|
1137
1254
|
const latest = await sessionStore.readSession(sessionMeta.id);
|
|
1138
1255
|
if (!suppressSummary) {
|
|
1139
1256
|
const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
|
|
1140
1257
|
if (summary) {
|
|
1141
|
-
console.log(
|
|
1258
|
+
console.log("\n" + chalk.green.bold(summary));
|
|
1142
1259
|
logLine(summary); // plain text in log, colored on stdout
|
|
1143
1260
|
}
|
|
1144
1261
|
}
|
|
1145
1262
|
}
|
|
1146
|
-
catch (error) {
|
|
1147
|
-
throw error;
|
|
1148
|
-
}
|
|
1149
1263
|
finally {
|
|
1150
1264
|
stream.end();
|
|
1151
1265
|
}
|
|
@@ -1153,14 +1267,14 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
|
|
|
1153
1267
|
async function launchDetachedSession(sessionId) {
|
|
1154
1268
|
return new Promise((resolve, reject) => {
|
|
1155
1269
|
try {
|
|
1156
|
-
const args = [
|
|
1270
|
+
const args = ["--", CLI_ENTRYPOINT, "--exec-session", sessionId];
|
|
1157
1271
|
const child = spawn(process.execPath, args, {
|
|
1158
1272
|
detached: true,
|
|
1159
|
-
stdio:
|
|
1273
|
+
stdio: "ignore",
|
|
1160
1274
|
env: process.env,
|
|
1161
1275
|
});
|
|
1162
|
-
child.once(
|
|
1163
|
-
child.once(
|
|
1276
|
+
child.once("error", reject);
|
|
1277
|
+
child.once("spawn", () => {
|
|
1164
1278
|
child.unref();
|
|
1165
1279
|
resolve(true);
|
|
1166
1280
|
});
|
|
@@ -1184,9 +1298,9 @@ async function restartSession(sessionId, options) {
|
|
|
1184
1298
|
return;
|
|
1185
1299
|
}
|
|
1186
1300
|
const sessionMode = getSessionMode(metadata);
|
|
1187
|
-
const engine = sessionMode ===
|
|
1301
|
+
const engine = sessionMode === "browser" ? "browser" : "api";
|
|
1188
1302
|
const browserConfig = getBrowserConfigFromMetadata(metadata);
|
|
1189
|
-
if (sessionMode ===
|
|
1303
|
+
if (sessionMode === "browser" && !browserConfig) {
|
|
1190
1304
|
console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
|
|
1191
1305
|
process.exitCode = 1;
|
|
1192
1306
|
return;
|
|
@@ -1195,8 +1309,10 @@ async function restartSession(sessionId, options) {
|
|
|
1195
1309
|
const cwd = metadata.cwd ?? process.cwd();
|
|
1196
1310
|
const storedOptions = metadata.options ?? {};
|
|
1197
1311
|
if (runOptions.file && runOptions.file.length > 0) {
|
|
1198
|
-
const isBrowserMode = engine ===
|
|
1199
|
-
const filesToValidate = isBrowserMode
|
|
1312
|
+
const isBrowserMode = engine === "browser";
|
|
1313
|
+
const filesToValidate = isBrowserMode
|
|
1314
|
+
? runOptions.file.filter((f) => !isMediaFile(f))
|
|
1315
|
+
: runOptions.file;
|
|
1200
1316
|
if (filesToValidate.length > 0) {
|
|
1201
1317
|
await readFiles(filesToValidate, {
|
|
1202
1318
|
cwd,
|
|
@@ -1219,14 +1335,14 @@ async function restartSession(sessionId, options) {
|
|
|
1219
1335
|
});
|
|
1220
1336
|
const remoteHost = remoteConfig.host;
|
|
1221
1337
|
const remoteToken = remoteConfig.token;
|
|
1222
|
-
if (remoteHost && engine !==
|
|
1223
|
-
throw new Error(
|
|
1338
|
+
if (remoteHost && engine !== "browser") {
|
|
1339
|
+
throw new Error("--remote-host requires a browser session.");
|
|
1224
1340
|
}
|
|
1225
1341
|
if (remoteHost) {
|
|
1226
1342
|
console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
|
|
1227
1343
|
}
|
|
1228
1344
|
if (remoteHost && waitPreference === false) {
|
|
1229
|
-
console.log(chalk.dim(
|
|
1345
|
+
console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
|
|
1230
1346
|
waitPreference = true;
|
|
1231
1347
|
}
|
|
1232
1348
|
let browserDeps;
|
|
@@ -1236,7 +1352,7 @@ async function restartSession(sessionId, options) {
|
|
|
1236
1352
|
};
|
|
1237
1353
|
console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
|
|
1238
1354
|
}
|
|
1239
|
-
else if (browserConfig && runOptions.model.startsWith(
|
|
1355
|
+
else if (browserConfig && runOptions.model.startsWith("gemini")) {
|
|
1240
1356
|
browserDeps = {
|
|
1241
1357
|
executeBrowser: createGeminiWebExecutor({
|
|
1242
1358
|
youtube: storedOptions.youtube,
|
|
@@ -1247,9 +1363,9 @@ async function restartSession(sessionId, options) {
|
|
|
1247
1363
|
showThoughts: storedOptions.geminiShowThoughts,
|
|
1248
1364
|
}),
|
|
1249
1365
|
};
|
|
1250
|
-
console.log(chalk.dim(
|
|
1251
|
-
if (browserConfig.modelStrategy && browserConfig.modelStrategy !==
|
|
1252
|
-
console.log(chalk.dim(
|
|
1366
|
+
console.log(chalk.dim("Using Gemini web client for browser automation"));
|
|
1367
|
+
if (browserConfig.modelStrategy && browserConfig.modelStrategy !== "select") {
|
|
1368
|
+
console.log(chalk.dim("Browser model strategy is ignored for Gemini web runs."));
|
|
1253
1369
|
}
|
|
1254
1370
|
}
|
|
1255
1371
|
const remoteExecutionActive = Boolean(browserDeps);
|
|
@@ -1274,7 +1390,7 @@ async function restartSession(sessionId, options) {
|
|
|
1274
1390
|
sessionId: sessionMeta.id,
|
|
1275
1391
|
effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
|
|
1276
1392
|
};
|
|
1277
|
-
const disableDetachEnv = process.env.ORACLE_NO_DETACH ===
|
|
1393
|
+
const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
|
|
1278
1394
|
const detachAllowed = remoteExecutionActive
|
|
1279
1395
|
? false
|
|
1280
1396
|
: shouldDetachSession({
|
|
@@ -1292,12 +1408,12 @@ async function restartSession(sessionId, options) {
|
|
|
1292
1408
|
});
|
|
1293
1409
|
if (!waitPreference) {
|
|
1294
1410
|
if (!detached) {
|
|
1295
|
-
console.log(chalk.red(
|
|
1411
|
+
console.log(chalk.red("Unable to start in background; use --wait to run inline."));
|
|
1296
1412
|
process.exitCode = 1;
|
|
1297
1413
|
return;
|
|
1298
1414
|
}
|
|
1299
1415
|
console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
|
|
1300
|
-
console.log(chalk.dim(
|
|
1416
|
+
console.log(chalk.dim("Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached."));
|
|
1301
1417
|
return;
|
|
1302
1418
|
}
|
|
1303
1419
|
if (detached === false) {
|
|
@@ -1343,37 +1459,62 @@ async function executeSession(sessionId) {
|
|
|
1343
1459
|
}
|
|
1344
1460
|
}
|
|
1345
1461
|
function printDebugHelp(cliName) {
|
|
1346
|
-
console.log(chalk.bold(
|
|
1462
|
+
console.log(chalk.bold("Advanced Options"));
|
|
1347
1463
|
printDebugOptionGroup([
|
|
1348
|
-
[
|
|
1349
|
-
[
|
|
1350
|
-
[
|
|
1464
|
+
["--search <on|off>", "Enable or disable the server-side search tool (default on)."],
|
|
1465
|
+
["--max-input <tokens>", "Override the input token budget."],
|
|
1466
|
+
["--max-output <tokens>", "Override the max output tokens (model default otherwise)."],
|
|
1351
1467
|
]);
|
|
1352
|
-
console.log(
|
|
1353
|
-
console.log(chalk.bold(
|
|
1468
|
+
console.log("");
|
|
1469
|
+
console.log(chalk.bold("Browser Options"));
|
|
1354
1470
|
printDebugOptionGroup([
|
|
1355
|
-
[
|
|
1356
|
-
[
|
|
1357
|
-
[
|
|
1358
|
-
[
|
|
1359
|
-
[
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
[
|
|
1364
|
-
[
|
|
1365
|
-
[
|
|
1366
|
-
[
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
[
|
|
1371
|
-
[
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1471
|
+
["--chatgpt-url <url>", "Override the ChatGPT web URL (workspace/folder targets)."],
|
|
1472
|
+
["--browser-chrome-profile <name>", "Reuse cookies from a specific Chrome profile."],
|
|
1473
|
+
["--browser-chrome-path <path>", "Point to a custom Chrome/Chromium binary."],
|
|
1474
|
+
["--browser-cookie-path <path>", "Use a specific Chrome/Chromium cookie store file."],
|
|
1475
|
+
[
|
|
1476
|
+
"--browser-attach-running",
|
|
1477
|
+
"Attach to your current Chrome session through its local remote debugging toggle.",
|
|
1478
|
+
],
|
|
1479
|
+
["--browser-url <url>", "Alias for --chatgpt-url."],
|
|
1480
|
+
["--browser-timeout <ms|s|m>", "Cap total wait time for the assistant response."],
|
|
1481
|
+
["--browser-input-timeout <ms|s|m>", "Cap how long we wait for the composer textarea."],
|
|
1482
|
+
[
|
|
1483
|
+
"--browser-recheck-delay <ms|s|m|h>",
|
|
1484
|
+
"After timeout, wait then revisit the conversation to retry capture.",
|
|
1485
|
+
],
|
|
1486
|
+
["--browser-recheck-timeout <ms|s|m|h>", "Time budget for the delayed recheck attempt."],
|
|
1487
|
+
[
|
|
1488
|
+
"--browser-reuse-wait <ms|s|m|h>",
|
|
1489
|
+
"Wait for a shared Chrome profile before launching (parallel runs).",
|
|
1490
|
+
],
|
|
1491
|
+
[
|
|
1492
|
+
"--browser-profile-lock-timeout <ms|s|m|h>",
|
|
1493
|
+
"Wait for the manual-login profile lock before sending.",
|
|
1494
|
+
],
|
|
1495
|
+
[
|
|
1496
|
+
"--browser-auto-reattach-delay <ms|s|m|h>",
|
|
1497
|
+
"Delay before periodic auto-reattach attempts after a timeout.",
|
|
1498
|
+
],
|
|
1499
|
+
[
|
|
1500
|
+
"--browser-auto-reattach-interval <ms|s|m|h>",
|
|
1501
|
+
"Interval between auto-reattach attempts (0 disables).",
|
|
1502
|
+
],
|
|
1503
|
+
["--browser-auto-reattach-timeout <ms|s|m|h>", "Time budget for each auto-reattach attempt."],
|
|
1504
|
+
[
|
|
1505
|
+
"--browser-cookie-wait <ms|s|m>",
|
|
1506
|
+
"Wait before retrying cookie sync when Chrome cookies are empty or locked.",
|
|
1507
|
+
],
|
|
1508
|
+
["--browser-no-cookie-sync", "Skip copying cookies from your main profile."],
|
|
1509
|
+
[
|
|
1510
|
+
"--browser-manual-login",
|
|
1511
|
+
"Skip cookie copy; reuse a persistent automation profile and log in manually.",
|
|
1512
|
+
],
|
|
1513
|
+
["--browser-headless", "Launch Chrome in headless mode."],
|
|
1514
|
+
["--browser-hide-window", "Hide the Chrome window (macOS headful only)."],
|
|
1515
|
+
["--browser-keep-browser", "Leave Chrome running after completion."],
|
|
1375
1516
|
]);
|
|
1376
|
-
console.log(
|
|
1517
|
+
console.log("");
|
|
1377
1518
|
console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
|
|
1378
1519
|
}
|
|
1379
1520
|
function printDebugOptionGroup(entries) {
|
|
@@ -1395,17 +1536,17 @@ function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engin
|
|
|
1395
1536
|
return true;
|
|
1396
1537
|
if (waitFlag === false)
|
|
1397
1538
|
return false;
|
|
1398
|
-
if (typeof storedPreference ===
|
|
1539
|
+
if (typeof storedPreference === "boolean")
|
|
1399
1540
|
return storedPreference;
|
|
1400
1541
|
return defaultWaitPreference(model, engine);
|
|
1401
1542
|
}
|
|
1402
1543
|
function resolveEffectiveModelIdForRun(model, stored) {
|
|
1403
1544
|
if (stored)
|
|
1404
1545
|
return stored;
|
|
1405
|
-
if (model.startsWith(
|
|
1546
|
+
if (model.startsWith("gemini")) {
|
|
1406
1547
|
return resolveGeminiModelId(model);
|
|
1407
1548
|
}
|
|
1408
|
-
return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
|
|
1549
|
+
return isKnownModel(model) ? (MODEL_CONFIGS[model].apiModel ?? model) : model;
|
|
1409
1550
|
}
|
|
1410
1551
|
program.action(async function () {
|
|
1411
1552
|
const options = this.optsWithGlobals();
|
|
@@ -1413,21 +1554,21 @@ program.action(async function () {
|
|
|
1413
1554
|
});
|
|
1414
1555
|
async function main() {
|
|
1415
1556
|
const parsePromise = program.parseAsync(normalizedArgv);
|
|
1416
|
-
const sigintPromise = once(process,
|
|
1417
|
-
const result = await Promise.race([parsePromise.then(() =>
|
|
1418
|
-
if (result ===
|
|
1419
|
-
console.log(chalk.yellow(
|
|
1557
|
+
const sigintPromise = once(process, "SIGINT").then(() => "sigint");
|
|
1558
|
+
const result = await Promise.race([parsePromise.then(() => "parsed"), sigintPromise]);
|
|
1559
|
+
if (result === "sigint") {
|
|
1560
|
+
console.log(chalk.yellow("\nCancelled."));
|
|
1420
1561
|
process.exitCode = 130;
|
|
1421
1562
|
}
|
|
1422
1563
|
}
|
|
1423
1564
|
void main().catch((error) => {
|
|
1424
1565
|
if (error instanceof Error) {
|
|
1425
1566
|
if (!isErrorLogged(error)) {
|
|
1426
|
-
console.error(chalk.red(
|
|
1567
|
+
console.error(chalk.red("✖"), error.message);
|
|
1427
1568
|
}
|
|
1428
1569
|
}
|
|
1429
1570
|
else {
|
|
1430
|
-
console.error(chalk.red(
|
|
1571
|
+
console.error(chalk.red("✖"), error);
|
|
1431
1572
|
}
|
|
1432
1573
|
process.exitCode = 1;
|
|
1433
1574
|
});
|