@steipete/oracle 0.8.1 → 0.8.4
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/LICENSE +1 -1
- package/README.md +2 -2
- package/dist/bin/oracle-cli.js +2 -0
- package/dist/src/browser/actions/attachments.js +274 -106
- package/dist/src/browser/actions/navigation.js +115 -0
- package/dist/src/browser/actions/thinkingTime.js +2 -0
- package/dist/src/browser/chromeLifecycle.js +2 -0
- package/dist/src/browser/config.js +2 -0
- package/dist/src/browser/cookies.js +29 -2
- package/dist/src/browser/index.js +48 -15
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/reattach.js +1 -0
- package/dist/src/cli/browserConfig.js +1 -0
- package/dist/src/cli/browserDefaults.js +3 -0
- package/package.json +5 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Oracle bundles your prompt and files so another AI can answer with real context.
|
|
|
18
18
|
Install globally: `npm install -g @steipete/oracle`
|
|
19
19
|
Homebrew: `brew install steipete/tap/oracle`
|
|
20
20
|
|
|
21
|
-
Requires Node 22+.
|
|
21
|
+
Requires Node 22+. Or use `npx -y @steipete/oracle …` (or pnpx).
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# Copy the bundle and paste into ChatGPT
|
|
@@ -149,7 +149,7 @@ Advanced flags
|
|
|
149
149
|
|
|
150
150
|
| Area | Flags |
|
|
151
151
|
| --- | --- |
|
|
152
|
-
| Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
152
|
+
| Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-cookie-wait`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
153
153
|
| Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |
|
|
154
154
|
|
|
155
155
|
Remote browser example
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -163,6 +163,7 @@ program
|
|
|
163
163
|
.addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
|
|
164
164
|
.addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
|
|
165
165
|
.addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
|
|
166
|
+
.addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
|
|
166
167
|
.addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
|
|
167
168
|
.argParser(parseIntOption))
|
|
168
169
|
.addOption(new Option('--browser-debug-port <port>', '(alias) Use a fixed Chrome DevTools port.').argParser(parseIntOption).hideHelp())
|
|
@@ -926,6 +927,7 @@ function printDebugHelp(cliName) {
|
|
|
926
927
|
['--browser-url <url>', 'Alias for --chatgpt-url.'],
|
|
927
928
|
['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
|
|
928
929
|
['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
|
|
930
|
+
['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
|
|
929
931
|
['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
|
|
930
932
|
['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
|
|
931
933
|
['--browser-headless', 'Launch Chrome in headless mode.'],
|
|
@@ -4,7 +4,7 @@ import { delay } from '../utils.js';
|
|
|
4
4
|
import { logDomFailure } from '../domDebug.js';
|
|
5
5
|
import { transferAttachmentViaDataTransfer } from './attachmentDataTransfer.js';
|
|
6
6
|
export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
7
|
-
const { runtime, dom } = deps;
|
|
7
|
+
const { runtime, dom, input } = deps;
|
|
8
8
|
if (!dom) {
|
|
9
9
|
throw new Error('DOM domain unavailable while uploading attachments.');
|
|
10
10
|
}
|
|
@@ -170,7 +170,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+files
|
|
173
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
174
174
|
const collectFileCount = (candidates) => {
|
|
175
175
|
let count = 0;
|
|
176
176
|
for (const node of candidates) {
|
|
@@ -204,7 +204,8 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
204
204
|
let hasFileHint = false;
|
|
205
205
|
for (const raw of values) {
|
|
206
206
|
if (!raw) continue;
|
|
207
|
-
|
|
207
|
+
const normalized = normalize(raw);
|
|
208
|
+
if (normalized.includes('file') || normalized.includes('attachment')) {
|
|
208
209
|
hasFileHint = true;
|
|
209
210
|
break;
|
|
210
211
|
}
|
|
@@ -273,43 +274,70 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
273
274
|
};
|
|
274
275
|
};
|
|
275
276
|
// New ChatGPT UI hides the real file input behind a composer "+" menu; click it pre-emptively.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
277
|
+
// Learned: synthetic `.click()` is sometimes ignored (isTrusted checks). Prefer a CDP mouse click when possible.
|
|
278
|
+
const clickPlusTrusted = async () => {
|
|
279
|
+
if (!input || typeof input.dispatchMouseEvent !== 'function')
|
|
280
|
+
return false;
|
|
281
|
+
const locate = await runtime
|
|
282
|
+
.evaluate({
|
|
283
|
+
expression: `(() => {
|
|
284
|
+
const selectors = [
|
|
285
|
+
'#composer-plus-btn',
|
|
286
|
+
'button[data-testid="composer-plus-btn"]',
|
|
287
|
+
'[data-testid*="plus"]',
|
|
288
|
+
'button[aria-label*="add"]',
|
|
289
|
+
'button[aria-label*="attachment"]',
|
|
290
|
+
'button[aria-label*="file"]',
|
|
291
|
+
];
|
|
292
|
+
for (const selector of selectors) {
|
|
293
|
+
const el = document.querySelector(selector);
|
|
294
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
295
|
+
const rect = el.getBoundingClientRect();
|
|
296
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
297
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
298
|
+
const nextRect = el.getBoundingClientRect();
|
|
299
|
+
return { ok: true, x: nextRect.left + nextRect.width / 2, y: nextRect.top + nextRect.height / 2 };
|
|
291
300
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
301
|
+
return { ok: false };
|
|
302
|
+
})()`,
|
|
303
|
+
returnByValue: true,
|
|
304
|
+
})
|
|
305
|
+
.then((res) => res?.result?.value)
|
|
306
|
+
.catch(() => undefined);
|
|
307
|
+
if (!locate?.ok || typeof locate.x !== 'number' || typeof locate.y !== 'number')
|
|
308
|
+
return false;
|
|
309
|
+
const x = locate.x;
|
|
310
|
+
const y = locate.y;
|
|
311
|
+
await input.dispatchMouseEvent({ type: 'mouseMoved', x, y });
|
|
312
|
+
await input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
313
|
+
await input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
314
|
+
return true;
|
|
315
|
+
};
|
|
316
|
+
const clickedTrusted = await clickPlusTrusted().catch(() => false);
|
|
317
|
+
if (!clickedTrusted) {
|
|
318
|
+
await Promise.resolve(runtime.evaluate({
|
|
319
|
+
expression: `(() => {
|
|
320
|
+
const selectors = [
|
|
321
|
+
'#composer-plus-btn',
|
|
322
|
+
'button[data-testid="composer-plus-btn"]',
|
|
323
|
+
'[data-testid*="plus"]',
|
|
324
|
+
'button[aria-label*="add"]',
|
|
325
|
+
'button[aria-label*="attachment"]',
|
|
326
|
+
'button[aria-label*="file"]',
|
|
327
|
+
];
|
|
328
|
+
for (const selector of selectors) {
|
|
329
|
+
const el = document.querySelector(selector);
|
|
330
|
+
if (el instanceof HTMLElement) {
|
|
331
|
+
el.click();
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
307
334
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
335
|
+
return false;
|
|
336
|
+
})()`,
|
|
337
|
+
returnByValue: true,
|
|
338
|
+
})).catch(() => undefined);
|
|
339
|
+
}
|
|
340
|
+
await delay(350);
|
|
313
341
|
const normalizeForMatch = (value) => String(value || '')
|
|
314
342
|
.toLowerCase()
|
|
315
343
|
.replace(/\s+/g, ' ')
|
|
@@ -461,7 +489,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
461
489
|
return /\\buploading\\b/.test(text) || /\\bprocessing\\b/.test(text);
|
|
462
490
|
});
|
|
463
491
|
});
|
|
464
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+files
|
|
492
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
465
493
|
const collectFileCount = (candidates) => {
|
|
466
494
|
let count = 0;
|
|
467
495
|
for (const node of candidates) {
|
|
@@ -495,7 +523,8 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
495
523
|
let hasFileHint = false;
|
|
496
524
|
for (const raw of values) {
|
|
497
525
|
if (!raw) continue;
|
|
498
|
-
|
|
526
|
+
const lowered = String(raw).toLowerCase();
|
|
527
|
+
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
499
528
|
hasFileHint = true;
|
|
500
529
|
break;
|
|
501
530
|
}
|
|
@@ -537,17 +566,28 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
537
566
|
}
|
|
538
567
|
|
|
539
568
|
// Mark candidates with stable indices so we can select them via DOM.querySelector.
|
|
569
|
+
// Learned: ChatGPT sometimes renders a zero-sized file input that does *not* trigger uploads;
|
|
570
|
+
// keep it as a fallback, but strongly prefer visible (even sr-only 1x1) inputs.
|
|
571
|
+
const localSet = new Set(localInputs);
|
|
540
572
|
let idx = 0;
|
|
541
573
|
let candidates = inputs.map((el) => {
|
|
542
574
|
const accept = el.getAttribute('accept') || '';
|
|
543
575
|
const imageOnly = acceptIsImageOnly(accept);
|
|
576
|
+
const rect = el instanceof HTMLElement ? el.getBoundingClientRect() : { width: 0, height: 0 };
|
|
577
|
+
const visible = rect.width > 0 && rect.height > 0;
|
|
578
|
+
const local = localSet.has(el);
|
|
544
579
|
const score =
|
|
545
580
|
(el.hasAttribute('multiple') ? 100 : 0) +
|
|
546
|
-
(
|
|
581
|
+
(local ? 40 : 0) +
|
|
582
|
+
(visible ? 30 : -200) +
|
|
583
|
+
(!imageOnly ? 30 : isImageAttachment ? 20 : 5);
|
|
547
584
|
el.setAttribute('data-oracle-upload-candidate', 'true');
|
|
548
585
|
el.setAttribute('data-oracle-upload-idx', String(idx));
|
|
549
586
|
return { idx: idx++, score, imageOnly };
|
|
550
587
|
});
|
|
588
|
+
|
|
589
|
+
// When the attachment isn't an image, avoid inputs that only accept images.
|
|
590
|
+
// Some ChatGPT surfaces expose multiple file inputs (e.g. image-only vs generic upload).
|
|
551
591
|
if (!isImageAttachment) {
|
|
552
592
|
const nonImage = candidates.filter((candidate) => !candidate.imageOnly);
|
|
553
593
|
if (nonImage.length > 0) {
|
|
@@ -798,8 +838,8 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
798
838
|
continue;
|
|
799
839
|
}
|
|
800
840
|
const baselineInputSnapshot = await readInputSnapshot(idx);
|
|
801
|
-
const gatherSignals = async () => {
|
|
802
|
-
const signalResult = await waitForAttachmentUiSignal(
|
|
841
|
+
const gatherSignals = async (waitMs = attachmentUiSignalWaitMs) => {
|
|
842
|
+
const signalResult = await waitForAttachmentUiSignal(waitMs);
|
|
803
843
|
const postInputSnapshot = await readInputSnapshot(idx);
|
|
804
844
|
const postInputSignals = inputSignalsFor(baselineInputSnapshot, postInputSnapshot);
|
|
805
845
|
const snapshot = await runtime
|
|
@@ -822,25 +862,25 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
822
862
|
const evaluateSignals = async (signalResult, postInputSignals, immediateInputMatch) => {
|
|
823
863
|
const expectedSatisfied = Boolean(signalResult?.expectedSatisfied) ||
|
|
824
864
|
(signalResult?.signals ? isExpectedSatisfied(signalResult.signals) : false);
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
865
|
+
const inputNameCandidates = resolveInputNameCandidates();
|
|
866
|
+
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
867
|
+
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
868
|
+
const inputEvidence = immediateInputMatch ||
|
|
869
|
+
postInputSignals.touched ||
|
|
870
|
+
Boolean(signalResult?.signals?.input) ||
|
|
871
|
+
Boolean(signalResult?.inputDelta) ||
|
|
872
|
+
inputHasFile;
|
|
873
|
+
const uiDirect = Boolean(signalResult?.signals?.ui) || expectedSatisfied;
|
|
874
|
+
const uiDelta = Boolean(signalResult?.chipDelta) || Boolean(signalResult?.uploadDelta) || Boolean(signalResult?.fileCountDelta);
|
|
875
|
+
if (uiDirect || (uiDelta && inputEvidence)) {
|
|
831
876
|
return { status: 'ui' };
|
|
832
877
|
}
|
|
833
878
|
const postSignals = await readAttachmentSignals(expectedName);
|
|
834
879
|
if (postSignals.ui ||
|
|
835
880
|
isExpectedSatisfied(postSignals) ||
|
|
836
|
-
hasChipDelta(postSignals) ||
|
|
837
|
-
hasUploadDelta(postSignals) ||
|
|
838
|
-
hasFileCountDelta(postSignals)) {
|
|
881
|
+
((hasChipDelta(postSignals) || hasUploadDelta(postSignals) || hasFileCountDelta(postSignals)) && inputEvidence)) {
|
|
839
882
|
return { status: 'ui' };
|
|
840
883
|
}
|
|
841
|
-
const inputNameCandidates = resolveInputNameCandidates();
|
|
842
|
-
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
843
|
-
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
844
884
|
const inputSignal = immediateInputMatch ||
|
|
845
885
|
postInputSignals.touched ||
|
|
846
886
|
Boolean(signalResult?.signals?.input) ||
|
|
@@ -883,15 +923,56 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
883
923
|
const evaluation = await evaluateSignals(signalState.signalResult, signalState.postInputSignals, immediateInputMatch);
|
|
884
924
|
return { evaluation, signalState, immediateInputMatch };
|
|
885
925
|
};
|
|
926
|
+
const dispatchInputEvents = async () => {
|
|
927
|
+
await runtime
|
|
928
|
+
.evaluate({
|
|
929
|
+
expression: `(() => {
|
|
930
|
+
const input = document.querySelector('input[type="file"][data-oracle-upload-idx="${idx}"]');
|
|
931
|
+
if (!(input instanceof HTMLInputElement)) return false;
|
|
932
|
+
try {
|
|
933
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
934
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
935
|
+
return true;
|
|
936
|
+
} catch {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
})()`,
|
|
940
|
+
returnByValue: true,
|
|
941
|
+
})
|
|
942
|
+
.catch(() => undefined);
|
|
943
|
+
};
|
|
886
944
|
let result = await runInputAttempt('set');
|
|
887
945
|
if (result.evaluation.status === 'ui') {
|
|
888
946
|
confirmedAttachment = true;
|
|
889
947
|
break;
|
|
890
948
|
}
|
|
891
949
|
if (result.evaluation.status === 'input') {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
950
|
+
await dispatchInputEvents();
|
|
951
|
+
await delay(150);
|
|
952
|
+
const forcedState = await gatherSignals(1_500);
|
|
953
|
+
const forcedEvaluation = await evaluateSignals(forcedState.signalResult, forcedState.postInputSignals, result.immediateInputMatch);
|
|
954
|
+
if (forcedEvaluation.status === 'ui') {
|
|
955
|
+
confirmedAttachment = true;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
if (forcedEvaluation.status === 'input') {
|
|
959
|
+
logger('Attachment input set; proceeding without UI confirmation.');
|
|
960
|
+
inputConfirmed = true;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
logger('Attachment input set; retrying with data transfer to trigger ChatGPT upload.');
|
|
964
|
+
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [] }).catch(() => undefined);
|
|
965
|
+
await delay(150);
|
|
966
|
+
result = await runInputAttempt('transfer');
|
|
967
|
+
if (result.evaluation.status === 'ui') {
|
|
968
|
+
confirmedAttachment = true;
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
if (result.evaluation.status === 'input') {
|
|
972
|
+
logger('Attachment input set; proceeding without UI confirmation.');
|
|
973
|
+
inputConfirmed = true;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
895
976
|
}
|
|
896
977
|
const lateSignals = await readAttachmentSignals(expectedName);
|
|
897
978
|
if (lateSignals.ui ||
|
|
@@ -1007,12 +1088,12 @@ export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
|
1007
1088
|
return parentHasSend ? parent : root;
|
|
1008
1089
|
})();
|
|
1009
1090
|
const removeSelectors = [
|
|
1010
|
-
'[aria-label
|
|
1011
|
-
'[aria-label
|
|
1012
|
-
'
|
|
1013
|
-
'
|
|
1014
|
-
'[data-testid*="remove"]',
|
|
1015
|
-
'[data-testid*="
|
|
1091
|
+
'[aria-label="Remove file"]',
|
|
1092
|
+
'button[aria-label="Remove file"]',
|
|
1093
|
+
'[aria-label*="Remove file"]',
|
|
1094
|
+
'[aria-label*="remove file"]',
|
|
1095
|
+
'[data-testid*="remove-attachment"]',
|
|
1096
|
+
'[data-testid*="attachment-remove"]',
|
|
1016
1097
|
];
|
|
1017
1098
|
const visible = (el) => {
|
|
1018
1099
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -1023,10 +1104,15 @@ export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
|
1023
1104
|
? Array.from(scope.querySelectorAll(removeSelectors.join(','))).filter(visible)
|
|
1024
1105
|
: [];
|
|
1025
1106
|
for (const button of removeButtons.slice(0, 20)) {
|
|
1026
|
-
try {
|
|
1107
|
+
try {
|
|
1108
|
+
if (button instanceof HTMLButtonElement) {
|
|
1109
|
+
// Ensure remove buttons never submit the composer form.
|
|
1110
|
+
button.type = 'button';
|
|
1111
|
+
}
|
|
1112
|
+
button.click();
|
|
1113
|
+
} catch {}
|
|
1027
1114
|
}
|
|
1028
|
-
const
|
|
1029
|
-
const chipCount = scope ? scope.querySelectorAll(chipSelector).length : 0;
|
|
1115
|
+
const chipCount = removeButtons.length;
|
|
1030
1116
|
const inputs = scope ? Array.from(scope.querySelectorAll('input[type="file"]')) : [];
|
|
1031
1117
|
let inputCount = 0;
|
|
1032
1118
|
for (const input of inputs) {
|
|
@@ -1189,7 +1275,7 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1189
1275
|
if (file?.name) inputNames.push(file.name.toLowerCase());
|
|
1190
1276
|
}
|
|
1191
1277
|
}
|
|
1192
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+files
|
|
1278
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1193
1279
|
const fileCountSelectors = [
|
|
1194
1280
|
'button',
|
|
1195
1281
|
'[role="button"]',
|
|
@@ -1235,7 +1321,8 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1235
1321
|
let hasFileHint = false;
|
|
1236
1322
|
for (const raw of candidates) {
|
|
1237
1323
|
if (!raw) continue;
|
|
1238
|
-
|
|
1324
|
+
const lowered = String(raw).toLowerCase();
|
|
1325
|
+
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
1239
1326
|
hasFileHint = true;
|
|
1240
1327
|
break;
|
|
1241
1328
|
}
|
|
@@ -1340,9 +1427,8 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1340
1427
|
if (stable && value.state === 'ready') {
|
|
1341
1428
|
return;
|
|
1342
1429
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1430
|
+
// Don't treat disabled button as complete - wait for it to become 'ready'.
|
|
1431
|
+
// The spinner detection is unreliable, so a disabled button likely means upload is in progress.
|
|
1346
1432
|
if (value.state === 'missing' && (value.filesAttached || fileCountSatisfied)) {
|
|
1347
1433
|
return;
|
|
1348
1434
|
}
|
|
@@ -1363,16 +1449,18 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1363
1449
|
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
1364
1450
|
return !inputNames.some((raw) => raw.includes(normalizedExpected) || (expectedNoExt.length >= 6 && raw.includes(expectedNoExt)));
|
|
1365
1451
|
});
|
|
1366
|
-
|
|
1452
|
+
// Don't include 'disabled' - a disabled button likely means upload is still in progress.
|
|
1453
|
+
const inputStateOk = value.state === 'ready' || value.state === 'missing';
|
|
1367
1454
|
const inputSeenNow = inputMissing.length === 0 || fileCountSatisfied;
|
|
1455
|
+
const inputEvidenceOk = Boolean(value.filesAttached) || Boolean(value.uploading) || fileCountSatisfied;
|
|
1368
1456
|
const stableThresholdMs = value.uploading ? 3000 : 1500;
|
|
1369
|
-
if (inputSeenNow && inputStateOk) {
|
|
1457
|
+
if (inputSeenNow && inputStateOk && inputEvidenceOk) {
|
|
1370
1458
|
if (inputMatchSince === null) {
|
|
1371
1459
|
inputMatchSince = Date.now();
|
|
1372
1460
|
}
|
|
1373
1461
|
sawInputMatch = true;
|
|
1374
1462
|
}
|
|
1375
|
-
if (inputMatchSince !== null && inputStateOk && Date.now() - inputMatchSince > stableThresholdMs) {
|
|
1463
|
+
if (inputMatchSince !== null && inputStateOk && inputEvidenceOk && Date.now() - inputMatchSince > stableThresholdMs) {
|
|
1376
1464
|
return;
|
|
1377
1465
|
}
|
|
1378
1466
|
if (!inputSeenNow && !sawInputMatch) {
|
|
@@ -1416,10 +1504,10 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1416
1504
|
'[title*="file"]',
|
|
1417
1505
|
'[title*="attachment"]',
|
|
1418
1506
|
];
|
|
1507
|
+
const attachmentUiCount = lastUser.querySelectorAll(attachmentSelectors.join(',')).length;
|
|
1419
1508
|
const hasAttachmentUi =
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
1509
|
+
attachmentUiCount > 0 || attrs.some((attr) => attr.includes('file') || attr.includes('attachment'));
|
|
1510
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1423
1511
|
const fileCountNodes = Array.from(lastUser.querySelectorAll('button,span,div,[aria-label],[title]'));
|
|
1424
1512
|
let fileCount = 0;
|
|
1425
1513
|
for (const node of fileCountNodes) {
|
|
@@ -1435,7 +1523,8 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1435
1523
|
let hasFileHint = false;
|
|
1436
1524
|
for (const raw of candidates) {
|
|
1437
1525
|
if (!raw) continue;
|
|
1438
|
-
|
|
1526
|
+
const lowered = String(raw).toLowerCase();
|
|
1527
|
+
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
1439
1528
|
hasFileHint = true;
|
|
1440
1529
|
break;
|
|
1441
1530
|
}
|
|
@@ -1452,7 +1541,7 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1452
1541
|
}
|
|
1453
1542
|
}
|
|
1454
1543
|
}
|
|
1455
|
-
return { ok: true, text, attrs, fileCount, hasAttachmentUi };
|
|
1544
|
+
return { ok: true, text, attrs, fileCount, hasAttachmentUi, attachmentUiCount };
|
|
1456
1545
|
})()`;
|
|
1457
1546
|
const deadline = Date.now() + timeoutMs;
|
|
1458
1547
|
let sawAttachmentUi = false;
|
|
@@ -1468,7 +1557,9 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1468
1557
|
}
|
|
1469
1558
|
const haystack = [value.text ?? '', ...(value.attrs ?? [])].join('\n');
|
|
1470
1559
|
const fileCount = typeof value.fileCount === 'number' ? value.fileCount : 0;
|
|
1560
|
+
const attachmentUiCount = typeof value.attachmentUiCount === 'number' ? value.attachmentUiCount : 0;
|
|
1471
1561
|
const fileCountSatisfied = fileCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1562
|
+
const attachmentUiSatisfied = attachmentUiCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1472
1563
|
const missing = expectedNormalized.filter((expected) => {
|
|
1473
1564
|
const baseName = expected.split('/').pop()?.split('\\').pop() ?? expected;
|
|
1474
1565
|
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
@@ -1479,7 +1570,7 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1479
1570
|
return false;
|
|
1480
1571
|
return true;
|
|
1481
1572
|
});
|
|
1482
|
-
if (missing.length === 0 || fileCountSatisfied) {
|
|
1573
|
+
if (missing.length === 0 || fileCountSatisfied || attachmentUiSatisfied) {
|
|
1483
1574
|
return true;
|
|
1484
1575
|
}
|
|
1485
1576
|
await delay(250);
|
|
@@ -1500,6 +1591,12 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
1500
1591
|
const expected = ${JSON.stringify(expectedName)};
|
|
1501
1592
|
const normalized = expected.toLowerCase();
|
|
1502
1593
|
const normalizedNoExt = normalized.replace(/\\.[a-z0-9]{1,10}$/i, '');
|
|
1594
|
+
const matchesExpectedFileName = (value) => {
|
|
1595
|
+
const text = String(value || '').toLowerCase();
|
|
1596
|
+
if (!text) return false;
|
|
1597
|
+
if (text.includes(normalized)) return true;
|
|
1598
|
+
return normalizedNoExt.length >= 6 && text.includes(normalizedNoExt);
|
|
1599
|
+
};
|
|
1503
1600
|
const matchNode = (node) => {
|
|
1504
1601
|
if (!node) return false;
|
|
1505
1602
|
if (node.tagName === 'INPUT' && node.type === 'file') return false;
|
|
@@ -1512,39 +1609,96 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
1512
1609
|
return candidates.some((value) => value.includes(normalized) || (normalizedNoExt.length >= 6 && value.includes(normalizedNoExt)));
|
|
1513
1610
|
};
|
|
1514
1611
|
|
|
1515
|
-
const
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1612
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
1613
|
+
for (const input of inputs) {
|
|
1614
|
+
if (!(input instanceof HTMLInputElement)) continue;
|
|
1615
|
+
const files = Array.from(input.files || []);
|
|
1616
|
+
if (files.some((file) => matchesExpectedFileName(file?.name))) {
|
|
1617
|
+
return { found: true, source: 'file-input' };
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
1622
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
1623
|
+
const findPromptNode = () => {
|
|
1624
|
+
for (const selector of promptSelectors) {
|
|
1625
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
1626
|
+
for (const node of nodes) {
|
|
1627
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
1628
|
+
const rect = node.getBoundingClientRect();
|
|
1629
|
+
if (rect.width > 0 && rect.height > 0) return node;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
for (const selector of promptSelectors) {
|
|
1633
|
+
const node = document.querySelector(selector);
|
|
1634
|
+
if (node) return node;
|
|
1635
|
+
}
|
|
1636
|
+
return null;
|
|
1637
|
+
};
|
|
1638
|
+
const attachmentSelectors = [
|
|
1639
|
+
'input[type="file"]',
|
|
1640
|
+
'[data-testid*="attachment"]',
|
|
1641
|
+
'[data-testid*="chip"]',
|
|
1642
|
+
'[data-testid*="upload"]',
|
|
1643
|
+
'[data-testid*="file"]',
|
|
1644
|
+
'[aria-label*="Remove"]',
|
|
1645
|
+
'[aria-label*="remove"]',
|
|
1646
|
+
];
|
|
1647
|
+
const locateComposerRoot = () => {
|
|
1648
|
+
const promptNode = findPromptNode();
|
|
1649
|
+
if (promptNode) {
|
|
1650
|
+
const initial =
|
|
1651
|
+
promptNode.closest('[data-testid*="composer"]') ??
|
|
1652
|
+
promptNode.closest('form') ??
|
|
1653
|
+
promptNode.parentElement ??
|
|
1654
|
+
document.body;
|
|
1655
|
+
let current = initial;
|
|
1656
|
+
let fallback = initial;
|
|
1657
|
+
while (current && current !== document.body) {
|
|
1658
|
+
const hasSend = sendSelectors.some((selector) => current.querySelector(selector));
|
|
1659
|
+
if (hasSend) {
|
|
1660
|
+
fallback = current;
|
|
1661
|
+
const hasAttachment = attachmentSelectors.some((selector) => current.querySelector(selector));
|
|
1662
|
+
if (hasAttachment) return current;
|
|
1663
|
+
}
|
|
1664
|
+
current = current.parentElement;
|
|
1665
|
+
}
|
|
1666
|
+
return fallback ?? initial;
|
|
1667
|
+
}
|
|
1668
|
+
return document.querySelector('form') ?? document.body;
|
|
1669
|
+
};
|
|
1670
|
+
const composerRoot = locateComposerRoot() ?? document.body;
|
|
1671
|
+
|
|
1672
|
+
const attachmentMatch = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]','[data-testid*="file"]'].some((selector) =>
|
|
1673
|
+
Array.from(composerRoot.querySelectorAll(selector)).some(matchNode),
|
|
1518
1674
|
);
|
|
1519
1675
|
if (attachmentMatch) {
|
|
1520
1676
|
return { found: true, source: 'attachments' };
|
|
1521
1677
|
}
|
|
1522
1678
|
|
|
1523
|
-
const
|
|
1679
|
+
const removeButtons = Array.from(
|
|
1680
|
+
(composerRoot ?? document).querySelectorAll('[aria-label*="Remove"],[aria-label*="remove"]'),
|
|
1681
|
+
);
|
|
1682
|
+
const visibleRemove = removeButtons.some((btn) => {
|
|
1683
|
+
if (!(btn instanceof HTMLElement)) return false;
|
|
1684
|
+
const rect = btn.getBoundingClientRect();
|
|
1685
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
1686
|
+
const style = window.getComputedStyle(btn);
|
|
1687
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
1688
|
+
});
|
|
1689
|
+
if (visibleRemove) {
|
|
1690
|
+
return { found: true, source: 'remove-button' };
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const cardTexts = Array.from(composerRoot.querySelectorAll('[aria-label*="Remove"]')).map((btn) =>
|
|
1524
1694
|
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
1525
1695
|
);
|
|
1526
1696
|
if (cardTexts.some((text) => text.includes(normalized) || (normalizedNoExt.length >= 6 && text.includes(normalizedNoExt)))) {
|
|
1527
1697
|
return { found: true, source: 'attachment-cards' };
|
|
1528
1698
|
}
|
|
1529
1699
|
|
|
1530
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+files
|
|
1531
|
-
const fileCountNodes = (()
|
|
1532
|
-
const nodes = [];
|
|
1533
|
-
const seen = new Set();
|
|
1534
|
-
const add = (node) => {
|
|
1535
|
-
if (!node || seen.has(node)) return;
|
|
1536
|
-
seen.add(node);
|
|
1537
|
-
nodes.push(node);
|
|
1538
|
-
};
|
|
1539
|
-
const root =
|
|
1540
|
-
document.querySelector('[data-testid*="composer"]') || document.querySelector('form') || document.body;
|
|
1541
|
-
const localNodes = root ? Array.from(root.querySelectorAll('button,span,div,[aria-label],[title]')) : [];
|
|
1542
|
-
for (const node of localNodes) add(node);
|
|
1543
|
-
for (const node of Array.from(document.querySelectorAll('button,span,div,[aria-label],[title]'))) {
|
|
1544
|
-
add(node);
|
|
1545
|
-
}
|
|
1546
|
-
return nodes;
|
|
1547
|
-
})();
|
|
1700
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1701
|
+
const fileCountNodes = Array.from(composerRoot.querySelectorAll('button,span,div,[aria-label],[title]'));
|
|
1548
1702
|
let fileCount = 0;
|
|
1549
1703
|
for (const node of fileCountNodes) {
|
|
1550
1704
|
if (!(node instanceof HTMLElement)) continue;
|
|
@@ -1577,7 +1731,8 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
1577
1731
|
let hasFileHint = false;
|
|
1578
1732
|
for (const raw of candidates) {
|
|
1579
1733
|
if (!raw) continue;
|
|
1580
|
-
|
|
1734
|
+
const lowered = String(raw).toLowerCase();
|
|
1735
|
+
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
1581
1736
|
hasFileHint = true;
|
|
1582
1737
|
break;
|
|
1583
1738
|
}
|
|
@@ -1635,12 +1790,24 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
1635
1790
|
return false;
|
|
1636
1791
|
};
|
|
1637
1792
|
|
|
1793
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
1794
|
+
for (const input of inputs) {
|
|
1795
|
+
if (!(input instanceof HTMLInputElement)) continue;
|
|
1796
|
+
for (const file of Array.from(input.files || [])) {
|
|
1797
|
+
if (file?.name && matchesExpected(file.name)) {
|
|
1798
|
+
return { found: true, text: 'file-input' };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1638
1803
|
const selectors = [
|
|
1639
1804
|
'[data-testid*="attachment"]',
|
|
1640
1805
|
'[data-testid*="chip"]',
|
|
1641
1806
|
'[data-testid*="upload"]',
|
|
1642
1807
|
'[aria-label*="Remove"]',
|
|
1643
1808
|
'button[aria-label*="Remove"]',
|
|
1809
|
+
'[aria-label*="remove"]',
|
|
1810
|
+
'button[aria-label*="remove"]',
|
|
1644
1811
|
];
|
|
1645
1812
|
for (const selector of selectors) {
|
|
1646
1813
|
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
@@ -1659,7 +1826,7 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
1659
1826
|
if (cards.some(matchesExpected)) {
|
|
1660
1827
|
return { found: true, text: cards.find(matchesExpected) };
|
|
1661
1828
|
}
|
|
1662
|
-
const countRegex = /(?:^|\\b)(\\d+)\\s+files
|
|
1829
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1663
1830
|
const fileCountNodes = (() => {
|
|
1664
1831
|
const nodes = [];
|
|
1665
1832
|
const seen = new Set();
|
|
@@ -1709,7 +1876,8 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
1709
1876
|
let hasFileHint = false;
|
|
1710
1877
|
for (const raw of candidates) {
|
|
1711
1878
|
if (!raw) continue;
|
|
1712
|
-
|
|
1879
|
+
const lowered = String(raw).toLowerCase();
|
|
1880
|
+
if (lowered.includes('file') || lowered.includes('attachment')) {
|
|
1713
1881
|
hasFileHint = true;
|
|
1714
1882
|
break;
|
|
1715
1883
|
}
|
|
@@ -1,11 +1,126 @@
|
|
|
1
1
|
import { CLOUDFLARE_SCRIPT_SELECTOR, CLOUDFLARE_TITLE, INPUT_SELECTORS, } from '../constants.js';
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure } from '../domDebug.js';
|
|
4
|
+
export function installJavaScriptDialogAutoDismissal(Page, logger) {
|
|
5
|
+
const pageAny = Page;
|
|
6
|
+
if (typeof pageAny.on !== 'function' || typeof pageAny.handleJavaScriptDialog !== 'function') {
|
|
7
|
+
return () => { };
|
|
8
|
+
}
|
|
9
|
+
const handler = async (params) => {
|
|
10
|
+
const type = typeof params?.type === 'string' ? params.type : 'unknown';
|
|
11
|
+
const message = typeof params?.message === 'string' ? params.message : '';
|
|
12
|
+
logger(`[nav] dismissing JS dialog (${type})${message ? `: ${message.slice(0, 140)}` : ''}`);
|
|
13
|
+
try {
|
|
14
|
+
await pageAny.handleJavaScriptDialog?.({ accept: true, promptText: '' });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
18
|
+
logger(`[nav] failed to dismiss JS dialog: ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
pageAny.on('javascriptDialogOpening', handler);
|
|
22
|
+
return () => {
|
|
23
|
+
try {
|
|
24
|
+
pageAny.off?.('javascriptDialogOpening', handler);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
try {
|
|
28
|
+
pageAny.removeListener?.('javascriptDialogOpening', handler);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
4
36
|
export async function navigateToChatGPT(Page, Runtime, url, logger) {
|
|
5
37
|
logger(`Navigating to ${url}`);
|
|
6
38
|
await Page.navigate({ url });
|
|
7
39
|
await waitForDocumentReady(Runtime, 45_000);
|
|
8
40
|
}
|
|
41
|
+
async function dismissBlockingUi(Runtime, logger) {
|
|
42
|
+
const outcome = await Runtime.evaluate({
|
|
43
|
+
expression: `(() => {
|
|
44
|
+
const isVisible = (el) => {
|
|
45
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
48
|
+
const style = window.getComputedStyle(el);
|
|
49
|
+
if (!style) return false;
|
|
50
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
54
|
+
const labelFor = (el) => normalize(el?.textContent || el?.getAttribute?.('aria-label') || el?.getAttribute?.('title'));
|
|
55
|
+
const buttonCandidates = (root) =>
|
|
56
|
+
Array.from(root.querySelectorAll('button,[role="button"],a')).filter((el) => isVisible(el));
|
|
57
|
+
|
|
58
|
+
const roots = [
|
|
59
|
+
...Array.from(document.querySelectorAll('[role="dialog"],dialog')),
|
|
60
|
+
document.body,
|
|
61
|
+
].filter(Boolean);
|
|
62
|
+
for (const root of roots) {
|
|
63
|
+
const buttons = buttonCandidates(root);
|
|
64
|
+
const close = buttons.find((el) => labelFor(el).includes('close'));
|
|
65
|
+
if (close) {
|
|
66
|
+
(close).click();
|
|
67
|
+
return { dismissed: true, action: 'close' };
|
|
68
|
+
}
|
|
69
|
+
const okLike = buttons.find((el) => {
|
|
70
|
+
const label = labelFor(el);
|
|
71
|
+
return (
|
|
72
|
+
label === 'ok' ||
|
|
73
|
+
label === 'got it' ||
|
|
74
|
+
label === 'dismiss' ||
|
|
75
|
+
label === 'continue' ||
|
|
76
|
+
label === 'back' ||
|
|
77
|
+
label.includes('back to chatgpt') ||
|
|
78
|
+
label.includes('go to chatgpt') ||
|
|
79
|
+
label.includes('return') ||
|
|
80
|
+
label.includes('take me')
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
if (okLike) {
|
|
84
|
+
(okLike).click();
|
|
85
|
+
return { dismissed: true, action: 'confirm' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { dismissed: false };
|
|
89
|
+
})()`,
|
|
90
|
+
returnByValue: true,
|
|
91
|
+
}).catch(() => null);
|
|
92
|
+
const value = outcome?.result?.value;
|
|
93
|
+
if (value?.dismissed) {
|
|
94
|
+
logger(`[nav] dismissed blocking UI (${value.action ?? 'unknown'})`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
export async function navigateToPromptReadyWithFallback(Page, Runtime, options, deps = {}) {
|
|
100
|
+
const { url, fallbackUrl, timeoutMs, fallbackTimeoutMs, headless, logger, } = options;
|
|
101
|
+
const navigate = deps.navigateToChatGPT ?? navigateToChatGPT;
|
|
102
|
+
const ensureBlocked = deps.ensureNotBlocked ?? ensureNotBlocked;
|
|
103
|
+
const ensureReady = deps.ensurePromptReady ?? ensurePromptReady;
|
|
104
|
+
await navigate(Page, Runtime, url, logger);
|
|
105
|
+
await ensureBlocked(Runtime, headless, logger);
|
|
106
|
+
await dismissBlockingUi(Runtime, logger).catch(() => false);
|
|
107
|
+
try {
|
|
108
|
+
await ensureReady(Runtime, timeoutMs, logger);
|
|
109
|
+
return { usedFallback: false };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!fallbackUrl || fallbackUrl === url) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
const fallbackTimeout = fallbackTimeoutMs ?? Math.max(timeoutMs * 2, 120_000);
|
|
116
|
+
logger(`Prompt not ready after ${Math.round(timeoutMs / 1000)}s on ${url}; retrying ${fallbackUrl} with ${Math.round(fallbackTimeout / 1000)}s timeout.`);
|
|
117
|
+
await navigate(Page, Runtime, fallbackUrl, logger);
|
|
118
|
+
await ensureBlocked(Runtime, headless, logger);
|
|
119
|
+
await dismissBlockingUi(Runtime, logger).catch(() => false);
|
|
120
|
+
await ensureReady(Runtime, fallbackTimeout, logger);
|
|
121
|
+
return { usedFallback: true };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
9
124
|
export async function ensureNotBlocked(Runtime, headless, logger) {
|
|
10
125
|
if (await isCloudflareInterstitial(Runtime)) {
|
|
11
126
|
const message = headless
|
|
@@ -110,6 +110,8 @@ function buildThinkingTimeExpression(level) {
|
|
|
110
110
|
for (const selector of CHIP_SELECTORS) {
|
|
111
111
|
const buttons = document.querySelectorAll(selector);
|
|
112
112
|
for (const btn of buttons) {
|
|
113
|
+
// Skip toggle buttons (no haspopup) - only click dropdown triggers to avoid disabling Pro mode
|
|
114
|
+
if (btn.getAttribute?.('aria-haspopup') !== 'menu') continue;
|
|
113
115
|
const aria = normalize(btn.getAttribute?.('aria-label') ?? '');
|
|
114
116
|
const text = normalize(btn.textContent ?? '');
|
|
115
117
|
if (aria.includes('thinking') || text.includes('thinking')) {
|
|
@@ -172,6 +172,8 @@ function buildChromeFlags(headless, debugBindAddress) {
|
|
|
172
172
|
'--disable-features=TranslateUI,AutomationControlled',
|
|
173
173
|
'--mute-audio',
|
|
174
174
|
'--window-size=1280,720',
|
|
175
|
+
'--lang=en-US',
|
|
176
|
+
'--accept-lang=en-US,en',
|
|
175
177
|
];
|
|
176
178
|
if (process.platform !== 'win32' && !isWsl()) {
|
|
177
179
|
flags.push('--password-store=basic', '--use-mock-keychain');
|
|
@@ -14,6 +14,7 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
14
14
|
inputTimeoutMs: 60_000,
|
|
15
15
|
cookieSync: true,
|
|
16
16
|
cookieNames: null,
|
|
17
|
+
cookieSyncWaitMs: 0,
|
|
17
18
|
inlineCookies: null,
|
|
18
19
|
inlineCookiesSource: null,
|
|
19
20
|
headless: false,
|
|
@@ -58,6 +59,7 @@ export function resolveBrowserConfig(config) {
|
|
|
58
59
|
inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
|
|
59
60
|
cookieSync: config?.cookieSync ?? cookieSyncDefault,
|
|
60
61
|
cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
|
|
62
|
+
cookieSyncWaitMs: config?.cookieSyncWaitMs ?? DEFAULT_BROWSER_CONFIG.cookieSyncWaitMs,
|
|
61
63
|
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
62
64
|
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
63
65
|
headless: config?.headless ?? DEFAULT_BROWSER_CONFIG.headless,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { COOKIE_URLS } from './constants.js';
|
|
2
|
+
import { delay } from './utils.js';
|
|
2
3
|
import { getCookies } from '@steipete/sweet-cookie';
|
|
3
4
|
export class ChromeCookieSyncError extends Error {
|
|
4
5
|
}
|
|
5
6
|
export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
6
|
-
const { allowErrors = false, filterNames, inlineCookies, cookiePath } = options;
|
|
7
|
+
const { allowErrors = false, filterNames, inlineCookies, cookiePath, waitMs = 0 } = options;
|
|
7
8
|
try {
|
|
8
9
|
// Learned: inline cookies are the most deterministic (avoid Keychain + profile ambiguity).
|
|
9
10
|
const cookies = inlineCookies?.length
|
|
10
11
|
? normalizeInlineCookies(inlineCookies, new URL(url).hostname)
|
|
11
|
-
: await
|
|
12
|
+
: await readChromeCookiesWithWait(url, profile, filterNames ?? undefined, cookiePath ?? undefined, waitMs, logger);
|
|
12
13
|
if (!cookies.length) {
|
|
13
14
|
return 0;
|
|
14
15
|
}
|
|
@@ -38,6 +39,32 @@ export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
|
38
39
|
throw error instanceof ChromeCookieSyncError ? error : new ChromeCookieSyncError(message);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
async function readChromeCookiesWithWait(url, profile, filterNames, cookiePath, waitMs, logger) {
|
|
43
|
+
if (waitMs <= 0) {
|
|
44
|
+
return readChromeCookies(url, profile, filterNames, cookiePath);
|
|
45
|
+
}
|
|
46
|
+
let cookies = [];
|
|
47
|
+
let firstError;
|
|
48
|
+
try {
|
|
49
|
+
cookies = await readChromeCookies(url, profile, filterNames, cookiePath);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
firstError = error;
|
|
53
|
+
}
|
|
54
|
+
if (cookies.length > 0 && !firstError) {
|
|
55
|
+
return cookies;
|
|
56
|
+
}
|
|
57
|
+
const waitLabel = waitMs >= 1000 ? `${Math.round(waitMs / 1000)}s` : `${waitMs}ms`;
|
|
58
|
+
const message = firstError instanceof Error ? firstError.message : String(firstError ?? '');
|
|
59
|
+
if (firstError) {
|
|
60
|
+
logger(`[cookies] Cookie read failed (${message}); waiting ${waitLabel} then retrying once.`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
logger(`[cookies] No cookies found; waiting ${waitLabel} then retrying once.`);
|
|
64
|
+
}
|
|
65
|
+
await delay(waitMs);
|
|
66
|
+
return readChromeCookies(url, profile, filterNames, cookiePath);
|
|
67
|
+
}
|
|
41
68
|
async function readChromeCookies(url, profile, filterNames, cookiePath) {
|
|
42
69
|
const origins = Array.from(new Set([stripQuery(url), ...COOKIE_URLS]));
|
|
43
70
|
const chromeProfile = cookiePath ?? profile ?? undefined;
|
|
@@ -5,7 +5,7 @@ import net from 'node:net';
|
|
|
5
5
|
import { resolveBrowserConfig } from './config.js';
|
|
6
6
|
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChrome, connectToRemoteChrome, closeRemoteChromeTarget, } from './chromeLifecycle.js';
|
|
7
7
|
import { syncCookies } from './cookies.js';
|
|
8
|
-
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from './pageActions.js';
|
|
8
|
+
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from './pageActions.js';
|
|
9
9
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
10
10
|
import { ensureThinkingTime } from './actions/thinkingTime.js';
|
|
11
11
|
import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
@@ -129,6 +129,7 @@ export async function runBrowserMode(options) {
|
|
|
129
129
|
let runStatus = 'attempted';
|
|
130
130
|
let connectionClosedUnexpectedly = false;
|
|
131
131
|
let stopThinkingMonitor = null;
|
|
132
|
+
let removeDialogHandler = null;
|
|
132
133
|
let appliedCookies = 0;
|
|
133
134
|
try {
|
|
134
135
|
try {
|
|
@@ -158,6 +159,7 @@ export async function runBrowserMode(options) {
|
|
|
158
159
|
domainEnablers.push(DOM.enable());
|
|
159
160
|
}
|
|
160
161
|
await Promise.all(domainEnablers);
|
|
162
|
+
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
161
163
|
if (!manualLogin) {
|
|
162
164
|
await Network.clearBrowserCookies();
|
|
163
165
|
}
|
|
@@ -179,6 +181,7 @@ export async function runBrowserMode(options) {
|
|
|
179
181
|
filterNames: config.cookieNames ?? undefined,
|
|
180
182
|
inlineCookies: config.inlineCookies ?? undefined,
|
|
181
183
|
cookiePath: config.chromeCookiePath ?? undefined,
|
|
184
|
+
waitMs: config.cookieSyncWaitMs ?? 0,
|
|
182
185
|
});
|
|
183
186
|
appliedCookies = cookieCount;
|
|
184
187
|
if (config.inlineCookies && cookieCount === 0) {
|
|
@@ -201,7 +204,8 @@ export async function runBrowserMode(options) {
|
|
|
201
204
|
// Learned: if the profile has no ChatGPT cookies, browser mode will just bounce to login.
|
|
202
205
|
// Fail early so the user knows to sign in.
|
|
203
206
|
throw new BrowserAutomationError('No ChatGPT cookies were applied from your Chrome profile; cannot proceed in browser mode. ' +
|
|
204
|
-
'Make sure ChatGPT is signed in in the selected profile,
|
|
207
|
+
'Make sure ChatGPT is signed in in the selected profile, use --browser-manual-login / inline cookies, ' +
|
|
208
|
+
'or retry with --browser-cookie-wait 5s if Keychain prompts are slow.', {
|
|
205
209
|
stage: 'execute-browser',
|
|
206
210
|
details: {
|
|
207
211
|
profile: config.chromeProfile ?? 'Default',
|
|
@@ -218,10 +222,17 @@ export async function runBrowserMode(options) {
|
|
|
218
222
|
// Learned: login checks must happen on the base domain before jumping into project URLs.
|
|
219
223
|
await raceWithDisconnect(waitForLogin({ runtime: Runtime, logger, appliedCookies, manualLogin, timeoutMs: config.timeoutMs }));
|
|
220
224
|
if (config.url !== baseUrl) {
|
|
221
|
-
await raceWithDisconnect(
|
|
222
|
-
|
|
225
|
+
await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
|
|
226
|
+
url: config.url,
|
|
227
|
+
fallbackUrl: baseUrl,
|
|
228
|
+
timeoutMs: config.inputTimeoutMs,
|
|
229
|
+
headless: config.headless,
|
|
230
|
+
logger,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
223
235
|
}
|
|
224
|
-
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
225
236
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
226
237
|
const captureRuntimeSnapshot = async () => {
|
|
227
238
|
try {
|
|
@@ -336,6 +347,7 @@ export async function runBrowserMode(options) {
|
|
|
336
347
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
337
348
|
const baselineAssistantText = typeof baselineSnapshot?.text === 'string' ? baselineSnapshot.text.trim() : '';
|
|
338
349
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
350
|
+
let attachmentWaitTimedOut = false;
|
|
339
351
|
let inputOnlyAttachments = false;
|
|
340
352
|
if (submissionAttachments.length > 0) {
|
|
341
353
|
if (!DOM) {
|
|
@@ -345,25 +357,38 @@ export async function runBrowserMode(options) {
|
|
|
345
357
|
for (let attachmentIndex = 0; attachmentIndex < submissionAttachments.length; attachmentIndex += 1) {
|
|
346
358
|
const attachment = submissionAttachments[attachmentIndex];
|
|
347
359
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
348
|
-
const uiConfirmed = await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger, { expectedCount: attachmentIndex + 1 });
|
|
360
|
+
const uiConfirmed = await uploadAttachmentFile({ runtime: Runtime, dom: DOM, input: Input }, attachment, logger, { expectedCount: attachmentIndex + 1 });
|
|
349
361
|
if (!uiConfirmed) {
|
|
350
362
|
inputOnlyAttachments = true;
|
|
351
363
|
}
|
|
352
364
|
await delay(500);
|
|
353
365
|
}
|
|
354
|
-
// Scale timeout based on number of files: base
|
|
366
|
+
// Scale timeout based on number of files: base 45s + 20s per additional file.
|
|
355
367
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
356
|
-
const perFileTimeout =
|
|
357
|
-
const waitBudget = Math.max(baseTimeout,
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
const perFileTimeout = 20_000;
|
|
369
|
+
const waitBudget = Math.max(baseTimeout, 45_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
370
|
+
try {
|
|
371
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
372
|
+
logger('All attachments uploaded');
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
376
|
+
if (/Attachments did not finish uploading before timeout/i.test(message)) {
|
|
377
|
+
attachmentWaitTimedOut = true;
|
|
378
|
+
logger(`[browser] Attachment upload timed out after ${Math.round(waitBudget / 1000)}s; continuing without confirmation.`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
360
384
|
}
|
|
361
385
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
362
386
|
// Learned: return baselineTurns so assistant polling can ignore earlier content.
|
|
387
|
+
const sendAttachmentNames = attachmentWaitTimedOut ? [] : attachmentNames;
|
|
363
388
|
const committedTurns = await submitPrompt({
|
|
364
389
|
runtime: Runtime,
|
|
365
390
|
input: Input,
|
|
366
|
-
attachmentNames,
|
|
391
|
+
attachmentNames: sendAttachmentNames,
|
|
367
392
|
baselineTurns: baselineTurns ?? undefined,
|
|
368
393
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
369
394
|
}, prompt, logger);
|
|
@@ -373,14 +398,18 @@ export async function runBrowserMode(options) {
|
|
|
373
398
|
}
|
|
374
399
|
}
|
|
375
400
|
if (attachmentNames.length > 0) {
|
|
376
|
-
if (
|
|
401
|
+
if (attachmentWaitTimedOut) {
|
|
402
|
+
logger('Attachment confirmation timed out; skipping user-turn attachment verification.');
|
|
403
|
+
}
|
|
404
|
+
else if (inputOnlyAttachments) {
|
|
377
405
|
logger('Attachment UI did not render before send; skipping user-turn attachment verification.');
|
|
378
406
|
}
|
|
379
407
|
else {
|
|
380
408
|
const verified = await waitForUserTurnAttachments(Runtime, attachmentNames, 20_000, logger);
|
|
381
|
-
if (verified) {
|
|
382
|
-
|
|
409
|
+
if (!verified) {
|
|
410
|
+
throw new Error('Sent user message did not expose attachment UI after upload.');
|
|
383
411
|
}
|
|
412
|
+
logger('Verified attachments present on sent user message');
|
|
384
413
|
}
|
|
385
414
|
}
|
|
386
415
|
// Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
|
|
@@ -618,6 +647,7 @@ export async function runBrowserMode(options) {
|
|
|
618
647
|
catch {
|
|
619
648
|
// ignore
|
|
620
649
|
}
|
|
650
|
+
removeDialogHandler?.();
|
|
621
651
|
removeTerminationHooks?.();
|
|
622
652
|
if (!effectiveKeepBrowser) {
|
|
623
653
|
if (!connectionClosedUnexpectedly) {
|
|
@@ -796,6 +826,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
796
826
|
let answerHtml = '';
|
|
797
827
|
let connectionClosedUnexpectedly = false;
|
|
798
828
|
let stopThinkingMonitor = null;
|
|
829
|
+
let removeDialogHandler = null;
|
|
799
830
|
try {
|
|
800
831
|
const connection = await connectToRemoteChrome(host, port, logger, config.url);
|
|
801
832
|
client = connection.client;
|
|
@@ -811,6 +842,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
811
842
|
domainEnablers.push(DOM.enable());
|
|
812
843
|
}
|
|
813
844
|
await Promise.all(domainEnablers);
|
|
845
|
+
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
814
846
|
// Skip cookie sync for remote Chrome - it already has cookies
|
|
815
847
|
logger('Skipping cookie sync for remote Chrome (using existing session)');
|
|
816
848
|
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
@@ -1082,6 +1114,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1082
1114
|
catch {
|
|
1083
1115
|
// ignore
|
|
1084
1116
|
}
|
|
1117
|
+
removeDialogHandler?.();
|
|
1085
1118
|
await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger);
|
|
1086
1119
|
// Don't kill remote Chrome - it's not ours to manage
|
|
1087
1120
|
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady } from './actions/navigation.js';
|
|
1
|
+
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, } from './actions/navigation.js';
|
|
2
2
|
export { ensureModelSelection } from './actions/modelSelection.js';
|
|
3
3
|
export { submitPrompt, clearPromptComposer } from './actions/promptComposer.js';
|
|
4
4
|
export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, } from './actions/attachments.js';
|
|
@@ -114,6 +114,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
114
114
|
filterNames: resolved.cookieNames ?? undefined,
|
|
115
115
|
inlineCookies: resolved.inlineCookies ?? undefined,
|
|
116
116
|
cookiePath: resolved.chromeCookiePath ?? undefined,
|
|
117
|
+
waitMs: resolved.cookieSyncWaitMs ?? 0,
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
@@ -82,6 +82,7 @@ export async function buildBrowserConfig(options) {
|
|
|
82
82
|
inputTimeoutMs: options.browserInputTimeout
|
|
83
83
|
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
84
84
|
: undefined,
|
|
85
|
+
cookieSyncWaitMs: options.browserCookieWait ? parseDuration(options.browserCookieWait, 0) : undefined,
|
|
85
86
|
cookieSync: options.browserNoCookieSync ? false : undefined,
|
|
86
87
|
cookieNames,
|
|
87
88
|
inlineCookies: inline?.cookies,
|
|
@@ -33,6 +33,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
33
33
|
if (isUnset('browserInputTimeout') && typeof browser.inputTimeoutMs === 'number') {
|
|
34
34
|
options.browserInputTimeout = String(browser.inputTimeoutMs);
|
|
35
35
|
}
|
|
36
|
+
if (isUnset('browserCookieWait') && typeof browser.cookieSyncWaitMs === 'number') {
|
|
37
|
+
options.browserCookieWait = String(browser.cookieSyncWaitMs);
|
|
38
|
+
}
|
|
36
39
|
if (isUnset('browserHeadless') && browser.headless !== undefined) {
|
|
37
40
|
options.browserHeadless = browser.headless;
|
|
38
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test:mcp:mcporter": "npx -y mcporter list oracle-local --schema --config config/mcporter.json && npx -y mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
|
|
24
24
|
"test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
|
|
25
25
|
"test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
|
|
26
|
+
"test:live:fast": "ORACLE_LIVE_TEST=1 ORACLE_LIVE_TEST_FAST=1 vitest run tests/live/browser-fast-live.test.ts",
|
|
26
27
|
"test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
|
|
27
28
|
"test:coverage": "vitest run --coverage",
|
|
28
29
|
"prepare": "pnpm run build",
|
|
@@ -79,14 +80,15 @@
|
|
|
79
80
|
"markdansi": "0.2.0",
|
|
80
81
|
"openai": "^6.15.0",
|
|
81
82
|
"osc-progress": "^0.2.0",
|
|
83
|
+
"qs": "^6.14.1",
|
|
82
84
|
"shiki": "^3.20.0",
|
|
83
85
|
"toasted-notifier": "^10.1.0",
|
|
84
86
|
"tokentally": "^0.1.1",
|
|
85
|
-
"zod": "^4.
|
|
87
|
+
"zod": "^4.3.5"
|
|
86
88
|
},
|
|
87
89
|
"devDependencies": {
|
|
88
90
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
89
|
-
"@biomejs/biome": "^2.3.
|
|
91
|
+
"@biomejs/biome": "^2.3.11",
|
|
90
92
|
"@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
|
|
91
93
|
"@types/chrome-remote-interface": "^0.33.0",
|
|
92
94
|
"@types/inquirer": "^9.0.9",
|