@steipete/oracle 0.7.6 → 0.8.1
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 +6 -3
- package/dist/bin/oracle-cli.js +4 -0
- package/dist/src/browser/actions/assistantResponse.js +437 -84
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1300 -132
- package/dist/src/browser/actions/modelSelection.js +9 -4
- package/dist/src/browser/actions/navigation.js +160 -5
- package/dist/src/browser/actions/promptComposer.js +54 -11
- package/dist/src/browser/actions/remoteFileTransfer.js +5 -156
- package/dist/src/browser/actions/thinkingTime.js +5 -0
- package/dist/src/browser/chromeLifecycle.js +9 -1
- package/dist/src/browser/config.js +11 -3
- package/dist/src/browser/constants.js +4 -1
- package/dist/src/browser/cookies.js +55 -21
- package/dist/src/browser/index.js +342 -69
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +16 -0
- package/dist/src/browser/reattach.js +27 -179
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/browserConfig.js +12 -5
- package/dist/src/cli/browserDefaults.js +12 -0
- package/dist/src/cli/sessionDisplay.js +7 -0
- package/dist/src/gemini-web/executor.js +107 -46
- package/dist/src/oracle/oscProgress.js +7 -0
- package/dist/src/oracle/run.js +23 -32
- package/dist/src/remote/server.js +30 -15
- package/package.json +8 -17
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function normalizeBrowserModelStrategy(value) {
|
|
2
|
+
if (value == null) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (normalized === 'select' || normalized === 'current' || normalized === 'ignore') {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`Invalid browser model strategy: "${value}". Expected "select", "current", or "ignore".`);
|
|
13
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady } from './actions/navigation.js';
|
|
2
2
|
export { ensureModelSelection } from './actions/modelSelection.js';
|
|
3
3
|
export { submitPrompt, clearPromptComposer } from './actions/promptComposer.js';
|
|
4
|
-
export { uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments } from './actions/attachments.js';
|
|
5
|
-
export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildConversationDebugExpressionForTest, } from './actions/assistantResponse.js';
|
|
4
|
+
export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, } from './actions/attachments.js';
|
|
5
|
+
export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildConversationDebugExpressionForTest, buildMarkdownFallbackExtractorForTest, buildCopyExpressionForTest, } from './actions/assistantResponse.js';
|
|
@@ -105,6 +105,22 @@ export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attemp
|
|
|
105
105
|
}
|
|
106
106
|
return { ok: false, error: 'unreachable' };
|
|
107
107
|
}
|
|
108
|
+
export async function shouldCleanupManualLoginProfileState(userDataDir, logger, options = {}) {
|
|
109
|
+
if (!options.connectionClosedUnexpectedly) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const port = await readDevToolsPort(userDataDir);
|
|
113
|
+
if (!port) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const probe = await (options.probe ?? verifyDevToolsReachable)({ port, host: options.host });
|
|
117
|
+
if (probe.ok) {
|
|
118
|
+
logger?.(`DevTools port ${port} still reachable; preserving manual-login profile state`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
logger?.(`DevTools port ${port} unreachable (${probe.error}); clearing stale profile state`);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
108
124
|
export async function cleanupStaleProfileState(userDataDir, logger, options = {}) {
|
|
109
125
|
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
110
126
|
try {
|
|
@@ -7,24 +7,8 @@ import { launchChrome, connectToChrome, hideChromeWindow } from './chromeLifecyc
|
|
|
7
7
|
import { resolveBrowserConfig } from './config.js';
|
|
8
8
|
import { syncCookies } from './cookies.js';
|
|
9
9
|
import { CHATGPT_URL } from './constants.js';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
if (!Array.isArray(targets) || targets.length === 0) {
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
if (runtime.chromeTargetId) {
|
|
16
|
-
const byId = targets.find((t) => t.targetId === runtime.chromeTargetId);
|
|
17
|
-
if (byId)
|
|
18
|
-
return byId;
|
|
19
|
-
}
|
|
20
|
-
if (runtime.tabUrl) {
|
|
21
|
-
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
22
|
-
targets.find((t) => runtime.tabUrl.startsWith(t.url || ''));
|
|
23
|
-
if (byUrl)
|
|
24
|
-
return byUrl;
|
|
25
|
-
}
|
|
26
|
-
return targets.find((t) => t.type === 'page') ?? targets[0];
|
|
27
|
-
}
|
|
10
|
+
import { cleanupStaleProfileState } from './profileState.js';
|
|
11
|
+
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from './reattachHelpers.js';
|
|
28
12
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
29
13
|
const recoverSession = deps.recoverSession ??
|
|
30
14
|
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
@@ -58,7 +42,10 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
58
42
|
const { result } = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
59
43
|
const href = typeof result?.value === 'string' ? result.value : '';
|
|
60
44
|
if (href.includes('/c/')) {
|
|
61
|
-
|
|
45
|
+
const currentId = extractConversationIdFromUrl(href);
|
|
46
|
+
if (!runtime.conversationId || (currentId && currentId === runtime.conversationId)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
62
49
|
}
|
|
63
50
|
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
64
51
|
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
|
|
@@ -76,8 +63,12 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
76
63
|
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
77
64
|
await withTimeout(Runtime.evaluate({ expression: '1+1', returnByValue: true }), pingTimeoutMs, 'Reattach target did not respond');
|
|
78
65
|
await ensureConversationOpen();
|
|
79
|
-
const
|
|
80
|
-
const
|
|
66
|
+
const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
|
|
67
|
+
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
68
|
+
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, 'Reattach response timed out');
|
|
69
|
+
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
70
|
+
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, 'Reattach markdown capture timed out')) ?? recovered.text;
|
|
71
|
+
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
81
72
|
if (client && typeof client.close === 'function') {
|
|
82
73
|
try {
|
|
83
74
|
await client.close();
|
|
@@ -86,7 +77,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
86
77
|
// ignore
|
|
87
78
|
}
|
|
88
79
|
}
|
|
89
|
-
return { answerText:
|
|
80
|
+
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
90
81
|
}
|
|
91
82
|
catch (error) {
|
|
92
83
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -143,7 +134,8 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
143
134
|
else {
|
|
144
135
|
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
145
136
|
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
|
|
146
|
-
preferProjects: resolved.url !== CHATGPT_URL
|
|
137
|
+
preferProjects: resolved.url !== CHATGPT_URL ||
|
|
138
|
+
Boolean(runtime.tabUrl && (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes('/project'))),
|
|
147
139
|
promptPreview: deps.promptPreview,
|
|
148
140
|
}, 15_000);
|
|
149
141
|
if (!opened) {
|
|
@@ -154,8 +146,12 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
154
146
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
155
147
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
156
148
|
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
157
|
-
const
|
|
158
|
-
const
|
|
149
|
+
const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
|
|
150
|
+
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
151
|
+
const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined);
|
|
152
|
+
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
153
|
+
const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
|
|
154
|
+
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
159
155
|
if (client && typeof client.close === 'function') {
|
|
160
156
|
try {
|
|
161
157
|
await client.close();
|
|
@@ -164,169 +160,21 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
164
160
|
// ignore
|
|
165
161
|
}
|
|
166
162
|
}
|
|
167
|
-
if (!resolved.keepBrowser
|
|
163
|
+
if (!resolved.keepBrowser) {
|
|
168
164
|
try {
|
|
169
165
|
await chrome.kill();
|
|
170
166
|
}
|
|
171
167
|
catch {
|
|
172
168
|
// ignore
|
|
173
169
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return { answerText: answer.text, answerMarkdown: markdown };
|
|
177
|
-
}
|
|
178
|
-
function extractConversationIdFromUrl(url) {
|
|
179
|
-
if (!url)
|
|
180
|
-
return undefined;
|
|
181
|
-
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
182
|
-
return match?.[1];
|
|
183
|
-
}
|
|
184
|
-
function buildConversationUrl(runtime, baseUrl) {
|
|
185
|
-
if (runtime.tabUrl) {
|
|
186
|
-
if (runtime.tabUrl.includes('/c/')) {
|
|
187
|
-
return runtime.tabUrl;
|
|
188
|
-
}
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
const conversationId = runtime.conversationId;
|
|
192
|
-
if (!conversationId) {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
try {
|
|
196
|
-
const base = new URL(baseUrl);
|
|
197
|
-
return `${base.origin}/c/${conversationId}`;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function withTimeout(task, ms, label) {
|
|
204
|
-
let timeoutId;
|
|
205
|
-
const timeout = new Promise((_, reject) => {
|
|
206
|
-
timeoutId = setTimeout(() => reject(new Error(label)), ms);
|
|
207
|
-
});
|
|
208
|
-
return Promise.race([task, timeout]).finally(() => {
|
|
209
|
-
if (timeoutId) {
|
|
210
|
-
clearTimeout(timeoutId);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
async function openConversationFromSidebar(Runtime, options) {
|
|
215
|
-
const response = await Runtime.evaluate({
|
|
216
|
-
expression: `(() => {
|
|
217
|
-
const conversationId = ${JSON.stringify(options.conversationId ?? null)};
|
|
218
|
-
const preferProjects = ${JSON.stringify(Boolean(options.preferProjects))};
|
|
219
|
-
const promptPreview = ${JSON.stringify(options.promptPreview ?? null)};
|
|
220
|
-
const promptNeedle = promptPreview ? promptPreview.trim().toLowerCase().slice(0, 100) : '';
|
|
221
|
-
const nav = document.querySelector('nav') || document.querySelector('aside') || document.body;
|
|
222
|
-
if (preferProjects) {
|
|
223
|
-
const projectLink = Array.from(nav.querySelectorAll('a,button'))
|
|
224
|
-
.find((el) => (el.textContent || '').trim().toLowerCase() === 'projects');
|
|
225
|
-
if (projectLink) {
|
|
226
|
-
projectLink.click();
|
|
170
|
+
if (manualLogin) {
|
|
171
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
|
|
227
172
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
document.querySelectorAll(
|
|
231
|
-
'a,button,[role="link"],[role="button"],[data-href],[data-url],[data-conversation-id],[data-testid*="conversation"],[data-testid*="history"]',
|
|
232
|
-
),
|
|
233
|
-
);
|
|
234
|
-
const getHref = (el) =>
|
|
235
|
-
el.getAttribute('href') ||
|
|
236
|
-
el.getAttribute('data-href') ||
|
|
237
|
-
el.getAttribute('data-url') ||
|
|
238
|
-
el.dataset?.href ||
|
|
239
|
-
el.dataset?.url ||
|
|
240
|
-
'';
|
|
241
|
-
const toCandidate = (el) => {
|
|
242
|
-
const clickable = el.closest('a,button,[role="link"],[role="button"]') || el;
|
|
243
|
-
const rawText = (el.textContent || clickable.textContent || '').trim();
|
|
244
|
-
return {
|
|
245
|
-
el,
|
|
246
|
-
clickable,
|
|
247
|
-
href: getHref(clickable) || getHref(el),
|
|
248
|
-
conversationId:
|
|
249
|
-
clickable.getAttribute('data-conversation-id') ||
|
|
250
|
-
el.getAttribute('data-conversation-id') ||
|
|
251
|
-
clickable.dataset?.conversationId ||
|
|
252
|
-
el.dataset?.conversationId ||
|
|
253
|
-
'',
|
|
254
|
-
testId: clickable.getAttribute('data-testid') || el.getAttribute('data-testid') || '',
|
|
255
|
-
text: rawText.replace(/\\s+/g, ' ').slice(0, 400),
|
|
256
|
-
inNav: Boolean(clickable.closest('nav,aside')),
|
|
257
|
-
};
|
|
258
|
-
};
|
|
259
|
-
const candidates = allElements.map(toCandidate);
|
|
260
|
-
const mainCandidates = candidates.filter((item) => !item.inNav);
|
|
261
|
-
const navCandidates = candidates.filter((item) => item.inNav);
|
|
262
|
-
const visible = (item) => {
|
|
263
|
-
const rect = item.clickable.getBoundingClientRect();
|
|
264
|
-
return rect.width > 0 && rect.height > 0;
|
|
265
|
-
};
|
|
266
|
-
const pick = (items) => (items.find(visible) || items[0] || null);
|
|
267
|
-
let target = null;
|
|
268
|
-
if (conversationId) {
|
|
269
|
-
const byId = (item) =>
|
|
270
|
-
(item.href && item.href.includes('/c/' + conversationId)) ||
|
|
271
|
-
(item.conversationId && item.conversationId === conversationId);
|
|
272
|
-
target = pick(mainCandidates.filter(byId)) || pick(navCandidates.filter(byId));
|
|
273
|
-
}
|
|
274
|
-
if (!target && promptNeedle) {
|
|
275
|
-
const byPrompt = (item) => item.text && item.text.toLowerCase().includes(promptNeedle);
|
|
276
|
-
target = pick(mainCandidates.filter(byPrompt)) || pick(navCandidates.filter(byPrompt));
|
|
277
|
-
}
|
|
278
|
-
if (!target) {
|
|
279
|
-
const byHref = (item) => item.href && item.href.includes('/c/');
|
|
280
|
-
target = pick(mainCandidates.filter(byHref)) || pick(navCandidates.filter(byHref));
|
|
281
|
-
}
|
|
282
|
-
if (!target) {
|
|
283
|
-
const byTestId = (item) => /conversation|history/i.test(item.testId || '');
|
|
284
|
-
target = pick(mainCandidates.filter(byTestId)) || pick(navCandidates.filter(byTestId));
|
|
285
|
-
}
|
|
286
|
-
if (target) {
|
|
287
|
-
target.clickable.scrollIntoView({ block: 'center' });
|
|
288
|
-
target.clickable.dispatchEvent(
|
|
289
|
-
new MouseEvent('click', { bubbles: true, cancelable: true, view: window }),
|
|
290
|
-
);
|
|
291
|
-
return {
|
|
292
|
-
ok: true,
|
|
293
|
-
href: target.href || '',
|
|
294
|
-
count: candidates.length,
|
|
295
|
-
scope: target.inNav ? 'nav' : 'main',
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
return { ok: false, count: candidates.length };
|
|
299
|
-
})()`,
|
|
300
|
-
returnByValue: true,
|
|
301
|
-
});
|
|
302
|
-
return Boolean(response.result?.value?.ok);
|
|
303
|
-
}
|
|
304
|
-
async function openConversationFromSidebarWithRetry(Runtime, options, timeoutMs) {
|
|
305
|
-
const start = Date.now();
|
|
306
|
-
let attempt = 0;
|
|
307
|
-
while (Date.now() - start < timeoutMs) {
|
|
308
|
-
// Retry because project list can hydrate after initial navigation.
|
|
309
|
-
const opened = await openConversationFromSidebar(Runtime, options);
|
|
310
|
-
if (opened) {
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
|
-
attempt += 1;
|
|
314
|
-
await delay(attempt < 5 ? 250 : 500);
|
|
315
|
-
}
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
async function waitForLocationChange(Runtime, timeoutMs) {
|
|
319
|
-
const start = Date.now();
|
|
320
|
-
let lastHref = '';
|
|
321
|
-
while (Date.now() - start < timeoutMs) {
|
|
322
|
-
const { result } = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
323
|
-
const href = typeof result?.value === 'string' ? result.value : '';
|
|
324
|
-
if (lastHref && href !== lastHref) {
|
|
325
|
-
return;
|
|
173
|
+
else {
|
|
174
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
326
175
|
}
|
|
327
|
-
lastHref = href;
|
|
328
|
-
await delay(200);
|
|
329
176
|
}
|
|
177
|
+
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
330
178
|
}
|
|
331
179
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
332
180
|
export const __test__ = {
|