@steipete/oracle 0.18.0 → 0.20.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 (169) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +76 -76
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +170 -45
  13. package/dist/src/browser/actions/modelSelection.js +91 -4
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +364 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +156 -16
  19. package/dist/src/browser/actions/webSearch.js +96 -0
  20. package/dist/src/browser/attachRunning.js +46 -2
  21. package/dist/src/browser/attachmentValidation.js +53 -0
  22. package/dist/src/browser/cancellation.js +105 -0
  23. package/dist/src/browser/chatgptImages.js +2 -0
  24. package/dist/src/browser/chatgptThrottle.js +104 -0
  25. package/dist/src/browser/chromeLifecycle.js +272 -47
  26. package/dist/src/browser/config.js +14 -1
  27. package/dist/src/browser/detect.js +1 -1
  28. package/dist/src/browser/index.js +525 -274
  29. package/dist/src/browser/liveTabs.js +3 -4
  30. package/dist/src/browser/modelDisplay.js +9 -0
  31. package/dist/src/browser/pageActions.js +2 -2
  32. package/dist/src/browser/profileState.js +114 -9
  33. package/dist/src/browser/projectSourcesRunner.js +83 -15
  34. package/dist/src/browser/prompt.js +272 -113
  35. package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
  36. package/dist/src/browser/reattach.js +31 -4
  37. package/dist/src/browser/recoveryTarget.js +131 -0
  38. package/dist/src/browser/sessionRunner.js +73 -11
  39. package/dist/src/browser/tabLeaseRegistry.js +434 -38
  40. package/dist/src/browser/targetClaim.js +54 -0
  41. package/dist/src/browser/utils.js +20 -2
  42. package/dist/src/cli/browserConfig.js +36 -4
  43. package/dist/src/cli/browserDefaults.js +10 -0
  44. package/dist/src/cli/browserTabs.js +15 -29
  45. package/dist/src/cli/detachedSession.js +69 -0
  46. package/dist/src/cli/dryRun.js +58 -48
  47. package/dist/src/cli/harvestIntegrity.js +140 -0
  48. package/dist/src/cli/options.js +15 -0
  49. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  50. package/dist/src/cli/runOptions.js +19 -4
  51. package/dist/src/cli/serveBrowserConfig.js +24 -0
  52. package/dist/src/cli/sessionDisplay.js +13 -7
  53. package/dist/src/cli/sessionRunner.js +95 -12
  54. package/dist/src/config.js +1 -0
  55. package/dist/src/gemini-web/client.js +67 -21
  56. package/dist/src/gemini-web/executor.js +4 -0
  57. package/dist/src/gemini-web/http.js +15 -0
  58. package/dist/src/gemini-web/models.js +15 -11
  59. package/dist/src/mcp/server.js +11 -14
  60. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  61. package/dist/src/mcp/tools/consult.js +86 -24
  62. package/dist/src/mcp/tools/projectSources.js +4 -6
  63. package/dist/src/mcp/tools/sessionResources.js +1 -1
  64. package/dist/src/mcp/tools/sessions.js +2 -2
  65. package/dist/src/mcp/tools/wait.js +182 -0
  66. package/dist/src/mcp/types.js +8 -1
  67. package/dist/src/oracle/config.js +14 -0
  68. package/dist/src/oracle/errors.js +11 -0
  69. package/dist/src/oracle/geminiModels.js +1 -0
  70. package/dist/src/oracle/providerRoutePlan.js +3 -2
  71. package/dist/src/oracle/run.js +27 -4
  72. package/dist/src/remote/client.js +73 -6
  73. package/dist/src/remote/health.js +6 -2
  74. package/dist/src/remote/runSlots.js +83 -0
  75. package/dist/src/remote/server.js +346 -71
  76. package/dist/src/sessionManager.js +36 -1
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  78. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  79. package/package.json +25 -23
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/dist/bin/oracle.js +0 -683
  83. package/dist/docs-site/.nojekyll +0 -0
  84. package/dist/docs-site/CNAME +0 -1
  85. package/dist/docs-site/RELEASING.html +0 -410
  86. package/dist/docs-site/agents.html +0 -374
  87. package/dist/docs-site/anthropic.html +0 -368
  88. package/dist/docs-site/bridge.html +0 -400
  89. package/dist/docs-site/browser-mode.html +0 -588
  90. package/dist/docs-site/chromium-forks.html +0 -347
  91. package/dist/docs-site/cli-reference.html +0 -346
  92. package/dist/docs-site/configuration.html +0 -436
  93. package/dist/docs-site/favicon.svg +0 -14
  94. package/dist/docs-site/followup.html +0 -368
  95. package/dist/docs-site/gemini.html +0 -379
  96. package/dist/docs-site/grok.html +0 -325
  97. package/dist/docs-site/index.html +0 -360
  98. package/dist/docs-site/install.html +0 -335
  99. package/dist/docs-site/linux.html +0 -321
  100. package/dist/docs-site/llms.txt +0 -43
  101. package/dist/docs-site/manual-tests.html +0 -595
  102. package/dist/docs-site/mcp.html +0 -368
  103. package/dist/docs-site/multimodel.html +0 -364
  104. package/dist/docs-site/mythical-pro-agents.html +0 -360
  105. package/dist/docs-site/notifier.html +0 -338
  106. package/dist/docs-site/openai-endpoints.html +0 -387
  107. package/dist/docs-site/openrouter.html +0 -344
  108. package/dist/docs-site/quickstart.html +0 -369
  109. package/dist/docs-site/refactor/ux.html +0 -532
  110. package/dist/docs-site/sessions.html +0 -388
  111. package/dist/docs-site/social-card.png +0 -0
  112. package/dist/docs-site/social-card.svg +0 -79
  113. package/dist/docs-site/spec.html +0 -363
  114. package/dist/docs-site/testing.html +0 -320
  115. package/dist/docs-site/tui-debug.html +0 -326
  116. package/dist/docs-site/windows-work.html +0 -323
  117. package/dist/docs-site/windows.html +0 -320
  118. package/dist/markdansi/types/index.js +0 -4
  119. package/dist/oracle/bin/oracle-cli.js +0 -472
  120. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  121. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  122. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  123. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  124. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  125. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  126. package/dist/oracle/src/browser/config.js +0 -33
  127. package/dist/oracle/src/browser/constants.js +0 -40
  128. package/dist/oracle/src/browser/cookies.js +0 -210
  129. package/dist/oracle/src/browser/domDebug.js +0 -36
  130. package/dist/oracle/src/browser/index.js +0 -331
  131. package/dist/oracle/src/browser/pageActions.js +0 -5
  132. package/dist/oracle/src/browser/prompt.js +0 -88
  133. package/dist/oracle/src/browser/promptSummary.js +0 -20
  134. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  135. package/dist/oracle/src/browser/types.js +0 -1
  136. package/dist/oracle/src/browser/utils.js +0 -62
  137. package/dist/oracle/src/browserMode.js +0 -1
  138. package/dist/oracle/src/cli/browserConfig.js +0 -44
  139. package/dist/oracle/src/cli/dryRun.js +0 -59
  140. package/dist/oracle/src/cli/engine.js +0 -17
  141. package/dist/oracle/src/cli/errorUtils.js +0 -9
  142. package/dist/oracle/src/cli/help.js +0 -70
  143. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  144. package/dist/oracle/src/cli/options.js +0 -103
  145. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  146. package/dist/oracle/src/cli/rootAlias.js +0 -30
  147. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  148. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  149. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  150. package/dist/oracle/src/heartbeat.js +0 -43
  151. package/dist/oracle/src/oracle/client.js +0 -48
  152. package/dist/oracle/src/oracle/config.js +0 -29
  153. package/dist/oracle/src/oracle/errors.js +0 -101
  154. package/dist/oracle/src/oracle/files.js +0 -220
  155. package/dist/oracle/src/oracle/format.js +0 -33
  156. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  157. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  158. package/dist/oracle/src/oracle/request.js +0 -48
  159. package/dist/oracle/src/oracle/run.js +0 -444
  160. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  161. package/dist/oracle/src/oracle/types.js +0 -1
  162. package/dist/oracle/src/oracle.js +0 -9
  163. package/dist/oracle/src/sessionManager.js +0 -205
  164. package/dist/oracle/src/version.js +0 -39
  165. package/dist/scripts/chrome/browser-tools.js +0 -295
  166. package/dist/src/browser/chromeCookies.js +0 -312
  167. package/dist/src/browser/keytarShim.js +0 -56
  168. package/dist/src/browser/profileSync.js +0 -141
  169. package/dist/src/browser/windowsCookies.js +0 -219
@@ -4,6 +4,7 @@ import { waitForAttachmentVisible } from "./attachments.js";
4
4
  import { delay } from "../utils.js";
5
5
  import { logDomFailure } from "../domDebug.js";
6
6
  import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
7
+ import { beginAttachmentEvidence } from "./attachmentEvidence.js";
7
8
  /**
8
9
  * Upload file to remote Chrome by transferring content via CDP
9
10
  * Used when browser is on a different machine than CLI
@@ -28,10 +29,11 @@ export async function uploadAttachmentViaDataTransfer(deps, attachment, logger)
28
29
  await logDomFailure(runtime, logger, "file-input");
29
30
  throw new Error("Unable to locate ChatGPT file attachment input.");
30
31
  }
31
- const transferResult = await transferAttachmentViaDataTransfer(runtime, attachment, fileInputSelector);
32
+ const evidenceId = await beginAttachmentEvidence(runtime, path.basename(attachment.path));
33
+ const transferResult = await transferAttachmentViaDataTransfer(runtime, attachment, fileInputSelector, deps.navigationUrl);
32
34
  logger(`File transferred: ${transferResult.fileName} (${transferResult.size} bytes)`);
33
35
  // Give ChatGPT a moment to process the file
34
36
  await delay(500);
35
- await waitForAttachmentVisible(runtime, transferResult.fileName, 10_000, logger);
37
+ await waitForAttachmentVisible(runtime, transferResult.fileName, 10_000, logger, evidenceId);
36
38
  logger("Attachment queued");
37
39
  }
@@ -1,5 +1,6 @@
1
1
  import { formatElapsed } from "../../oracle/format.js";
2
2
  import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
3
+ import { buildConversationTurnListExpression } from "../conversationTurns.js";
3
4
  const THINKING_STALE_HINT_MS = 10 * 60_000;
4
5
  export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
5
6
  const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
@@ -438,7 +439,6 @@ const ACTIVE_THINKING_LABELS = [
438
439
  function buildThinkingActivityPredicateJs(fnName, detailed) {
439
440
  const stopLiteral = JSON.stringify(STOP_BUTTON_SELECTORS.join(", "));
440
441
  const activeLabelsLiteral = JSON.stringify(ACTIVE_THINKING_LABELS);
441
- const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
442
442
  const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
443
443
  const strong = detailed ? "{ active: true, strong: true }" : "true";
444
444
  const weak = detailed ? "{ active: true, strong: false }" : "true";
@@ -446,7 +446,6 @@ function buildThinkingActivityPredicateJs(fnName, detailed) {
446
446
  return `const ${fnName} = () => {
447
447
  const STOP_SELECTOR = ${stopLiteral};
448
448
  const ACTIVE_LABELS = ${activeLabelsLiteral};
449
- const CONVERSATION_SELECTOR = ${conversationLiteral};
450
449
  const ASSISTANT_SELECTOR = ${assistantLiteral};
451
450
  const isVisible = (node) => {
452
451
  if (!(node instanceof HTMLElement)) return false;
@@ -549,9 +548,7 @@ function buildThinkingActivityPredicateJs(fnName, detailed) {
549
548
  // a visible progress bar mounted indefinitely (review P1), and a document-wide veto would
550
549
  // then hold thinkingActive true until the watchdog timeout on a completed response. The
551
550
  // sidecar check below covers verified reasoning panels; here only the latest turn counts.
552
- const turns = (() => {
553
- try { return document.querySelectorAll(CONVERSATION_SELECTOR); } catch { return []; }
554
- })();
551
+ const turns = ${buildConversationTurnListExpression()};
555
552
  const lastTurn = turns.length ? turns[turns.length - 1] : null;
556
553
  if (
557
554
  lastTurn instanceof HTMLElement &&
@@ -52,8 +52,8 @@ function logPickerDiagnostic(result, logger) {
52
52
  /**
53
53
  * Selects a thinking-time level in ChatGPT's composer.
54
54
  *
55
- * Missing controls remain best-effort except Pro Extended, which fails closed
56
- * unless the selected option is confirmed.
55
+ * Returns the UI selection observed at capturedAt, not backend attestation.
56
+ * Explicit Pro and Pro Extended requests still fail closed when unconfirmed.
57
57
  */
58
58
  export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
59
59
  const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
@@ -65,13 +65,24 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
65
65
  // own, independently of the legacy Pro-model + "extended" combination.
66
66
  const strictProEffort = level === "pro" ||
67
67
  ((targetModelKind === "pro" || observedModelKind === "pro") && level === "extended");
68
+ const evidence = (status, resolvedLabel) => ({
69
+ requestedLevel: level,
70
+ status,
71
+ resolvedLabel,
72
+ verified: status === "already-selected" || status === "switched",
73
+ strictFailClosed: strictProEffort,
74
+ targetModelKind: targetModelKind ?? null,
75
+ observedModelKind: observedModelKind ?? null,
76
+ source: "chatgpt-thinking-picker",
77
+ capturedAt: new Date().toISOString(),
78
+ });
68
79
  switch (result?.status) {
69
80
  case "already-selected":
70
81
  logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} (already selected)`));
71
- return;
82
+ return evidence("already-selected", result.label ?? null);
72
83
  case "switched":
73
84
  logger(formatBrowserThinkingLog(result.label ?? capitalizedLevel));
74
- return;
85
+ return evidence("switched", result.label ?? null);
75
86
  case "option-disabled": {
76
87
  await logDomFailure(Runtime, logger, "thinking-option-disabled");
77
88
  logPickerDiagnostic(result, logger);
@@ -82,7 +93,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
82
93
  // strict error's "refusing to submit" wording: the request really is sent,
83
94
  // at whatever effort ChatGPT already had selected.
84
95
  logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} is unavailable on this account (${result.notice ?? "no reason given"}); keeping the effort already selected in ChatGPT.`));
85
- return;
96
+ return evidence("unverified", null);
86
97
  }
87
98
  case "chip-not-found":
88
99
  case "menu-not-found":
@@ -108,7 +119,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
108
119
  ? "the effort in ChatGPT is unconfirmed"
109
120
  : "keeping the effort already selected in ChatGPT";
110
121
  logger(formatBrowserThinkingLog(`${message}; ${outcome}.`));
111
- return;
122
+ return evidence("unverified", null);
112
123
  }
113
124
  default: {
114
125
  await logDomFailure(Runtime, logger, "thinking-time-unknown");
@@ -118,7 +129,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
118
129
  throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed ${target}.`);
119
130
  }
120
131
  logger(formatBrowserThinkingLog(`unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`));
121
- return;
132
+ return evidence("unverified", null);
122
133
  }
123
134
  }
124
135
  }
@@ -181,6 +192,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
181
192
  const targetLevelLiteral = JSON.stringify(level.toLowerCase());
182
193
  const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
183
194
  const targetIsGpt56ModelLiteral = JSON.stringify(/(?:^|[^0-9])5[._ -]6(?:[^0-9]|$)/i.test(desiredModel ?? ""));
195
+ // Astra resolves the gpt-6-pro request to this exact model-picker alias. Keep
196
+ // this deliberately narrow: a generic unknown model must not claim its
197
+ // version-prefixed effort pill whose ownership we cannot establish.
198
+ const targetIsAstraLatestLiteral = JSON.stringify((desiredModel ?? "").trim().toLowerCase() === "latest");
184
199
  return `(async () => {
185
200
  ${buildClickDispatcher()}
186
201
 
@@ -190,13 +205,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
190
205
  const TARGET_LEVEL = ${targetLevelLiteral};
191
206
  const TARGET_MODEL_KIND = ${targetModelKindLiteral};
192
207
  const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
208
+ const TARGET_IS_ASTRA_LATEST = ${targetIsAstraLatestLiteral};
193
209
 
194
- // Multilingual matchers: English level token + observed German/Japanese/Chinese variants.
210
+ // Multilingual matchers: English level token + observed localized variants.
195
211
  const LEVEL_TOKENS = {
196
- light: ['light', 'instant', 'sofort', 'leicht', '最速', '轻', '极速'],
197
- standard: ['standard', 'medium', 'mittel', '中程度', '标准', '中'],
198
- extended: ['extended', 'high', 'hoch', 'erweitert', '高い', '扩展', '深度', '加强', '高'],
199
- 'extra-high': ['extra high', 'sehr hoch', '非常に高い', '极高'],
212
+ light: ['light', 'instant', 'sofort', 'leicht', '最速', '轻', '极速', '즉시'],
213
+ standard: ['standard', 'medium', 'mittel', '中程度', '标准', '中', '중간'],
214
+ extended: ['extended', 'high', 'hoch', 'erweitert', '高い', '扩展', '深度', '加强', '高', '높음'],
215
+ 'extra-high': ['extra high', 'sehr hoch', '非常に高い', '極高', '极高', '매우 높음'],
200
216
  heavy: ['heavy', 'schwer', '重度', '加重'],
201
217
  };
202
218
  // Pro is a tier you can request, but it is also a MODEL name, so it must never
@@ -205,6 +221,29 @@ function buildThinkingTimeExpression(level, desiredModel) {
205
221
  // "Instant"/"Pro" would look like a tier list. Keep it to target matching only.
206
222
  const TARGET_LEVEL_TOKENS = { ...LEVEL_TOKENS, pro: ['pro'] };
207
223
  const targetTokens = TARGET_LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
224
+ const normalizeAstraLabel = (value) =>
225
+ String(value ?? '')
226
+ .normalize('NFC')
227
+ .toLowerCase()
228
+ .split(String.fromCharCode(9)).join(' ')
229
+ .split(String.fromCharCode(10)).join(' ')
230
+ .split(String.fromCharCode(13)).join(' ')
231
+ .split(String.fromCharCode(12)).join(' ')
232
+ .split(' ').filter(Boolean).join(' ');
233
+ const isAstraLatestEffortPill = (label) =>
234
+ TARGET_IS_ASTRA_LATEST &&
235
+ Object.values(TARGET_LEVEL_TOKENS).some((tokens) =>
236
+ tokens.some((token) => {
237
+ // Do not use the generic matcher here: ownership needs an exact
238
+ // version prefix plus a known localized tier, not token containment.
239
+ const tier = normalizeAstraLabel(token);
240
+ const observed = normalizeAstraLabel(label);
241
+ return observed === '6 ' + tier || observed === '6' + tier;
242
+ }),
243
+ );
244
+ const isSolModelPillForLatest = (label) =>
245
+ TARGET_IS_ASTRA_LATEST &&
246
+ ['5.6 pro', '5.6pro', '5 6 pro'].includes(normalizeAstraLabel(label));
208
247
 
209
248
  const INITIAL_WAIT_MS = 150;
210
249
  const STEP_WAIT_MS = 200;
@@ -215,7 +254,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
215
254
  const INTELLIGENCE_WAIT_MS = 2500;
216
255
 
217
256
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
218
- // Keep CJK characters, including Japanese kana, so localized labels survive
257
+ // Keep CJK characters, including Japanese kana and Hangul, so labels survive
219
258
  // normalization before being matched against LEVEL_TOKENS and picker controls.
220
259
  const normalize = (value) => (value || '')
221
260
  // Compose first so NFD umlauts fold too, then map them onto ASCII before
@@ -226,7 +265,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
226
265
  .replace(/ö/g, 'o')
227
266
  .replace(/ü/g, 'u')
228
267
  .replace(/ß/g, 'ss')
229
- .replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9fff]+/g, ' ')
268
+ .replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9fff\\uac00-\\ud7af]+/g, ' ')
230
269
  .replace(/\\s+/g, ' ')
231
270
  .trim();
232
271
  const hasToken = (text, token) => normalize(text).split(' ').includes(token);
@@ -255,6 +294,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
255
294
  if (token === 'extra high') return hasPhrase(t, 'extra high');
256
295
  if (token === 'hoch') return hasPhrase(t, 'hoch') && !hasPhrase(t, 'sehr hoch');
257
296
  if (token === 'sehr hoch') return hasPhrase(t, 'sehr hoch');
297
+ if (token === '높음') return hasPhrase(t, '높음') && !hasPhrase(t, '매우 높음');
298
+ if (token === '매우 높음') return hasPhrase(t, '매우 높음');
258
299
  if (token === '高い' || token === '非常に高い') {
259
300
  return t === token || hasToken(t, token);
260
301
  }
@@ -842,11 +883,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
842
883
  // token matching cannot be used here — substring is deliberate, as in
843
884
  // countEffortLevels above.
844
885
  const ADVANCED_WORDS = [
845
- 'advanced', 'erweitert', '高级', '詳細設定', '詳細表示',
886
+ 'advanced', 'erweitert', '高级', '詳細設定', '詳細表示', '고급',
846
887
  'avanzado', 'avancado', 'avance',
847
888
  ];
848
889
  const EFFORT_WORDS = [
849
- 'effort', 'aufwand', '强度', '努力', '推論レベル',
890
+ 'effort', 'aufwand', '强度', '努力', '推論レベル', '思考量', '추론 수준',
850
891
  'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
851
892
  ];
852
893
  const containsAny = (label, words) => words.some((word) => label.includes(word));
@@ -945,6 +986,94 @@ function buildThinkingTimeExpression(level, desiredModel) {
945
986
  return null;
946
987
  };
947
988
 
989
+ // The direct-slider rollout removes the Effort submenu entirely. Its keyboard
990
+ // owner announces the actual tier via aria-describedby; neither the pill nor
991
+ // the slider's maximum position alone proves that Pro was selected.
992
+ const selectDirectEffortSlider = async (menu) => {
993
+ const view = menu.querySelector?.('[data-model-selection-view="true"]');
994
+ const simple = view?.querySelector?.('[data-testid="composer-model-picker-slider-simple-view"]');
995
+ if (!simple || simple.getAttribute('data-active') !== 'true' || !isVisible(simple)) return null;
996
+ const resolve = () => {
997
+ const currentView = menu.querySelector?.('[data-model-selection-view="true"]');
998
+ const currentSimple = currentView?.querySelector?.('[data-testid="composer-model-picker-slider-simple-view"]');
999
+ if (currentSimple?.getAttribute('data-active') !== 'true') return null;
1000
+ const slider = currentSimple.querySelector('[data-model-reasoning-effort-slider]');
1001
+ const control = slider?.closest?.('[role="menuitem"]');
1002
+ const thumb = slider?.querySelector?.('[role="slider"]');
1003
+ if (!control || !thumb || !isVisible(control)) return null;
1004
+ const levels = ['light', 'standard', 'extended', 'extra-high', 'pro'];
1005
+ // The selected label leads localized aria-describedby prose, while punctuation
1006
+ // and ordinal grammar vary by locale. Match only that leading label and let
1007
+ // the thumb's numeric ARIA state independently prove its position.
1008
+ const readLeadingLevel = (description) => {
1009
+ const value = (description ?? '').normalize('NFC').trim();
1010
+ if (!value) return null;
1011
+ const matches = levels.flatMap((level, index) => {
1012
+ const token = [...TARGET_LEVEL_TOKENS[level]]
1013
+ .sort((left, right) => right.length - left.length)
1014
+ .find((candidate) => {
1015
+ const normalizedCandidate = candidate.normalize('NFC');
1016
+ const prefix = value.slice(0, normalizedCandidate.length);
1017
+ if (normalize(prefix) !== normalize(normalizedCandidate)) return false;
1018
+ // normalize() erases Unicode letters and marks; inspect the boundary intact.
1019
+ const suffix = value.slice(prefix.length);
1020
+ return !suffix || /^[\\s\\p{P}]/u.test(suffix);
1021
+ });
1022
+ return token
1023
+ ? [{ level, index, label: value.slice(0, token.normalize('NFC').length) }]
1024
+ : [];
1025
+ });
1026
+ return matches.length === 1 ? matches[0] : null;
1027
+ };
1028
+ const selections = describedIds(control)
1029
+ .map((id) => readLeadingLevel(document.getElementById?.(id)?.textContent ?? ''))
1030
+ .filter(Boolean);
1031
+ if (selections.length !== 1) return null;
1032
+ const { label, index, level } = selections[0];
1033
+ // This adapter owns the observed five-tier layout only. A different range
1034
+ // or contradictory announcement must not turn a numeric guess into proof.
1035
+ if (thumb.getAttribute('aria-valuemin') !== '0' || thumb.getAttribute('aria-valuemax') !== '4' ||
1036
+ thumb.getAttribute('aria-valuenow') !== String(index)) return null;
1037
+ return { control, label, index, level };
1038
+ };
1039
+ let current = resolve();
1040
+ const finish = (result) => { closeOpenMenus(); return result; };
1041
+ // The picker can expose its simple view before the keyboard owner is mounted or visible.
1042
+ const readyDeadline = performance.now() + MAX_WAIT_MS;
1043
+ while (!current && performance.now() < readyDeadline) {
1044
+ await sleep(100);
1045
+ current = resolve();
1046
+ }
1047
+ if (!current) return finish(failure('selection-unverified'));
1048
+ // Preserve the legacy Pro-model + extended contract on unified pickers.
1049
+ const target = TARGET_MODEL_KIND === 'pro' && TARGET_LEVEL === 'extended' ? 'pro' : TARGET_LEVEL;
1050
+ const targetIndex = ['light', 'standard', 'extended', 'extra-high', 'pro'].indexOf(target);
1051
+ if (targetIndex < 0) {
1052
+ if (TARGET_IS_GPT56_MODEL && TARGET_LEVEL === 'heavy' && current.level === 'pro') {
1053
+ return finish({ status: 'already-selected', label: current.label });
1054
+ }
1055
+ return finish(failure('option-not-found'));
1056
+ }
1057
+ if (current.level === target) return finish({ status: 'already-selected', label: current.label });
1058
+ const deadline = performance.now() + MAX_WAIT_MS;
1059
+ for (let attempt = 0; attempt < 4 && performance.now() < deadline; attempt += 1) {
1060
+ if (isOptionDisabled(current.control)) return finish(failure('option-disabled', { label: current.label }));
1061
+ const previousIndex = current.index;
1062
+ const key = targetIndex > previousIndex ? 'ArrowRight' : 'ArrowLeft';
1063
+ current.control.focus?.();
1064
+ current.control.dispatchEvent(new KeyboardEvent('keydown', { key, code: key, bubbles: true, cancelable: true }));
1065
+ current = null;
1066
+ while (performance.now() < deadline) {
1067
+ await sleep(100);
1068
+ const next = resolve();
1069
+ if (next && next.index !== previousIndex) { current = next; break; }
1070
+ }
1071
+ if (!current) return finish(failure('selection-unverified'));
1072
+ if (current.level === target) return finish({ status: 'switched', label: current.label });
1073
+ }
1074
+ return finish(failure('selection-unverified'));
1075
+ };
1076
+
948
1077
  // Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
949
1078
  // controlled menu contains the effort levels. Prefer this ownership boundary
950
1079
  // before probing older model-picker layouts.
@@ -961,6 +1090,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
961
1090
  if (seen.has(button) || !isVisible(button)) continue;
962
1091
  seen.add(button);
963
1092
  if (button.getAttribute?.('data-testid') === 'model-switcher-dropdown-button') continue;
1093
+ // A 5.6 Pro model pill is not Astra Latest's 6-prefixed effort owner.
1094
+ // Keep this rejection ahead of the generic compatibility matcher.
1095
+ if (isSolModelPillForLatest(button.textContent ?? '')) continue;
964
1096
  const label = normalize(
965
1097
  (button.getAttribute?.('aria-label') ?? '') + ' ' +
966
1098
  (button.getAttribute?.('data-testid') ?? '') + ' ' +
@@ -970,6 +1102,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
970
1102
  (TARGET_MODEL_KIND === 'pro' && hasToken(label, 'pro') && !hasToken(label, 'thinking')) ||
971
1103
  (TARGET_MODEL_KIND === 'thinking' && hasToken(label, 'thinking') && !hasToken(label, 'pro')) ||
972
1104
  (!TARGET_MODEL_KIND && hasToken(label, 'thinking')) ||
1105
+ // Astra Latest prefixes a supported effort label with "6" (for
1106
+ // example, "6 Pro" or textContent-concatenated "6Pro"). This is
1107
+ // recognized only for the exact Latest target; selection still
1108
+ // requires the direct slider's leading label and numeric ARIA proof.
1109
+ isAstraLatestEffortPill(button.textContent ?? '') ||
973
1110
  (button.matches?.('button.__composer-pill') && matchesAnyEffortLevel(label))
974
1111
  ) {
975
1112
  return button;
@@ -1056,6 +1193,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
1056
1193
  tokens.some((token) => normalize(token) === pillLabel),
1057
1194
  );
1058
1195
  const pillNamesEffortNotModel =
1196
+ TARGET_IS_ASTRA_LATEST ||
1059
1197
  TARGET_IS_GPT56_MODEL ||
1060
1198
  (pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
1061
1199
  const composerModelKind =
@@ -1069,6 +1207,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
1069
1207
  while (performance.now() < deadline) {
1070
1208
  const menu = findVisibleEffortMenu(composerEffortPill);
1071
1209
  if (menu) {
1210
+ const sliderResult = await selectDirectEffortSlider(menu);
1211
+ if (sliderResult) return sliderResult;
1072
1212
  const proEffortResult = await selectProEffortFromSubmenu();
1073
1213
  if (proEffortResult) {
1074
1214
  return proEffortResult;
@@ -0,0 +1,96 @@
1
+ import { BrowserAutomationError } from "../../oracle/errors.js";
2
+ import { INPUT_SELECTORS } from "../constants.js";
3
+ import { delay } from "../utils.js";
4
+ import { activateComposerPlus, captureComposerNavigationUrl, assertComposerPlusStayedInPlace, } from "./attachments.js";
5
+ import { buildComposerNavigationValidationExpression } from "./attachmentContext.js";
6
+ import { buildClickDispatcher } from "./domEvents.js";
7
+ export function matchesWebSearchMenuLabel(value) {
8
+ return [
9
+ "search",
10
+ "searchfindontheweb",
11
+ "websearch",
12
+ "websearchfindreal-timenewsandinfo",
13
+ ].includes(value.replace(/\s+/g, "").toLowerCase());
14
+ }
15
+ export function buildWebSearchVerificationExpression(prompt) {
16
+ return `(() => {
17
+ const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
18
+ const editor = ${JSON.stringify(INPUT_SELECTORS)}.flatMap(selector => Array.from(document.querySelectorAll(selector))).find(visible);
19
+ if (!editor) return { selected: false, promptMatches: false };
20
+ const chip = editor.querySelector('[data-inline-selection-pill][data-id="search"][data-system-hint-type="search"]');
21
+ const copy = editor.cloneNode(true);
22
+ copy.querySelectorAll('[data-inline-selection-pill], [data-inline-selection-pill-cursor-target]').forEach(node => node.remove());
23
+ const readText = node => {
24
+ if (node.nodeType === 3) return node.textContent ?? '';
25
+ const text = Array.from(node.childNodes).map(readText).join('');
26
+ return text + (['P', 'DIV', 'BR', 'LI', 'PRE'].includes(node.nodeName) ? '\\n' : '');
27
+ };
28
+ const normalize = text => String(text ?? '').replace(/[\\u200b\\ufeff]/g, '').replace(/\\s+/g, ' ').trim();
29
+ return { selected: Boolean(chip && visible(chip)), promptMatches: normalize(readText(copy)) === normalize(${JSON.stringify(prompt)}) };
30
+ })()`;
31
+ }
32
+ export function buildWebSearchSelectionExpression(navigationUrl) {
33
+ return `(() => {
34
+ ${buildClickDispatcher()}
35
+ const matchesLabel = ${matchesWebSearchMenuLabel.toString()};
36
+ const navigation = ${buildComposerNavigationValidationExpression(navigationUrl)};
37
+ if (!navigation.contextMatches || navigation.workSelected || navigation.modeUnverified) return 'context-changed';
38
+ const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
39
+ const roots = Array.from(document.querySelectorAll('main .popover, [data-radix-popper-content-wrapper], [data-floating-ui-portal], [role="menu"], [role="listbox"]')).filter(visible);
40
+ const candidates = roots.flatMap(root => Array.from(root.querySelectorAll('[data-radix-collection-item], [role="menuitem"], [role="option"], .__menu-item, [class*="menu-item"]')));
41
+ const match = candidates.find(node => {
42
+ if (!visible(node) || node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true') return false;
43
+ return matchesLabel(node.textContent ?? '');
44
+ });
45
+ if (!match) return 'missing';
46
+ dispatchClickSequence(match);
47
+ return 'clicked';
48
+ })()`;
49
+ }
50
+ /** Web Search is an inline editor hint, so activate it after staging text/attachments. */
51
+ export async function activateWebSearch(runtime, input, prompt, logger) {
52
+ const navigationUrl = await captureComposerNavigationUrl(runtime);
53
+ const verify = async () => {
54
+ const { result, exceptionDetails } = await runtime.evaluate({
55
+ expression: buildWebSearchVerificationExpression(prompt),
56
+ returnByValue: true,
57
+ });
58
+ if (exceptionDetails)
59
+ return false;
60
+ return result?.value?.selected === true && result?.value?.promptMatches === true;
61
+ };
62
+ if (await verify())
63
+ return;
64
+ const activated = await activateComposerPlus(runtime, input, navigationUrl);
65
+ if (activated.method === "unavailable")
66
+ throw new BrowserAutomationError("Web Search requires the ChatGPT composer tools menu.", {
67
+ stage: "web-search-activate",
68
+ });
69
+ const deadline = Date.now() + 5_000;
70
+ let clicked = false;
71
+ while (Date.now() < deadline) {
72
+ const outcome = await runtime.evaluate({
73
+ expression: buildWebSearchSelectionExpression(navigationUrl),
74
+ returnByValue: true,
75
+ });
76
+ if (outcome.result?.value === "clicked") {
77
+ clicked = true;
78
+ break;
79
+ }
80
+ if (outcome.exceptionDetails || outcome.result?.value !== "missing")
81
+ break;
82
+ await delay(100);
83
+ }
84
+ if (clicked) {
85
+ const confirmationDeadline = Date.now() + 3_000;
86
+ do {
87
+ await assertComposerPlusStayedInPlace(runtime, navigationUrl);
88
+ if (await verify()) {
89
+ logger("Web Search selected; inline search hint and staged prompt verified.");
90
+ return;
91
+ }
92
+ await delay(100);
93
+ } while (Date.now() < confirmationDeadline);
94
+ }
95
+ throw new BrowserAutomationError("Web Search selection could not be verified; the prompt was not submitted. This pilot supports the English ChatGPT Web search control.", { stage: "web-search-activate" });
96
+ }
@@ -1,4 +1,4 @@
1
- import { discoverDevToolsActivePortCandidates, } from "./detect.js";
1
+ import { discoverDevToolsActivePortCandidates, formatWebSocketHost, resolveDevToolsActivePortDiscoveryRoots, } from "./detect.js";
2
2
  export async function resolveAttachRunningConnection(config, logger) {
3
3
  const host = config.remoteChrome?.host ?? "127.0.0.1";
4
4
  const port = config.remoteChrome?.port ?? 9222;
@@ -12,7 +12,17 @@ export async function resolveAttachRunningConnection(config, logger) {
12
12
  .filter((candidate) => candidate.port === port)
13
13
  .sort(compareDevToolsCandidates);
14
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.`);
15
+ const probe = await probeDevToolsBrowserWSEndpoint({ host, port });
16
+ if (probe.ok) {
17
+ return {
18
+ host,
19
+ port,
20
+ browserWSEndpoint: probe.browserWSEndpoint,
21
+ profileRoot: null,
22
+ };
23
+ }
24
+ const discoveryRoots = resolveDevToolsActivePortDiscoveryRoots();
25
+ throw new Error(`No running browser matched ${host}:${port}. DevToolsActivePort discovery searched ${discoveryRoots.join(", ") || "no roots"}, and endpoint probe http://${formatWebSocketHost(host)}:${port}/json/version failed: ${probe.error}.`);
16
26
  }
17
27
  const candidate = candidates[0];
18
28
  logger(`Selected attach-running browser metadata from ${candidate.path}`);
@@ -23,6 +33,40 @@ export async function resolveAttachRunningConnection(config, logger) {
23
33
  profileRoot: candidate.profileRoot,
24
34
  };
25
35
  }
36
+ async function probeDevToolsBrowserWSEndpoint({ host, port, attempts = 2, timeoutMs = 1000, }) {
37
+ const versionUrl = `http://${formatWebSocketHost(host)}:${port}/json/version`;
38
+ for (let attempt = 0; attempt < attempts; attempt++) {
39
+ try {
40
+ const controller = new AbortController();
41
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
42
+ try {
43
+ const response = await fetch(versionUrl, { signal: controller.signal });
44
+ if (!response.ok) {
45
+ throw new Error(`HTTP ${response.status}`);
46
+ }
47
+ const version = (await response.json());
48
+ if (typeof version.webSocketDebuggerUrl !== "string" || !version.webSocketDebuggerUrl) {
49
+ throw new Error("response did not include webSocketDebuggerUrl");
50
+ }
51
+ return { ok: true, browserWSEndpoint: version.webSocketDebuggerUrl };
52
+ }
53
+ finally {
54
+ // Headers can arrive before a stalled body; bound and clean up the whole request.
55
+ clearTimeout(timeout);
56
+ controller.abort();
57
+ }
58
+ }
59
+ catch (error) {
60
+ if (attempt < attempts - 1) {
61
+ await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
62
+ continue;
63
+ }
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ return { ok: false, error: message };
66
+ }
67
+ }
68
+ return { ok: false, error: "unreachable" };
69
+ }
26
70
  function compareDevToolsCandidates(left, right) {
27
71
  if (right.mtimeMs !== left.mtimeMs) {
28
72
  return right.mtimeMs - left.mtimeMs;
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ function compareStrings(left, right) {
3
+ if (left < right)
4
+ return -1;
5
+ if (left > right)
6
+ return 1;
7
+ return 0;
8
+ }
9
+ export function findAttachmentBasenameCollisions(attachments) {
10
+ const attachmentsByBasename = new Map();
11
+ for (const attachment of attachments) {
12
+ const basename = path.basename(attachment.path);
13
+ const matchingAttachments = attachmentsByBasename.get(basename) ?? [];
14
+ matchingAttachments.push(attachment);
15
+ attachmentsByBasename.set(basename, matchingAttachments);
16
+ }
17
+ return [...attachmentsByBasename.entries()]
18
+ .filter(([, matchingAttachments]) => matchingAttachments.length > 1)
19
+ .sort(([leftBasename], [rightBasename]) => compareStrings(leftBasename, rightBasename))
20
+ .map(([basename, matchingAttachments]) => ({
21
+ basename,
22
+ attachments: [...matchingAttachments].sort((left, right) => compareStrings(left.displayPath, right.displayPath) ||
23
+ compareStrings(left.path, right.path)),
24
+ }));
25
+ }
26
+ export function buildAttachmentBasenameCollisionDetails(collisions, displayFile) {
27
+ const collisionDetails = collisions.map((collision) => ({
28
+ basename: collision.basename,
29
+ files: collision.attachments.map(displayFile),
30
+ }));
31
+ return {
32
+ collisions: collisionDetails,
33
+ files: collisionDetails.flatMap((collision) => collision.files),
34
+ };
35
+ }
36
+ export function formatAttachmentBasenameCollisionMessage(subject, collisions) {
37
+ const collisionLines = collisions.length === 1
38
+ ? [
39
+ `${subject} cannot safely include multiple files named "${collisions[0].basename}":`,
40
+ ...collisions[0].files.map((file) => `- ${file}`),
41
+ ]
42
+ : [
43
+ `${subject} cannot safely include files with colliding basenames:`,
44
+ ...collisions.flatMap((collision) => [
45
+ `Files named "${collision.basename}":`,
46
+ ...collision.files.map((file) => `- ${file}`),
47
+ ]),
48
+ ];
49
+ return [
50
+ ...collisionLines,
51
+ "Use --browser-bundle-files (and --browser-bundle-format zip for raw or binary files), or choose unique names before retrying.",
52
+ ].join("\n");
53
+ }