@steipete/oracle 0.9.0 → 0.11.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.
Files changed (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,35 +1,37 @@
1
- import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
2
- import { logDomFailure } from '../domDebug.js';
3
- import { buildClickDispatcher } from './domEvents.js';
1
+ import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
2
+ import { logDomFailure } from "../domDebug.js";
3
+ import { buildClickDispatcher } from "./domEvents.js";
4
4
  /**
5
- * Selects a specific thinking time level in ChatGPT's composer pill menu.
5
+ * Selects a specific thinking time level in ChatGPT's composer.
6
+ *
7
+ * Best-effort: if the chip / menu / option is missing (e.g. ChatGPT moved the
8
+ * effort selector into the per-model trailing button and we can't navigate it,
9
+ * or the language pack uses tokens we don't yet match), we log a debug dump
10
+ * and continue with whatever effort the UI defaults to.
11
+ *
6
12
  * @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
7
13
  */
8
14
  export async function ensureThinkingTime(Runtime, level, logger) {
9
15
  const result = await evaluateThinkingTimeSelection(Runtime, level);
10
16
  const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
11
17
  switch (result?.status) {
12
- case 'already-selected':
18
+ case "already-selected":
13
19
  logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
14
20
  return;
15
- case 'switched':
21
+ case "switched":
16
22
  logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
17
23
  return;
18
- case 'chip-not-found': {
19
- await logDomFailure(Runtime, logger, 'thinking-chip');
20
- throw new Error('Unable to find the Thinking chip button in the composer area.');
21
- }
22
- case 'menu-not-found': {
23
- await logDomFailure(Runtime, logger, 'thinking-time-menu');
24
- throw new Error('Unable to find the Thinking time dropdown menu.');
25
- }
26
- case 'option-not-found': {
27
- await logDomFailure(Runtime, logger, `${level}-option`);
28
- throw new Error(`Unable to find the ${capitalizedLevel} option in the Thinking time menu.`);
24
+ case "chip-not-found":
25
+ case "menu-not-found":
26
+ case "option-not-found": {
27
+ await logDomFailure(Runtime, logger, `thinking-${result.status}`);
28
+ logger(`Thinking time: ${result.status.replaceAll("-", " ")} (requested ${capitalizedLevel}); continuing with ChatGPT default.`);
29
+ return;
29
30
  }
30
31
  default: {
31
- await logDomFailure(Runtime, logger, 'thinking-time-unknown');
32
- throw new Error(`Unknown error selecting ${capitalizedLevel} thinking time.`);
32
+ await logDomFailure(Runtime, logger, "thinking-time-unknown");
33
+ logger(`Thinking time: unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`);
34
+ return;
33
35
  }
34
36
  }
35
37
  }
@@ -43,22 +45,22 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
43
45
  const result = await evaluateThinkingTimeSelection(Runtime, level);
44
46
  const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
45
47
  switch (result?.status) {
46
- case 'already-selected':
48
+ case "already-selected":
47
49
  logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
48
50
  return true;
49
- case 'switched':
51
+ case "switched":
50
52
  logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
51
53
  return true;
52
- case 'chip-not-found':
53
- case 'menu-not-found':
54
- case 'option-not-found':
54
+ case "chip-not-found":
55
+ case "menu-not-found":
56
+ case "option-not-found":
55
57
  if (logger.verbose) {
56
- logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
58
+ logger(`Thinking time: ${result.status.replaceAll("-", " ")}; continuing with default.`);
57
59
  }
58
60
  return false;
59
61
  default:
60
62
  if (logger.verbose) {
61
- logger('Thinking time: unknown outcome; continuing with default.');
63
+ logger("Thinking time: unknown outcome; continuing with default.");
62
64
  }
63
65
  return false;
64
66
  }
@@ -67,7 +69,7 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
67
69
  const message = error instanceof Error ? error.message : String(error);
68
70
  if (logger.verbose) {
69
71
  logger(`Thinking time selection failed (${message}); continuing with default.`);
70
- await logDomFailure(Runtime, logger, 'thinking-time');
72
+ await logDomFailure(Runtime, logger, "thinking-time");
71
73
  }
72
74
  return false;
73
75
  }
@@ -83,124 +85,219 @@ async function evaluateThinkingTimeSelection(Runtime, level) {
83
85
  function buildThinkingTimeExpression(level) {
84
86
  const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
85
87
  const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
88
+ const modelButtonLiteral = JSON.stringify(MODEL_BUTTON_SELECTOR);
86
89
  const targetLevelLiteral = JSON.stringify(level.toLowerCase());
87
90
  return `(async () => {
88
91
  ${buildClickDispatcher()}
89
92
 
90
93
  const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
91
94
  const MENU_ITEM_SELECTOR = ${menuItemLiteral};
95
+ const MODEL_BUTTON_SELECTOR = ${modelButtonLiteral};
92
96
  const TARGET_LEVEL = ${targetLevelLiteral};
93
97
 
94
- const CHIP_SELECTORS = [
95
- '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
96
- 'button.__composer-pill[aria-haspopup="menu"]',
97
- '.__composer-pill-composite button[aria-haspopup="menu"]',
98
- ];
98
+ // Bilingual matchers: English level token + observed Chinese variants.
99
+ const LEVEL_TOKENS = {
100
+ light: ['light', '轻'],
101
+ standard: ['standard', '标准'],
102
+ extended: ['extended', '扩展', '深度', '加强'],
103
+ heavy: ['heavy', '重度', '加重', '高'],
104
+ };
105
+ const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
99
106
 
100
107
  const INITIAL_WAIT_MS = 150;
101
- const MAX_WAIT_MS = 10000;
108
+ const STEP_WAIT_MS = 200;
109
+ const MAX_WAIT_MS = 8000;
102
110
 
111
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
112
+ // Keep CJK characters so we can match Chinese labels against LEVEL_TOKENS.
103
113
  const normalize = (value) => (value || '')
104
114
  .toLowerCase()
105
- .replace(/[^a-z0-9]+/g, ' ')
115
+ .replace(/[^a-z0-9\\u4e00-\\u9fa5]+/g, ' ')
106
116
  .replace(/\\s+/g, ' ')
107
117
  .trim();
118
+ const matchesLevel = (text) => {
119
+ const t = normalize(text);
120
+ return targetTokens.some((tok) => t.includes(String(tok).toLowerCase()));
121
+ };
122
+ const optionIsSelected = (node) => {
123
+ if (!(node instanceof HTMLElement)) return false;
124
+ const ariaChecked = node.getAttribute('aria-checked');
125
+ const dataState = (node.getAttribute('data-state') || '').toLowerCase();
126
+ if (ariaChecked === 'true') return true;
127
+ return dataState === 'checked' || dataState === 'selected' || dataState === 'on';
128
+ };
129
+ const closeOpenMenus = () => {
130
+ try {
131
+ document.dispatchEvent(
132
+ new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true }),
133
+ );
134
+ } catch {}
135
+ };
108
136
 
109
- const findThinkingChip = () => {
110
- for (const selector of CHIP_SELECTORS) {
111
- const buttons = document.querySelectorAll(selector);
112
- for (const btn of buttons) {
113
- // Skip toggle buttons (no haspopup) - only click dropdown triggers to avoid disabling Pro mode
137
+ // ---------- OLD UI: standalone composer chip labelled "Thinking" ----------
138
+ const OLD_CHIP_SELECTORS = [
139
+ '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
140
+ '.__composer-pill-composite button[aria-haspopup="menu"]',
141
+ ];
142
+ const findOldChip = () => {
143
+ for (const selector of OLD_CHIP_SELECTORS) {
144
+ for (const btn of document.querySelectorAll(selector)) {
114
145
  if (btn.getAttribute?.('aria-haspopup') !== 'menu') continue;
146
+ // The new model picker pill also reuses .__composer-pill — skip it.
147
+ if (btn.matches?.(MODEL_BUTTON_SELECTOR)) continue;
115
148
  const aria = normalize(btn.getAttribute?.('aria-label') ?? '');
116
149
  const text = normalize(btn.textContent ?? '');
117
- if (aria.includes('thinking') || text.includes('thinking')) {
118
- return btn;
119
- }
120
-
121
- // In some cases the pill is labeled "Pro".
122
- if (aria.includes('pro') || text.includes('pro')) {
123
- return btn;
124
- }
150
+ if (aria.includes('thinking') || text.includes('thinking')) return btn;
151
+ }
152
+ }
153
+ return null;
154
+ };
155
+ const findOldEffortMenu = () => {
156
+ const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="group"]');
157
+ for (const menu of menus) {
158
+ const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
159
+ if (normalize(label?.textContent ?? '').includes('thinking time')) return menu;
160
+ const text = normalize(menu.textContent ?? '');
161
+ if (text.includes('standard') && text.includes('extended')) return menu;
162
+ }
163
+ return null;
164
+ };
165
+ const findOptionInMenu = (menu) => {
166
+ for (const item of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
167
+ if (
168
+ matchesLevel(item.textContent ?? '') ||
169
+ matchesLevel(item.getAttribute?.('aria-label') ?? '')
170
+ ) {
171
+ return item;
125
172
  }
126
173
  }
127
174
  return null;
128
175
  };
129
176
 
130
- const chip = findThinkingChip();
131
- if (!chip) {
177
+ const oldChip = findOldChip();
178
+ if (oldChip) {
179
+ dispatchClickSequence(oldChip);
180
+ const start = performance.now();
181
+ while (performance.now() - start < MAX_WAIT_MS) {
182
+ await sleep(100);
183
+ const menu = findOldEffortMenu();
184
+ if (!menu) continue;
185
+ const opt = findOptionInMenu(menu);
186
+ if (!opt) {
187
+ closeOpenMenus();
188
+ return { status: 'option-not-found' };
189
+ }
190
+ const already = optionIsSelected(opt);
191
+ const label = opt.textContent?.trim?.() || null;
192
+ dispatchClickSequence(opt);
193
+ await sleep(STEP_WAIT_MS);
194
+ closeOpenMenus();
195
+ return { status: already ? 'already-selected' : 'switched', label };
196
+ }
197
+ closeOpenMenus();
198
+ return { status: 'menu-not-found' };
199
+ }
200
+
201
+ // ---------- NEW UI: thinking effort lives inside the model picker ----------
202
+ // Each eligible model row carries a trailing button:
203
+ // [data-model-picker-thinking-effort-action="true"] (role="menuitem", aria-haspopup="menu")
204
+ // Clicking it expands a submenu of effort options. We use aria-controls to
205
+ // resolve the submenu deterministically rather than scoring menu contents.
206
+ const TRAILING_SELECTOR = '[data-model-picker-thinking-effort-action="true"]';
207
+
208
+ const findModelButton = () => document.querySelector(MODEL_BUTTON_SELECTOR);
209
+ const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
210
+ const pickTrailingForCurrentModel = () => {
211
+ const trailings = findTrailingButtons();
212
+ if (trailings.length === 0) return null;
213
+ if (trailings.length === 1) return trailings[0];
214
+ // Prefer the trailing button whose model row is currently selected.
215
+ for (const t of trailings) {
216
+ const row = t.closest('[role="menuitem"], [role="menuitemradio"], [data-radix-collection-item]');
217
+ if (row && (optionIsSelected(row) || row.querySelector('[aria-checked="true"]'))) return t;
218
+ }
219
+ // Fallback: first one with non-zero box.
220
+ for (const t of trailings) {
221
+ const r = t.getBoundingClientRect?.();
222
+ if (r && r.width > 0 && r.height > 0) return t;
223
+ }
224
+ return trailings[0];
225
+ };
226
+
227
+ const modelBtn = findModelButton();
228
+ if (!modelBtn) {
132
229
  return { status: 'chip-not-found' };
133
230
  }
134
231
 
135
- dispatchClickSequence(chip);
232
+ // Open model menu (idempotent — leaves it open if already open).
233
+ if (modelBtn.getAttribute('aria-expanded') !== 'true') {
234
+ dispatchClickSequence(modelBtn);
235
+ await sleep(INITIAL_WAIT_MS);
236
+ }
136
237
 
137
- return new Promise((resolve) => {
138
- const start = performance.now();
238
+ let trailing = null;
239
+ const trailingDeadline = performance.now() + MAX_WAIT_MS;
240
+ while (performance.now() < trailingDeadline) {
241
+ trailing = pickTrailingForCurrentModel();
242
+ if (trailing) break;
243
+ await sleep(100);
244
+ }
245
+ if (!trailing) {
246
+ closeOpenMenus();
247
+ return { status: 'chip-not-found' };
248
+ }
139
249
 
140
- const findMenu = () => {
141
- const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="group"]');
142
- for (const menu of menus) {
143
- const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
144
- if (normalize(label?.textContent ?? '').includes('thinking time')) {
145
- return menu;
146
- }
147
- const text = normalize(menu.textContent ?? '');
148
- if (text.includes('standard') && text.includes('extended')) {
149
- return menu;
150
- }
151
- }
152
- return null;
153
- };
154
-
155
- const findTargetOption = (menu) => {
156
- const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
157
- for (const item of items) {
158
- const text = normalize(item.textContent ?? '');
159
- if (text.includes(TARGET_LEVEL)) {
160
- return item;
161
- }
162
- }
163
- return null;
164
- };
165
-
166
- const optionIsSelected = (node) => {
167
- if (!(node instanceof HTMLElement)) return false;
168
- const ariaChecked = node.getAttribute('aria-checked');
169
- const dataState = (node.getAttribute('data-state') || '').toLowerCase();
170
- if (ariaChecked === 'true') return true;
171
- if (dataState === 'checked' || dataState === 'selected' || dataState === 'on') return true;
172
- return false;
173
- };
250
+ dispatchClickSequence(trailing);
251
+ await sleep(STEP_WAIT_MS);
174
252
 
175
- const attempt = () => {
176
- const menu = findMenu();
177
- if (!menu) {
178
- if (performance.now() - start > MAX_WAIT_MS) {
179
- resolve({ status: 'menu-not-found' });
180
- return;
181
- }
182
- setTimeout(attempt, 100);
183
- return;
253
+ // Resolve the effort submenu via aria-controls when ChatGPT exposes it,
254
+ // otherwise fall back to scanning newly opened menus for our level tokens.
255
+ const resolveEffortMenu = () => {
256
+ const id = trailing.getAttribute('aria-controls');
257
+ if (id) {
258
+ const node = document.getElementById(id);
259
+ if (node) return node;
260
+ }
261
+ const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="group"]');
262
+ let best = null;
263
+ for (const menu of menus) {
264
+ if (menu === modelBtn || menu.contains(trailing)) continue;
265
+ const text = normalize(menu.textContent ?? '');
266
+ let hits = 0;
267
+ for (const tokens of Object.values(LEVEL_TOKENS)) {
268
+ if (tokens.some((tok) => text.includes(String(tok).toLowerCase()))) hits += 1;
184
269
  }
270
+ if (hits >= 2 && (!best || hits > best.hits)) best = { menu, hits };
271
+ }
272
+ return best?.menu ?? null;
273
+ };
185
274
 
186
- const targetOption = findTargetOption(menu);
187
- if (!targetOption) {
188
- resolve({ status: 'option-not-found' });
189
- return;
190
- }
275
+ let effortMenu = null;
276
+ const effortDeadline = performance.now() + MAX_WAIT_MS;
277
+ while (performance.now() < effortDeadline) {
278
+ effortMenu = resolveEffortMenu();
279
+ if (effortMenu) break;
280
+ await sleep(100);
281
+ }
282
+ if (!effortMenu) {
283
+ closeOpenMenus();
284
+ return { status: 'menu-not-found' };
285
+ }
191
286
 
192
- const alreadySelected =
193
- optionIsSelected(targetOption) ||
194
- optionIsSelected(targetOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
195
- const label = targetOption.textContent?.trim?.() || null;
196
- dispatchClickSequence(targetOption);
197
- resolve({ status: alreadySelected ? 'already-selected' : 'switched', label });
198
- };
287
+ const targetOption = findOptionInMenu(effortMenu);
288
+ if (!targetOption) {
289
+ closeOpenMenus();
290
+ return { status: 'option-not-found' };
291
+ }
199
292
 
200
- setTimeout(attempt, INITIAL_WAIT_MS);
201
- });
293
+ const already = optionIsSelected(targetOption);
294
+ const label = targetOption.textContent?.trim?.() || null;
295
+ dispatchClickSequence(targetOption);
296
+ await sleep(STEP_WAIT_MS);
297
+ closeOpenMenus();
298
+ return { status: already ? 'already-selected' : 'switched', label };
202
299
  })()`;
203
300
  }
204
- export function buildThinkingTimeExpressionForTest(level = 'extended') {
301
+ export function buildThinkingTimeExpressionForTest(level = "extended") {
205
302
  return buildThinkingTimeExpression(level);
206
303
  }
@@ -0,0 +1,150 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getOracleHomeDir } from "../oracleHome.js";
4
+ const ARTIFACTS_DIRNAME = "artifacts";
5
+ function sanitizePathSegment(value, fallback) {
6
+ const sanitized = value
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9._-]+/g, "-")
9
+ .replace(/-+/g, "-")
10
+ .replace(/^-|-$/g, "")
11
+ .slice(0, 80);
12
+ return sanitized || fallback;
13
+ }
14
+ function normalizeSessionId(sessionId) {
15
+ return sanitizePathSegment(path.basename(sessionId), "session");
16
+ }
17
+ export function resolveSessionArtifactsDir(sessionId) {
18
+ return path.join(getOracleHomeDir(), "sessions", normalizeSessionId(sessionId), ARTIFACTS_DIRNAME);
19
+ }
20
+ async function pathExists(targetPath) {
21
+ try {
22
+ await fs.access(targetPath);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function resolveUniquePath(basePath) {
30
+ const ext = path.extname(basePath);
31
+ const stem = ext ? path.basename(basePath, ext) : path.basename(basePath);
32
+ const dir = path.dirname(basePath);
33
+ let candidate = basePath;
34
+ let suffix = 2;
35
+ while (await pathExists(candidate)) {
36
+ candidate = path.join(dir, `${stem}-${suffix}${ext}`);
37
+ suffix += 1;
38
+ }
39
+ return candidate;
40
+ }
41
+ async function readSizeBytes(targetPath) {
42
+ try {
43
+ return (await fs.stat(targetPath)).size;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ export async function writeTextBrowserArtifact(params) {
50
+ const text = params.contents.trim();
51
+ if (!params.sessionId || text.length === 0) {
52
+ return null;
53
+ }
54
+ const dir = resolveSessionArtifactsDir(params.sessionId);
55
+ await fs.mkdir(dir, { recursive: true });
56
+ const filename = sanitizePathSegment(params.filename, "artifact.md");
57
+ const targetPath = await resolveUniquePath(path.join(dir, filename));
58
+ await fs.writeFile(targetPath, `${text}\n`, "utf8");
59
+ params.logger?.(`[browser] Saved ${params.kind} artifact to ${targetPath}`);
60
+ return {
61
+ kind: params.kind,
62
+ path: targetPath,
63
+ label: params.label,
64
+ mimeType: params.mimeType ?? "text/markdown",
65
+ sizeBytes: await readSizeBytes(targetPath),
66
+ sourceUrl: params.sourceUrl,
67
+ };
68
+ }
69
+ function isToolOnlyPlaceholder(text) {
70
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
71
+ return (normalized === "called tool" ||
72
+ normalized === "used tool" ||
73
+ normalized === "użyto narzędzia" ||
74
+ normalized === "narzędzie wywołane");
75
+ }
76
+ export async function saveDeepResearchReportArtifact(params) {
77
+ const report = params.reportMarkdown.trim();
78
+ if (report.length < 40 || isToolOnlyPlaceholder(report)) {
79
+ return null;
80
+ }
81
+ return writeTextBrowserArtifact({
82
+ sessionId: params.sessionId,
83
+ kind: "deep-research-report",
84
+ filename: "deep-research-report.md",
85
+ contents: report,
86
+ label: "Deep Research report",
87
+ mimeType: "text/markdown",
88
+ sourceUrl: params.conversationUrl,
89
+ logger: params.logger,
90
+ });
91
+ }
92
+ export async function saveBrowserTranscriptArtifact(params) {
93
+ const answer = params.answerMarkdown.trim();
94
+ if (!answer) {
95
+ return null;
96
+ }
97
+ const artifactLines = params.artifacts && params.artifacts.length > 0
98
+ ? [
99
+ "",
100
+ "## Artifacts",
101
+ "",
102
+ ...params.artifacts.map((artifact) => {
103
+ const label = artifact.label ?? artifact.kind;
104
+ return `- ${label}: ${artifact.path}`;
105
+ }),
106
+ ]
107
+ : [];
108
+ const conversationLines = params.conversationUrl
109
+ ? ["", `Conversation: ${params.conversationUrl}`, ""]
110
+ : ["", ""];
111
+ const body = [
112
+ "# Oracle Browser Transcript",
113
+ ...conversationLines,
114
+ "## Prompt",
115
+ "",
116
+ params.prompt.trim(),
117
+ "",
118
+ "## Answer",
119
+ "",
120
+ answer,
121
+ ...artifactLines,
122
+ ].join("\n");
123
+ return writeTextBrowserArtifact({
124
+ sessionId: params.sessionId,
125
+ kind: "transcript",
126
+ filename: "transcript.md",
127
+ contents: body,
128
+ label: "Browser transcript",
129
+ mimeType: "text/markdown",
130
+ sourceUrl: params.conversationUrl,
131
+ logger: params.logger,
132
+ });
133
+ }
134
+ export function appendArtifacts(existing, additions) {
135
+ const merged = new Map();
136
+ for (const artifact of existing ?? []) {
137
+ merged.set(`${artifact.kind}:${artifact.path}`, artifact);
138
+ }
139
+ for (const artifact of additions) {
140
+ if (artifact) {
141
+ merged.set(`${artifact.kind}:${artifact.path}`, artifact);
142
+ }
143
+ }
144
+ const values = Array.from(merged.values());
145
+ return values.length > 0 ? values : undefined;
146
+ }
147
+ export const __test__ = {
148
+ normalizeSessionId,
149
+ sanitizePathSegment,
150
+ };
@@ -0,0 +1,31 @@
1
+ import { discoverDevToolsActivePortCandidates, } from "./detect.js";
2
+ export async function resolveAttachRunningConnection(config, logger) {
3
+ const host = config.remoteChrome?.host ?? "127.0.0.1";
4
+ const port = config.remoteChrome?.port ?? 9222;
5
+ if (config.chromePath) {
6
+ logger("Note: --browser-chrome-path is ignored when --browser-attach-running is enabled.");
7
+ }
8
+ logger(config.remoteChrome
9
+ ? `Using explicit attach-running target ${host}:${port}.`
10
+ : `Using default attach-running target ${host}:${port}.`);
11
+ const candidates = (await discoverDevToolsActivePortCandidates({ host }))
12
+ .filter((candidate) => candidate.port === port)
13
+ .sort(compareDevToolsCandidates);
14
+ if (candidates.length === 0) {
15
+ throw new Error(`No running browser with attach metadata matched ${host}:${port}. Enable remote debugging in chrome://inspect/#remote-debugging first.`);
16
+ }
17
+ const candidate = candidates[0];
18
+ logger(`Selected attach-running browser metadata from ${candidate.path}`);
19
+ return {
20
+ host,
21
+ port: candidate.port,
22
+ browserWSEndpoint: candidate.browserWSEndpoint,
23
+ profileRoot: candidate.profileRoot,
24
+ };
25
+ }
26
+ function compareDevToolsCandidates(left, right) {
27
+ if (right.mtimeMs !== left.mtimeMs) {
28
+ return right.mtimeMs - left.mtimeMs;
29
+ }
30
+ return left.path.localeCompare(right.path);
31
+ }