@steipete/oracle 1.0.7 → 1.1.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.
- package/README.md +3 -0
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +9 -3
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/src/cli/help.js +1 -0
- package/dist/src/cli/markdownRenderer.js +18 -0
- package/dist/src/cli/rootAlias.js +14 -0
- package/dist/src/cli/sessionCommand.js +60 -2
- package/dist/src/cli/sessionDisplay.js +129 -4
- package/dist/src/oracle/oscProgress.js +60 -0
- package/dist/src/oracle/run.js +63 -51
- package/dist/src/sessionManager.js +17 -0
- package/package.json +14 -22
|
@@ -0,0 +1,471 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
}
|