@steipete/oracle 0.5.1 → 0.5.3
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 +1 -1
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +11 -9
- package/dist/src/browser/actions/assistantResponse.js +7 -3
- package/dist/src/browser/actions/attachments.js +24 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +6 -9
- package/dist/src/browser/actions/promptComposer.js +10 -15
- package/dist/src/browser/actions/remoteFileTransfer.js +2 -0
- package/dist/src/cli/browserDefaults.js +15 -11
- package/dist/src/cli/tui/index.js +50 -16
- package/package.json +15 -16
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ npx @steipete/oracle status --hours 72
|
|
|
38
38
|
npx @steipete/oracle session <id> --render
|
|
39
39
|
|
|
40
40
|
# TUI (interactive, only for humans)
|
|
41
|
-
npx @steipete/oracle
|
|
41
|
+
npx @steipete/oracle tui
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser is stable on macOS and works on Linux and Windows. On Linux pass `--browser-chrome-path/--browser-cookie-path` if detection fails; on Windows prefer `--browser-manual-login` or inline cookies if decryption is blocked.
|
package/dist/.DS_Store
CHANGED
|
Binary file
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -49,7 +49,6 @@ const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
|
|
|
49
49
|
const rawCliArgs = process.argv.slice(2);
|
|
50
50
|
const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
|
|
51
51
|
const isTty = process.stdout.isTTY;
|
|
52
|
-
const tuiEnabled = () => isTty && process.env.ORACLE_NO_TUI !== '1';
|
|
53
52
|
const program = new Command();
|
|
54
53
|
let introPrinted = false;
|
|
55
54
|
program.hook('preAction', () => {
|
|
@@ -66,8 +65,8 @@ program.hook('preAction', (thisCommand) => {
|
|
|
66
65
|
if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
|
|
67
66
|
return;
|
|
68
67
|
}
|
|
69
|
-
if (userCliArgs.length === 0
|
|
70
|
-
//
|
|
68
|
+
if (userCliArgs.length === 0) {
|
|
69
|
+
// Let the root action handle zero-arg entry (help + hint to `oracle tui`).
|
|
71
70
|
return;
|
|
72
71
|
}
|
|
73
72
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -212,6 +211,13 @@ program
|
|
|
212
211
|
token: commandOptions.token,
|
|
213
212
|
});
|
|
214
213
|
});
|
|
214
|
+
program
|
|
215
|
+
.command('tui')
|
|
216
|
+
.description('Launch the interactive terminal UI for humans (no automation).')
|
|
217
|
+
.action(async () => {
|
|
218
|
+
await sessionStore.ensureStorage();
|
|
219
|
+
await launchTui({ version: VERSION, printIntro: false });
|
|
220
|
+
});
|
|
215
221
|
const sessionCommand = program
|
|
216
222
|
.command('session [id]')
|
|
217
223
|
.description('Attach to a stored session or list recent sessions when no ID is provided.')
|
|
@@ -422,12 +428,8 @@ async function runRootCommand(options) {
|
|
|
422
428
|
console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
|
|
423
429
|
}
|
|
424
430
|
if (userCliArgs.length === 0) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
|
|
430
|
-
program.help({ error: false });
|
|
431
|
+
console.log(chalk.yellow('No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI.'));
|
|
432
|
+
program.outputHelp();
|
|
431
433
|
return;
|
|
432
434
|
}
|
|
433
435
|
const retentionHours = typeof options.retainHours === 'number' ? options.retainHours : undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from '../constants.js';
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression } from '../domDebug.js';
|
|
4
|
+
import { buildClickDispatcher } from './domEvents.js';
|
|
4
5
|
const ASSISTANT_POLL_TIMEOUT_ERROR = 'assistant-response-watchdog-timeout';
|
|
5
6
|
export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
|
|
6
7
|
logger('Waiting for ChatGPT response');
|
|
@@ -257,6 +258,7 @@ function buildAssistantSnapshotExpression() {
|
|
|
257
258
|
function buildResponseObserverExpression(timeoutMs) {
|
|
258
259
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
259
260
|
return `(() => {
|
|
261
|
+
${buildClickDispatcher()}
|
|
260
262
|
const SELECTORS = ${selectorsLiteral};
|
|
261
263
|
const STOP_SELECTOR = '${STOP_BUTTON_SELECTOR}';
|
|
262
264
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
@@ -293,7 +295,7 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
293
295
|
if (ariaLabel.toLowerCase().includes('stop')) {
|
|
294
296
|
return;
|
|
295
297
|
}
|
|
296
|
-
stop
|
|
298
|
+
dispatchClickSequence(stop);
|
|
297
299
|
}, 500);
|
|
298
300
|
setTimeout(() => {
|
|
299
301
|
if (stopInterval) {
|
|
@@ -340,6 +342,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
340
342
|
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
341
343
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
342
344
|
return `const ${functionName} = () => {
|
|
345
|
+
${buildClickDispatcher()}
|
|
343
346
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
344
347
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
345
348
|
const isAssistantTurn = (node) => {
|
|
@@ -367,7 +370,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
367
370
|
testid.includes('markdown') ||
|
|
368
371
|
testid.includes('toggle')
|
|
369
372
|
) {
|
|
370
|
-
button
|
|
373
|
+
dispatchClickSequence(button);
|
|
371
374
|
}
|
|
372
375
|
}
|
|
373
376
|
};
|
|
@@ -397,6 +400,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
397
400
|
}
|
|
398
401
|
function buildCopyExpression(meta) {
|
|
399
402
|
return `(() => {
|
|
403
|
+
${buildClickDispatcher()}
|
|
400
404
|
const BUTTON_SELECTOR = '${COPY_BUTTON_SELECTOR}';
|
|
401
405
|
const TIMEOUT_MS = 5000;
|
|
402
406
|
|
|
@@ -498,7 +502,7 @@ function buildCopyExpression(meta) {
|
|
|
498
502
|
|
|
499
503
|
button.addEventListener('copy', handleCopy, true);
|
|
500
504
|
button.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
501
|
-
button
|
|
505
|
+
dispatchClickSequence(button);
|
|
502
506
|
pollId = setInterval(() => {
|
|
503
507
|
const payload = readIntercepted();
|
|
504
508
|
if (payload.success) {
|
|
@@ -28,6 +28,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
28
28
|
await logDomFailure(runtime, logger, 'file-upload');
|
|
29
29
|
throw new Error('Attachment did not register with the ChatGPT composer in time.');
|
|
30
30
|
}
|
|
31
|
+
await waitForAttachmentVisible(runtime, expectedName, 10_000, logger);
|
|
31
32
|
logger('Attachment queued');
|
|
32
33
|
}
|
|
33
34
|
export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
@@ -83,6 +84,29 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
|
83
84
|
await logDomFailure(Runtime, logger ?? (() => { }), 'file-upload-timeout');
|
|
84
85
|
throw new Error('Attachments did not finish uploading before timeout.');
|
|
85
86
|
}
|
|
87
|
+
export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs, logger) {
|
|
88
|
+
const deadline = Date.now() + Math.min(timeoutMs, 2_000);
|
|
89
|
+
const expression = `(() => {
|
|
90
|
+
const expected = ${JSON.stringify(expectedName)};
|
|
91
|
+
const turns = Array.from(document.querySelectorAll('article[data-testid^="conversation-turn"]'));
|
|
92
|
+
const userTurns = turns.filter((node) => node.querySelector('[data-message-author-role="user"]'));
|
|
93
|
+
const lastUser = userTurns[userTurns.length - 1];
|
|
94
|
+
if (!lastUser) return { found: false, userTurns: userTurns.length };
|
|
95
|
+
const chips = Array.from(lastUser.querySelectorAll('a, div')).some((el) => (el.textContent || '').includes(expected));
|
|
96
|
+
return { found: chips, userTurns: userTurns.length };
|
|
97
|
+
})()`;
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
100
|
+
const value = result?.value;
|
|
101
|
+
if (value?.found) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await delay(200);
|
|
105
|
+
}
|
|
106
|
+
logger?.('Attachment not visible in composer; giving up.');
|
|
107
|
+
await logDomFailure(Runtime, logger ?? (() => { }), 'attachment-visible');
|
|
108
|
+
throw new Error('Attachment did not appear in ChatGPT composer.');
|
|
109
|
+
}
|
|
86
110
|
async function waitForAttachmentSelection(Runtime, expectedName, timeoutMs) {
|
|
87
111
|
const deadline = Date.now() + timeoutMs;
|
|
88
112
|
const expression = `(() => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const CLICK_TYPES = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
|
|
2
|
+
export function buildClickDispatcher(functionName = 'dispatchClickSequence') {
|
|
3
|
+
const typesLiteral = JSON.stringify(CLICK_TYPES);
|
|
4
|
+
return `function ${functionName}(target){
|
|
5
|
+
if(!target || !(target instanceof EventTarget)) return false;
|
|
6
|
+
const types = ${typesLiteral};
|
|
7
|
+
for (const type of types) {
|
|
8
|
+
const common = { bubbles: true, cancelable: true, view: window };
|
|
9
|
+
let event;
|
|
10
|
+
if (type.startsWith('pointer') && 'PointerEvent' in window) {
|
|
11
|
+
event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' });
|
|
12
|
+
} else {
|
|
13
|
+
event = new MouseEvent(type, common);
|
|
14
|
+
}
|
|
15
|
+
target.dispatchEvent(event);
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}`;
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from '../constants.js';
|
|
2
2
|
import { logDomFailure } from '../domDebug.js';
|
|
3
|
+
import { buildClickDispatcher } from './domEvents.js';
|
|
3
4
|
export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
4
5
|
const outcome = await Runtime.evaluate({
|
|
5
6
|
expression: buildModelSelectionExpression(desiredModel),
|
|
@@ -37,6 +38,7 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
37
38
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
38
39
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
39
40
|
return `(() => {
|
|
41
|
+
${buildClickDispatcher()}
|
|
40
42
|
// Capture the selectors and matcher literals up front so the browser expression stays pure.
|
|
41
43
|
const BUTTON_SELECTOR = '${MODEL_BUTTON_SELECTOR}';
|
|
42
44
|
const LABEL_TOKENS = ${labelLiteral};
|
|
@@ -69,14 +71,9 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
69
71
|
|
|
70
72
|
let lastPointerClick = 0;
|
|
71
73
|
const pointerClick = () => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const click = new MouseEvent('click', { bubbles: true });
|
|
76
|
-
button.dispatchEvent(down);
|
|
77
|
-
button.dispatchEvent(up);
|
|
78
|
-
button.dispatchEvent(click);
|
|
79
|
-
lastPointerClick = performance.now();
|
|
74
|
+
if (dispatchClickSequence(button)) {
|
|
75
|
+
lastPointerClick = performance.now();
|
|
76
|
+
}
|
|
80
77
|
};
|
|
81
78
|
|
|
82
79
|
const getOptionLabel = (node) => node?.textContent?.trim() ?? '';
|
|
@@ -188,7 +185,7 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
188
185
|
resolve({ status: 'already-selected', label: match.label });
|
|
189
186
|
return;
|
|
190
187
|
}
|
|
191
|
-
match.node
|
|
188
|
+
dispatchClickSequence(match.node);
|
|
192
189
|
resolve({ status: 'switched', label: match.label });
|
|
193
190
|
return;
|
|
194
191
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, CONVERSATION_TURN_SELECTOR, } from '../constants.js';
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure } from '../domDebug.js';
|
|
4
|
+
import { buildClickDispatcher } from './domEvents.js';
|
|
4
5
|
const ENTER_KEY_EVENT = {
|
|
5
6
|
key: 'Enter',
|
|
6
7
|
code: 'Enter',
|
|
@@ -13,20 +14,13 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
13
14
|
const encodedPrompt = JSON.stringify(prompt);
|
|
14
15
|
const focusResult = await runtime.evaluate({
|
|
15
16
|
expression: `(() => {
|
|
17
|
+
${buildClickDispatcher()}
|
|
16
18
|
const SELECTORS = ${JSON.stringify(INPUT_SELECTORS)};
|
|
17
|
-
const dispatchPointer = (target) => {
|
|
18
|
-
if (!(target instanceof HTMLElement)) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
22
|
-
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
19
|
const focusNode = (node) => {
|
|
26
20
|
if (!node) {
|
|
27
21
|
return false;
|
|
28
22
|
}
|
|
29
|
-
|
|
23
|
+
dispatchClickSequence(node);
|
|
30
24
|
if (typeof node.focus === 'function') {
|
|
31
25
|
node.focus();
|
|
32
26
|
}
|
|
@@ -89,6 +83,8 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
89
83
|
const editor = document.querySelector(${primarySelectorLiteral});
|
|
90
84
|
if (editor) {
|
|
91
85
|
editor.textContent = ${encodedPrompt};
|
|
86
|
+
// Nudge ProseMirror to register the textContent write so its state/send-button updates
|
|
87
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
92
88
|
}
|
|
93
89
|
})()`,
|
|
94
90
|
});
|
|
@@ -115,6 +111,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
115
111
|
}
|
|
116
112
|
async function attemptSendButton(Runtime) {
|
|
117
113
|
const script = `(() => {
|
|
114
|
+
${buildClickDispatcher()}
|
|
118
115
|
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
119
116
|
let button = null;
|
|
120
117
|
for (const selector of selectors) {
|
|
@@ -132,7 +129,8 @@ async function attemptSendButton(Runtime) {
|
|
|
132
129
|
style.pointerEvents === 'none' ||
|
|
133
130
|
style.display === 'none';
|
|
134
131
|
if (disabled) return 'disabled';
|
|
135
|
-
|
|
132
|
+
// Use unified pointer/mouse sequence to satisfy React handlers.
|
|
133
|
+
dispatchClickSequence(button);
|
|
136
134
|
return 'clicked';
|
|
137
135
|
})()`;
|
|
138
136
|
const deadline = Date.now() + 2_000;
|
|
@@ -150,6 +148,7 @@ async function attemptSendButton(Runtime) {
|
|
|
150
148
|
}
|
|
151
149
|
async function clickAnswerNowIfPresent(Runtime, logger) {
|
|
152
150
|
const script = `(() => {
|
|
151
|
+
${buildClickDispatcher()}
|
|
153
152
|
const matchesText = (el) => (el?.textContent || '').trim().toLowerCase() === 'answer now';
|
|
154
153
|
const candidate = Array.from(document.querySelectorAll('button,span')).find(matchesText);
|
|
155
154
|
if (!candidate) return 'missing';
|
|
@@ -161,11 +160,7 @@ async function clickAnswerNowIfPresent(Runtime, logger) {
|
|
|
161
160
|
style.pointerEvents === 'none' ||
|
|
162
161
|
style.display === 'none';
|
|
163
162
|
if (disabled) return 'disabled';
|
|
164
|
-
(button)
|
|
165
|
-
(button).dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
166
|
-
(button).dispatchEvent(new MouseEvent('pointerup', { bubbles: true, cancelable: true }));
|
|
167
|
-
(button).dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
168
|
-
(button).dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
163
|
+
dispatchClickSequence(button);
|
|
169
164
|
return 'clicked';
|
|
170
165
|
})()`;
|
|
171
166
|
const deadline = Date.now() + 3_000;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { FILE_INPUT_SELECTORS } from '../constants.js';
|
|
4
|
+
import { waitForAttachmentVisible } from './attachments.js';
|
|
4
5
|
import { delay } from '../utils.js';
|
|
5
6
|
import { logDomFailure } from '../domDebug.js';
|
|
6
7
|
/**
|
|
@@ -132,6 +133,7 @@ export async function uploadAttachmentViaDataTransfer(deps, attachment, logger)
|
|
|
132
133
|
logger(`File transferred: ${uploadResult.fileName} (${uploadResult.size} bytes)`);
|
|
133
134
|
// Give ChatGPT a moment to process the file
|
|
134
135
|
await delay(500);
|
|
136
|
+
await waitForAttachmentVisible(runtime, fileName, 10_000, logger);
|
|
135
137
|
logger('Attachment queued');
|
|
136
138
|
}
|
|
137
139
|
function guessMimeType(fileName) {
|
|
@@ -3,39 +3,43 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
3
3
|
const browser = config.browser;
|
|
4
4
|
if (!browser)
|
|
5
5
|
return;
|
|
6
|
+
const isUnset = (key) => {
|
|
7
|
+
const source = getSource(key);
|
|
8
|
+
return source === undefined || source === 'default';
|
|
9
|
+
};
|
|
6
10
|
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
7
11
|
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
8
|
-
if ((
|
|
12
|
+
if (isUnset('chatgptUrl') && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
9
13
|
options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? '', CHATGPT_URL);
|
|
10
14
|
}
|
|
11
|
-
if (
|
|
15
|
+
if (isUnset('browserChromeProfile') && browser.chromeProfile !== undefined) {
|
|
12
16
|
options.browserChromeProfile = browser.chromeProfile ?? undefined;
|
|
13
17
|
}
|
|
14
|
-
if (
|
|
18
|
+
if (isUnset('browserChromePath') && browser.chromePath !== undefined) {
|
|
15
19
|
options.browserChromePath = browser.chromePath ?? undefined;
|
|
16
20
|
}
|
|
17
|
-
if (
|
|
21
|
+
if (isUnset('browserCookiePath') && browser.chromeCookiePath !== undefined) {
|
|
18
22
|
options.browserCookiePath = browser.chromeCookiePath ?? undefined;
|
|
19
23
|
}
|
|
20
|
-
if ((
|
|
24
|
+
if (isUnset('browserUrl') && options.browserUrl === undefined && browser.url !== undefined) {
|
|
21
25
|
options.browserUrl = browser.url;
|
|
22
26
|
}
|
|
23
|
-
if (
|
|
27
|
+
if (isUnset('browserTimeout') && typeof browser.timeoutMs === 'number') {
|
|
24
28
|
options.browserTimeout = String(browser.timeoutMs);
|
|
25
29
|
}
|
|
26
|
-
if (
|
|
30
|
+
if (isUnset('browserPort') && typeof browser.debugPort === 'number') {
|
|
27
31
|
options.browserPort = browser.debugPort;
|
|
28
32
|
}
|
|
29
|
-
if (
|
|
33
|
+
if (isUnset('browserInputTimeout') && typeof browser.inputTimeoutMs === 'number') {
|
|
30
34
|
options.browserInputTimeout = String(browser.inputTimeoutMs);
|
|
31
35
|
}
|
|
32
|
-
if (
|
|
36
|
+
if (isUnset('browserHeadless') && browser.headless !== undefined) {
|
|
33
37
|
options.browserHeadless = browser.headless;
|
|
34
38
|
}
|
|
35
|
-
if (
|
|
39
|
+
if (isUnset('browserHideWindow') && browser.hideWindow !== undefined) {
|
|
36
40
|
options.browserHideWindow = browser.hideWindow;
|
|
37
41
|
}
|
|
38
|
-
if (
|
|
42
|
+
if (isUnset('browserKeepBrowser') && browser.keepBrowser !== undefined) {
|
|
39
43
|
options.browserKeepBrowser = browser.keepBrowser;
|
|
40
44
|
}
|
|
41
45
|
}
|
|
@@ -21,6 +21,8 @@ const PAGE_SIZE = 10;
|
|
|
21
21
|
export async function launchTui({ version, printIntro = true }) {
|
|
22
22
|
const userConfig = (await loadUserConfig()).config;
|
|
23
23
|
const rich = isTty();
|
|
24
|
+
let pagingFailures = 0;
|
|
25
|
+
let exitMessageShown = false;
|
|
24
26
|
if (printIntro) {
|
|
25
27
|
if (rich) {
|
|
26
28
|
console.log(chalk.bold('🧿 oracle'), `${version}`, dim('— Whispering your tokens to the silicon sage'));
|
|
@@ -66,7 +68,7 @@ export async function launchTui({ version, printIntro = true }) {
|
|
|
66
68
|
const prompt = inquirer.prompt([
|
|
67
69
|
{
|
|
68
70
|
name: 'selection',
|
|
69
|
-
type: '
|
|
71
|
+
type: 'select',
|
|
70
72
|
message: 'Select a session or action',
|
|
71
73
|
choices,
|
|
72
74
|
pageSize: 16,
|
|
@@ -76,12 +78,31 @@ export async function launchTui({ version, printIntro = true }) {
|
|
|
76
78
|
prompt
|
|
77
79
|
.then(({ selection: answer }) => resolve(answer))
|
|
78
80
|
.catch((error) => {
|
|
79
|
-
|
|
81
|
+
pagingFailures += 1;
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
if (message.includes('SIGINT') || message.includes('force closed the prompt')) {
|
|
84
|
+
console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
|
|
85
|
+
exitMessageShown = true;
|
|
86
|
+
resolve('__exit__');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.error(chalk.red('Paging failed; returning to recent list.'), message);
|
|
90
|
+
if (message.includes('setRawMode') || message.includes('EIO') || pagingFailures >= 3) {
|
|
91
|
+
console.error(chalk.red('Terminal input unavailable; exiting TUI.'), dim('Try `stty sane` then rerun oracle, or use `oracle recent`.'));
|
|
92
|
+
resolve('__exit__');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
80
95
|
resolve('__reset__');
|
|
81
96
|
});
|
|
82
97
|
});
|
|
98
|
+
if (process.env.ORACLE_DEBUG_TUI === '1') {
|
|
99
|
+
console.error(`[tui] selection=${JSON.stringify(selection)}`);
|
|
100
|
+
}
|
|
101
|
+
pagingFailures = 0;
|
|
83
102
|
if (selection === '__exit__') {
|
|
84
|
-
|
|
103
|
+
if (!exitMessageShown) {
|
|
104
|
+
console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
|
|
105
|
+
}
|
|
85
106
|
return;
|
|
86
107
|
}
|
|
87
108
|
if (selection === '__ask__') {
|
|
@@ -153,14 +174,26 @@ async function showSessionDetail(sessionId) {
|
|
|
153
174
|
...(isRunning ? [{ name: 'Refresh', value: 'refresh' }] : []),
|
|
154
175
|
{ name: 'Back', value: 'back' },
|
|
155
176
|
];
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
let next;
|
|
178
|
+
try {
|
|
179
|
+
({ next } = await inquirer.prompt([
|
|
180
|
+
{
|
|
181
|
+
name: 'next',
|
|
182
|
+
type: 'select',
|
|
183
|
+
message: 'Actions',
|
|
184
|
+
choices: actions,
|
|
185
|
+
},
|
|
186
|
+
]));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
if (message.includes('SIGINT') || message.includes('force closed the prompt')) {
|
|
191
|
+
console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.error(chalk.red('Paging failed; returning to session list.'), message);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
164
197
|
if (next === 'back') {
|
|
165
198
|
return;
|
|
166
199
|
}
|
|
@@ -235,7 +268,7 @@ async function askOracleFlow(version, userConfig) {
|
|
|
235
268
|
const hasApiKey = Boolean(process.env.OPENAI_API_KEY);
|
|
236
269
|
const initialMode = hasApiKey ? 'api' : 'browser';
|
|
237
270
|
const preferredMode = userConfig.engine ?? initialMode;
|
|
238
|
-
const
|
|
271
|
+
const wizardQuestions = [
|
|
239
272
|
{
|
|
240
273
|
name: 'promptInput',
|
|
241
274
|
type: 'input',
|
|
@@ -245,7 +278,7 @@ async function askOracleFlow(version, userConfig) {
|
|
|
245
278
|
? [
|
|
246
279
|
{
|
|
247
280
|
name: 'mode',
|
|
248
|
-
type: '
|
|
281
|
+
type: 'select',
|
|
249
282
|
message: 'Engine',
|
|
250
283
|
default: preferredMode,
|
|
251
284
|
choices: [
|
|
@@ -257,7 +290,7 @@ async function askOracleFlow(version, userConfig) {
|
|
|
257
290
|
: [
|
|
258
291
|
{
|
|
259
292
|
name: 'mode',
|
|
260
|
-
type: '
|
|
293
|
+
type: 'select',
|
|
261
294
|
message: 'Engine',
|
|
262
295
|
default: preferredMode,
|
|
263
296
|
choices: [{ name: 'Browser', value: 'browser' }],
|
|
@@ -270,7 +303,7 @@ async function askOracleFlow(version, userConfig) {
|
|
|
270
303
|
},
|
|
271
304
|
{
|
|
272
305
|
name: 'model',
|
|
273
|
-
type: '
|
|
306
|
+
type: 'select',
|
|
274
307
|
message: 'Model',
|
|
275
308
|
default: DEFAULT_MODEL,
|
|
276
309
|
choices: modelChoices,
|
|
@@ -323,7 +356,8 @@ async function askOracleFlow(version, userConfig) {
|
|
|
323
356
|
default: false,
|
|
324
357
|
when: (ans) => ans.mode === 'browser',
|
|
325
358
|
},
|
|
326
|
-
]
|
|
359
|
+
];
|
|
360
|
+
const answers = await inquirer.prompt(wizardQuestions);
|
|
327
361
|
const mode = (answers.mode ?? initialMode);
|
|
328
362
|
const prompt = await resolvePromptInput(answers.promptInput);
|
|
329
363
|
if (!prompt.trim()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.1 Pro, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"homepage": "https://github.com/steipete/oracle#readme",
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
48
|
-
"@google/genai": "^1.
|
|
48
|
+
"@google/genai": "^1.31.0",
|
|
49
49
|
"@google/generative-ai": "^0.24.1",
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"chrome-cookies-secure": "3.0.0",
|
|
53
53
|
"chrome-launcher": "^1.2.1",
|
|
@@ -57,33 +57,32 @@
|
|
|
57
57
|
"dotenv": "^17.2.3",
|
|
58
58
|
"fast-glob": "^3.3.3",
|
|
59
59
|
"gpt-tokenizer": "^3.4.0",
|
|
60
|
-
"inquirer": "
|
|
60
|
+
"inquirer": "13.0.2",
|
|
61
61
|
"json5": "^2.2.3",
|
|
62
62
|
"keytar": "^7.9.0",
|
|
63
63
|
"kleur": "^4.1.5",
|
|
64
64
|
"markdansi": "^0.1.3",
|
|
65
|
-
"openai": "^6.
|
|
66
|
-
"shiki": "^3.
|
|
65
|
+
"openai": "^6.10.0",
|
|
66
|
+
"shiki": "^3.19.0",
|
|
67
67
|
"sqlite3": "^5.1.7",
|
|
68
68
|
"toasted-notifier": "^10.1.0",
|
|
69
|
-
"zod": "
|
|
69
|
+
"zod": "^4.1.13"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
73
|
-
"@biomejs/biome": "^2.3.
|
|
73
|
+
"@biomejs/biome": "^2.3.8",
|
|
74
74
|
"@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
|
|
75
|
-
"@types/chrome-remote-interface": "^0.
|
|
75
|
+
"@types/chrome-remote-interface": "^0.33.0",
|
|
76
76
|
"@types/inquirer": "^9.0.9",
|
|
77
|
-
"@types/json5": "^2.2.0",
|
|
78
77
|
"@types/node": "^24.10.1",
|
|
79
|
-
"@vitest/coverage-v8": "4.0.
|
|
80
|
-
"devtools-protocol": "^0.0.
|
|
78
|
+
"@vitest/coverage-v8": "4.0.15",
|
|
79
|
+
"devtools-protocol": "^0.0.1551306",
|
|
81
80
|
"es-toolkit": "^1.42.0",
|
|
82
|
-
"esbuild": "^0.27.
|
|
83
|
-
"puppeteer-core": "^24.
|
|
84
|
-
"tsx": "^4.
|
|
81
|
+
"esbuild": "^0.27.1",
|
|
82
|
+
"puppeteer-core": "^24.32.0",
|
|
83
|
+
"tsx": "^4.21.0",
|
|
85
84
|
"typescript": "^5.9.3",
|
|
86
|
-
"vitest": "^4.0.
|
|
85
|
+
"vitest": "^4.0.15"
|
|
87
86
|
},
|
|
88
87
|
"optionalDependencies": {
|
|
89
88
|
"win-dpapi": "npm:@primno/dpapi@2.0.1"
|