@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
@@ -0,0 +1,662 @@
1
+ import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, CONVERSATION_TURN_SELECTOR, } from "../constants.js";
2
+ import { delay } from "../utils.js";
3
+ import { buildClickDispatcher } from "./domEvents.js";
4
+ import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
5
+ import { BrowserAutomationError } from "../../oracle/errors.js";
6
+ /**
7
+ * Activates Deep Research mode through ChatGPT's slash command, with the
8
+ * composer tools menu as a fallback for older UI variants.
9
+ */
10
+ export async function activateDeepResearch(Runtime, _Input, logger) {
11
+ const expression = buildActivateDeepResearchExpression();
12
+ const outcome = await Runtime.evaluate({
13
+ expression,
14
+ awaitPromise: true,
15
+ returnByValue: true,
16
+ });
17
+ const result = outcome.result?.value;
18
+ switch (result?.status) {
19
+ case "activated":
20
+ logger("Deep Research mode activated");
21
+ return;
22
+ case "already-active":
23
+ logger("Deep Research mode already active");
24
+ return;
25
+ case "plus-button-missing":
26
+ throw new BrowserAutomationError("Could not find the composer plus button to activate Deep Research.", { stage: "deep-research-activate", code: "plus-button-missing" });
27
+ case "dropdown-item-missing": {
28
+ const hint = result.available?.length
29
+ ? ` Available options: ${result.available.join(", ")}`
30
+ : "";
31
+ throw new BrowserAutomationError(`"Deep research" option not found in composer dropdown.${hint} ` +
32
+ "This feature may require a ChatGPT Plus or Pro subscription.", { stage: "deep-research-activate", code: "dropdown-item-missing" });
33
+ }
34
+ case "pill-not-confirmed":
35
+ throw new BrowserAutomationError("Deep Research pill did not appear after selection. The UI may have changed.", { stage: "deep-research-activate", code: "pill-not-confirmed" });
36
+ default:
37
+ throw new BrowserAutomationError("Unexpected result from Deep Research activation.", {
38
+ stage: "deep-research-activate",
39
+ });
40
+ }
41
+ }
42
+ /**
43
+ * After prompt submission, waits for the research plan to appear and
44
+ * auto-confirm (~60s countdown + 10s safety margin).
45
+ */
46
+ export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS) {
47
+ // Phase A: Detect research plan appearance (up to 60s)
48
+ const planDeadline = Date.now() + 60_000;
49
+ let planDetected = false;
50
+ while (Date.now() < planDeadline) {
51
+ const { result } = await Runtime.evaluate({
52
+ expression: `(() => {
53
+ const iframes = document.querySelectorAll('iframe');
54
+ const hasResearchIframe = Array.from(iframes).some(f => {
55
+ const rect = f.getBoundingClientRect();
56
+ return rect.width > 200 && rect.height > 200;
57
+ });
58
+ const assistantText = (document.querySelector('[data-message-author-role="assistant"]')?.textContent || '').toLowerCase();
59
+ const hasResearchText = assistantText.includes('researching') ||
60
+ assistantText.includes('research plan') ||
61
+ assistantText.includes('survey') ||
62
+ assistantText.includes('analyze');
63
+ return { hasResearchIframe, hasResearchText };
64
+ })()`,
65
+ returnByValue: true,
66
+ });
67
+ const val = result?.value;
68
+ if (val?.hasResearchIframe || val?.hasResearchText) {
69
+ planDetected = true;
70
+ logger("Research plan detected, waiting for auto-confirm countdown...");
71
+ break;
72
+ }
73
+ await delay(2_000);
74
+ }
75
+ if (!planDetected) {
76
+ logger("Warning: Research plan not detected within 60s; continuing (may have auto-confirmed already)");
77
+ return;
78
+ }
79
+ // Phase B: Wait for auto-confirm countdown
80
+ const confirmStart = Date.now();
81
+ while (Date.now() - confirmStart < autoConfirmWaitMs) {
82
+ const { result } = await Runtime.evaluate({
83
+ expression: `(() => {
84
+ const iframes = document.querySelectorAll('iframe');
85
+ const hasLargeIframe = Array.from(iframes).some(f => {
86
+ const rect = f.getBoundingClientRect();
87
+ return rect.width > 200 && rect.height > 200;
88
+ });
89
+ const text = (document.body?.innerText || '').toLowerCase();
90
+ const isResearching = text.includes('researching...') ||
91
+ text.includes('reading sources') ||
92
+ text.includes('considering');
93
+ return { hasLargeIframe, isResearching };
94
+ })()`,
95
+ returnByValue: true,
96
+ });
97
+ const val = result?.value;
98
+ if (val?.isResearching) {
99
+ logger("Research plan confirmed, execution started");
100
+ return;
101
+ }
102
+ await delay(5_000);
103
+ }
104
+ logger("Auto-confirm wait complete, proceeding to monitor research progress");
105
+ }
106
+ /**
107
+ * Polls for Deep Research completion over 5-30+ minutes.
108
+ * Returns the full response text, optional HTML, and turn metadata.
109
+ */
110
+ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs = DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, minTurnIndex, Page, client) {
111
+ const start = Date.now();
112
+ let lastLogTime = start;
113
+ let lastTextLength = 0;
114
+ const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
115
+ ? Math.floor(minTurnIndex)
116
+ : -1;
117
+ logger(`Monitoring Deep Research (timeout: ${Math.round(timeoutMs / 60_000)}min)...`);
118
+ while (Date.now() - start < timeoutMs) {
119
+ const { result } = await Runtime.evaluate({
120
+ expression: buildDeepResearchCompletionPollExpression(minTurnLiteral),
121
+ returnByValue: true,
122
+ });
123
+ const val = result?.value;
124
+ if (val?.accountBlocked) {
125
+ throw new BrowserAutomationError("ChatGPT account security block detected during Deep Research. Open chatgpt.com in Chrome, secure the account, then rerun Oracle.", { stage: "chatgpt-account-blocked", code: "chatgpt-account-blocked" });
126
+ }
127
+ const frameResult = Page
128
+ ? await readDeepResearchFrameResult(Runtime, Page).catch(() => null)
129
+ : client
130
+ ? await readDeepResearchTargetResult(client).catch(() => null)
131
+ : null;
132
+ const scopedToNewTurns = minTurnLiteral >= 0;
133
+ if (frameResult?.completed &&
134
+ frameResult.text &&
135
+ (!scopedToNewTurns || val?.hasActiveScopedResearch)) {
136
+ logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
137
+ return {
138
+ text: frameResult.text,
139
+ html: frameResult.html,
140
+ meta: { turnId: null, messageId: null },
141
+ };
142
+ }
143
+ // Completion detected
144
+ if (val?.finished) {
145
+ logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
146
+ return await extractDeepResearchResult(Runtime, logger, minTurnIndex ?? undefined);
147
+ }
148
+ // Progress logging every 60 seconds
149
+ const now = Date.now();
150
+ if (now - lastLogTime >= 60_000) {
151
+ const elapsed = Math.round((now - start) / 1000);
152
+ const chars = Math.max(val?.textLength ?? 0, frameResult?.textLength ?? 0);
153
+ const phase = frameResult?.inProgress || val?.hasIframe
154
+ ? "researching"
155
+ : val?.stopVisible
156
+ ? "generating"
157
+ : "waiting";
158
+ logger(`Deep Research ${phase}... ${elapsed}s elapsed, ~${chars} chars`);
159
+ lastLogTime = now;
160
+ }
161
+ lastTextLength = Math.max(val?.textLength ?? 0, frameResult?.textLength ?? 0, lastTextLength);
162
+ await delay(DEEP_RESEARCH_POLL_INTERVAL_MS);
163
+ }
164
+ // Timeout — throw with metadata for potential reattach
165
+ const elapsed = Math.round((Date.now() - start) / 1000);
166
+ throw new BrowserAutomationError(`Deep Research did not complete within ${Math.round(timeoutMs / 60_000)} minutes (${elapsed}s elapsed). ` +
167
+ "Use 'oracle session <id>' to reattach later, or increase --timeout.", {
168
+ stage: "deep-research-timeout",
169
+ code: "deep-research-timeout",
170
+ elapsedMs: Date.now() - start,
171
+ lastTextLength,
172
+ });
173
+ }
174
+ /**
175
+ * Extracts the Deep Research result using existing assistant response
176
+ * extraction logic (readAssistantSnapshot + captureAssistantMarkdown).
177
+ */
178
+ export async function extractDeepResearchResult(Runtime, logger, minTurnIndex) {
179
+ const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
180
+ const meta = {
181
+ turnId: snapshot?.turnId ?? null,
182
+ messageId: snapshot?.messageId ?? null,
183
+ };
184
+ // Try the copy-button approach first for clean markdown
185
+ const markdown = await captureAssistantMarkdown(Runtime, meta, logger);
186
+ if (markdown && !isDeepResearchPlaceholderText(markdown)) {
187
+ return { text: markdown, html: snapshot?.html ?? undefined, meta };
188
+ }
189
+ // Fall back to snapshot text
190
+ if (snapshot?.text && !isDeepResearchPlaceholderText(snapshot.text)) {
191
+ return { text: snapshot.text, html: snapshot.html ?? undefined, meta };
192
+ }
193
+ throw new BrowserAutomationError("Deep Research completed but failed to extract the response text.", { stage: "deep-research-extract", code: "extraction-failed" });
194
+ }
195
+ function isDeepResearchPlaceholderText(text) {
196
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
197
+ return (normalized === "called tool" ||
198
+ normalized === "used tool" ||
199
+ normalized === "użyto narzędzia" ||
200
+ normalized === "narzędzie wywołane");
201
+ }
202
+ export function isDeepResearchPlaceholderTextForTest(text) {
203
+ return isDeepResearchPlaceholderText(text);
204
+ }
205
+ async function readDeepResearchFrameResult(Runtime, Page) {
206
+ const pageWithFrames = Page;
207
+ if (typeof pageWithFrames.getFrameTree !== "function" ||
208
+ typeof pageWithFrames.createIsolatedWorld !== "function") {
209
+ return null;
210
+ }
211
+ const frameTree = (await pageWithFrames.getFrameTree())?.frameTree;
212
+ const frameId = findDeepResearchFrameId(frameTree);
213
+ if (!frameId) {
214
+ return null;
215
+ }
216
+ const world = await pageWithFrames.createIsolatedWorld({
217
+ frameId,
218
+ worldName: "oracle-deep-research",
219
+ grantUniveralAccess: true,
220
+ });
221
+ if (typeof world.executionContextId !== "number") {
222
+ return null;
223
+ }
224
+ const { result } = await Runtime.evaluate({
225
+ expression: buildDeepResearchFrameStatusExpression(),
226
+ contextId: world.executionContextId,
227
+ returnByValue: true,
228
+ });
229
+ return result?.value ?? null;
230
+ }
231
+ async function readDeepResearchTargetResult(client) {
232
+ const rawClient = client;
233
+ if (typeof rawClient.send !== "function") {
234
+ return null;
235
+ }
236
+ const sessionIds = new Set();
237
+ const ownedSessionIds = new Set();
238
+ const onAttached = (params, sessionId) => {
239
+ const targetInfo = params
240
+ ?.targetInfo;
241
+ const eventSessionId = params?.sessionId ?? sessionId;
242
+ const url = targetInfo?.url ?? "";
243
+ const type = targetInfo?.type ?? "";
244
+ if (eventSessionId && isDeepResearchTarget(url, type)) {
245
+ sessionIds.add(eventSessionId);
246
+ ownedSessionIds.add(eventSessionId);
247
+ }
248
+ };
249
+ client.on?.("Target.attachedToTarget", onAttached);
250
+ try {
251
+ await rawClient.send("Target.setDiscoverTargets", { discover: true }).catch(() => undefined);
252
+ await rawClient
253
+ .send("Target.setAutoAttach", {
254
+ autoAttach: true,
255
+ waitForDebuggerOnStart: false,
256
+ flatten: true,
257
+ })
258
+ .catch(() => undefined);
259
+ await delay(100);
260
+ const targets = (await rawClient.send("Target.getTargets", {}));
261
+ for (const target of targets?.targetInfos ?? []) {
262
+ if (!target.targetId || !isDeepResearchTarget(target.url ?? "", target.type ?? "")) {
263
+ continue;
264
+ }
265
+ const attached = (await rawClient
266
+ .send("Target.attachToTarget", { targetId: target.targetId, flatten: true })
267
+ .catch(() => null));
268
+ if (attached?.sessionId) {
269
+ sessionIds.add(attached.sessionId);
270
+ ownedSessionIds.add(attached.sessionId);
271
+ }
272
+ }
273
+ for (const sessionId of sessionIds) {
274
+ const value = await readDeepResearchTargetSession(rawClient, sessionId);
275
+ if (value?.completed) {
276
+ return value;
277
+ }
278
+ if (value?.inProgress || value?.textLength) {
279
+ return value;
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+ finally {
285
+ await rawClient
286
+ .send("Target.setAutoAttach", {
287
+ autoAttach: false,
288
+ waitForDebuggerOnStart: false,
289
+ flatten: true,
290
+ })
291
+ .catch(() => undefined);
292
+ await Promise.all(Array.from(ownedSessionIds, (sessionId) => rawClient.send("Target.detachFromTarget", { sessionId }).catch(() => undefined)));
293
+ client.removeListener?.("Target.attachedToTarget", onAttached);
294
+ }
295
+ }
296
+ async function readDeepResearchTargetSession(rawClient, sessionId) {
297
+ await rawClient.send("Runtime.enable", {}, sessionId).catch(() => undefined);
298
+ await rawClient.send("Page.enable", {}, sessionId).catch(() => undefined);
299
+ const frameTree = (await rawClient
300
+ .send("Page.getFrameTree", {}, sessionId)
301
+ .catch(() => null));
302
+ const frameIds = collectDeepResearchFrameIds(frameTree?.frameTree);
303
+ let best = null;
304
+ for (const frameId of frameIds) {
305
+ const world = (await rawClient
306
+ .send("Page.createIsolatedWorld", {
307
+ frameId,
308
+ worldName: "oracle-deep-research",
309
+ grantUniveralAccess: true,
310
+ }, sessionId)
311
+ .catch(() => null));
312
+ if (typeof world?.executionContextId !== "number") {
313
+ continue;
314
+ }
315
+ const value = await evaluateDeepResearchFrameStatus(rawClient, sessionId, world.executionContextId);
316
+ if (value?.completed) {
317
+ return value;
318
+ }
319
+ if ((value?.textLength ?? 0) > (best?.textLength ?? 0) || value?.inProgress) {
320
+ best = value;
321
+ }
322
+ }
323
+ const topFrameValue = await evaluateDeepResearchFrameStatus(rawClient, sessionId);
324
+ if (topFrameValue?.completed) {
325
+ return topFrameValue;
326
+ }
327
+ if ((topFrameValue?.textLength ?? 0) > (best?.textLength ?? 0) || topFrameValue?.inProgress) {
328
+ best = topFrameValue;
329
+ }
330
+ return best;
331
+ }
332
+ async function evaluateDeepResearchFrameStatus(rawClient, sessionId, contextId) {
333
+ const response = (await rawClient
334
+ .send("Runtime.evaluate", {
335
+ expression: buildDeepResearchFrameStatusExpression(),
336
+ returnByValue: true,
337
+ ...(typeof contextId === "number" ? { contextId } : {}),
338
+ }, sessionId)
339
+ .catch(() => null));
340
+ return response?.result?.value ?? null;
341
+ }
342
+ function isDeepResearchTarget(url, type) {
343
+ const lowerUrl = url.toLowerCase();
344
+ const lowerType = type.toLowerCase();
345
+ return (lowerType === "iframe" ||
346
+ lowerUrl.includes("connector_openai_deep_research") ||
347
+ lowerUrl.includes("deep-research"));
348
+ }
349
+ function findDeepResearchFrameId(tree) {
350
+ if (!tree?.frame) {
351
+ return null;
352
+ }
353
+ const url = tree.frame.url ?? "";
354
+ const name = tree.frame.name ?? "";
355
+ if (url.includes("connector_openai_deep_research") ||
356
+ url.includes("deep-research") ||
357
+ name.includes("deep-research")) {
358
+ return tree.frame.id ?? null;
359
+ }
360
+ for (const child of tree.childFrames ?? []) {
361
+ const match = findDeepResearchFrameId(child);
362
+ if (match) {
363
+ return match;
364
+ }
365
+ }
366
+ return null;
367
+ }
368
+ function collectDeepResearchFrameIds(tree) {
369
+ if (!tree?.frame) {
370
+ return [];
371
+ }
372
+ const ids = [];
373
+ const url = tree.frame.url ?? "";
374
+ const name = tree.frame.name ?? "";
375
+ if (url.includes("connector_openai_deep_research") ||
376
+ url.includes("deep-research") ||
377
+ name.includes("deep-research") ||
378
+ name === "root") {
379
+ if (tree.frame.id) {
380
+ ids.push(tree.frame.id);
381
+ }
382
+ }
383
+ for (const child of tree.childFrames ?? []) {
384
+ ids.push(...collectDeepResearchFrameIds(child));
385
+ }
386
+ return ids;
387
+ }
388
+ function buildDeepResearchFrameStatusExpression() {
389
+ return `(() => {
390
+ const rawText = document.body?.innerText || '';
391
+ const html = document.body?.innerHTML || '';
392
+ const isPlaceholder = (line) => /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/i.test(line);
393
+ const isCompletionLine = (line) =>
394
+ /^(research completed|badanie ukończone)\\b/i.test(line);
395
+ const isCounterLine = (line) =>
396
+ /^(\\d+\\s+)?(citation|citations|source|sources|search|searches|cytat|cytaty|cytatów|źródło|źródła|wyszukiwanie|wyszukiwania|wyszukiwań)\\b/i.test(line);
397
+ const normalizeReport = (text) => {
398
+ const lines = String(text || '')
399
+ .split(/\\n+/)
400
+ .map((line) => line.trim())
401
+ .filter(Boolean)
402
+ .filter((line) => !/^\\d+$/.test(line));
403
+ const reportIndex = lines.findIndex((line) => /deep research report/i.test(line));
404
+ const candidates = reportIndex >= 0 ? lines.slice(reportIndex + 1) : lines;
405
+ let started = false;
406
+ const reportLines = candidates.filter((line) => {
407
+ if (!started) {
408
+ if (
409
+ /deep research report/i.test(line) ||
410
+ isCompletionLine(line) ||
411
+ isCounterLine(line) ||
412
+ isPlaceholder(line)
413
+ ) {
414
+ return false;
415
+ }
416
+ started = true;
417
+ }
418
+ return true;
419
+ });
420
+ if (reportLines.length > 1 && reportLines[0] === reportLines[1]) {
421
+ reportLines.shift();
422
+ }
423
+ return reportLines.join('\\n').trim();
424
+ };
425
+ const reportText = normalizeReport(rawText);
426
+ const completed = /research completed|badanie ukończone/i.test(rawText) &&
427
+ reportText.length >= 40 &&
428
+ !isPlaceholder(reportText);
429
+ const inProgress = /researching|badanie|searching|searches|wyszukiwa|citation|cytat|source|źród|reading|completed|ukończone/i.test(rawText);
430
+ return {
431
+ completed,
432
+ inProgress,
433
+ textLength: reportText.length || rawText.trim().length,
434
+ text: completed ? reportText : undefined,
435
+ html: completed ? html : undefined,
436
+ };
437
+ })()`;
438
+ }
439
+ export function findDeepResearchFrameIdForTest(tree) {
440
+ return findDeepResearchFrameId(tree);
441
+ }
442
+ export function buildDeepResearchFrameStatusExpressionForTest() {
443
+ return buildDeepResearchFrameStatusExpression();
444
+ }
445
+ /**
446
+ * Quick status check for Deep Research — used during reattach to determine
447
+ * whether research has completed, is still in progress, or is in an unknown state.
448
+ */
449
+ export async function checkDeepResearchStatus(Runtime, _logger) {
450
+ const { result } = await Runtime.evaluate({
451
+ expression: buildDeepResearchStatusExpression(),
452
+ returnByValue: true,
453
+ });
454
+ const val = result?.value;
455
+ return {
456
+ completed: val?.completed ?? false,
457
+ inProgress: val?.inProgress ?? false,
458
+ hasIframe: val?.hasIframe ?? false,
459
+ textLength: val?.textLength ?? 0,
460
+ placeholderOnly: val?.placeholderOnly ?? false,
461
+ };
462
+ }
463
+ // ---------------------------------------------------------------------------
464
+ // DOM expression builder
465
+ // ---------------------------------------------------------------------------
466
+ function buildDeepResearchStatusExpression() {
467
+ const finishedSelector = JSON.stringify(FINISHED_ACTIONS_SELECTOR);
468
+ const stopSelector = JSON.stringify(STOP_BUTTON_SELECTOR);
469
+ return `(() => {
470
+ const stopVisible = Boolean(document.querySelector(${stopSelector}));
471
+ const iframes = Array.from(document.querySelectorAll('iframe')).filter(f => {
472
+ const rect = f.getBoundingClientRect();
473
+ return rect.width > 200 && rect.height > 200;
474
+ });
475
+ const turns = document.querySelectorAll('[data-message-author-role="assistant"]');
476
+ const lastTurn = turns[turns.length - 1];
477
+ const finished = Boolean(lastTurn?.querySelector?.(${finishedSelector}));
478
+ const text = (lastTurn?.textContent || '').trim();
479
+ const normalized = text.toLowerCase().replace(/\\s+/g, ' ').trim();
480
+ const placeholderOnly = /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/.test(normalized);
481
+ const textLength = text.length;
482
+ return {
483
+ completed: finished && !placeholderOnly && textLength >= 40,
484
+ inProgress: stopVisible || iframes.length > 0,
485
+ hasIframe: iframes.length > 0,
486
+ textLength,
487
+ placeholderOnly,
488
+ };
489
+ })()`;
490
+ }
491
+ function buildDeepResearchCompletionPollExpression(minTurnIndex) {
492
+ const finishedSelector = JSON.stringify(FINISHED_ACTIONS_SELECTOR);
493
+ const stopSelector = JSON.stringify(STOP_BUTTON_SELECTOR);
494
+ const turnSelector = JSON.stringify(CONVERSATION_TURN_SELECTOR);
495
+ return `(() => {
496
+ const MIN_TURN_INDEX = ${minTurnIndex};
497
+ const stopVisible = Boolean(document.querySelector(${stopSelector}));
498
+ const scopedToNewTurns = MIN_TURN_INDEX >= 0;
499
+ const pageText = String(document.body?.innerText || '').toLowerCase().replace(/\\s+/g, ' ');
500
+ const accountBlocked = pageText.includes('suspicious activity detected') &&
501
+ pageText.includes('secure your account') &&
502
+ pageText.includes('regain access');
503
+ const isAssistantTurn = (node) => {
504
+ const attr = String(node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
505
+ return attr === 'assistant' ||
506
+ Boolean(node.querySelector('[data-message-author-role="assistant"], [data-turn="assistant"]')) ||
507
+ String(node.getAttribute('data-testid') || '').toLowerCase().includes('conversation-turn') &&
508
+ /chatgpt\\s+said/i.test(node.innerText || node.textContent || '');
509
+ };
510
+ const conversationTurns = Array.from(document.querySelectorAll(${turnSelector}));
511
+ const allAssistantTurns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"], [data-turn="assistant"]'));
512
+ const scopedTurns = scopedToNewTurns
513
+ ? conversationTurns.slice(MIN_TURN_INDEX).filter(isAssistantTurn)
514
+ : allAssistantTurns;
515
+ const lastTurn = scopedTurns[scopedTurns.length - 1] || (scopedToNewTurns ? null : allAssistantTurns[allAssistantTurns.length - 1]);
516
+ const text = (lastTurn?.textContent || '').trim();
517
+ const normalized = text.toLowerCase().replace(/\\s+/g, ' ').trim();
518
+ const textLength = text.length;
519
+ const isToolStub = normalized === 'called tool' ||
520
+ normalized === 'used tool' ||
521
+ normalized === 'użyto narzędzia' ||
522
+ normalized === 'narzędzie wywołane';
523
+ const finished = Boolean(lastTurn?.querySelector(${finishedSelector})) &&
524
+ textLength >= 40 &&
525
+ !isToolStub;
526
+ const hasIframe = Array.from(document.querySelectorAll('iframe')).some(f => {
527
+ const rect = f.getBoundingClientRect();
528
+ return rect.width > 200 && rect.height > 200;
529
+ });
530
+ const hasActiveScopedResearch = scopedToNewTurns && Boolean(lastTurn) && hasIframe &&
531
+ (textLength < 40 || isToolStub || /chatgpt\\s+said:?$/i.test(text));
532
+ return { finished, stopVisible, textLength, hasIframe, isToolStub, hasActiveScopedResearch, accountBlocked };
533
+ })()`;
534
+ }
535
+ export function buildDeepResearchStatusExpressionForTest() {
536
+ return buildDeepResearchStatusExpression();
537
+ }
538
+ export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex = -1) {
539
+ return buildDeepResearchCompletionPollExpression(minTurnIndex);
540
+ }
541
+ function buildActivateDeepResearchExpression() {
542
+ const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
543
+ const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
544
+ const pillLabel = JSON.stringify(DEEP_RESEARCH_PILL_LABEL);
545
+ // pillLabel is used inside the expression for verification
546
+ void pillLabel;
547
+ return `(async () => {
548
+ ${buildClickDispatcher()}
549
+
550
+ const findDeepResearchPill = () => {
551
+ const pills = document.querySelectorAll('.__composer-pill-composite, .__composer-pill, [class*="composer-pill"]');
552
+ for (const pill of pills) {
553
+ const text = pill.textContent?.trim() || '';
554
+ const aria = pill.getAttribute('aria-label') ||
555
+ pill.querySelector('button')?.getAttribute('aria-label') ||
556
+ '';
557
+ if (text.toLowerCase().includes('deep research') ||
558
+ aria.toLowerCase().includes('deep research')) {
559
+ return pill;
560
+ }
561
+ }
562
+ return null;
563
+ };
564
+
565
+ const waitForPill = () => new Promise((resolve) => {
566
+ let elapsed = 0;
567
+ const tick = () => {
568
+ if (findDeepResearchPill()) {
569
+ resolve(true); return;
570
+ }
571
+ elapsed += 200;
572
+ if (elapsed > 5000) { resolve(false); return; }
573
+ setTimeout(tick, 200);
574
+ };
575
+ setTimeout(tick, 200);
576
+ });
577
+
578
+ const clearComposer = (composer) => {
579
+ if (!composer) return;
580
+ if ('value' in composer) composer.value = '';
581
+ else composer.textContent = '';
582
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward', data: null }));
583
+ };
584
+
585
+ const setComposerText = (composer, text) => {
586
+ composer.focus?.();
587
+ if ('value' in composer) composer.value = text;
588
+ else composer.textContent = text;
589
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
590
+ };
591
+
592
+ const findDeepResearchItem = () => {
593
+ const target = ${targetText}.toLowerCase();
594
+ const candidates = Array.from(document.querySelectorAll('[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"]'));
595
+ return candidates.find(item => (item.textContent || '').trim().toLowerCase() === target) || null;
596
+ };
597
+
598
+ // Step 0: Check if already active
599
+ if (findDeepResearchPill()) {
600
+ return { status: 'already-active' };
601
+ }
602
+
603
+ // Step 1: Prefer the official slash command flow.
604
+ const composer = document.querySelector('[contenteditable="true"], textarea');
605
+ if (composer) {
606
+ setComposerText(composer, '/Deepresearch');
607
+ await new Promise(resolve => setTimeout(resolve, 600));
608
+ const slashItem = findDeepResearchItem();
609
+ if (slashItem) {
610
+ dispatchClickSequence(slashItem);
611
+ if (await waitForPill()) return { status: 'activated' };
612
+ }
613
+ clearComposer(composer);
614
+ }
615
+
616
+ // Step 2: Fall back to the composer tools menu.
617
+ const plusBtn = document.querySelector(${plusBtnSelector}) ||
618
+ Array.from(document.querySelectorAll('button')).find(
619
+ b => (b.getAttribute('aria-label') || '').toLowerCase().includes('add files')
620
+ );
621
+ if (!plusBtn) return { status: 'plus-button-missing' };
622
+ dispatchClickSequence(plusBtn);
623
+
624
+ // Step 3: Wait for dropdown
625
+ const waitForDropdown = () => new Promise((resolve) => {
626
+ let elapsed = 0;
627
+ const tick = () => {
628
+ const items = document.querySelectorAll('[data-radix-collection-item], [role="menuitem"], [role="menuitemradio"], [role="option"], [cmdk-item]');
629
+ if (items.length > 0) { resolve(items); return; }
630
+ elapsed += 150;
631
+ if (elapsed > 3000) { resolve(null); return; }
632
+ setTimeout(tick, 150);
633
+ };
634
+ setTimeout(tick, 150);
635
+ });
636
+ const items = await waitForDropdown();
637
+ if (!items) return { status: 'dropdown-item-missing', available: [] };
638
+
639
+ // Step 4: Find "Deep research" item
640
+ const target = ${targetText}.toLowerCase();
641
+ let match = null;
642
+ const available = [];
643
+ for (const item of items) {
644
+ const text = (item.textContent || '').trim();
645
+ available.push(text);
646
+ if (text.toLowerCase() === target) {
647
+ match = item;
648
+ }
649
+ }
650
+ if (!match) return { status: 'dropdown-item-missing', available };
651
+
652
+ // Step 5: Click it
653
+ dispatchClickSequence(match);
654
+
655
+ // Step 6: Verify pill appeared
656
+ const pillConfirmed = await waitForPill();
657
+ return pillConfirmed ? { status: 'activated' } : { status: 'pill-not-confirmed' };
658
+ })()`;
659
+ }
660
+ export function buildActivateDeepResearchExpressionForTest() {
661
+ return buildActivateDeepResearchExpression();
662
+ }
@@ -1,5 +1,5 @@
1
- const CLICK_TYPES = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
2
- export function buildClickDispatcher(functionName = 'dispatchClickSequence') {
1
+ const CLICK_TYPES = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"];
2
+ export function buildClickDispatcher(functionName = "dispatchClickSequence") {
3
3
  const typesLiteral = JSON.stringify(CLICK_TYPES);
4
4
  return `function ${functionName}(target){
5
5
  if(!target || !(target instanceof EventTarget)) return false;