@steipete/oracle 0.18.0 → 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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  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 +119 -16
  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 +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. 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
- }