@steipete/oracle 0.17.3 → 0.19.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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +75 -64
  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 +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -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 +267 -15
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +15 -3
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +447 -263
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/policies.js +6 -2
  31. package/dist/src/browser/profileState.js +114 -9
  32. package/dist/src/browser/projectSourcesRunner.js +88 -17
  33. package/dist/src/browser/prompt.js +272 -113
  34. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  35. package/dist/src/browser/reattach.js +14 -4
  36. package/dist/src/browser/sessionRunner.js +71 -11
  37. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  38. package/dist/src/browser/utils.js +20 -2
  39. package/dist/src/cli/browserConfig.js +40 -15
  40. package/dist/src/cli/browserDefaults.js +13 -0
  41. package/dist/src/cli/browserTabs.js +12 -29
  42. package/dist/src/cli/detachedSession.js +69 -0
  43. package/dist/src/cli/dryRun.js +58 -47
  44. package/dist/src/cli/harvestIntegrity.js +140 -0
  45. package/dist/src/cli/projectSources.js +5 -3
  46. package/dist/src/cli/serveBrowserConfig.js +24 -0
  47. package/dist/src/cli/sessionDisplay.js +9 -2
  48. package/dist/src/cli/sessionRunner.js +91 -7
  49. package/dist/src/config.js +1 -0
  50. package/dist/src/gemini-web/client.js +67 -21
  51. package/dist/src/gemini-web/executor.js +4 -0
  52. package/dist/src/gemini-web/http.js +15 -0
  53. package/dist/src/gemini-web/models.js +15 -11
  54. package/dist/src/mcp/server.js +11 -14
  55. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  56. package/dist/src/mcp/tools/consult.js +97 -26
  57. package/dist/src/mcp/tools/projectSources.js +4 -6
  58. package/dist/src/mcp/tools/sessionResources.js +1 -1
  59. package/dist/src/mcp/tools/sessions.js +2 -2
  60. package/dist/src/mcp/tools/wait.js +182 -0
  61. package/dist/src/mcp/types.js +7 -0
  62. package/dist/src/oracle/errors.js +11 -0
  63. package/dist/src/oracle/providerRoutePlan.js +3 -2
  64. package/dist/src/remote/client.js +70 -6
  65. package/dist/src/remote/health.js +6 -2
  66. package/dist/src/remote/runSlots.js +83 -0
  67. package/dist/src/remote/server.js +351 -72
  68. package/dist/src/sessionManager.js +36 -1
  69. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  71. package/package.json +25 -23
  72. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  74. package/dist/bin/oracle.js +0 -683
  75. package/dist/docs-site/.nojekyll +0 -0
  76. package/dist/docs-site/CNAME +0 -1
  77. package/dist/docs-site/RELEASING.html +0 -410
  78. package/dist/docs-site/agents.html +0 -374
  79. package/dist/docs-site/anthropic.html +0 -368
  80. package/dist/docs-site/bridge.html +0 -400
  81. package/dist/docs-site/browser-mode.html +0 -588
  82. package/dist/docs-site/chromium-forks.html +0 -347
  83. package/dist/docs-site/cli-reference.html +0 -346
  84. package/dist/docs-site/configuration.html +0 -436
  85. package/dist/docs-site/favicon.svg +0 -14
  86. package/dist/docs-site/followup.html +0 -368
  87. package/dist/docs-site/gemini.html +0 -379
  88. package/dist/docs-site/grok.html +0 -325
  89. package/dist/docs-site/index.html +0 -360
  90. package/dist/docs-site/install.html +0 -335
  91. package/dist/docs-site/linux.html +0 -321
  92. package/dist/docs-site/llms.txt +0 -43
  93. package/dist/docs-site/manual-tests.html +0 -595
  94. package/dist/docs-site/mcp.html +0 -368
  95. package/dist/docs-site/multimodel.html +0 -364
  96. package/dist/docs-site/mythical-pro-agents.html +0 -360
  97. package/dist/docs-site/notifier.html +0 -338
  98. package/dist/docs-site/openai-endpoints.html +0 -387
  99. package/dist/docs-site/openrouter.html +0 -344
  100. package/dist/docs-site/quickstart.html +0 -369
  101. package/dist/docs-site/refactor/ux.html +0 -532
  102. package/dist/docs-site/sessions.html +0 -388
  103. package/dist/docs-site/social-card.png +0 -0
  104. package/dist/docs-site/social-card.svg +0 -79
  105. package/dist/docs-site/spec.html +0 -363
  106. package/dist/docs-site/testing.html +0 -320
  107. package/dist/docs-site/tui-debug.html +0 -326
  108. package/dist/docs-site/windows-work.html +0 -323
  109. package/dist/docs-site/windows.html +0 -320
  110. package/dist/markdansi/types/index.js +0 -4
  111. package/dist/oracle/bin/oracle-cli.js +0 -472
  112. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  113. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  114. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  115. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  116. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  117. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  118. package/dist/oracle/src/browser/config.js +0 -33
  119. package/dist/oracle/src/browser/constants.js +0 -40
  120. package/dist/oracle/src/browser/cookies.js +0 -210
  121. package/dist/oracle/src/browser/domDebug.js +0 -36
  122. package/dist/oracle/src/browser/index.js +0 -331
  123. package/dist/oracle/src/browser/pageActions.js +0 -5
  124. package/dist/oracle/src/browser/prompt.js +0 -88
  125. package/dist/oracle/src/browser/promptSummary.js +0 -20
  126. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  127. package/dist/oracle/src/browser/types.js +0 -1
  128. package/dist/oracle/src/browser/utils.js +0 -62
  129. package/dist/oracle/src/browserMode.js +0 -1
  130. package/dist/oracle/src/cli/browserConfig.js +0 -44
  131. package/dist/oracle/src/cli/dryRun.js +0 -59
  132. package/dist/oracle/src/cli/engine.js +0 -17
  133. package/dist/oracle/src/cli/errorUtils.js +0 -9
  134. package/dist/oracle/src/cli/help.js +0 -70
  135. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  136. package/dist/oracle/src/cli/options.js +0 -103
  137. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  138. package/dist/oracle/src/cli/rootAlias.js +0 -30
  139. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  140. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  141. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  142. package/dist/oracle/src/heartbeat.js +0 -43
  143. package/dist/oracle/src/oracle/client.js +0 -48
  144. package/dist/oracle/src/oracle/config.js +0 -29
  145. package/dist/oracle/src/oracle/errors.js +0 -101
  146. package/dist/oracle/src/oracle/files.js +0 -220
  147. package/dist/oracle/src/oracle/format.js +0 -33
  148. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  149. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  150. package/dist/oracle/src/oracle/request.js +0 -48
  151. package/dist/oracle/src/oracle/run.js +0 -444
  152. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  153. package/dist/oracle/src/oracle/types.js +0 -1
  154. package/dist/oracle/src/oracle.js +0 -9
  155. package/dist/oracle/src/sessionManager.js +0 -205
  156. package/dist/oracle/src/version.js +0 -39
  157. package/dist/scripts/chrome/browser-tools.js +0 -295
  158. package/dist/src/browser/chromeCookies.js +0 -312
  159. package/dist/src/browser/keytarShim.js +0 -56
  160. package/dist/src/browser/profileSync.js +0 -141
  161. package/dist/src/browser/windowsCookies.js +0 -219
@@ -1,471 +0,0 @@
1
- import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, STOP_BUTTON_SELECTOR, } from '../constants.js';
2
- import { delay } from '../utils.js';
3
- import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression } from '../domDebug.js';
4
- const ASSISTANT_POLL_TIMEOUT_ERROR = 'assistant-response-watchdog-timeout';
5
- export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
6
- logger('Waiting for ChatGPT response');
7
- const expression = buildResponseObserverExpression(timeoutMs);
8
- const evaluationPromise = Runtime.evaluate({ expression, awaitPromise: true, returnByValue: true });
9
- const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: 'evaluation', value }), (error) => {
10
- throw { source: 'evaluation', error };
11
- });
12
- const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs).then((value) => {
13
- if (!value) {
14
- throw { source: 'poll', error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
15
- }
16
- return { kind: 'poll', value };
17
- }, (error) => {
18
- throw { source: 'poll', error };
19
- });
20
- let evaluation = null;
21
- try {
22
- const winner = await Promise.race([raceReadyEvaluation, pollerPromise]);
23
- if (winner.kind === 'poll') {
24
- logger('Captured assistant response via snapshot watchdog');
25
- evaluationPromise.catch(() => undefined);
26
- await terminateRuntimeExecution(Runtime);
27
- return winner.value;
28
- }
29
- evaluation = winner.value;
30
- }
31
- catch (wrappedError) {
32
- if (wrappedError && typeof wrappedError === 'object' && 'source' in wrappedError && 'error' in wrappedError) {
33
- const { source, error } = wrappedError;
34
- if (source === 'poll' && error instanceof Error && error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
35
- evaluation = await evaluationPromise;
36
- }
37
- else if (source === 'poll') {
38
- throw error;
39
- }
40
- else if (source === 'evaluation') {
41
- const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger);
42
- if (recovered) {
43
- return recovered;
44
- }
45
- await logDomFailure(Runtime, logger, 'assistant-response');
46
- throw error ?? new Error('Failed to capture assistant response');
47
- }
48
- }
49
- else {
50
- throw wrappedError;
51
- }
52
- }
53
- if (!evaluation) {
54
- await logDomFailure(Runtime, logger, 'assistant-response');
55
- throw new Error('Failed to capture assistant response');
56
- }
57
- const parsed = await parseAssistantEvaluationResult(Runtime, evaluation, timeoutMs, logger);
58
- if (parsed) {
59
- return parsed;
60
- }
61
- await logDomFailure(Runtime, logger, 'assistant-response');
62
- throw new Error('Unable to capture assistant response');
63
- }
64
- export async function readAssistantSnapshot(Runtime) {
65
- const { result } = await Runtime.evaluate({ expression: buildAssistantSnapshotExpression(), returnByValue: true });
66
- const value = result?.value;
67
- if (value && typeof value === 'object') {
68
- return value;
69
- }
70
- return null;
71
- }
72
- export async function captureAssistantMarkdown(Runtime, meta, logger) {
73
- const { result } = await Runtime.evaluate({
74
- expression: buildCopyExpression(meta),
75
- returnByValue: true,
76
- awaitPromise: true,
77
- });
78
- if (result?.value?.success && typeof result.value.markdown === 'string') {
79
- return result.value.markdown;
80
- }
81
- const status = result?.value?.status;
82
- if (status && status !== 'missing-button') {
83
- logger(`Copy button fallback status: ${status}`);
84
- await logDomFailure(Runtime, logger, 'copy-markdown');
85
- }
86
- if (!status) {
87
- await logDomFailure(Runtime, logger, 'copy-markdown');
88
- }
89
- return null;
90
- }
91
- export function buildAssistantExtractorForTest(name) {
92
- return buildAssistantExtractor(name);
93
- }
94
- export function buildConversationDebugExpressionForTest() {
95
- return buildConversationDebugExpression();
96
- }
97
- async function recoverAssistantResponse(Runtime, timeoutMs, logger) {
98
- const snapshot = await waitForAssistantSnapshot(Runtime, Math.min(timeoutMs, 10_000));
99
- const recovered = normalizeAssistantSnapshot(snapshot);
100
- if (recovered) {
101
- logger('Recovered assistant response via polling fallback');
102
- return recovered;
103
- }
104
- await logConversationSnapshot(Runtime, logger).catch(() => undefined);
105
- return null;
106
- }
107
- async function parseAssistantEvaluationResult(Runtime, evaluation, timeoutMs, logger) {
108
- const { result } = evaluation;
109
- if (result.type === 'object' && result.value && typeof result.value === 'object' && 'text' in result.value) {
110
- const html = typeof result.value.html === 'string'
111
- ? (result.value.html ?? undefined)
112
- : undefined;
113
- const turnId = typeof result.value.turnId === 'string'
114
- ? (result.value.turnId ?? undefined)
115
- : undefined;
116
- const messageId = typeof result.value.messageId === 'string'
117
- ? (result.value.messageId ?? undefined)
118
- : undefined;
119
- return {
120
- text: String(result.value.text ?? ''),
121
- html,
122
- meta: { turnId, messageId },
123
- };
124
- }
125
- const fallbackText = typeof result.value === 'string' ? result.value : '';
126
- if (!fallbackText) {
127
- const recovered = await recoverAssistantResponse(Runtime, Math.min(timeoutMs, 10_000), logger);
128
- if (recovered) {
129
- return recovered;
130
- }
131
- return null;
132
- }
133
- return { text: fallbackText, html: undefined, meta: {} };
134
- }
135
- async function terminateRuntimeExecution(Runtime) {
136
- if (typeof Runtime.terminateExecution !== 'function') {
137
- return;
138
- }
139
- try {
140
- await Runtime.terminateExecution();
141
- }
142
- catch {
143
- // ignore termination failures
144
- }
145
- }
146
- async function pollAssistantCompletion(Runtime, timeoutMs) {
147
- const watchdogDeadline = Date.now() + timeoutMs;
148
- let previousLength = 0;
149
- let stableCycles = 0;
150
- const requiredStableCycles = 6;
151
- while (Date.now() < watchdogDeadline) {
152
- const snapshot = await readAssistantSnapshot(Runtime);
153
- const normalized = normalizeAssistantSnapshot(snapshot);
154
- if (normalized) {
155
- const currentLength = normalized.text.length;
156
- if (currentLength > previousLength) {
157
- previousLength = currentLength;
158
- stableCycles = 0;
159
- }
160
- else {
161
- stableCycles += 1;
162
- }
163
- const stopVisible = await isStopButtonVisible(Runtime);
164
- if (!stopVisible && stableCycles >= requiredStableCycles) {
165
- return normalized;
166
- }
167
- }
168
- else {
169
- previousLength = 0;
170
- stableCycles = 0;
171
- }
172
- await delay(400);
173
- }
174
- return null;
175
- }
176
- async function isStopButtonVisible(Runtime) {
177
- try {
178
- const { result } = await Runtime.evaluate({
179
- expression: `Boolean(document.querySelector('${STOP_BUTTON_SELECTOR}'))`,
180
- returnByValue: true,
181
- });
182
- return Boolean(result?.value);
183
- }
184
- catch {
185
- return false;
186
- }
187
- }
188
- function normalizeAssistantSnapshot(snapshot) {
189
- const text = snapshot?.text?.trim();
190
- if (!text) {
191
- return null;
192
- }
193
- return {
194
- text,
195
- html: snapshot?.html ?? undefined,
196
- meta: { turnId: snapshot?.turnId ?? undefined, messageId: snapshot?.messageId ?? undefined },
197
- };
198
- }
199
- async function waitForAssistantSnapshot(Runtime, timeoutMs) {
200
- return waitForCondition(() => readAssistantSnapshot(Runtime), timeoutMs);
201
- }
202
- async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
203
- const deadline = Date.now() + timeoutMs;
204
- while (Date.now() < deadline) {
205
- const value = await getter();
206
- if (value) {
207
- return value;
208
- }
209
- await delay(pollIntervalMs);
210
- }
211
- return null;
212
- }
213
- function buildAssistantSnapshotExpression() {
214
- return `(() => {
215
- ${buildAssistantExtractor('extractAssistantTurn')}
216
- return extractAssistantTurn();
217
- })()`;
218
- }
219
- function buildResponseObserverExpression(timeoutMs) {
220
- const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
221
- return `(() => {
222
- const SELECTORS = ${selectorsLiteral};
223
- const STOP_SELECTOR = '${STOP_BUTTON_SELECTOR}';
224
- const settleDelayMs = 800;
225
- ${buildAssistantExtractor('extractFromTurns')}
226
-
227
- const captureViaObserver = () =>
228
- new Promise((resolve, reject) => {
229
- const deadline = Date.now() + ${timeoutMs};
230
- let stopInterval = null;
231
- const observer = new MutationObserver(() => {
232
- const extracted = extractFromTurns();
233
- if (extracted) {
234
- observer.disconnect();
235
- if (stopInterval) {
236
- clearInterval(stopInterval);
237
- }
238
- resolve(extracted);
239
- } else if (Date.now() > deadline) {
240
- observer.disconnect();
241
- if (stopInterval) {
242
- clearInterval(stopInterval);
243
- }
244
- reject(new Error('Response timeout'));
245
- }
246
- });
247
- observer.observe(document.body, { childList: true, subtree: true, characterData: true });
248
- stopInterval = setInterval(() => {
249
- const stop = document.querySelector(STOP_SELECTOR);
250
- if (!stop) {
251
- return;
252
- }
253
- const ariaLabel = stop.getAttribute('aria-label') || '';
254
- if (ariaLabel.toLowerCase().includes('stop')) {
255
- return;
256
- }
257
- stop.click();
258
- }, 500);
259
- setTimeout(() => {
260
- if (stopInterval) {
261
- clearInterval(stopInterval);
262
- }
263
- observer.disconnect();
264
- reject(new Error('Response timeout'));
265
- }, ${timeoutMs});
266
- });
267
-
268
- const waitForSettle = async (snapshot) => {
269
- const settleWindowMs = 5000;
270
- const settleIntervalMs = 400;
271
- const deadline = Date.now() + settleWindowMs;
272
- let latest = snapshot;
273
- let lastLength = snapshot?.text?.length ?? 0;
274
- while (Date.now() < deadline) {
275
- await new Promise((resolve) => setTimeout(resolve, settleIntervalMs));
276
- const refreshed = extractFromTurns();
277
- if (refreshed && (refreshed.text?.length ?? 0) >= lastLength) {
278
- latest = refreshed;
279
- lastLength = refreshed.text?.length ?? lastLength;
280
- }
281
- const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
282
- if (!stopVisible) {
283
- break;
284
- }
285
- }
286
- return latest ?? snapshot;
287
- };
288
-
289
- const extracted = extractFromTurns();
290
- if (extracted) {
291
- return waitForSettle(extracted);
292
- }
293
- return captureViaObserver().then((payload) => waitForSettle(payload));
294
- })()`;
295
- }
296
- function buildAssistantExtractor(functionName) {
297
- const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
298
- const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
299
- return `const ${functionName} = () => {
300
- const CONVERSATION_SELECTOR = ${conversationLiteral};
301
- const ASSISTANT_SELECTOR = ${assistantLiteral};
302
- const isAssistantTurn = (node) => {
303
- if (!(node instanceof HTMLElement)) return false;
304
- const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
305
- if (role === 'assistant') {
306
- return true;
307
- }
308
- const testId = (node.getAttribute('data-testid') || '').toLowerCase();
309
- if (testId.includes('assistant')) {
310
- return true;
311
- }
312
- return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
313
- };
314
-
315
- const expandCollapsibles = (root) => {
316
- const buttons = Array.from(root.querySelectorAll('button'));
317
- for (const button of buttons) {
318
- const label = (button.textContent || '').toLowerCase();
319
- const testid = (button.getAttribute('data-testid') || '').toLowerCase();
320
- if (
321
- label.includes('more') ||
322
- label.includes('expand') ||
323
- label.includes('show') ||
324
- testid.includes('markdown') ||
325
- testid.includes('toggle')
326
- ) {
327
- button.click();
328
- }
329
- }
330
- };
331
-
332
- const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
333
- for (let index = turns.length - 1; index >= 0; index -= 1) {
334
- const turn = turns[index];
335
- if (!isAssistantTurn(turn)) {
336
- continue;
337
- }
338
- const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) ?? turn;
339
- expandCollapsibles(messageRoot);
340
- const preferred =
341
- messageRoot.querySelector('.markdown') ||
342
- messageRoot.querySelector('[data-message-content]') ||
343
- messageRoot;
344
- const text = preferred?.innerText ?? '';
345
- const html = preferred?.innerHTML ?? '';
346
- const messageId = messageRoot.getAttribute('data-message-id');
347
- const turnId = messageRoot.getAttribute('data-testid');
348
- if (text.trim()) {
349
- return { text, html, messageId, turnId };
350
- }
351
- }
352
- return null;
353
- };`;
354
- }
355
- function buildCopyExpression(meta) {
356
- return `(() => {
357
- const BUTTON_SELECTOR = '${COPY_BUTTON_SELECTOR}';
358
- const TIMEOUT_MS = 5000;
359
-
360
- const locateButton = () => {
361
- const hint = ${JSON.stringify(meta ?? {})};
362
- if (hint?.messageId) {
363
- const node = document.querySelector('[data-message-id="' + hint.messageId + '"]');
364
- const buttons = node ? Array.from(node.querySelectorAll('${COPY_BUTTON_SELECTOR}')) : [];
365
- const button = buttons.at(-1) ?? null;
366
- if (button) {
367
- return button;
368
- }
369
- }
370
- if (hint?.turnId) {
371
- const node = document.querySelector('[data-testid="' + hint.turnId + '"]');
372
- const buttons = node ? Array.from(node.querySelectorAll('${COPY_BUTTON_SELECTOR}')) : [];
373
- const button = buttons.at(-1) ?? null;
374
- if (button) {
375
- return button;
376
- }
377
- }
378
- const all = Array.from(document.querySelectorAll(BUTTON_SELECTOR));
379
- return all.at(-1) ?? null;
380
- };
381
-
382
- const interceptClipboard = () => {
383
- const clipboard = navigator.clipboard;
384
- const state = { text: '' };
385
- if (!clipboard) {
386
- return { state, restore: () => {} };
387
- }
388
- const originalWriteText = clipboard.writeText;
389
- const originalWrite = clipboard.write;
390
- clipboard.writeText = (value) => {
391
- state.text = typeof value === 'string' ? value : '';
392
- return Promise.resolve();
393
- };
394
- clipboard.write = async (items) => {
395
- try {
396
- const list = Array.isArray(items) ? items : items ? [items] : [];
397
- for (const item of list) {
398
- if (!item) continue;
399
- const types = Array.isArray(item.types) ? item.types : [];
400
- if (types.includes('text/plain') && typeof item.getType === 'function') {
401
- const blob = await item.getType('text/plain');
402
- const text = await blob.text();
403
- state.text = text ?? '';
404
- break;
405
- }
406
- }
407
- } catch {
408
- state.text = '';
409
- }
410
- return Promise.resolve();
411
- };
412
- return {
413
- state,
414
- restore: () => {
415
- clipboard.writeText = originalWriteText;
416
- clipboard.write = originalWrite;
417
- },
418
- };
419
- };
420
-
421
- return new Promise((resolve) => {
422
- const button = locateButton();
423
- if (!button) {
424
- resolve({ success: false, status: 'missing-button' });
425
- return;
426
- }
427
- const interception = interceptClipboard();
428
- let settled = false;
429
- let pollId = null;
430
- let timeoutId = null;
431
- const finish = (payload) => {
432
- if (settled) {
433
- return;
434
- }
435
- settled = true;
436
- if (pollId) {
437
- clearInterval(pollId);
438
- }
439
- if (timeoutId) {
440
- clearTimeout(timeoutId);
441
- }
442
- button.removeEventListener('copy', handleCopy, true);
443
- interception.restore?.();
444
- resolve(payload);
445
- };
446
-
447
- const readIntercepted = () => {
448
- const markdown = interception.state.text ?? '';
449
- return { success: Boolean(markdown.trim()), markdown };
450
- };
451
-
452
- const handleCopy = () => {
453
- finish(readIntercepted());
454
- };
455
-
456
- button.addEventListener('copy', handleCopy, true);
457
- button.scrollIntoView({ block: 'center', behavior: 'instant' });
458
- button.click();
459
- pollId = setInterval(() => {
460
- const payload = readIntercepted();
461
- if (payload.success) {
462
- finish(payload);
463
- }
464
- }, 100);
465
- timeoutId = setTimeout(() => {
466
- button.removeEventListener('copy', handleCopy, true);
467
- finish({ success: false, status: 'timeout' });
468
- }, TIMEOUT_MS);
469
- });
470
- })()`;
471
- }
@@ -1,82 +0,0 @@
1
- import path from 'node:path';
2
- import { FILE_INPUT_SELECTOR, GENERIC_FILE_INPUT_SELECTOR, SEND_BUTTON_SELECTOR, UPLOAD_STATUS_SELECTORS, } from '../constants.js';
3
- import { delay } from '../utils.js';
4
- import { logDomFailure } from '../domDebug.js';
5
- export async function uploadAttachmentFile(deps, attachment, logger) {
6
- const { runtime, dom } = deps;
7
- if (!dom) {
8
- throw new Error('DOM domain unavailable while uploading attachments.');
9
- }
10
- const documentNode = await dom.getDocument();
11
- const selectors = [FILE_INPUT_SELECTOR, GENERIC_FILE_INPUT_SELECTOR];
12
- let targetNodeId;
13
- for (const selector of selectors) {
14
- const result = await dom.querySelector({ nodeId: documentNode.root.nodeId, selector });
15
- if (result.nodeId) {
16
- targetNodeId = result.nodeId;
17
- break;
18
- }
19
- }
20
- if (!targetNodeId) {
21
- await logDomFailure(runtime, logger, 'file-input');
22
- throw new Error('Unable to locate ChatGPT file attachment input.');
23
- }
24
- await dom.setFileInputFiles({ nodeId: targetNodeId, files: [attachment.path] });
25
- const expectedName = path.basename(attachment.path);
26
- const ready = await waitForAttachmentSelection(runtime, expectedName, 10_000);
27
- if (!ready) {
28
- await logDomFailure(runtime, logger, 'file-upload');
29
- throw new Error('Attachment did not register with the ChatGPT composer in time.');
30
- }
31
- logger(`Attachment queued: ${attachment.displayPath}`);
32
- }
33
- export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
34
- const deadline = Date.now() + timeoutMs;
35
- const expression = `(() => {
36
- const button = document.querySelector('${SEND_BUTTON_SELECTOR}');
37
- if (!button) {
38
- return { state: 'missing', uploading: false };
39
- }
40
- const disabled = button.hasAttribute('disabled') || button.getAttribute('aria-disabled') === 'true';
41
- const uploadingSelectors = ${JSON.stringify(UPLOAD_STATUS_SELECTORS)};
42
- const uploading = uploadingSelectors.some((selector) => {
43
- return Array.from(document.querySelectorAll(selector)).some((node) => {
44
- const text = node.textContent?.toLowerCase?.() ?? '';
45
- return text.includes('upload') || text.includes('processing');
46
- });
47
- });
48
- return { state: disabled ? 'disabled' : 'ready', uploading };
49
- })()`;
50
- while (Date.now() < deadline) {
51
- const { result } = await Runtime.evaluate({ expression, returnByValue: true });
52
- const value = result?.value;
53
- if (value && value.state === 'ready' && !value.uploading) {
54
- return;
55
- }
56
- await delay(250);
57
- }
58
- logger?.('Attachment upload timed out while waiting for ChatGPT composer to become ready.');
59
- await logDomFailure(Runtime, logger ?? (() => { }), 'file-upload-timeout');
60
- throw new Error('Attachments did not finish uploading before timeout.');
61
- }
62
- async function waitForAttachmentSelection(Runtime, expectedName, timeoutMs) {
63
- const deadline = Date.now() + timeoutMs;
64
- const expression = `(() => {
65
- const selector = ${JSON.stringify(GENERIC_FILE_INPUT_SELECTOR)};
66
- const input = document.querySelector(selector);
67
- if (!input || !input.files) {
68
- return { matched: false, names: [] };
69
- }
70
- const names = Array.from(input.files).map((file) => file?.name ?? '');
71
- return { matched: names.some((name) => name === ${JSON.stringify(expectedName)}), names };
72
- })()`;
73
- while (Date.now() < deadline) {
74
- const { result } = await Runtime.evaluate({ expression, returnByValue: true });
75
- const matched = Boolean(result?.value?.matched);
76
- if (matched) {
77
- return true;
78
- }
79
- await delay(150);
80
- }
81
- return false;
82
- }