@indykish/oracle 0.9.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 +21 -0
- package/README.md +215 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1252 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +125 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1378 -0
- package/dist/scripts/test-browser.js +103 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1067 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1910 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +485 -0
- package/dist/src/browser/actions/navigation.js +445 -0
- package/dist/src/browser/actions/promptComposer.js +485 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +344 -0
- package/dist/src/browser/config.js +103 -0
- package/dist/src/browser/constants.js +71 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1741 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/profileState.js +280 -0
- package/dist/src/browser/prompt.js +152 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/reattach.js +186 -0
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browser/sessionRunner.js +119 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +73 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +107 -0
- package/dist/src/cli/bridge/host.js +259 -0
- package/dist/src/cli/browserConfig.js +278 -0
- package/dist/src/cli/browserDefaults.js +81 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +11 -0
- package/dist/src/cli/dryRun.js +105 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +306 -0
- package/dist/src/cli/options.js +281 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +78 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +567 -0
- package/dist/src/cli/sessionRunner.js +602 -0
- package/dist/src/cli/sessionTable.js +92 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +486 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/client.js +328 -0
- package/dist/src/gemini-web/executor.js +285 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +290 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +105 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +37 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +101 -0
- package/dist/src/oracle/client.js +197 -0
- package/dist/src/oracle/config.js +227 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +378 -0
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +195 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +13 -0
- package/dist/src/oracle/request.js +50 -0
- package/dist/src/oracle/run.js +596 -0
- package/dist/src/oracle/runUtils.js +31 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +533 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +637 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +115 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, isTemporaryChatUrl, normalizeChatgptUrl, parseDuration } from '../browserMode.js';
|
|
4
|
+
import { normalizeBrowserModelStrategy } from '../browser/modelStrategy.js';
|
|
5
|
+
import { getOracleHomeDir } from '../oracleHome.js';
|
|
6
|
+
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
7
|
+
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 60_000;
|
|
8
|
+
const DEFAULT_BROWSER_RECHECK_TIMEOUT_MS = 120_000;
|
|
9
|
+
const DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS = 120_000;
|
|
10
|
+
const DEFAULT_CHROME_PROFILE = 'Default';
|
|
11
|
+
// Ordered array: most specific models first to ensure correct selection.
|
|
12
|
+
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
13
|
+
const BROWSER_MODEL_LABELS = [
|
|
14
|
+
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
15
|
+
['gpt-5.2-thinking', 'GPT-5.2 Thinking'],
|
|
16
|
+
['gpt-5.2-instant', 'GPT-5.2 Instant'],
|
|
17
|
+
['gpt-5.2-pro', 'GPT-5.2 Pro'],
|
|
18
|
+
['gpt-5.1-pro', 'GPT-5.2 Pro'],
|
|
19
|
+
['gpt-5-pro', 'GPT-5.2 Pro'],
|
|
20
|
+
// Base models last (least specific)
|
|
21
|
+
['gpt-5.2', 'GPT-5.2'], // Selects "Auto" in ChatGPT UI
|
|
22
|
+
['gpt-5.1', 'GPT-5.2'], // Legacy alias → Auto
|
|
23
|
+
['gemini-3-pro', 'Gemini 3 Pro'],
|
|
24
|
+
];
|
|
25
|
+
export function normalizeChatGptModelForBrowser(model) {
|
|
26
|
+
const normalized = model.toLowerCase();
|
|
27
|
+
if (!normalized.startsWith('gpt-') || normalized.includes('codex')) {
|
|
28
|
+
return model;
|
|
29
|
+
}
|
|
30
|
+
// Pro variants: always resolve to the latest Pro model in ChatGPT.
|
|
31
|
+
if (normalized === 'gpt-5-pro' || normalized === 'gpt-5.1-pro' || normalized.endsWith('-pro')) {
|
|
32
|
+
return 'gpt-5.2-pro';
|
|
33
|
+
}
|
|
34
|
+
// Explicit model variants: keep as-is (they have their own browser labels)
|
|
35
|
+
if (normalized === 'gpt-5.2-thinking' || normalized === 'gpt-5.2-instant') {
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
// Legacy aliases: map to base GPT-5.2 (Auto)
|
|
39
|
+
if (normalized === 'gpt-5.1') {
|
|
40
|
+
return 'gpt-5.2';
|
|
41
|
+
}
|
|
42
|
+
return model;
|
|
43
|
+
}
|
|
44
|
+
export async function buildBrowserConfig(options) {
|
|
45
|
+
const desiredModelOverride = options.browserModelLabel?.trim();
|
|
46
|
+
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? '';
|
|
47
|
+
const baseModel = options.model.toLowerCase();
|
|
48
|
+
const isChatGptModel = baseModel.startsWith('gpt-') && !baseModel.includes('codex');
|
|
49
|
+
const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
50
|
+
const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
|
|
51
|
+
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
52
|
+
let inline = await resolveInlineCookies({
|
|
53
|
+
inlineArg: options.browserInlineCookies,
|
|
54
|
+
inlineFileArg: options.browserInlineCookiesFile,
|
|
55
|
+
envPayload: process.env.ORACLE_BROWSER_COOKIES_JSON,
|
|
56
|
+
envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
});
|
|
59
|
+
if (inline?.source?.startsWith('home:') && options.browserNoCookieSync !== true) {
|
|
60
|
+
inline = undefined;
|
|
61
|
+
}
|
|
62
|
+
let remoteChrome;
|
|
63
|
+
if (options.remoteChrome) {
|
|
64
|
+
remoteChrome = parseRemoteChromeTarget(options.remoteChrome);
|
|
65
|
+
}
|
|
66
|
+
const rawUrl = options.chatgptUrl ?? options.browserUrl;
|
|
67
|
+
const url = rawUrl ? normalizeChatgptUrl(rawUrl, CHATGPT_URL) : undefined;
|
|
68
|
+
const desiredModel = isChatGptModel
|
|
69
|
+
? mapModelToBrowserLabel(options.model)
|
|
70
|
+
: shouldUseOverride
|
|
71
|
+
? desiredModelOverride
|
|
72
|
+
: mapModelToBrowserLabel(options.model);
|
|
73
|
+
if (modelStrategy === 'select' && url && isTemporaryChatUrl(url) && /\bpro\b/i.test(desiredModel ?? '')) {
|
|
74
|
+
throw new Error('Temporary Chat mode does not expose Pro models in the ChatGPT model picker. ' +
|
|
75
|
+
'Remove "temporary-chat=true" from --chatgpt-url (or omit --chatgpt-url), or use a non-Pro model (e.g. --model gpt-5.2).');
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
|
|
79
|
+
chromePath: options.browserChromePath ?? null,
|
|
80
|
+
chromeCookiePath: options.browserCookiePath ?? null,
|
|
81
|
+
url,
|
|
82
|
+
debugPort: selectBrowserPort(options),
|
|
83
|
+
timeoutMs: options.browserTimeout ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS) : undefined,
|
|
84
|
+
inputTimeoutMs: options.browserInputTimeout
|
|
85
|
+
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
86
|
+
: undefined,
|
|
87
|
+
assistantRecheckDelayMs: options.browserRecheckDelay
|
|
88
|
+
? parseDuration(options.browserRecheckDelay, 0)
|
|
89
|
+
: undefined,
|
|
90
|
+
assistantRecheckTimeoutMs: options.browserRecheckTimeout
|
|
91
|
+
? parseDuration(options.browserRecheckTimeout, DEFAULT_BROWSER_RECHECK_TIMEOUT_MS)
|
|
92
|
+
: undefined,
|
|
93
|
+
reuseChromeWaitMs: options.browserReuseWait ? parseDuration(options.browserReuseWait, 0) : undefined,
|
|
94
|
+
profileLockTimeoutMs: options.browserProfileLockTimeout
|
|
95
|
+
? parseDuration(options.browserProfileLockTimeout, 0)
|
|
96
|
+
: undefined,
|
|
97
|
+
autoReattachDelayMs: options.browserAutoReattachDelay
|
|
98
|
+
? parseDuration(options.browserAutoReattachDelay, 0)
|
|
99
|
+
: undefined,
|
|
100
|
+
autoReattachIntervalMs: options.browserAutoReattachInterval
|
|
101
|
+
? parseDuration(options.browserAutoReattachInterval, 0)
|
|
102
|
+
: undefined,
|
|
103
|
+
autoReattachTimeoutMs: options.browserAutoReattachTimeout
|
|
104
|
+
? parseDuration(options.browserAutoReattachTimeout, DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS)
|
|
105
|
+
: undefined,
|
|
106
|
+
cookieSyncWaitMs: options.browserCookieWait ? parseDuration(options.browserCookieWait, 0) : undefined,
|
|
107
|
+
cookieSync: options.browserNoCookieSync ? false : undefined,
|
|
108
|
+
cookieNames,
|
|
109
|
+
inlineCookies: inline?.cookies,
|
|
110
|
+
inlineCookiesSource: inline?.source ?? null,
|
|
111
|
+
headless: undefined, // disable headless; Cloudflare blocks it
|
|
112
|
+
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
113
|
+
manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
|
|
114
|
+
manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
|
|
115
|
+
hideWindow: options.browserHideWindow ? true : undefined,
|
|
116
|
+
desiredModel,
|
|
117
|
+
modelStrategy,
|
|
118
|
+
debug: options.verbose ? true : undefined,
|
|
119
|
+
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
120
|
+
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
121
|
+
remoteChrome,
|
|
122
|
+
thinkingTime: options.browserThinkingTime,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function selectBrowserPort(options) {
|
|
126
|
+
const candidate = options.browserPort ?? options.browserDebugPort;
|
|
127
|
+
if (candidate === undefined || candidate === null)
|
|
128
|
+
return null;
|
|
129
|
+
if (!Number.isFinite(candidate) || candidate <= 0 || candidate > 65_535) {
|
|
130
|
+
throw new Error(`Invalid browser port: ${candidate}. Expected a number between 1 and 65535.`);
|
|
131
|
+
}
|
|
132
|
+
return candidate;
|
|
133
|
+
}
|
|
134
|
+
export function mapModelToBrowserLabel(model) {
|
|
135
|
+
const normalized = normalizeChatGptModelForBrowser(model);
|
|
136
|
+
// Iterate ordered array to find first match (most specific first)
|
|
137
|
+
for (const [key, label] of BROWSER_MODEL_LABELS) {
|
|
138
|
+
if (key === normalized) {
|
|
139
|
+
return label;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return DEFAULT_MODEL_TARGET;
|
|
143
|
+
}
|
|
144
|
+
export function resolveBrowserModelLabel(input, model) {
|
|
145
|
+
const trimmed = input?.trim?.() ?? '';
|
|
146
|
+
if (!trimmed) {
|
|
147
|
+
return mapModelToBrowserLabel(model);
|
|
148
|
+
}
|
|
149
|
+
const normalizedInput = trimmed.toLowerCase();
|
|
150
|
+
if (normalizedInput === model.toLowerCase()) {
|
|
151
|
+
return mapModelToBrowserLabel(model);
|
|
152
|
+
}
|
|
153
|
+
return trimmed;
|
|
154
|
+
}
|
|
155
|
+
function parseRemoteChromeTarget(raw) {
|
|
156
|
+
const target = raw.trim();
|
|
157
|
+
if (!target) {
|
|
158
|
+
throw new Error('Invalid remote-chrome value: expected host:port but received an empty string.');
|
|
159
|
+
}
|
|
160
|
+
const ipv6Match = target.match(/^\[(.+)]:(\d+)$/);
|
|
161
|
+
let host;
|
|
162
|
+
let portSegment;
|
|
163
|
+
if (ipv6Match) {
|
|
164
|
+
host = ipv6Match[1]?.trim();
|
|
165
|
+
portSegment = ipv6Match[2]?.trim();
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const lastColon = target.lastIndexOf(':');
|
|
169
|
+
if (lastColon === -1) {
|
|
170
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Expected host:port (IPv6 must use [host]:port notation).`);
|
|
171
|
+
}
|
|
172
|
+
host = target.slice(0, lastColon).trim();
|
|
173
|
+
portSegment = target.slice(lastColon + 1).trim();
|
|
174
|
+
if (host.includes(':')) {
|
|
175
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Wrap IPv6 addresses in brackets, e.g. --remote-chrome "[2001:db8::1]:9222".`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!host) {
|
|
179
|
+
throw new Error(`Invalid remote-chrome format: ${target}. Host portion is missing; expected host:port.`);
|
|
180
|
+
}
|
|
181
|
+
const port = Number.parseInt(portSegment ?? '', 10);
|
|
182
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
|
|
183
|
+
throw new Error(`Invalid remote-chrome port: "${portSegment ?? ''}". Expected a number between 1 and 65535.`);
|
|
184
|
+
}
|
|
185
|
+
return { host, port };
|
|
186
|
+
}
|
|
187
|
+
function parseCookieNames(raw) {
|
|
188
|
+
if (!raw)
|
|
189
|
+
return undefined;
|
|
190
|
+
const names = raw
|
|
191
|
+
.split(',')
|
|
192
|
+
.map((entry) => entry.trim())
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
return names.length ? names : undefined;
|
|
195
|
+
}
|
|
196
|
+
async function resolveInlineCookies({ inlineArg, inlineFileArg, envPayload, envFile, cwd, }) {
|
|
197
|
+
const tryLoad = async (source, allowPathResolution) => {
|
|
198
|
+
if (!source)
|
|
199
|
+
return undefined;
|
|
200
|
+
const trimmed = source.trim();
|
|
201
|
+
if (!trimmed)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (allowPathResolution) {
|
|
204
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.join(cwd, trimmed);
|
|
205
|
+
try {
|
|
206
|
+
const stat = await fs.stat(resolved);
|
|
207
|
+
if (stat.isFile()) {
|
|
208
|
+
const fileContent = await fs.readFile(resolved, 'utf8');
|
|
209
|
+
const parsed = parseInlineCookiesPayload(fileContent);
|
|
210
|
+
if (parsed)
|
|
211
|
+
return parsed;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// not a file; treat as payload below
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return parseInlineCookiesPayload(trimmed);
|
|
219
|
+
};
|
|
220
|
+
const sources = [
|
|
221
|
+
{ value: inlineFileArg, allowPath: true, source: 'inline-file' },
|
|
222
|
+
{ value: inlineArg, allowPath: true, source: 'inline-arg' },
|
|
223
|
+
{ value: envFile, allowPath: true, source: 'env-file' },
|
|
224
|
+
{ value: envPayload, allowPath: false, source: 'env-payload' },
|
|
225
|
+
];
|
|
226
|
+
for (const { value, allowPath, source } of sources) {
|
|
227
|
+
const parsed = await tryLoad(value, allowPath);
|
|
228
|
+
if (parsed)
|
|
229
|
+
return { cookies: parsed, source };
|
|
230
|
+
}
|
|
231
|
+
// fallback: ~/.oracle/cookies.{json,base64}
|
|
232
|
+
const oracleHome = getOracleHomeDir();
|
|
233
|
+
const candidates = ['cookies.json', 'cookies.base64'];
|
|
234
|
+
for (const file of candidates) {
|
|
235
|
+
const fullPath = path.join(oracleHome, file);
|
|
236
|
+
try {
|
|
237
|
+
const stat = await fs.stat(fullPath);
|
|
238
|
+
if (!stat.isFile())
|
|
239
|
+
continue;
|
|
240
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
241
|
+
const parsed = parseInlineCookiesPayload(content);
|
|
242
|
+
if (parsed)
|
|
243
|
+
return { cookies: parsed, source: `home:${file}` };
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// ignore missing/invalid
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
function parseInlineCookiesPayload(raw) {
|
|
252
|
+
if (!raw)
|
|
253
|
+
return undefined;
|
|
254
|
+
const text = raw.trim();
|
|
255
|
+
if (!text)
|
|
256
|
+
return undefined;
|
|
257
|
+
let jsonPayload = text;
|
|
258
|
+
// Attempt base64 decode first; fall back to raw text on failure.
|
|
259
|
+
try {
|
|
260
|
+
const decoded = Buffer.from(text, 'base64').toString('utf8');
|
|
261
|
+
if (decoded.trim().startsWith('[')) {
|
|
262
|
+
jsonPayload = decoded;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// not base64; continue with raw text
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(jsonPayload);
|
|
270
|
+
if (Array.isArray(parsed)) {
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// invalid json; skip silently to keep this hidden flag non-fatal
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizeChatgptUrl, CHATGPT_URL } from '../browserMode.js';
|
|
2
|
+
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
3
|
+
const browser = config.browser;
|
|
4
|
+
if (!browser)
|
|
5
|
+
return;
|
|
6
|
+
const isUnset = (key) => {
|
|
7
|
+
const source = getSource(key);
|
|
8
|
+
return source === undefined || source === 'default';
|
|
9
|
+
};
|
|
10
|
+
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
11
|
+
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
12
|
+
if (isUnset('chatgptUrl') && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
13
|
+
options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? '', CHATGPT_URL);
|
|
14
|
+
}
|
|
15
|
+
if (isUnset('browserChromeProfile') && browser.chromeProfile !== undefined) {
|
|
16
|
+
options.browserChromeProfile = browser.chromeProfile ?? undefined;
|
|
17
|
+
}
|
|
18
|
+
if (isUnset('browserChromePath') && browser.chromePath !== undefined) {
|
|
19
|
+
options.browserChromePath = browser.chromePath ?? undefined;
|
|
20
|
+
}
|
|
21
|
+
if (isUnset('browserCookiePath') && browser.chromeCookiePath !== undefined) {
|
|
22
|
+
options.browserCookiePath = browser.chromeCookiePath ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
if (isUnset('browserUrl') && options.browserUrl === undefined && browser.url !== undefined) {
|
|
25
|
+
options.browserUrl = browser.url;
|
|
26
|
+
}
|
|
27
|
+
if (isUnset('browserTimeout') && typeof browser.timeoutMs === 'number') {
|
|
28
|
+
options.browserTimeout = String(browser.timeoutMs);
|
|
29
|
+
}
|
|
30
|
+
if (isUnset('browserPort') && typeof browser.debugPort === 'number') {
|
|
31
|
+
options.browserPort = browser.debugPort;
|
|
32
|
+
}
|
|
33
|
+
if (isUnset('browserInputTimeout') && typeof browser.inputTimeoutMs === 'number') {
|
|
34
|
+
options.browserInputTimeout = String(browser.inputTimeoutMs);
|
|
35
|
+
}
|
|
36
|
+
if (isUnset('browserRecheckDelay') && typeof browser.assistantRecheckDelayMs === 'number') {
|
|
37
|
+
options.browserRecheckDelay = String(browser.assistantRecheckDelayMs);
|
|
38
|
+
}
|
|
39
|
+
if (isUnset('browserRecheckTimeout') && typeof browser.assistantRecheckTimeoutMs === 'number') {
|
|
40
|
+
options.browserRecheckTimeout = String(browser.assistantRecheckTimeoutMs);
|
|
41
|
+
}
|
|
42
|
+
if (isUnset('browserReuseWait') && typeof browser.reuseChromeWaitMs === 'number') {
|
|
43
|
+
options.browserReuseWait = String(browser.reuseChromeWaitMs);
|
|
44
|
+
}
|
|
45
|
+
if (isUnset('browserProfileLockTimeout') && typeof browser.profileLockTimeoutMs === 'number') {
|
|
46
|
+
options.browserProfileLockTimeout = String(browser.profileLockTimeoutMs);
|
|
47
|
+
}
|
|
48
|
+
if (isUnset('browserAutoReattachDelay') && typeof browser.autoReattachDelayMs === 'number') {
|
|
49
|
+
options.browserAutoReattachDelay = String(browser.autoReattachDelayMs);
|
|
50
|
+
}
|
|
51
|
+
if (isUnset('browserAutoReattachInterval') && typeof browser.autoReattachIntervalMs === 'number') {
|
|
52
|
+
options.browserAutoReattachInterval = String(browser.autoReattachIntervalMs);
|
|
53
|
+
}
|
|
54
|
+
if (isUnset('browserAutoReattachTimeout') && typeof browser.autoReattachTimeoutMs === 'number') {
|
|
55
|
+
options.browserAutoReattachTimeout = String(browser.autoReattachTimeoutMs);
|
|
56
|
+
}
|
|
57
|
+
if (isUnset('browserCookieWait') && typeof browser.cookieSyncWaitMs === 'number') {
|
|
58
|
+
options.browserCookieWait = String(browser.cookieSyncWaitMs);
|
|
59
|
+
}
|
|
60
|
+
if (isUnset('browserHeadless') && browser.headless !== undefined) {
|
|
61
|
+
options.browserHeadless = browser.headless;
|
|
62
|
+
}
|
|
63
|
+
if (isUnset('browserHideWindow') && browser.hideWindow !== undefined) {
|
|
64
|
+
options.browserHideWindow = browser.hideWindow;
|
|
65
|
+
}
|
|
66
|
+
if (isUnset('browserKeepBrowser') && browser.keepBrowser !== undefined) {
|
|
67
|
+
options.browserKeepBrowser = browser.keepBrowser;
|
|
68
|
+
}
|
|
69
|
+
if (isUnset('browserModelStrategy') && browser.modelStrategy !== undefined) {
|
|
70
|
+
options.browserModelStrategy = browser.modelStrategy;
|
|
71
|
+
}
|
|
72
|
+
if (isUnset('browserThinkingTime') && browser.thinkingTime !== undefined) {
|
|
73
|
+
options.browserThinkingTime = browser.thinkingTime;
|
|
74
|
+
}
|
|
75
|
+
if (isUnset('browserManualLogin') && browser.manualLogin !== undefined) {
|
|
76
|
+
options.browserManualLogin = browser.manualLogin;
|
|
77
|
+
}
|
|
78
|
+
if (isUnset('browserManualLoginProfileDir') && browser.manualLoginProfileDir !== undefined) {
|
|
79
|
+
options.browserManualLoginProfileDir = browser.manualLoginProfileDir;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function warnIfOversizeBundle(estimatedTokens, threshold = 196_000, log = console.log) {
|
|
3
|
+
if (Number.isNaN(estimatedTokens) || estimatedTokens <= threshold) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const msg = `Warning: bundle is ~${estimatedTokens.toLocaleString()} tokens (>${threshold.toLocaleString()}); may exceed model limits.`;
|
|
7
|
+
log(chalk.red(msg));
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isProModel } from '../oracle/modelResolver.js';
|
|
2
|
+
export function shouldDetachSession({
|
|
3
|
+
// Params kept for future policy tweaks; currently only model/disableDetachEnv matter.
|
|
4
|
+
engine, model, waitPreference: _waitPreference, disableDetachEnv, }) {
|
|
5
|
+
if (disableDetachEnv)
|
|
6
|
+
return false;
|
|
7
|
+
// Only Pro-tier API runs should start detached by default; browser runs stay inline so failures surface.
|
|
8
|
+
if (isProModel(model) && engine === 'api')
|
|
9
|
+
return true;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from '../oracle.js';
|
|
3
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
4
|
+
import { assembleBrowserPrompt } from '../browser/prompt.js';
|
|
5
|
+
import { buildTokenEstimateSuffix, formatAttachmentLabel } from '../browser/promptSummary.js';
|
|
6
|
+
import { buildCookiePlan } from '../browser/policies.js';
|
|
7
|
+
export async function runDryRunSummary({ engine, runOptions, cwd, version, log, browserConfig, }, deps = {}) {
|
|
8
|
+
if (engine === 'browser') {
|
|
9
|
+
await runBrowserDryRun({ runOptions, cwd, version, log, browserConfig }, deps);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
await runApiDryRun({ runOptions, cwd, version, log }, deps);
|
|
13
|
+
}
|
|
14
|
+
async function runApiDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
15
|
+
const readFilesImpl = deps.readFilesImpl ?? readFiles;
|
|
16
|
+
const files = await readFilesImpl(runOptions.file ?? [], { cwd });
|
|
17
|
+
const systemPrompt = runOptions.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
18
|
+
const combinedPrompt = buildPrompt(runOptions.prompt ?? '', files, cwd);
|
|
19
|
+
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
20
|
+
const tokenizer = modelConfig.tokenizer;
|
|
21
|
+
const estimatedInputTokens = tokenizer([
|
|
22
|
+
{ role: 'system', content: systemPrompt },
|
|
23
|
+
{ role: 'user', content: combinedPrompt },
|
|
24
|
+
], TOKENIZER_OPTIONS);
|
|
25
|
+
const headerLine = `[dry-run] Oracle (${version}) would call ${runOptions.model} with ~${estimatedInputTokens.toLocaleString()} tokens and ${files.length} files.`;
|
|
26
|
+
log(chalk.cyan(headerLine));
|
|
27
|
+
if (files.length === 0) {
|
|
28
|
+
log(chalk.dim('[dry-run] No files matched the provided --file patterns.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const inputBudget = runOptions.maxInput ?? modelConfig.inputLimit;
|
|
32
|
+
const stats = getFileTokenStats(files, {
|
|
33
|
+
cwd,
|
|
34
|
+
tokenizer,
|
|
35
|
+
tokenizerOptions: TOKENIZER_OPTIONS,
|
|
36
|
+
inputTokenBudget: inputBudget,
|
|
37
|
+
});
|
|
38
|
+
printFileTokenStats(stats, { inputTokenBudget: inputBudget, log });
|
|
39
|
+
}
|
|
40
|
+
async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig, }, deps) {
|
|
41
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
42
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
43
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
44
|
+
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
45
|
+
log(chalk.cyan(headerLine));
|
|
46
|
+
logBrowserCookieStrategy(browserConfig, log, 'dry-run');
|
|
47
|
+
logBrowserFileSummary(artifacts, log, 'dry-run');
|
|
48
|
+
}
|
|
49
|
+
function logBrowserCookieStrategy(browserConfig, log, label) {
|
|
50
|
+
if (!browserConfig)
|
|
51
|
+
return;
|
|
52
|
+
const plan = buildCookiePlan(browserConfig);
|
|
53
|
+
log(chalk.bold(`[${label}] ${plan.description}`));
|
|
54
|
+
}
|
|
55
|
+
function logBrowserFileSummary(artifacts, log, label) {
|
|
56
|
+
if (artifacts.attachments.length > 0) {
|
|
57
|
+
const prefix = artifacts.bundled ? `[${label}] Bundled upload:` : `[${label}] Attachments to upload:`;
|
|
58
|
+
log(chalk.bold(prefix));
|
|
59
|
+
artifacts.attachments.forEach((attachment) => {
|
|
60
|
+
log(` • ${formatAttachmentLabel(attachment)}`);
|
|
61
|
+
});
|
|
62
|
+
if (artifacts.bundled) {
|
|
63
|
+
log(chalk.dim(` (bundled ${artifacts.bundled.originalCount} files into ${artifacts.bundled.bundlePath})`));
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (artifacts.inlineFileCount > 0) {
|
|
68
|
+
log(chalk.bold(`[${label}] Inline file content:`));
|
|
69
|
+
log(` • ${artifacts.inlineFileCount} file${artifacts.inlineFileCount === 1 ? '' : 's'} pasted directly into the composer.`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
log(chalk.dim(`[${label}] No files attached.`));
|
|
73
|
+
}
|
|
74
|
+
export async function runBrowserPreview({ runOptions, cwd, version, previewMode, log, }, deps = {}) {
|
|
75
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
76
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
77
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
78
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
79
|
+
log(chalk.cyan(headerLine));
|
|
80
|
+
logBrowserFileSummary(artifacts, log, 'preview');
|
|
81
|
+
if (previewMode === 'json' || previewMode === 'full') {
|
|
82
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
83
|
+
path: attachment.path,
|
|
84
|
+
displayPath: attachment.displayPath,
|
|
85
|
+
sizeBytes: attachment.sizeBytes,
|
|
86
|
+
}));
|
|
87
|
+
const previewPayload = {
|
|
88
|
+
model: runOptions.model,
|
|
89
|
+
engine: 'browser',
|
|
90
|
+
composerText: artifacts.composerText,
|
|
91
|
+
attachments: attachmentSummary,
|
|
92
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
93
|
+
bundled: artifacts.bundled,
|
|
94
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
95
|
+
};
|
|
96
|
+
log('');
|
|
97
|
+
log(chalk.bold('Preview JSON'));
|
|
98
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
99
|
+
}
|
|
100
|
+
if (previewMode === 'full') {
|
|
101
|
+
log('');
|
|
102
|
+
log(chalk.bold('Composer Text'));
|
|
103
|
+
log(artifacts.composerText || chalk.dim('(empty prompt)'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export async function shouldBlockDuplicatePrompt({ prompt, force, sessionStore, log = console.log, }) {
|
|
3
|
+
if (force)
|
|
4
|
+
return false;
|
|
5
|
+
const normalized = prompt?.trim();
|
|
6
|
+
if (!normalized)
|
|
7
|
+
return false;
|
|
8
|
+
const running = (await sessionStore.listSessions()).filter((entry) => entry.status === 'running');
|
|
9
|
+
const duplicate = running.find((entry) => (entry.options?.prompt?.trim?.() ?? '') === normalized);
|
|
10
|
+
if (!duplicate)
|
|
11
|
+
return false;
|
|
12
|
+
log(chalk.yellow(`A session with the same prompt is already running (${duplicate.id}). Reattach with "oracle session ${duplicate.id}" or rerun with --force to start another run.`));
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isProModel } from '../oracle/modelResolver.js';
|
|
2
|
+
export function defaultWaitPreference(model, engine) {
|
|
3
|
+
// Pro-class API runs can take a long time; prefer non-blocking unless explicitly overridden.
|
|
4
|
+
if (engine === 'api' && isProModel(model)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return true; // browser or non-pro models are fast enough to block by default
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Determine which engine to use based on CLI flags and the environment.
|
|
11
|
+
*
|
|
12
|
+
* Precedence:
|
|
13
|
+
* 1) Legacy --browser flag forces browser.
|
|
14
|
+
* 2) Explicit --engine value.
|
|
15
|
+
* 3) ORACLE_ENGINE environment override (api|browser).
|
|
16
|
+
* 4) OPENAI_API_KEY decides: api when set, otherwise browser.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveEngine({ engine, browserFlag, env, }) {
|
|
19
|
+
if (browserFlag) {
|
|
20
|
+
return 'browser';
|
|
21
|
+
}
|
|
22
|
+
if (engine) {
|
|
23
|
+
return engine;
|
|
24
|
+
}
|
|
25
|
+
const envEngine = normalizeEngineMode(env.ORACLE_ENGINE);
|
|
26
|
+
if (envEngine) {
|
|
27
|
+
return envEngine;
|
|
28
|
+
}
|
|
29
|
+
return env.OPENAI_API_KEY ? 'api' : 'browser';
|
|
30
|
+
}
|
|
31
|
+
function normalizeEngineMode(raw) {
|
|
32
|
+
if (typeof raw !== 'string') {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const normalized = raw.trim().toLowerCase();
|
|
36
|
+
if (normalized === 'api')
|
|
37
|
+
return 'api';
|
|
38
|
+
if (normalized === 'browser')
|
|
39
|
+
return 'browser';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const LOGGED_SYMBOL = Symbol('oracle.alreadyLogged');
|
|
2
|
+
export function markErrorLogged(error) {
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
error[LOGGED_SYMBOL] = true;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function isErrorLogged(error) {
|
|
8
|
+
return Boolean(error instanceof Error && error[LOGGED_SYMBOL]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatCompactNumber(value) {
|
|
2
|
+
if (Number.isNaN(value) || !Number.isFinite(value))
|
|
3
|
+
return '0';
|
|
4
|
+
const abs = Math.abs(value);
|
|
5
|
+
const stripTrailingZero = (text) => text.replace(/\.0$/, '');
|
|
6
|
+
if (abs >= 1_000_000) {
|
|
7
|
+
return `${stripTrailingZero((value / 1_000_000).toFixed(1))}m`;
|
|
8
|
+
}
|
|
9
|
+
if (abs >= 1_000) {
|
|
10
|
+
return `${stripTrailingZero((value / 1_000).toFixed(1))}k`;
|
|
11
|
+
}
|
|
12
|
+
return value.toLocaleString();
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
const createColorWrapper = (isTty) => (styler) => (text) => isTty ? styler(text) : text;
|
|
3
|
+
export function applyHelpStyling(program, version, isTty) {
|
|
4
|
+
const wrap = createColorWrapper(isTty);
|
|
5
|
+
const colors = {
|
|
6
|
+
banner: wrap((text) => kleur.bold().blue(text)),
|
|
7
|
+
subtitle: wrap((text) => kleur.dim(text)),
|
|
8
|
+
section: wrap((text) => kleur.bold().white(text)),
|
|
9
|
+
bullet: wrap((text) => kleur.blue(text)),
|
|
10
|
+
command: wrap((text) => kleur.bold().blue(text)),
|
|
11
|
+
option: wrap((text) => kleur.cyan(text)),
|
|
12
|
+
argument: wrap((text) => kleur.magenta(text)),
|
|
13
|
+
description: wrap((text) => kleur.white(text)),
|
|
14
|
+
muted: wrap((text) => kleur.gray(text)),
|
|
15
|
+
accent: wrap((text) => kleur.cyan(text)),
|
|
16
|
+
};
|
|
17
|
+
program.configureHelp({
|
|
18
|
+
styleTitle(title) {
|
|
19
|
+
return colors.section(title);
|
|
20
|
+
},
|
|
21
|
+
styleDescriptionText(text) {
|
|
22
|
+
return colors.description(text);
|
|
23
|
+
},
|
|
24
|
+
styleCommandText(text) {
|
|
25
|
+
return colors.command(text);
|
|
26
|
+
},
|
|
27
|
+
styleSubcommandText(text) {
|
|
28
|
+
return colors.command(text);
|
|
29
|
+
},
|
|
30
|
+
styleOptionText(text) {
|
|
31
|
+
return colors.option(text);
|
|
32
|
+
},
|
|
33
|
+
styleArgumentText(text) {
|
|
34
|
+
return colors.argument(text);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
program.addHelpText('beforeAll', () => renderHelpBanner(version, colors));
|
|
38
|
+
program.addHelpText('after', () => renderHelpFooter(program, colors));
|
|
39
|
+
}
|
|
40
|
+
function renderHelpBanner(version, colors) {
|
|
41
|
+
const subtitle = 'Prompt + files required — GPT-5.2 Pro/GPT-5.2 for tough questions with code/file context.';
|
|
42
|
+
return `${colors.banner(`Oracle CLI v${version}`)} ${colors.subtitle(`— ${subtitle}`)}\n`;
|
|
43
|
+
}
|
|
44
|
+
function renderHelpFooter(program, colors) {
|
|
45
|
+
const tips = [
|
|
46
|
+
`${colors.bullet('•')} Required: always pass a prompt AND ${colors.accent('--file …')} (directories/globs are fine); Oracle cannot see your project otherwise.`,
|
|
47
|
+
`${colors.bullet('•')} Attach lots of source (whole directories beat single files) and keep total input under ~196k tokens.`,
|
|
48
|
+
`${colors.bullet('•')} Oracle starts empty—open with a short project briefing (stack, services, build steps), spell out the question and prior attempts, and why it matters; the more explanation and context you provide, the better the response will be.`,
|
|
49
|
+
`${colors.bullet('•')} Spell out the project + platform + version requirements (repo name, target OS/toolchain versions, API dependencies) so Oracle doesn’t guess defaults.`,
|
|
50
|
+
`${colors.bullet('•')} When comparing multiple repos/files, spell out each repo + path + role (e.g., “Project A SettingsView → apps/project-a/Sources/SettingsView.swift; Project B SettingsView → ../project-b/mac/...”) so the model knows exactly which file is which.`,
|
|
51
|
+
`${colors.bullet('•')} Best results: 6–30 sentences plus key source files; very short prompts often yield generic answers.`,
|
|
52
|
+
`${colors.bullet('•')} Oracle is one-shot: it does not remember prior runs, so start fresh each time with full context.`,
|
|
53
|
+
`${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
|
|
54
|
+
`${colors.bullet('•')} Non-preview runs spawn detached sessions (especially gpt-5.2-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent('oracle session <slug>')} to resume/inspect the existing run.`,
|
|
55
|
+
`${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
|
56
|
+
`${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
|
|
57
|
+
`${colors.bullet('•')} Need hidden flags? Run ${colors.accent(`${program.name()} --help --verbose`)} to list search/token/browser overrides.`,
|
|
58
|
+
`${colors.bullet('•')} If any Oracle session is already running, do not start new API runs. Attach to the existing browser session instead; only trigger API calls when you explicitly mean to.`,
|
|
59
|
+
`${colors.bullet('•')} Duplicate prompt guard: if the same prompt is already running, new runs are blocked unless you pass ${colors.accent('--force')}—prefer reattaching instead of spawning duplicates.`,
|
|
60
|
+
].join('\n');
|
|
61
|
+
const formatExample = (command, description) => `${colors.command(` ${command}`)}\n${colors.muted(` ${description}`)}`;
|
|
62
|
+
const examples = [
|
|
63
|
+
formatExample(`${program.name()} --render --copy --prompt "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"`, 'Build the bundle, print it, and copy it for manual paste into ChatGPT.'),
|
|
64
|
+
formatExample(`${program.name()} --prompt "Cross-check the data layer assumptions" --models gpt-5.2-pro,gemini-3-pro --file "src/**/*.ts"`, 'Run multiple API models in one go and aggregate cost/usage.'),
|
|
65
|
+
formatExample(`${program.name()} status --hours 72 --limit 50`, 'Show sessions from the last 72h (capped at 50 entries).'),
|
|
66
|
+
formatExample(`${program.name()} session <sessionId>`, 'Attach to a running/completed session and stream the saved transcript.'),
|
|
67
|
+
formatExample(`${program.name()} --prompt "Ship review" --slug "release-readiness-audit"`, 'Encourage the model to hand you a 3–5 word slug and pass it along with --slug.'),
|
|
68
|
+
formatExample(`${program.name()} --prompt "Tabs frozen: compare Project A SettingsView (apps/project-a/Sources/SettingsView.swift) vs Project B SettingsView (../project-b/mac/App/Presentation/Views/SettingsView.swift)" --file apps/project-a/Sources/SettingsView.swift --file ../project-b/mac/App/Presentation/Views/SettingsView.swift`, 'Spell out what each attached file is (repo + path + role) before asking for comparisons so the model knows exactly what it is reading.'),
|
|
69
|
+
].join('\n\n');
|
|
70
|
+
return `
|
|
71
|
+
${colors.section('Tips')}
|
|
72
|
+
${tips}
|
|
73
|
+
|
|
74
|
+
${colors.section('Examples')}
|
|
75
|
+
${examples}
|
|
76
|
+
`;
|
|
77
|
+
}
|