@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 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
@@ -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 && tuiEnabled()) {
70
- // Skip prompt enforcement; runRootCommand will launch the TUI.
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
- if (tuiEnabled()) {
426
- await launchTui({ version: VERSION, printIntro: false });
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.click();
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.click();
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.click();
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
- // Some menus ignore synthetic click events.
73
- const down = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
74
- const up = new PointerEvent('pointerup', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
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.click();
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
- dispatchPointer(node);
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
- (button as HTMLElement).click();
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).dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, cancelable: true }));
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 ((getSource('chatgptUrl') === 'default' || getSource('chatgptUrl') === undefined) && !cliChatgptSet && configuredChatgptUrl !== undefined) {
12
+ if (isUnset('chatgptUrl') && !cliChatgptSet && configuredChatgptUrl !== undefined) {
9
13
  options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? '', CHATGPT_URL);
10
14
  }
11
- if (getSource('browserChromeProfile') === 'default' && browser.chromeProfile !== undefined) {
15
+ if (isUnset('browserChromeProfile') && browser.chromeProfile !== undefined) {
12
16
  options.browserChromeProfile = browser.chromeProfile ?? undefined;
13
17
  }
14
- if (getSource('browserChromePath') === 'default' && browser.chromePath !== undefined) {
18
+ if (isUnset('browserChromePath') && browser.chromePath !== undefined) {
15
19
  options.browserChromePath = browser.chromePath ?? undefined;
16
20
  }
17
- if (getSource('browserCookiePath') === 'default' && browser.chromeCookiePath !== undefined) {
21
+ if (isUnset('browserCookiePath') && browser.chromeCookiePath !== undefined) {
18
22
  options.browserCookiePath = browser.chromeCookiePath ?? undefined;
19
23
  }
20
- if ((getSource('browserUrl') === 'default' || getSource('browserUrl') === undefined) && options.browserUrl === undefined && browser.url !== undefined) {
24
+ if (isUnset('browserUrl') && options.browserUrl === undefined && browser.url !== undefined) {
21
25
  options.browserUrl = browser.url;
22
26
  }
23
- if (getSource('browserTimeout') === 'default' && typeof browser.timeoutMs === 'number') {
27
+ if (isUnset('browserTimeout') && typeof browser.timeoutMs === 'number') {
24
28
  options.browserTimeout = String(browser.timeoutMs);
25
29
  }
26
- if (getSource('browserPort') === 'default' && typeof browser.debugPort === 'number') {
30
+ if (isUnset('browserPort') && typeof browser.debugPort === 'number') {
27
31
  options.browserPort = browser.debugPort;
28
32
  }
29
- if (getSource('browserInputTimeout') === 'default' && typeof browser.inputTimeoutMs === 'number') {
33
+ if (isUnset('browserInputTimeout') && typeof browser.inputTimeoutMs === 'number') {
30
34
  options.browserInputTimeout = String(browser.inputTimeoutMs);
31
35
  }
32
- if (getSource('browserHeadless') === 'default' && browser.headless !== undefined) {
36
+ if (isUnset('browserHeadless') && browser.headless !== undefined) {
33
37
  options.browserHeadless = browser.headless;
34
38
  }
35
- if (getSource('browserHideWindow') === 'default' && browser.hideWindow !== undefined) {
39
+ if (isUnset('browserHideWindow') && browser.hideWindow !== undefined) {
36
40
  options.browserHideWindow = browser.hideWindow;
37
41
  }
38
- if (getSource('browserKeepBrowser') === 'default' && browser.keepBrowser !== undefined) {
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: 'list',
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
- console.error(chalk.red('Paging failed; returning to recent list.'), error instanceof Error ? error.message : error);
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
- console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
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
- const { next } = await inquirer.prompt([
157
- {
158
- name: 'next',
159
- type: 'list',
160
- message: 'Actions',
161
- choices: actions,
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 answers = await inquirer.prompt([
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: 'list',
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: 'list',
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: 'list',
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.1",
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.30.0",
48
+ "@google/genai": "^1.31.0",
49
49
  "@google/generative-ai": "^0.24.1",
50
- "@modelcontextprotocol/sdk": "^1.22.0",
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": "9.3.8",
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.9.1",
66
- "shiki": "^3.15.0",
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": "3.24.1"
69
+ "zod": "^4.1.13"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@anthropic-ai/tokenizer": "^0.0.4",
73
- "@biomejs/biome": "^2.3.7",
73
+ "@biomejs/biome": "^2.3.8",
74
74
  "@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
75
- "@types/chrome-remote-interface": "^0.31.14",
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.13",
80
- "devtools-protocol": "^0.0.1548823",
78
+ "@vitest/coverage-v8": "4.0.15",
79
+ "devtools-protocol": "^0.0.1551306",
81
80
  "es-toolkit": "^1.42.0",
82
- "esbuild": "^0.27.0",
83
- "puppeteer-core": "^24.31.0",
84
- "tsx": "^4.20.6",
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.13"
85
+ "vitest": "^4.0.15"
87
86
  },
88
87
  "optionalDependencies": {
89
88
  "win-dpapi": "npm:@primno/dpapi@2.0.1"