@steipete/oracle 1.1.0 → 1.2.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 +29 -4
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +169 -18
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/src/browser/actions/modelSelection.js +117 -29
- package/dist/src/browser/cookies.js +1 -1
- package/dist/src/browser/index.js +2 -1
- package/dist/src/browser/prompt.js +6 -5
- package/dist/src/browser/sessionRunner.js +4 -2
- package/dist/src/cli/dryRun.js +41 -5
- package/dist/src/cli/engine.js +7 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/hiddenAliases.js +17 -0
- package/dist/src/cli/markdownRenderer.js +79 -0
- package/dist/src/cli/notifier.js +223 -0
- package/dist/src/cli/promptRequirement.js +3 -0
- package/dist/src/cli/runOptions.js +29 -0
- package/dist/src/cli/sessionCommand.js +1 -1
- package/dist/src/cli/sessionDisplay.js +94 -7
- package/dist/src/cli/sessionRunner.js +21 -2
- package/dist/src/cli/tui/index.js +436 -0
- package/dist/src/config.js +27 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +158 -0
- package/dist/src/mcp/tools/sessionResources.js +64 -0
- package/dist/src/mcp/tools/sessions.js +106 -0
- package/dist/src/mcp/types.js +17 -0
- package/dist/src/mcp/utils.js +24 -0
- package/dist/src/oracle/files.js +143 -6
- package/dist/src/oracle/run.js +41 -20
- package/dist/src/oracle/tokenEstimate.js +34 -0
- package/dist/src/sessionManager.js +48 -3
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -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/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +39 -13
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -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
- package/dist/.DS_Store +0 -0
|
@@ -9,7 +9,8 @@ export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
|
9
9
|
const result = outcome.result?.value;
|
|
10
10
|
switch (result?.status) {
|
|
11
11
|
case 'already-selected':
|
|
12
|
-
case 'switched':
|
|
12
|
+
case 'switched':
|
|
13
|
+
case 'switched-best-effort': {
|
|
13
14
|
const label = result.label ?? desiredModel;
|
|
14
15
|
logger(`Model picker: ${label}`);
|
|
15
16
|
return;
|
|
@@ -24,18 +25,26 @@ export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds the DOM expression that runs inside the ChatGPT tab to select a model.
|
|
30
|
+
* The string is evaluated inside Chrome, so keep it self-contained and well-commented.
|
|
31
|
+
*/
|
|
27
32
|
function buildModelSelectionExpression(targetModel) {
|
|
28
33
|
const matchers = buildModelMatchersLiteral(targetModel);
|
|
29
34
|
const labelLiteral = JSON.stringify(matchers.labelTokens);
|
|
30
35
|
const idLiteral = JSON.stringify(matchers.testIdTokens);
|
|
36
|
+
const primaryLabelLiteral = JSON.stringify(targetModel);
|
|
31
37
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
32
38
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
33
39
|
return `(() => {
|
|
40
|
+
// Capture the selectors and matcher literals up front so the browser expression stays pure.
|
|
34
41
|
const BUTTON_SELECTOR = '${MODEL_BUTTON_SELECTOR}';
|
|
35
42
|
const LABEL_TOKENS = ${labelLiteral};
|
|
36
43
|
const TEST_IDS = ${idLiteral};
|
|
37
|
-
const
|
|
38
|
-
const
|
|
44
|
+
const PRIMARY_LABEL = ${primaryLabelLiteral};
|
|
45
|
+
const INITIAL_WAIT_MS = 150;
|
|
46
|
+
const REOPEN_INTERVAL_MS = 400;
|
|
47
|
+
const MAX_WAIT_MS = 20000;
|
|
39
48
|
const normalizeText = (value) => {
|
|
40
49
|
if (!value) {
|
|
41
50
|
return '';
|
|
@@ -46,6 +55,12 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
46
55
|
.replace(/\\s+/g, ' ')
|
|
47
56
|
.trim();
|
|
48
57
|
};
|
|
58
|
+
// Normalize every candidate token to keep fuzzy matching deterministic.
|
|
59
|
+
const normalizedTarget = normalizeText(PRIMARY_LABEL);
|
|
60
|
+
const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
|
|
61
|
+
.map((token) => normalizeText(token))
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const targetWords = normalizedTarget.split(' ').filter(Boolean);
|
|
49
64
|
|
|
50
65
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
51
66
|
if (!button) {
|
|
@@ -54,6 +69,7 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
54
69
|
|
|
55
70
|
let lastPointerClick = 0;
|
|
56
71
|
const pointerClick = () => {
|
|
72
|
+
// Some menus ignore synthetic click events.
|
|
57
73
|
const down = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
|
|
58
74
|
const up = new PointerEvent('pointerup', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
|
|
59
75
|
const click = new MouseEvent('click', { bubbles: true });
|
|
@@ -86,64 +102,109 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
86
102
|
return false;
|
|
87
103
|
};
|
|
88
104
|
|
|
89
|
-
const
|
|
105
|
+
const scoreOption = (normalizedText, testid) => {
|
|
106
|
+
// Assign a score to every node so we can pick the most likely match without brittle equality checks.
|
|
107
|
+
if (!normalizedText && !testid) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
let score = 0;
|
|
111
|
+
const normalizedTestId = (testid ?? '').toLowerCase();
|
|
112
|
+
if (normalizedTestId && TEST_IDS.some((id) => normalizedTestId.includes(id))) {
|
|
113
|
+
score += 1000;
|
|
114
|
+
}
|
|
115
|
+
if (normalizedText && normalizedTarget) {
|
|
116
|
+
if (normalizedText === normalizedTarget) {
|
|
117
|
+
score += 500;
|
|
118
|
+
} else if (normalizedText.startsWith(normalizedTarget)) {
|
|
119
|
+
score += 420;
|
|
120
|
+
} else if (normalizedText.includes(normalizedTarget)) {
|
|
121
|
+
score += 380;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const token of normalizedTokens) {
|
|
125
|
+
// Reward partial matches to the expanded label/token set.
|
|
126
|
+
if (token && normalizedText.includes(token)) {
|
|
127
|
+
const tokenWeight = Math.min(120, Math.max(10, token.length * 4));
|
|
128
|
+
score += tokenWeight;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (targetWords.length > 1) {
|
|
132
|
+
let missing = 0;
|
|
133
|
+
for (const word of targetWords) {
|
|
134
|
+
if (!normalizedText.includes(word)) {
|
|
135
|
+
missing += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
score -= missing * 12;
|
|
139
|
+
}
|
|
140
|
+
return Math.max(score, 0);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const findBestOption = () => {
|
|
144
|
+
// Walk through every menu item and keep whichever earns the highest score.
|
|
145
|
+
let bestMatch = null;
|
|
90
146
|
const menus = Array.from(document.querySelectorAll(${menuContainerLiteral}));
|
|
91
147
|
for (const menu of menus) {
|
|
92
148
|
const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
|
|
93
149
|
for (const option of buttons) {
|
|
94
|
-
const testid = (option.getAttribute('data-testid') ?? '').toLowerCase();
|
|
95
150
|
const text = option.textContent ?? '';
|
|
96
151
|
const normalizedText = normalizeText(text);
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (matchesTestId || matchesText) {
|
|
106
|
-
return option;
|
|
152
|
+
const testid = option.getAttribute('data-testid') ?? '';
|
|
153
|
+
const score = scoreOption(normalizedText, testid);
|
|
154
|
+
if (score <= 0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const label = getOptionLabel(option);
|
|
158
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
159
|
+
bestMatch = { node: option, label, score };
|
|
107
160
|
}
|
|
108
161
|
}
|
|
109
162
|
}
|
|
110
|
-
return
|
|
163
|
+
return bestMatch;
|
|
111
164
|
};
|
|
112
165
|
|
|
113
|
-
pointerClick();
|
|
114
166
|
return new Promise((resolve) => {
|
|
115
167
|
const start = performance.now();
|
|
116
168
|
const ensureMenuOpen = () => {
|
|
117
169
|
const menuOpen = document.querySelector('[role="menu"], [data-radix-collection-root]');
|
|
118
|
-
if (!menuOpen && performance.now() - lastPointerClick >
|
|
170
|
+
if (!menuOpen && performance.now() - lastPointerClick > REOPEN_INTERVAL_MS) {
|
|
119
171
|
pointerClick();
|
|
120
172
|
}
|
|
121
173
|
};
|
|
122
|
-
|
|
174
|
+
|
|
175
|
+
// Open once and wait a tick before first scan.
|
|
176
|
+
pointerClick();
|
|
177
|
+
const openDelay = () => new Promise((r) => setTimeout(r, INITIAL_WAIT_MS));
|
|
178
|
+
let initialized = false;
|
|
179
|
+
const attempt = async () => {
|
|
180
|
+
if (!initialized) {
|
|
181
|
+
initialized = true;
|
|
182
|
+
await openDelay();
|
|
183
|
+
}
|
|
123
184
|
ensureMenuOpen();
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
if (optionIsSelected(
|
|
127
|
-
resolve({ status: 'already-selected', label:
|
|
185
|
+
const match = findBestOption();
|
|
186
|
+
if (match) {
|
|
187
|
+
if (optionIsSelected(match.node)) {
|
|
188
|
+
resolve({ status: 'already-selected', label: match.label });
|
|
128
189
|
return;
|
|
129
190
|
}
|
|
130
|
-
|
|
131
|
-
resolve({ status: 'switched', label:
|
|
191
|
+
match.node.click();
|
|
192
|
+
resolve({ status: 'switched', label: match.label });
|
|
132
193
|
return;
|
|
133
194
|
}
|
|
134
195
|
if (performance.now() - start > MAX_WAIT_MS) {
|
|
135
196
|
resolve({ status: 'option-not-found' });
|
|
136
197
|
return;
|
|
137
198
|
}
|
|
138
|
-
|
|
139
|
-
pointerClick();
|
|
140
|
-
}
|
|
141
|
-
setTimeout(attempt, CLICK_INTERVAL_MS);
|
|
199
|
+
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
142
200
|
};
|
|
143
201
|
attempt();
|
|
144
202
|
});
|
|
145
203
|
})()`;
|
|
146
204
|
}
|
|
205
|
+
export function buildModelMatchersLiteralForTest(targetModel) {
|
|
206
|
+
return buildModelMatchersLiteral(targetModel);
|
|
207
|
+
}
|
|
147
208
|
function buildModelMatchersLiteral(targetModel) {
|
|
148
209
|
const base = targetModel.trim().toLowerCase();
|
|
149
210
|
const labelTokens = new Set();
|
|
@@ -164,6 +225,28 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
164
225
|
push(`chatgpt ${dotless}`, labelTokens);
|
|
165
226
|
push(`gpt ${base}`, labelTokens);
|
|
166
227
|
push(`gpt ${dotless}`, labelTokens);
|
|
228
|
+
// Numeric variations (5.1 ↔ 51 ↔ gpt-5-1)
|
|
229
|
+
if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
|
|
230
|
+
push('5.1', labelTokens);
|
|
231
|
+
push('gpt-5.1', labelTokens);
|
|
232
|
+
push('gpt5.1', labelTokens);
|
|
233
|
+
push('gpt-5-1', labelTokens);
|
|
234
|
+
push('gpt5-1', labelTokens);
|
|
235
|
+
push('gpt51', labelTokens);
|
|
236
|
+
push('chatgpt 5.1', labelTokens);
|
|
237
|
+
testIdTokens.add('gpt-5-1');
|
|
238
|
+
testIdTokens.add('gpt5-1');
|
|
239
|
+
testIdTokens.add('gpt51');
|
|
240
|
+
}
|
|
241
|
+
// Pro / research variants
|
|
242
|
+
if (base.includes('pro')) {
|
|
243
|
+
push('proresearch', labelTokens);
|
|
244
|
+
push('research grade', labelTokens);
|
|
245
|
+
push('advanced reasoning', labelTokens);
|
|
246
|
+
testIdTokens.add('gpt-5-pro');
|
|
247
|
+
testIdTokens.add('pro');
|
|
248
|
+
testIdTokens.add('proresearch');
|
|
249
|
+
}
|
|
167
250
|
base
|
|
168
251
|
.split(/\s+/)
|
|
169
252
|
.map((token) => token.trim())
|
|
@@ -175,8 +258,10 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
175
258
|
push(hyphenated, testIdTokens);
|
|
176
259
|
push(collapsed, testIdTokens);
|
|
177
260
|
push(dotless, testIdTokens);
|
|
261
|
+
// data-testid values observed in the ChatGPT picker (e.g., model-switcher-gpt-5-pro)
|
|
178
262
|
push(`model-switcher-${hyphenated}`, testIdTokens);
|
|
179
263
|
push(`model-switcher-${collapsed}`, testIdTokens);
|
|
264
|
+
push(`model-switcher-${dotless}`, testIdTokens);
|
|
180
265
|
if (!labelTokens.size) {
|
|
181
266
|
labelTokens.add(base);
|
|
182
267
|
}
|
|
@@ -188,3 +273,6 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
188
273
|
testIdTokens: Array.from(testIdTokens).filter(Boolean),
|
|
189
274
|
};
|
|
190
275
|
}
|
|
276
|
+
export function buildModelSelectionExpressionForTest(targetModel) {
|
|
277
|
+
return buildModelSelectionExpression(targetModel);
|
|
278
|
+
}
|
|
@@ -177,7 +177,7 @@ async function attemptSqliteRebuild() {
|
|
|
177
177
|
}
|
|
178
178
|
attemptedSqliteRebuild = true;
|
|
179
179
|
if (process.env.ORACLE_ALLOW_SQLITE_REBUILD !== '1') {
|
|
180
|
-
console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want
|
|
180
|
+
console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want oracle to attempt an automatic rebuild, or run `pnpm rebuild chrome-cookies-secure sqlite3 keytar --workspace-root` manually.');
|
|
181
181
|
return false;
|
|
182
182
|
}
|
|
183
183
|
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
@@ -64,6 +64,7 @@ export async function runBrowserMode(options) {
|
|
|
64
64
|
await Promise.all(domainEnablers);
|
|
65
65
|
await Network.clearBrowserCookies();
|
|
66
66
|
if (config.cookieSync) {
|
|
67
|
+
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.');
|
|
67
68
|
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, config.allowCookieErrors ?? false);
|
|
68
69
|
logger(cookieCount > 0
|
|
69
70
|
? `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? 'Default'}`
|
|
@@ -155,7 +156,7 @@ export async function runBrowserMode(options) {
|
|
|
155
156
|
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
156
157
|
logger(normalizedError.stack);
|
|
157
158
|
}
|
|
158
|
-
throw new Error('Chrome window closed before
|
|
159
|
+
throw new Error('Chrome window closed before oracle finished. Please keep it open until completion.', {
|
|
159
160
|
cause: normalizedError,
|
|
160
161
|
});
|
|
161
162
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { readFiles, createFileSections,
|
|
4
|
+
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS } from '../oracle.js';
|
|
5
5
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
6
6
|
const cwd = deps.cwd ?? process.cwd();
|
|
7
7
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
8
8
|
const files = await readFilesFn(runOptions.file ?? [], { cwd });
|
|
9
9
|
const basePrompt = (runOptions.prompt ?? '').trim();
|
|
10
10
|
const userPrompt = basePrompt;
|
|
11
|
-
const systemPrompt = runOptions.system?.trim() ||
|
|
11
|
+
const systemPrompt = runOptions.system?.trim() || '';
|
|
12
12
|
const sections = createFileSections(files, cwd);
|
|
13
13
|
const lines = ['[SYSTEM]', systemPrompt, '', '[USER]', userPrompt, ''];
|
|
14
14
|
sections.forEach((section) => {
|
|
@@ -43,7 +43,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
43
43
|
sizeBytes: Buffer.byteLength(section.content, 'utf8'),
|
|
44
44
|
}));
|
|
45
45
|
const MAX_BROWSER_ATTACHMENTS = 10;
|
|
46
|
-
|
|
46
|
+
const shouldBundle = !inlineFiles && (runOptions.browserBundleFiles || attachments.length > MAX_BROWSER_ATTACHMENTS);
|
|
47
|
+
if (shouldBundle) {
|
|
47
48
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
48
49
|
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
49
50
|
const bundleLines = [];
|
|
@@ -57,7 +58,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
57
58
|
attachments.length = 0;
|
|
58
59
|
attachments.push({
|
|
59
60
|
path: bundlePath,
|
|
60
|
-
displayPath:
|
|
61
|
+
displayPath: bundlePath,
|
|
61
62
|
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -81,7 +82,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
81
82
|
attachments,
|
|
82
83
|
inlineFileCount,
|
|
83
84
|
tokenEstimateIncludesInlineFiles,
|
|
84
|
-
bundled:
|
|
85
|
+
bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
|
|
85
86
|
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
86
87
|
: null,
|
|
87
88
|
};
|
|
@@ -16,14 +16,14 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
16
16
|
const attachmentList = promptArtifacts.attachments.map((attachment) => attachment.displayPath).join(', ');
|
|
17
17
|
log(chalk.dim(`[verbose] Browser attachments: ${attachmentList}`));
|
|
18
18
|
if (promptArtifacts.bundled) {
|
|
19
|
-
log(chalk.yellow(`[browser]
|
|
19
|
+
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
else if (runOptions.file && runOptions.file.length > 0 && runOptions.browserInlineFiles) {
|
|
23
23
|
log(chalk.dim('[verbose] Browser inline file fallback enabled (pasting file contents).'));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
const headerLine = `
|
|
26
|
+
const headerLine = `oracle (${cliVersion}) launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens`;
|
|
27
27
|
const automationLogger = ((message) => {
|
|
28
28
|
if (typeof message === 'string') {
|
|
29
29
|
log(message);
|
|
@@ -56,6 +56,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
56
56
|
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim('(no text output)'));
|
|
57
57
|
log('');
|
|
58
58
|
}
|
|
59
|
+
const answerText = browserResult.answerMarkdown || browserResult.answerText || '';
|
|
59
60
|
const usage = {
|
|
60
61
|
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
61
62
|
outputTokens: browserResult.answerTokens,
|
|
@@ -76,5 +77,6 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
76
77
|
chromePort: browserResult.chromePort,
|
|
77
78
|
userDataDir: browserResult.userDataDir,
|
|
78
79
|
},
|
|
80
|
+
answerText,
|
|
79
81
|
};
|
|
80
82
|
}
|
package/dist/src/cli/dryRun.js
CHANGED
|
@@ -40,20 +40,56 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
|
40
40
|
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
41
41
|
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
42
42
|
log(chalk.cyan(headerLine));
|
|
43
|
-
logBrowserFileSummary(artifacts, log);
|
|
43
|
+
logBrowserFileSummary(artifacts, log, 'dry-run');
|
|
44
44
|
}
|
|
45
|
-
function logBrowserFileSummary(artifacts, log) {
|
|
45
|
+
function logBrowserFileSummary(artifacts, log, label) {
|
|
46
46
|
if (artifacts.attachments.length > 0) {
|
|
47
|
-
|
|
47
|
+
const prefix = artifacts.bundled ? `[${label}] Bundled upload:` : `[${label}] Attachments to upload:`;
|
|
48
|
+
log(chalk.bold(prefix));
|
|
48
49
|
artifacts.attachments.forEach((attachment) => {
|
|
49
50
|
log(` • ${formatAttachmentLabel(attachment)}`);
|
|
50
51
|
});
|
|
52
|
+
if (artifacts.bundled) {
|
|
53
|
+
log(chalk.dim(` (bundled ${artifacts.bundled.originalCount} files into ${artifacts.bundled.bundlePath})`));
|
|
54
|
+
}
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
57
|
if (artifacts.inlineFileCount > 0) {
|
|
54
|
-
log(chalk.bold(
|
|
58
|
+
log(chalk.bold(`[${label}] Inline file content:`));
|
|
55
59
|
log(` • ${artifacts.inlineFileCount} file${artifacts.inlineFileCount === 1 ? '' : 's'} pasted directly into the composer.`);
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
|
-
log(chalk.dim(
|
|
62
|
+
log(chalk.dim(`[${label}] No files attached.`));
|
|
63
|
+
}
|
|
64
|
+
export async function runBrowserPreview({ runOptions, cwd, version, previewMode, log, }, deps = {}) {
|
|
65
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
66
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
67
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
68
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
69
|
+
log(chalk.cyan(headerLine));
|
|
70
|
+
logBrowserFileSummary(artifacts, log, 'preview');
|
|
71
|
+
if (previewMode === 'json' || previewMode === 'full') {
|
|
72
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
73
|
+
path: attachment.path,
|
|
74
|
+
displayPath: attachment.displayPath,
|
|
75
|
+
sizeBytes: attachment.sizeBytes,
|
|
76
|
+
}));
|
|
77
|
+
const previewPayload = {
|
|
78
|
+
model: runOptions.model,
|
|
79
|
+
engine: 'browser',
|
|
80
|
+
composerText: artifacts.composerText,
|
|
81
|
+
attachments: attachmentSummary,
|
|
82
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
83
|
+
bundled: artifacts.bundled,
|
|
84
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
85
|
+
};
|
|
86
|
+
log('');
|
|
87
|
+
log(chalk.bold('Preview JSON'));
|
|
88
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
89
|
+
}
|
|
90
|
+
if (previewMode === 'full') {
|
|
91
|
+
log('');
|
|
92
|
+
log(chalk.bold('Composer Text'));
|
|
93
|
+
log(artifacts.composerText || chalk.dim('(empty prompt)'));
|
|
94
|
+
}
|
|
59
95
|
}
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export function defaultWaitPreference(model, engine) {
|
|
2
|
+
// gpt-5-pro (API) can take up to 10 minutes; default to non-blocking
|
|
3
|
+
if (engine === 'api' && model === 'gpt-5-pro') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
return true; // browser or gpt-5.1 are fast enough to block by default
|
|
7
|
+
}
|
|
1
8
|
/**
|
|
2
9
|
* Determine which engine to use based on CLI flags and the environment.
|
|
3
10
|
*
|
package/dist/src/cli/help.js
CHANGED
|
@@ -47,7 +47,7 @@ function renderHelpFooter(program, colors) {
|
|
|
47
47
|
`${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.`,
|
|
48
48
|
`${colors.bullet('•')} Oracle is one-shot: it does not remember prior runs, so start fresh each time with full context.`,
|
|
49
49
|
`${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
|
|
50
|
-
`${colors.bullet('•')} Non-preview runs spawn detached sessions
|
|
50
|
+
`${colors.bullet('•')} Non-preview runs spawn detached sessions (especially gpt-5-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent('oracle session <slug>')} to resume/inspect the existing run.`,
|
|
51
51
|
`${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
|
52
52
|
`${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
|
|
53
53
|
`${colors.bullet('•')} Need hidden flags? Run ${colors.accent(`${program.name()} --help --verbose`)} to list search/token/browser overrides.`,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize hidden alias flags so they behave like their primary counterparts.
|
|
3
|
+
*
|
|
4
|
+
* - `--message` maps to `--prompt` when no prompt is provided.
|
|
5
|
+
* - `--include` extends the `--file` list.
|
|
6
|
+
*/
|
|
7
|
+
export function applyHiddenAliases(options, setOptionValue) {
|
|
8
|
+
if (options.include && options.include.length > 0) {
|
|
9
|
+
const mergedFiles = [...(options.file ?? []), ...options.include];
|
|
10
|
+
options.file = mergedFiles;
|
|
11
|
+
setOptionValue?.('file', mergedFiles);
|
|
12
|
+
}
|
|
13
|
+
if (!options.prompt && options.message) {
|
|
14
|
+
options.prompt = options.message;
|
|
15
|
+
setOptionValue?.('prompt', options.message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,4 +1,82 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { render as renderMarkdown } from 'markdansi';
|
|
3
|
+
import { bundledLanguages, bundledThemes, createHighlighter, } from 'shiki';
|
|
4
|
+
const DEFAULT_THEME = 'github-dark';
|
|
5
|
+
const HIGHLIGHT_LANGS = ['ts', 'tsx', 'js', 'jsx', 'json', 'swift'];
|
|
6
|
+
const SUPPORTED_LANG_ALIASES = {
|
|
7
|
+
ts: 'ts',
|
|
8
|
+
typescript: 'ts',
|
|
9
|
+
tsx: 'tsx',
|
|
10
|
+
js: 'js',
|
|
11
|
+
javascript: 'js',
|
|
12
|
+
jsx: 'jsx',
|
|
13
|
+
json: 'json',
|
|
14
|
+
swift: 'swift',
|
|
15
|
+
};
|
|
16
|
+
const shikiPromise = createHighlighter({
|
|
17
|
+
themes: [bundledThemes[DEFAULT_THEME]],
|
|
18
|
+
langs: HIGHLIGHT_LANGS.map((lang) => bundledLanguages[lang]),
|
|
19
|
+
});
|
|
20
|
+
let shiki = null;
|
|
21
|
+
void shikiPromise
|
|
22
|
+
.then((instance) => {
|
|
23
|
+
shiki = instance;
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {
|
|
26
|
+
shiki = null;
|
|
27
|
+
});
|
|
28
|
+
export async function ensureShikiReady() {
|
|
29
|
+
if (shiki)
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
shiki = await shikiPromise;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
shiki = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function normalizeLanguage(lang) {
|
|
39
|
+
if (!lang)
|
|
40
|
+
return null;
|
|
41
|
+
const key = lang.toLowerCase();
|
|
42
|
+
return SUPPORTED_LANG_ALIASES[key] ?? null;
|
|
43
|
+
}
|
|
44
|
+
function styleToken(text, fontStyle = 0) {
|
|
45
|
+
let styled = text;
|
|
46
|
+
if (fontStyle & 1)
|
|
47
|
+
styled = chalk.italic(styled);
|
|
48
|
+
if (fontStyle & 2)
|
|
49
|
+
styled = chalk.bold(styled);
|
|
50
|
+
if (fontStyle & 4)
|
|
51
|
+
styled = chalk.underline(styled);
|
|
52
|
+
if (fontStyle & 8)
|
|
53
|
+
styled = chalk.strikethrough(styled);
|
|
54
|
+
return styled;
|
|
55
|
+
}
|
|
56
|
+
function shikiHighlighter(code, lang) {
|
|
57
|
+
if (!process.stdout.isTTY || !shiki)
|
|
58
|
+
return code;
|
|
59
|
+
const normalizedLang = normalizeLanguage(lang);
|
|
60
|
+
if (!normalizedLang)
|
|
61
|
+
return code;
|
|
62
|
+
try {
|
|
63
|
+
if (!shiki.getLoadedLanguages().includes(normalizedLang)) {
|
|
64
|
+
return code;
|
|
65
|
+
}
|
|
66
|
+
const { tokens } = shiki.codeToTokens(code, { lang: normalizedLang, theme: DEFAULT_THEME });
|
|
67
|
+
return tokens
|
|
68
|
+
.map((line) => line
|
|
69
|
+
.map((token) => {
|
|
70
|
+
const colored = token.color ? chalk.hex(token.color)(token.content) : token.content;
|
|
71
|
+
return styleToken(colored, token.fontStyle);
|
|
72
|
+
})
|
|
73
|
+
.join(''))
|
|
74
|
+
.join('\n');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return code;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
2
80
|
export function renderMarkdownAnsi(markdown) {
|
|
3
81
|
try {
|
|
4
82
|
const color = Boolean(process.stdout.isTTY);
|
|
@@ -9,6 +87,7 @@ export function renderMarkdownAnsi(markdown) {
|
|
|
9
87
|
width,
|
|
10
88
|
wrap: true,
|
|
11
89
|
hyperlinks,
|
|
90
|
+
highlighter: color ? shikiHighlighter : undefined,
|
|
12
91
|
});
|
|
13
92
|
}
|
|
14
93
|
catch {
|