@steipete/oracle 0.5.1 → 0.5.2

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/dist/.DS_Store CHANGED
Binary file
@@ -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
  }
@@ -66,7 +66,7 @@ export async function launchTui({ version, printIntro = true }) {
66
66
  const prompt = inquirer.prompt([
67
67
  {
68
68
  name: 'selection',
69
- type: 'list',
69
+ type: 'select',
70
70
  message: 'Select a session or action',
71
71
  choices,
72
72
  pageSize: 16,
@@ -80,6 +80,9 @@ export async function launchTui({ version, printIntro = true }) {
80
80
  resolve('__reset__');
81
81
  });
82
82
  });
83
+ if (process.env.ORACLE_DEBUG_TUI === '1') {
84
+ console.error(`[tui] selection=${JSON.stringify(selection)}`);
85
+ }
83
86
  if (selection === '__exit__') {
84
87
  console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
85
88
  return;
@@ -156,7 +159,7 @@ async function showSessionDetail(sessionId) {
156
159
  const { next } = await inquirer.prompt([
157
160
  {
158
161
  name: 'next',
159
- type: 'list',
162
+ type: 'select',
160
163
  message: 'Actions',
161
164
  choices: actions,
162
165
  },
@@ -235,7 +238,7 @@ async function askOracleFlow(version, userConfig) {
235
238
  const hasApiKey = Boolean(process.env.OPENAI_API_KEY);
236
239
  const initialMode = hasApiKey ? 'api' : 'browser';
237
240
  const preferredMode = userConfig.engine ?? initialMode;
238
- const answers = await inquirer.prompt([
241
+ const wizardQuestions = [
239
242
  {
240
243
  name: 'promptInput',
241
244
  type: 'input',
@@ -245,7 +248,7 @@ async function askOracleFlow(version, userConfig) {
245
248
  ? [
246
249
  {
247
250
  name: 'mode',
248
- type: 'list',
251
+ type: 'select',
249
252
  message: 'Engine',
250
253
  default: preferredMode,
251
254
  choices: [
@@ -257,7 +260,7 @@ async function askOracleFlow(version, userConfig) {
257
260
  : [
258
261
  {
259
262
  name: 'mode',
260
- type: 'list',
263
+ type: 'select',
261
264
  message: 'Engine',
262
265
  default: preferredMode,
263
266
  choices: [{ name: 'Browser', value: 'browser' }],
@@ -270,7 +273,7 @@ async function askOracleFlow(version, userConfig) {
270
273
  },
271
274
  {
272
275
  name: 'model',
273
- type: 'list',
276
+ type: 'select',
274
277
  message: 'Model',
275
278
  default: DEFAULT_MODEL,
276
279
  choices: modelChoices,
@@ -323,7 +326,8 @@ async function askOracleFlow(version, userConfig) {
323
326
  default: false,
324
327
  when: (ans) => ans.mode === 'browser',
325
328
  },
326
- ]);
329
+ ];
330
+ const answers = await inquirer.prompt(wizardQuestions);
327
331
  const mode = (answers.mode ?? initialMode);
328
332
  const prompt = await resolvePromptInput(answers.promptInput);
329
333
  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.2",
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"