@steipete/oracle 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +161 -44
- package/dist/src/browser/config.js +6 -0
- package/dist/src/browser/cookies.js +49 -11
- package/dist/src/browser/index.js +18 -5
- package/dist/src/browser/sessionRunner.js +10 -1
- package/dist/src/cli/browserConfig.js +109 -2
- package/dist/src/cli/detach.js +12 -0
- package/dist/src/cli/dryRun.js +19 -3
- package/dist/src/cli/help.js +2 -0
- package/dist/src/cli/options.js +22 -0
- package/dist/src/cli/runOptions.js +16 -2
- package/dist/src/cli/sessionRunner.js +11 -0
- package/dist/src/cli/tui/index.js +68 -47
- package/dist/src/oracle/client.js +24 -6
- package/dist/src/oracle/config.js +10 -0
- package/dist/src/oracle/files.js +8 -2
- package/dist/src/oracle/format.js +2 -7
- package/dist/src/oracle/fsAdapter.js +4 -1
- package/dist/src/oracle/gemini.js +161 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/oscProgress.js +7 -1
- package/dist/src/oracle/run.js +111 -48
- package/dist/src/oracle.js +1 -0
- package/dist/src/sessionManager.js +2 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +16 -26
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/vendor/oracle-notifier/README.md +0 -24
package/README.md
CHANGED
|
@@ -21,7 +21,10 @@ Oracle gives your agents a simple, reliable way to **bundle a prompt plus the ri
|
|
|
21
21
|
If you omit `--engine`, Oracle prefers the API engine when `OPENAI_API_KEY` is present; otherwise it falls back to browser mode. Switch explicitly with `-e, --engine {api|browser}` when you want to override the auto choice. Everything else (prompt assembly, file handling, session logging) stays the same.
|
|
22
22
|
|
|
23
23
|
Note: Browser engine is considered experimental, requires an OpenAI Pro account and only works on macOS with Chrome.
|
|
24
|
-
|
|
24
|
+
Windows/Linux browser support is in progress; until then, use `--engine api` or bundle files and paste manually.
|
|
25
|
+
Your system password is needed to copy cookies. To skip Chrome/Keychain entirely, pass inline cookies via
|
|
26
|
+
`--browser-inline-cookies <json|base64>` or `--browser-inline-cookies-file <path>` (fallback files at
|
|
27
|
+
`~/.oracle/cookies.json` or `~/.oracle/cookies.base64`). API engine is stable and should be preferred.
|
|
25
28
|
|
|
26
29
|
## Quick start
|
|
27
30
|
|
|
@@ -38,6 +41,12 @@ npx -y @steipete/oracle -p "Review the TS data layer" --file "src/**/*.ts" --fil
|
|
|
38
41
|
# Mixed glob + single file
|
|
39
42
|
npx -y @steipete/oracle -p "Audit data layer" --file "src/**/*.ts" --file README.md
|
|
40
43
|
|
|
44
|
+
# Dry-run (no API call) with summary estimate
|
|
45
|
+
oracle --dry-run summary -p "Check release notes" --file docs/release-notes.md
|
|
46
|
+
|
|
47
|
+
# Alternate base URL (LiteLLM, Azure, self-hosted gateways)
|
|
48
|
+
OPENAI_API_KEY=sk-... oracle --base-url https://litellm.example.com/v1 -p "Summarize the risk register"
|
|
49
|
+
|
|
41
50
|
# Inspect past sessions
|
|
42
51
|
oracle status --clear --hours 168 # prune a week of cached runs
|
|
43
52
|
oracle status # list runs; grab an ID
|
|
@@ -86,13 +95,12 @@ Put per-user defaults in `~/.oracle/config.json` (parsed as JSON5, so comments/t
|
|
|
86
95
|
| `-f, --file <paths...>` | Attach files/dirs (supports globs and `!` excludes). |
|
|
87
96
|
| `-e, --engine <api\|browser>` | Choose API or browser automation. Omitted: API when `OPENAI_API_KEY` is set, otherwise browser. |
|
|
88
97
|
| `-m, --model <name>` | `gpt-5-pro` (default) or `gpt-5.1`. |
|
|
98
|
+
| `--base-url <url>` | Point the API engine at any OpenAI-compatible endpoint (LiteLLM, Azure, etc.). |
|
|
99
|
+
| `--azure-endpoint <url>` | Use Azure OpenAI (switches client automatically). |
|
|
89
100
|
| `--files-report` | Print per-file token usage. |
|
|
90
|
-
| `--
|
|
91
|
-
| `--render-markdown` | Print the assembled `[SYSTEM]/[USER]/[FILE]` bundle. |
|
|
92
|
-
| `--wait` / `--no-wait` | Block until completion. Default: `wait` for gpt-5.1/browser; `no-wait` for gpt-5-pro API (reattach later). |
|
|
93
|
-
| `-v, --verbose` | Extra logging (also surfaces advanced flags with `--help`). |
|
|
101
|
+
| `--dry-run [summary\|json\|full]` | Inspect the request without sending (alias: `--preview`). |
|
|
94
102
|
|
|
95
|
-
|
|
103
|
+
See [docs/openai-endpoints.md](docs/openai-endpoints.md) for advanced Azure/LiteLLM configuration.
|
|
96
104
|
|
|
97
105
|
## Sessions & background runs
|
|
98
106
|
|
package/dist/.DS_Store
ADDED
|
Binary file
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { once } from 'node:events';
|
|
5
6
|
import { Command, Option } from 'commander';
|
|
6
7
|
import { resolveEngine, defaultWaitPreference } from '../src/cli/engine.js';
|
|
7
8
|
import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
|
|
8
9
|
import chalk from 'chalk';
|
|
9
10
|
import { ensureSessionStorage, initializeSession, readSessionMetadata, createSessionLogWriter, deleteSessionsOlderThan, } from '../src/sessionManager.js';
|
|
10
|
-
import {
|
|
11
|
+
import { renderPromptMarkdown, readFiles } from '../src/oracle.js';
|
|
11
12
|
import { CHATGPT_URL } from '../src/browserMode.js';
|
|
12
13
|
import { applyHelpStyling } from '../src/cli/help.js';
|
|
13
|
-
import { collectPaths, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, } from '../src/cli/options.js';
|
|
14
|
+
import { collectPaths, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, } from '../src/cli/options.js';
|
|
15
|
+
import { shouldDetachSession } from '../src/cli/detach.js';
|
|
14
16
|
import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
|
|
15
17
|
import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
|
|
16
18
|
import { performSessionRun } from '../src/cli/sessionRunner.js';
|
|
17
19
|
import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
|
|
20
|
+
import { resolveGeminiModelId } from '../src/oracle/gemini.js';
|
|
18
21
|
import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
|
|
19
22
|
import { isErrorLogged } from '../src/cli/errorUtils.js';
|
|
20
23
|
import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
|
|
@@ -76,10 +79,17 @@ program
|
|
|
76
79
|
.addOption(new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
|
|
77
80
|
.default(undefined))
|
|
78
81
|
.addOption(new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
|
|
79
|
-
.
|
|
80
|
-
.
|
|
82
|
+
.addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 20m for gpt-5-pro, 30s otherwise).')
|
|
83
|
+
.argParser(parseTimeoutOption)
|
|
84
|
+
.default('auto'))
|
|
85
|
+
.addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
|
|
86
|
+
.hideHelp()
|
|
81
87
|
.choices(['summary', 'json', 'full'])
|
|
82
88
|
.preset('summary'))
|
|
89
|
+
.addOption(new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
|
|
90
|
+
.choices(['summary', 'json', 'full'])
|
|
91
|
+
.preset('summary')
|
|
92
|
+
.default(false))
|
|
83
93
|
.addOption(new Option('--exec-session <id>').hideHelp())
|
|
84
94
|
.addOption(new Option('--session <id>').hideHelp())
|
|
85
95
|
.addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
|
|
@@ -94,12 +104,19 @@ program
|
|
|
94
104
|
.addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
|
|
95
105
|
.argParser(parseIntOption)
|
|
96
106
|
.hideHelp())
|
|
107
|
+
.option('--base-url <url>', 'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).')
|
|
108
|
+
.option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
|
|
109
|
+
.option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
|
|
110
|
+
.option('--azure-api-version <version>', 'Azure OpenAI API Version.')
|
|
97
111
|
.addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
|
|
98
112
|
.addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
|
|
99
113
|
.addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
|
|
100
114
|
.addOption(new Option('--browser-url <url>', `Override the ChatGPT URL (default ${CHATGPT_URL}).`).hideHelp())
|
|
101
115
|
.addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 900s).').hideHelp())
|
|
102
116
|
.addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
|
|
117
|
+
.addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
|
|
118
|
+
.addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
|
|
119
|
+
.addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
|
|
103
120
|
.addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
|
|
104
121
|
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
105
122
|
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
@@ -188,20 +205,32 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
188
205
|
if (!options.prompt) {
|
|
189
206
|
throw new Error('Prompt is required.');
|
|
190
207
|
}
|
|
208
|
+
const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
|
|
209
|
+
const azure = options.azureEndpoint || overrides.azure?.endpoint
|
|
210
|
+
? {
|
|
211
|
+
endpoint: overrides.azure?.endpoint ?? options.azureEndpoint,
|
|
212
|
+
deployment: overrides.azure?.deployment ?? options.azureDeployment,
|
|
213
|
+
apiVersion: overrides.azure?.apiVersion ?? options.azureApiVersion,
|
|
214
|
+
}
|
|
215
|
+
: undefined;
|
|
191
216
|
return {
|
|
192
217
|
prompt: options.prompt,
|
|
193
218
|
model: options.model,
|
|
219
|
+
effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
|
|
194
220
|
file: overrides.file ?? options.file ?? [],
|
|
195
221
|
slug: overrides.slug ?? options.slug,
|
|
196
222
|
filesReport: overrides.filesReport ?? options.filesReport,
|
|
197
223
|
maxInput: overrides.maxInput ?? options.maxInput,
|
|
198
224
|
maxOutput: overrides.maxOutput ?? options.maxOutput,
|
|
199
225
|
system: overrides.system ?? options.system,
|
|
226
|
+
timeoutSeconds: overrides.timeoutSeconds ?? options.timeout,
|
|
200
227
|
silent: overrides.silent ?? options.silent,
|
|
201
228
|
search: overrides.search ?? options.search,
|
|
202
229
|
preview: overrides.preview ?? undefined,
|
|
203
230
|
previewMode: overrides.previewMode ?? options.previewMode,
|
|
204
231
|
apiKey: overrides.apiKey ?? options.apiKey,
|
|
232
|
+
baseUrl: normalizedBaseUrl,
|
|
233
|
+
azure,
|
|
205
234
|
sessionId: overrides.sessionId ?? options.sessionId,
|
|
206
235
|
verbose: overrides.verbose ?? options.verbose,
|
|
207
236
|
heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
|
|
@@ -227,6 +256,7 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
227
256
|
return {
|
|
228
257
|
prompt: stored.prompt ?? '',
|
|
229
258
|
model: stored.model ?? 'gpt-5-pro',
|
|
259
|
+
effectiveModelId: stored.effectiveModelId ?? stored.model,
|
|
230
260
|
file: stored.file ?? [],
|
|
231
261
|
slug: stored.slug,
|
|
232
262
|
filesReport: stored.filesReport,
|
|
@@ -238,6 +268,8 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
238
268
|
preview: false,
|
|
239
269
|
previewMode: undefined,
|
|
240
270
|
apiKey: undefined,
|
|
271
|
+
baseUrl: normalizeBaseUrl(stored.baseUrl),
|
|
272
|
+
azure: stored.azure,
|
|
241
273
|
sessionId: metadata.id,
|
|
242
274
|
verbose: stored.verbose,
|
|
243
275
|
heartbeatIntervalMs: stored.heartbeatIntervalMs,
|
|
@@ -253,8 +285,18 @@ function getBrowserConfigFromMetadata(metadata) {
|
|
|
253
285
|
return metadata.options?.browserConfig ?? metadata.browser?.config;
|
|
254
286
|
}
|
|
255
287
|
async function runRootCommand(options) {
|
|
288
|
+
if (process.env.ORACLE_FORCE_TUI === '1') {
|
|
289
|
+
await ensureSessionStorage();
|
|
290
|
+
await launchTui({ version: VERSION });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
256
293
|
const userConfig = (await loadUserConfig()).config;
|
|
257
294
|
const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
|
|
295
|
+
const optionUsesDefault = (name) => {
|
|
296
|
+
// Commander reports undefined for untouched options, so treat undefined/default the same
|
|
297
|
+
const source = program.getOptionValueSource?.(name);
|
|
298
|
+
return source == null || source === 'default';
|
|
299
|
+
};
|
|
258
300
|
if (helpRequested) {
|
|
259
301
|
if (options.verbose) {
|
|
260
302
|
console.log('');
|
|
@@ -264,7 +306,7 @@ async function runRootCommand(options) {
|
|
|
264
306
|
program.help({ error: false });
|
|
265
307
|
return;
|
|
266
308
|
}
|
|
267
|
-
const previewMode = resolvePreviewMode(options.preview);
|
|
309
|
+
const previewMode = resolvePreviewMode(options.dryRun || options.preview);
|
|
268
310
|
if (userCliArgs.length === 0) {
|
|
269
311
|
if (tuiEnabled()) {
|
|
270
312
|
await launchTui({ version: VERSION });
|
|
@@ -278,32 +320,68 @@ async function runRootCommand(options) {
|
|
|
278
320
|
printDebugHelp(program.name());
|
|
279
321
|
return;
|
|
280
322
|
}
|
|
281
|
-
if (options.dryRun && previewMode) {
|
|
282
|
-
throw new Error('--dry-run cannot be combined with --preview.');
|
|
283
|
-
}
|
|
284
323
|
if (options.dryRun && options.renderMarkdown) {
|
|
285
324
|
throw new Error('--dry-run cannot be combined with --render-markdown.');
|
|
286
325
|
}
|
|
287
326
|
const preferredEngine = options.engine ?? userConfig.engine;
|
|
288
|
-
|
|
327
|
+
let engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
|
|
289
328
|
if (options.browser) {
|
|
290
329
|
console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
|
|
291
330
|
}
|
|
292
|
-
if (
|
|
331
|
+
if (optionUsesDefault('model') && userConfig.model) {
|
|
293
332
|
options.model = userConfig.model;
|
|
294
333
|
}
|
|
295
|
-
if (
|
|
334
|
+
if (optionUsesDefault('search') && userConfig.search) {
|
|
296
335
|
options.search = userConfig.search === 'on';
|
|
297
336
|
}
|
|
298
|
-
if (
|
|
337
|
+
if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
|
|
299
338
|
options.filesReport = Boolean(userConfig.filesReport);
|
|
300
339
|
}
|
|
301
|
-
if (
|
|
340
|
+
if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
|
|
302
341
|
options.heartbeat = userConfig.heartbeatSeconds;
|
|
303
342
|
}
|
|
343
|
+
if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
|
|
344
|
+
options.baseUrl = userConfig.apiBaseUrl;
|
|
345
|
+
}
|
|
346
|
+
if (optionUsesDefault('azureEndpoint')) {
|
|
347
|
+
if (process.env.AZURE_OPENAI_ENDPOINT) {
|
|
348
|
+
options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
349
|
+
}
|
|
350
|
+
else if (userConfig.azure?.endpoint) {
|
|
351
|
+
options.azureEndpoint = userConfig.azure.endpoint;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (optionUsesDefault('azureDeployment')) {
|
|
355
|
+
if (process.env.AZURE_OPENAI_DEPLOYMENT) {
|
|
356
|
+
options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
357
|
+
}
|
|
358
|
+
else if (userConfig.azure?.deployment) {
|
|
359
|
+
options.azureDeployment = userConfig.azure.deployment;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (optionUsesDefault('azureApiVersion')) {
|
|
363
|
+
if (process.env.AZURE_OPENAI_API_VERSION) {
|
|
364
|
+
options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
|
|
365
|
+
}
|
|
366
|
+
else if (userConfig.azure?.apiVersion) {
|
|
367
|
+
options.azureApiVersion = userConfig.azure.apiVersion;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
304
370
|
const cliModelArg = normalizeModelOption(options.model) || 'gpt-5-pro';
|
|
305
|
-
const
|
|
371
|
+
const resolvedModelCandidate = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
|
|
372
|
+
const isGemini = resolvedModelCandidate.startsWith('gemini');
|
|
373
|
+
const userForcedBrowser = options.browser || options.engine === 'browser';
|
|
374
|
+
if (isGemini && userForcedBrowser) {
|
|
375
|
+
throw new Error('Gemini is only supported via API. Use --engine api.');
|
|
376
|
+
}
|
|
377
|
+
if (isGemini && engine === 'browser') {
|
|
378
|
+
engine = 'api';
|
|
379
|
+
}
|
|
380
|
+
const resolvedModel = isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate;
|
|
381
|
+
const effectiveModelId = resolvedModel.startsWith('gemini') ? resolveGeminiModelId(resolvedModel) : resolvedModel;
|
|
382
|
+
const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? process.env.OPENAI_BASE_URL);
|
|
306
383
|
const resolvedOptions = { ...options, model: resolvedModel };
|
|
384
|
+
resolvedOptions.baseUrl = resolvedBaseUrl;
|
|
307
385
|
// Decide whether to block until completion:
|
|
308
386
|
// - explicit --wait / --no-wait wins
|
|
309
387
|
// - otherwise block for fast models (gpt-5.1, browser) and detach by default for gpt-5-pro API
|
|
@@ -333,13 +411,13 @@ async function runRootCommand(options) {
|
|
|
333
411
|
}
|
|
334
412
|
if (previewMode) {
|
|
335
413
|
if (!options.prompt) {
|
|
336
|
-
throw new Error('Prompt is required when using --preview.');
|
|
414
|
+
throw new Error('Prompt is required when using --dry-run/preview.');
|
|
337
415
|
}
|
|
338
416
|
if (userConfig.promptSuffix) {
|
|
339
417
|
options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
|
|
340
418
|
}
|
|
341
419
|
resolvedOptions.prompt = options.prompt;
|
|
342
|
-
const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode });
|
|
420
|
+
const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
|
|
343
421
|
if (engine === 'browser') {
|
|
344
422
|
await runBrowserPreview({
|
|
345
423
|
runOptions,
|
|
@@ -350,7 +428,24 @@ async function runRootCommand(options) {
|
|
|
350
428
|
}, {});
|
|
351
429
|
return;
|
|
352
430
|
}
|
|
353
|
-
|
|
431
|
+
// API dry-run/preview path
|
|
432
|
+
if (previewMode === 'summary') {
|
|
433
|
+
await runDryRunSummary({
|
|
434
|
+
engine,
|
|
435
|
+
runOptions,
|
|
436
|
+
cwd: process.cwd(),
|
|
437
|
+
version: VERSION,
|
|
438
|
+
log: console.log,
|
|
439
|
+
}, {});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await runDryRunSummary({
|
|
443
|
+
engine,
|
|
444
|
+
runOptions,
|
|
445
|
+
cwd: process.cwd(),
|
|
446
|
+
version: VERSION,
|
|
447
|
+
log: console.log,
|
|
448
|
+
}, {});
|
|
354
449
|
return;
|
|
355
450
|
}
|
|
356
451
|
if (!options.prompt) {
|
|
@@ -360,17 +455,6 @@ async function runRootCommand(options) {
|
|
|
360
455
|
options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
|
|
361
456
|
}
|
|
362
457
|
resolvedOptions.prompt = options.prompt;
|
|
363
|
-
if (options.dryRun) {
|
|
364
|
-
const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
|
|
365
|
-
await runDryRunSummary({
|
|
366
|
-
engine,
|
|
367
|
-
runOptions: baseRunOptions,
|
|
368
|
-
cwd: process.cwd(),
|
|
369
|
-
version: VERSION,
|
|
370
|
-
log: console.log,
|
|
371
|
-
}, {});
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
458
|
if (options.file && options.file.length > 0) {
|
|
375
459
|
await readFiles(options.file, { cwd: process.cwd() });
|
|
376
460
|
}
|
|
@@ -384,17 +468,34 @@ async function runRootCommand(options) {
|
|
|
384
468
|
const sessionMode = engine === 'browser' ? 'browser' : 'api';
|
|
385
469
|
const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
|
|
386
470
|
const browserConfig = sessionMode === 'browser'
|
|
387
|
-
? buildBrowserConfig({
|
|
471
|
+
? await buildBrowserConfig({
|
|
388
472
|
...options,
|
|
389
473
|
model: resolvedModel,
|
|
390
474
|
browserModelLabel: browserModelLabelOverride,
|
|
391
475
|
})
|
|
392
476
|
: undefined;
|
|
477
|
+
if (options.dryRun) {
|
|
478
|
+
const baseRunOptions = buildRunOptions(resolvedOptions, {
|
|
479
|
+
preview: false,
|
|
480
|
+
previewMode: undefined,
|
|
481
|
+
baseUrl: resolvedBaseUrl,
|
|
482
|
+
});
|
|
483
|
+
await runDryRunSummary({
|
|
484
|
+
engine,
|
|
485
|
+
runOptions: baseRunOptions,
|
|
486
|
+
cwd: process.cwd(),
|
|
487
|
+
version: VERSION,
|
|
488
|
+
log: console.log,
|
|
489
|
+
browserConfig,
|
|
490
|
+
}, {});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
393
493
|
await ensureSessionStorage();
|
|
394
494
|
const baseRunOptions = buildRunOptions(resolvedOptions, {
|
|
395
495
|
preview: false,
|
|
396
496
|
previewMode: undefined,
|
|
397
497
|
background: userConfig.background ?? resolvedOptions.background,
|
|
498
|
+
baseUrl: resolvedBaseUrl,
|
|
398
499
|
});
|
|
399
500
|
enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
|
|
400
501
|
if (sessionMode === 'browser' && baseRunOptions.search === false) {
|
|
@@ -406,12 +507,19 @@ async function runRootCommand(options) {
|
|
|
406
507
|
mode: sessionMode,
|
|
407
508
|
browserConfig,
|
|
408
509
|
}, process.cwd(), notifications);
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
510
|
+
const liveRunOptions = {
|
|
511
|
+
...baseRunOptions,
|
|
512
|
+
sessionId: sessionMeta.id,
|
|
513
|
+
effectiveModelId,
|
|
514
|
+
};
|
|
515
|
+
const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
|
|
516
|
+
const detachAllowed = shouldDetachSession({
|
|
517
|
+
engine,
|
|
518
|
+
model: resolvedModel,
|
|
519
|
+
waitPreference,
|
|
520
|
+
disableDetachEnv,
|
|
521
|
+
});
|
|
522
|
+
const detached = !detachAllowed
|
|
415
523
|
? false
|
|
416
524
|
: await launchDetachedSession(sessionMeta.id).catch((error) => {
|
|
417
525
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -429,17 +537,15 @@ async function runRootCommand(options) {
|
|
|
429
537
|
return;
|
|
430
538
|
}
|
|
431
539
|
if (detached === false) {
|
|
432
|
-
await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig,
|
|
433
|
-
console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
|
|
540
|
+
await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true);
|
|
434
541
|
return;
|
|
435
542
|
}
|
|
436
543
|
if (detached) {
|
|
437
544
|
console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
|
|
438
545
|
await attachSession(sessionMeta.id, { suppressMetadata: true });
|
|
439
|
-
console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
|
|
440
546
|
}
|
|
441
547
|
}
|
|
442
|
-
async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig) {
|
|
548
|
+
async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false) {
|
|
443
549
|
const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
|
|
444
550
|
let headerAugmented = false;
|
|
445
551
|
const combinedLog = (message = '') => {
|
|
@@ -474,10 +580,12 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
|
|
|
474
580
|
notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
|
|
475
581
|
});
|
|
476
582
|
const latest = await readSessionMetadata(sessionMeta.id);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
583
|
+
if (!suppressSummary) {
|
|
584
|
+
const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
|
|
585
|
+
if (summary) {
|
|
586
|
+
console.log('\n' + chalk.green.bold(summary));
|
|
587
|
+
logLine(summary); // plain text in log, colored on stdout
|
|
588
|
+
}
|
|
481
589
|
}
|
|
482
590
|
}
|
|
483
591
|
catch (error) {
|
|
@@ -611,7 +719,16 @@ program.action(async function () {
|
|
|
611
719
|
const options = this.optsWithGlobals();
|
|
612
720
|
await runRootCommand(options);
|
|
613
721
|
});
|
|
614
|
-
|
|
722
|
+
async function main() {
|
|
723
|
+
const parsePromise = program.parseAsync(process.argv);
|
|
724
|
+
const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
|
|
725
|
+
const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
|
|
726
|
+
if (result === 'sigint') {
|
|
727
|
+
console.log(chalk.yellow('\nCancelled.'));
|
|
728
|
+
process.exitCode = 130;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
void main().catch((error) => {
|
|
615
732
|
if (error instanceof Error) {
|
|
616
733
|
if (!isErrorLogged(error)) {
|
|
617
734
|
console.error(chalk.red('✖'), error.message);
|
|
@@ -6,6 +6,9 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
6
6
|
timeoutMs: 900_000,
|
|
7
7
|
inputTimeoutMs: 30_000,
|
|
8
8
|
cookieSync: true,
|
|
9
|
+
cookieNames: null,
|
|
10
|
+
inlineCookies: null,
|
|
11
|
+
inlineCookiesSource: null,
|
|
9
12
|
headless: false,
|
|
10
13
|
keepBrowser: false,
|
|
11
14
|
hideWindow: false,
|
|
@@ -21,6 +24,9 @@ export function resolveBrowserConfig(config) {
|
|
|
21
24
|
timeoutMs: config?.timeoutMs ?? DEFAULT_BROWSER_CONFIG.timeoutMs,
|
|
22
25
|
inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
|
|
23
26
|
cookieSync: config?.cookieSync ?? DEFAULT_BROWSER_CONFIG.cookieSync,
|
|
27
|
+
cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
|
|
28
|
+
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
29
|
+
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
24
30
|
headless: config?.headless ?? DEFAULT_BROWSER_CONFIG.headless,
|
|
25
31
|
keepBrowser: config?.keepBrowser ?? DEFAULT_BROWSER_CONFIG.keepBrowser,
|
|
26
32
|
hideWindow: config?.hideWindow ?? DEFAULT_BROWSER_CONFIG.hideWindow,
|
|
@@ -4,21 +4,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { COOKIE_URLS } from './constants.js';
|
|
5
5
|
export class ChromeCookieSyncError extends Error {
|
|
6
6
|
}
|
|
7
|
-
export async function syncCookies(Network, url, profile, logger,
|
|
7
|
+
export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
8
|
+
const { allowErrors = false, filterNames, inlineCookies } = options;
|
|
8
9
|
try {
|
|
9
|
-
const cookies =
|
|
10
|
+
const cookies = inlineCookies?.length
|
|
11
|
+
? normalizeInlineCookies(inlineCookies, new URL(url).hostname)
|
|
12
|
+
: await readChromeCookies(url, profile, filterNames ?? undefined);
|
|
10
13
|
if (!cookies.length) {
|
|
11
14
|
return 0;
|
|
12
15
|
}
|
|
13
16
|
let applied = 0;
|
|
14
17
|
for (const cookie of cookies) {
|
|
15
|
-
const cookieWithUrl =
|
|
16
|
-
if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
|
|
17
|
-
cookieWithUrl.url = url;
|
|
18
|
-
}
|
|
19
|
-
else if (!cookieWithUrl.domain.startsWith('.')) {
|
|
20
|
-
cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
|
|
21
|
-
}
|
|
18
|
+
const cookieWithUrl = attachUrl(cookie, url);
|
|
22
19
|
try {
|
|
23
20
|
const result = await Network.setCookie(cookieWithUrl);
|
|
24
21
|
if (result?.success) {
|
|
@@ -41,10 +38,11 @@ export async function syncCookies(Network, url, profile, logger, allowErrors = f
|
|
|
41
38
|
throw error instanceof ChromeCookieSyncError ? error : new ChromeCookieSyncError(message);
|
|
42
39
|
}
|
|
43
40
|
}
|
|
44
|
-
async function readChromeCookies(url, profile) {
|
|
41
|
+
async function readChromeCookies(url, profile, filterNames) {
|
|
45
42
|
const chromeModule = await loadChromeCookiesModule();
|
|
46
43
|
const urlsToCheck = Array.from(new Set([stripQuery(url), ...COOKIE_URLS]));
|
|
47
44
|
const merged = new Map();
|
|
45
|
+
const allowlist = normalizeCookieNames(filterNames);
|
|
48
46
|
for (const candidateUrl of urlsToCheck) {
|
|
49
47
|
let rawCookies;
|
|
50
48
|
rawCookies = await chromeModule.getCookiesPromised(candidateUrl, 'puppeteer', profile ?? undefined);
|
|
@@ -54,7 +52,7 @@ async function readChromeCookies(url, profile) {
|
|
|
54
52
|
const fallbackHostname = new URL(candidateUrl).hostname;
|
|
55
53
|
for (const cookie of rawCookies) {
|
|
56
54
|
const normalized = normalizeCookie(cookie, fallbackHostname);
|
|
57
|
-
if (!normalized) {
|
|
55
|
+
if (!normalized || (allowlist && !allowlist.has(normalized.name))) {
|
|
58
56
|
continue;
|
|
59
57
|
}
|
|
60
58
|
const key = `${normalized.domain ?? fallbackHostname}:${normalized.name}`;
|
|
@@ -83,6 +81,46 @@ function normalizeCookie(cookie, fallbackHost) {
|
|
|
83
81
|
httpOnly,
|
|
84
82
|
};
|
|
85
83
|
}
|
|
84
|
+
function normalizeInlineCookies(rawCookies, fallbackHost) {
|
|
85
|
+
const merged = new Map();
|
|
86
|
+
for (const cookie of rawCookies) {
|
|
87
|
+
if (!cookie?.name)
|
|
88
|
+
continue;
|
|
89
|
+
const normalized = {
|
|
90
|
+
...cookie,
|
|
91
|
+
name: cookie.name,
|
|
92
|
+
value: cookie.value ?? '',
|
|
93
|
+
domain: cookie.domain ?? fallbackHost,
|
|
94
|
+
path: cookie.path ?? '/',
|
|
95
|
+
expires: normalizeExpiration(cookie.expires),
|
|
96
|
+
secure: cookie.secure ?? true,
|
|
97
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
98
|
+
};
|
|
99
|
+
const key = `${normalized.domain ?? fallbackHost}:${normalized.name}`;
|
|
100
|
+
if (!merged.has(key)) {
|
|
101
|
+
merged.set(key, normalized);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return Array.from(merged.values());
|
|
105
|
+
}
|
|
106
|
+
function normalizeCookieNames(names) {
|
|
107
|
+
if (!names || names.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return new Set(names.map((name) => name.trim()).filter(Boolean));
|
|
111
|
+
}
|
|
112
|
+
function attachUrl(cookie, fallbackUrl) {
|
|
113
|
+
const cookieWithUrl = { ...cookie };
|
|
114
|
+
if (!cookieWithUrl.url) {
|
|
115
|
+
if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
|
|
116
|
+
cookieWithUrl.url = fallbackUrl;
|
|
117
|
+
}
|
|
118
|
+
else if (!cookieWithUrl.domain.startsWith('.')) {
|
|
119
|
+
cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return cookieWithUrl;
|
|
123
|
+
}
|
|
86
124
|
function stripQuery(url) {
|
|
87
125
|
try {
|
|
88
126
|
const parsed = new URL(url);
|
|
@@ -64,11 +64,24 @@ export async function runBrowserMode(options) {
|
|
|
64
64
|
await Promise.all(domainEnablers);
|
|
65
65
|
await Network.clearBrowserCookies();
|
|
66
66
|
if (config.cookieSync) {
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
if (!config.inlineCookies) {
|
|
68
|
+
logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; approve it to stay signed in or rerun with --browser-no-cookie-sync / --browser-allow-cookie-errors / --browser-inline-cookies[(-file)]. Inline cookies skip Chrome + Keychain entirely.');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
logger('Applying inline cookies (skipping Chrome profile read and Keychain prompt)');
|
|
72
|
+
}
|
|
73
|
+
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, {
|
|
74
|
+
allowErrors: config.allowCookieErrors ?? false,
|
|
75
|
+
filterNames: config.cookieNames ?? undefined,
|
|
76
|
+
inlineCookies: config.inlineCookies ?? undefined,
|
|
77
|
+
});
|
|
69
78
|
logger(cookieCount > 0
|
|
70
|
-
?
|
|
71
|
-
|
|
79
|
+
? config.inlineCookies
|
|
80
|
+
? `Applied ${cookieCount} inline cookies`
|
|
81
|
+
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? 'Default'}`
|
|
82
|
+
: config.inlineCookies
|
|
83
|
+
? 'No inline cookies applied; continuing without session reuse'
|
|
84
|
+
: 'No Chrome cookies found; continuing without session reuse');
|
|
72
85
|
}
|
|
73
86
|
else {
|
|
74
87
|
logger('Skipping Chrome cookie sync (--browser-no-cookie-sync)');
|
|
@@ -212,7 +225,7 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
|
212
225
|
.toString()
|
|
213
226
|
.padStart(3, ' ');
|
|
214
227
|
const statusLabel = message ? ` — ${message}` : '';
|
|
215
|
-
return
|
|
228
|
+
return `${bar} ${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
216
229
|
}
|
|
217
230
|
function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
|
|
218
231
|
let stopped = false;
|
|
@@ -4,6 +4,11 @@ import { runBrowserMode } from '../browserMode.js';
|
|
|
4
4
|
import { assembleBrowserPrompt } from './prompt.js';
|
|
5
5
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
6
6
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log, cliVersion }, deps = {}) {
|
|
7
|
+
if (runOptions.model.startsWith('gemini')) {
|
|
8
|
+
throw new BrowserAutomationError('Gemini models are not available in browser mode. Re-run with --engine api.', {
|
|
9
|
+
stage: 'preflight',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
7
12
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
8
13
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
9
14
|
const promptArtifacts = await assemblePrompt(runOptions, { cwd });
|
|
@@ -24,6 +29,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
const headerLine = `oracle (${cliVersion}) launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens`;
|
|
32
|
+
if (promptArtifacts.bundled) {
|
|
33
|
+
log(chalk.yellow(`[browser] Packed ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}. If automation fails, you can drag this file into ChatGPT manually.`));
|
|
34
|
+
}
|
|
27
35
|
const automationLogger = ((message) => {
|
|
28
36
|
if (typeof message === 'string') {
|
|
29
37
|
log(message);
|
|
@@ -64,7 +72,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
64
72
|
totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
|
|
65
73
|
};
|
|
66
74
|
const tokensDisplay = `${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens}`;
|
|
67
|
-
const
|
|
75
|
+
const tokensLabel = runOptions.verbose ? 'tokens (input/output/reasoning/total)' : 'tok(i/o/r/t)';
|
|
76
|
+
const statsParts = [`${runOptions.model}[browser]`, `${tokensLabel}=${tokensDisplay}`];
|
|
68
77
|
if (runOptions.file && runOptions.file.length > 0) {
|
|
69
78
|
statsParts.push(`files=${runOptions.file.length}`);
|
|
70
79
|
}
|