@andrewting19/oracle 0.9.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/LICENSE +21 -0
- package/README.md +300 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1480 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +127 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1386 -0
- package/dist/scripts/test-browser.js +106 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1085 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
- package/dist/src/browser/actions/attachments.js +1939 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +549 -0
- package/dist/src/browser/actions/navigation.js +451 -0
- package/dist/src/browser/actions/promptComposer.js +487 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +346 -0
- package/dist/src/browser/config.js +105 -0
- package/dist/src/browser/constants.js +75 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1826 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +46 -0
- package/dist/src/browser/profileState.js +285 -0
- package/dist/src/browser/prompt.js +182 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
- package/dist/src/browser/providers/index.js +2 -0
- package/dist/src/browser/reattach.js +189 -0
- package/dist/src/browser/reattachHelpers.js +387 -0
- package/dist/src/browser/sessionRunner.js +131 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +81 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +115 -0
- package/dist/src/cli/bridge/host.js +261 -0
- package/dist/src/cli/browserConfig.js +293 -0
- package/dist/src/cli/browserDefaults.js +82 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +14 -0
- package/dist/src/cli/dryRun.js +109 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/fileSize.js +11 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +21 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +316 -0
- package/dist/src/cli/options.js +305 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +90 -0
- package/dist/src/cli/sessionCommand.js +121 -0
- package/dist/src/cli/sessionDisplay.js +670 -0
- package/dist/src/cli/sessionLineage.js +60 -0
- package/dist/src/cli/sessionRunner.js +630 -0
- package/dist/src/cli/sessionTable.js +96 -0
- package/dist/src/cli/tagline.js +255 -0
- package/dist/src/cli/tui/index.js +499 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/browserSessionManager.js +81 -0
- package/dist/src/gemini-web/client.js +339 -0
- package/dist/src/gemini-web/executionClients.js +1 -0
- package/dist/src/gemini-web/executionMode.js +16 -0
- package/dist/src/gemini-web/executor.js +443 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +307 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +114 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +45 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +107 -0
- package/dist/src/oracle/client.js +235 -0
- package/dist/src/oracle/config.js +192 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +402 -0
- package/dist/src/oracle/finishLine.js +34 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +194 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +16 -0
- package/dist/src/oracle/request.js +58 -0
- package/dist/src/oracle/run.js +628 -0
- package/dist/src/oracle/runUtils.js +34 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +544 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +643 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +26 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +120 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +26 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { joinSelectors } from "../providerDomFlow.js";
|
|
2
|
+
const UI_TIMEOUT_MS = 60_000;
|
|
3
|
+
const RESPONSE_TIMEOUT_MS = 10 * 60_000;
|
|
4
|
+
export const GEMINI_DEEP_THINK_SELECTORS = {
|
|
5
|
+
input: [
|
|
6
|
+
"rich-textarea .ql-editor",
|
|
7
|
+
'[role="textbox"][aria-label*="prompt" i]',
|
|
8
|
+
'div[contenteditable="true"]',
|
|
9
|
+
],
|
|
10
|
+
sendButton: ["button.send-button", 'button[aria-label="Send message"]'],
|
|
11
|
+
toolsButton: ["button.toolbox-drawer-button", 'button[aria-label="Tools"]'],
|
|
12
|
+
toolsMenuItem: ['[role="menuitemcheckbox"]', ".toolbox-drawer-item-list-button"],
|
|
13
|
+
deepThinkActive: [
|
|
14
|
+
".toolbox-drawer-item-deselect-button",
|
|
15
|
+
'button[aria-label*="Deselect Deep Think"]',
|
|
16
|
+
],
|
|
17
|
+
uploadButton: ['button[aria-label="Open upload file menu"]', ".upload-card-button"],
|
|
18
|
+
uploadMenuItem: ['[role="menuitem"]'],
|
|
19
|
+
uploadTrigger: [".hidden-local-file-upload-button", ".hidden-local-upload-button"],
|
|
20
|
+
uploaderContainer: [".uploader-button-container", ".file-uploader"],
|
|
21
|
+
uploaderElement: ["uploader.upload-button"],
|
|
22
|
+
userTurnAttachment: [".file-preview-container"],
|
|
23
|
+
responseTurn: ["model-response"],
|
|
24
|
+
responseText: ["message-content", ".model-response-text message-content"],
|
|
25
|
+
responseComplete: [".response-footer.complete"],
|
|
26
|
+
userQuery: ["user-query"],
|
|
27
|
+
userQueryText: ["user-query-content", ".query-text"],
|
|
28
|
+
spinner: ['[role="progressbar"]'],
|
|
29
|
+
thoughtsToggle: [".thoughts-header-button", '[data-test-id="thoughts-header-button"]'],
|
|
30
|
+
thoughtsContent: ["model-thoughts", '[data-test-id="model-thoughts"]'],
|
|
31
|
+
hasThoughts: [".has-thoughts"],
|
|
32
|
+
};
|
|
33
|
+
function asSelectorLiteral(selectors) {
|
|
34
|
+
return JSON.stringify(joinSelectors(selectors));
|
|
35
|
+
}
|
|
36
|
+
function readTimeouts(ctx) {
|
|
37
|
+
const state = ctx.state;
|
|
38
|
+
const uiTimeoutMs = typeof state?.inputTimeoutMs === "number" && Number.isFinite(state.inputTimeoutMs)
|
|
39
|
+
? Math.max(1_000, state.inputTimeoutMs)
|
|
40
|
+
: UI_TIMEOUT_MS;
|
|
41
|
+
const responseTimeoutMs = typeof state?.timeoutMs === "number" && Number.isFinite(state.timeoutMs)
|
|
42
|
+
? Math.max(1_000, state.timeoutMs)
|
|
43
|
+
: RESPONSE_TIMEOUT_MS;
|
|
44
|
+
return { uiTimeoutMs, responseTimeoutMs };
|
|
45
|
+
}
|
|
46
|
+
async function waitForUi(ctx) {
|
|
47
|
+
ctx.log?.("[gemini-web] Waiting for Gemini UI to load...");
|
|
48
|
+
const inputSelector = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.input);
|
|
49
|
+
const { uiTimeoutMs } = readTimeouts(ctx);
|
|
50
|
+
const uiDeadline = Date.now() + uiTimeoutMs;
|
|
51
|
+
let uiReady = false;
|
|
52
|
+
let sawLoginRedirect = false;
|
|
53
|
+
while (Date.now() < uiDeadline) {
|
|
54
|
+
const state = await ctx.evaluate(`(() => {
|
|
55
|
+
const editor = document.querySelector(${inputSelector});
|
|
56
|
+
const href = location.href || '';
|
|
57
|
+
const bodyText = (document.body?.innerText || '').toLowerCase();
|
|
58
|
+
const requiresLogin =
|
|
59
|
+
href.includes('accounts.google.com') ||
|
|
60
|
+
(bodyText.includes('sign in') && bodyText.includes('google'));
|
|
61
|
+
return { ready: Boolean(editor), requiresLogin };
|
|
62
|
+
})()`);
|
|
63
|
+
if (state?.ready) {
|
|
64
|
+
uiReady = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (state?.requiresLogin) {
|
|
68
|
+
sawLoginRedirect = true;
|
|
69
|
+
}
|
|
70
|
+
await ctx.delay(1_000);
|
|
71
|
+
}
|
|
72
|
+
if (!uiReady) {
|
|
73
|
+
if (sawLoginRedirect) {
|
|
74
|
+
throw new Error("Gemini is showing a sign-in flow. Please sign in in Chrome and retry.");
|
|
75
|
+
}
|
|
76
|
+
throw new Error("Timed out waiting for Gemini UI prompt input to become ready.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function selectMode(ctx) {
|
|
80
|
+
const toolsButtonSelectors = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.toolsButton);
|
|
81
|
+
const toolsClickResult = await ctx.evaluate(`(() => {
|
|
82
|
+
const btn = document.querySelector(${toolsButtonSelectors});
|
|
83
|
+
if (btn instanceof HTMLElement) {
|
|
84
|
+
btn.click();
|
|
85
|
+
return 'clicked';
|
|
86
|
+
}
|
|
87
|
+
return 'not-found';
|
|
88
|
+
})()`);
|
|
89
|
+
if (toolsClickResult !== "clicked") {
|
|
90
|
+
throw new Error("Unable to open Gemini tools menu; Deep Think toggle is not accessible.");
|
|
91
|
+
}
|
|
92
|
+
await ctx.delay(1_000);
|
|
93
|
+
const deepThinkItemSelectors = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.toolsMenuItem);
|
|
94
|
+
const deepThinkClickResult = await ctx.evaluate(`(() => {
|
|
95
|
+
const items = Array.from(document.querySelectorAll(${deepThinkItemSelectors}));
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
const text = item.textContent?.trim().toLowerCase() ?? '';
|
|
98
|
+
if (!text.includes('deep think')) continue;
|
|
99
|
+
if (item instanceof HTMLElement) item.click();
|
|
100
|
+
return 'clicked';
|
|
101
|
+
}
|
|
102
|
+
return 'not-found';
|
|
103
|
+
})()`);
|
|
104
|
+
if (deepThinkClickResult !== "clicked") {
|
|
105
|
+
throw new Error('Unable to select "Deep Think" from Gemini tools menu.');
|
|
106
|
+
}
|
|
107
|
+
await ctx.delay(1_500);
|
|
108
|
+
const deepThinkActiveSelectors = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.deepThinkActive);
|
|
109
|
+
const deepThinkActive = await ctx.evaluate(`(() => {
|
|
110
|
+
const active = document.querySelector(${deepThinkActiveSelectors});
|
|
111
|
+
if (!(active instanceof HTMLElement)) return false;
|
|
112
|
+
const label = active.getAttribute('aria-label')?.toLowerCase() ?? '';
|
|
113
|
+
const text = active.textContent?.toLowerCase() ?? '';
|
|
114
|
+
return label.includes('deep think') || text.includes('deep think');
|
|
115
|
+
})()`);
|
|
116
|
+
if (!deepThinkActive) {
|
|
117
|
+
throw new Error("Deep Think did not appear selected after clicking the tools menu item.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function typePrompt(ctx) {
|
|
121
|
+
ctx.log?.("[gemini-web] Typing prompt...");
|
|
122
|
+
const inputSelector = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.input);
|
|
123
|
+
const typeResult = await ctx.evaluate(`(() => {
|
|
124
|
+
const editor = document.querySelector(${inputSelector});
|
|
125
|
+
if (!(editor instanceof HTMLElement)) return 'no-editor';
|
|
126
|
+
editor.focus();
|
|
127
|
+
editor.textContent = '';
|
|
128
|
+
if (typeof document.execCommand === 'function') {
|
|
129
|
+
document.execCommand('insertText', false, ${JSON.stringify(ctx.prompt)});
|
|
130
|
+
} else {
|
|
131
|
+
editor.textContent = ${JSON.stringify(ctx.prompt)};
|
|
132
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${JSON.stringify(ctx.prompt)} }));
|
|
133
|
+
}
|
|
134
|
+
const typed = (editor.textContent || '').trim().length > 0;
|
|
135
|
+
return typed ? 'typed' : 'empty';
|
|
136
|
+
})()`);
|
|
137
|
+
if (typeResult !== "typed") {
|
|
138
|
+
throw new Error(`Failed to type Gemini prompt (status=${typeResult ?? "unknown"}).`);
|
|
139
|
+
}
|
|
140
|
+
await ctx.delay(500);
|
|
141
|
+
}
|
|
142
|
+
async function submitPrompt(ctx) {
|
|
143
|
+
ctx.log?.("[gemini-web] Sending prompt...");
|
|
144
|
+
const inputSelector = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.input);
|
|
145
|
+
const sendButtonSelectors = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.sendButton);
|
|
146
|
+
const sendResult = await ctx.evaluate(`(() => {
|
|
147
|
+
const btn = document.querySelector(${sendButtonSelectors});
|
|
148
|
+
if (btn instanceof HTMLElement) {
|
|
149
|
+
btn.click();
|
|
150
|
+
return 'clicked';
|
|
151
|
+
}
|
|
152
|
+
const editor = document.querySelector(${inputSelector});
|
|
153
|
+
if (editor instanceof HTMLElement) {
|
|
154
|
+
editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
155
|
+
editor.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
156
|
+
return 'enter';
|
|
157
|
+
}
|
|
158
|
+
return 'not-found';
|
|
159
|
+
})()`);
|
|
160
|
+
if (sendResult !== "clicked" && sendResult !== "enter") {
|
|
161
|
+
throw new Error("Failed to submit prompt in Gemini Deep Think mode (send control not found).");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function waitForResponse(ctx) {
|
|
165
|
+
ctx.log?.("[gemini-web] Waiting for Deep Think response (this may take a while)...");
|
|
166
|
+
const responseTurnSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.responseTurn);
|
|
167
|
+
const responseTextSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.responseText);
|
|
168
|
+
const responseCompleteSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.responseComplete);
|
|
169
|
+
const spinnerSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.spinner);
|
|
170
|
+
const { responseTimeoutMs } = readTimeouts(ctx);
|
|
171
|
+
const responseDeadline = Date.now() + responseTimeoutMs;
|
|
172
|
+
let lastLog = 0;
|
|
173
|
+
let responseText = "";
|
|
174
|
+
while (Date.now() < responseDeadline) {
|
|
175
|
+
const payload = await ctx.evaluate(`(() => {
|
|
176
|
+
const turns = document.querySelectorAll(${responseTurnSel});
|
|
177
|
+
if (turns.length === 0) return JSON.stringify({ status: 'waiting' });
|
|
178
|
+
const lastTurn = turns[turns.length - 1];
|
|
179
|
+
const footer = lastTurn.querySelector(${responseCompleteSel});
|
|
180
|
+
const content = lastTurn.querySelector(${responseTextSel});
|
|
181
|
+
const text = content?.textContent?.trim() ?? '';
|
|
182
|
+
const lower = text.toLowerCase();
|
|
183
|
+
if (lower.includes('generating your response') || lower.includes('check back later') || lower.includes("i'm on it")) {
|
|
184
|
+
return JSON.stringify({ status: 'generating' });
|
|
185
|
+
}
|
|
186
|
+
if (footer && text.length > 0) {
|
|
187
|
+
return JSON.stringify({ status: 'done', text });
|
|
188
|
+
}
|
|
189
|
+
const spinners = lastTurn.querySelectorAll(${spinnerSel});
|
|
190
|
+
const visibleSpinners = Array.from(spinners).filter((s) => s instanceof HTMLElement && s.offsetParent !== null);
|
|
191
|
+
if (text.length > 0 && visibleSpinners.length === 0 && !footer) {
|
|
192
|
+
return JSON.stringify({ status: 'streaming' });
|
|
193
|
+
}
|
|
194
|
+
return JSON.stringify({ status: 'generating' });
|
|
195
|
+
})()`);
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(payload ?? "{}");
|
|
198
|
+
if (parsed.status === "done" && typeof parsed.text === "string" && parsed.text.length > 0) {
|
|
199
|
+
responseText = parsed.text;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
if (now - lastLog > 10_000) {
|
|
204
|
+
ctx.log?.(`[gemini-web] Deep Think still generating... (${parsed.status ?? "unknown"})`);
|
|
205
|
+
lastLog = now;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// ignore parse errors while polling
|
|
210
|
+
}
|
|
211
|
+
await ctx.delay(3_000);
|
|
212
|
+
}
|
|
213
|
+
if (!responseText) {
|
|
214
|
+
throw new Error(`Deep Think timed out waiting for response (${Math.ceil(responseTimeoutMs / 1000)} seconds).`);
|
|
215
|
+
}
|
|
216
|
+
return { text: responseText };
|
|
217
|
+
}
|
|
218
|
+
async function extractThoughts(ctx) {
|
|
219
|
+
const thoughtsToggleSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.thoughtsToggle);
|
|
220
|
+
const thoughtsContentSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.thoughtsContent);
|
|
221
|
+
const thinkResult = await ctx.evaluate(`(() => {
|
|
222
|
+
const toggle = document.querySelector(${thoughtsToggleSel});
|
|
223
|
+
if (!(toggle instanceof HTMLElement)) return 'no-toggle';
|
|
224
|
+
toggle.click();
|
|
225
|
+
return 'clicked';
|
|
226
|
+
})()`);
|
|
227
|
+
if (thinkResult !== "clicked") {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
await ctx.delay(1_500);
|
|
231
|
+
const extractedThoughts = await ctx.evaluate(`(() => {
|
|
232
|
+
const el = document.querySelector(${thoughtsContentSel});
|
|
233
|
+
if (!el) return '';
|
|
234
|
+
const full = el.textContent?.trim() ?? '';
|
|
235
|
+
const btn = el.querySelector('.thoughts-header-button, [data-test-id="thoughts-header-button"]');
|
|
236
|
+
const btnText = btn?.textContent?.trim() ?? '';
|
|
237
|
+
if (btnText && full.startsWith(btnText)) {
|
|
238
|
+
return full.slice(btnText.length).trim();
|
|
239
|
+
}
|
|
240
|
+
return full;
|
|
241
|
+
})()`);
|
|
242
|
+
return typeof extractedThoughts === "string" && extractedThoughts.length > 0
|
|
243
|
+
? extractedThoughts
|
|
244
|
+
: null;
|
|
245
|
+
}
|
|
246
|
+
export const geminiDeepThinkDomProvider = {
|
|
247
|
+
providerName: "gemini-web",
|
|
248
|
+
waitForUi,
|
|
249
|
+
selectMode,
|
|
250
|
+
typePrompt,
|
|
251
|
+
submitPrompt,
|
|
252
|
+
waitForResponse,
|
|
253
|
+
extractThoughts,
|
|
254
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import CDP from "chrome-remote-interface";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
5
|
+
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
|
|
6
|
+
import { launchChrome, connectToChrome, hideChromeWindow } from "./chromeLifecycle.js";
|
|
7
|
+
import { resolveBrowserConfig } from "./config.js";
|
|
8
|
+
import { syncCookies } from "./cookies.js";
|
|
9
|
+
import { CHATGPT_URL } from "./constants.js";
|
|
10
|
+
import { cleanupStaleProfileState } from "./profileState.js";
|
|
11
|
+
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
|
|
12
|
+
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
13
|
+
const recoverSession = deps.recoverSession ??
|
|
14
|
+
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
15
|
+
if (!runtime.chromePort) {
|
|
16
|
+
logger("No running Chrome detected; reopening browser to locate the session.");
|
|
17
|
+
return recoverSession(runtime, config);
|
|
18
|
+
}
|
|
19
|
+
const host = runtime.chromeHost ?? "127.0.0.1";
|
|
20
|
+
try {
|
|
21
|
+
const listTargets = deps.listTargets ??
|
|
22
|
+
(async () => {
|
|
23
|
+
const targets = await CDP.List({ host, port: runtime.chromePort });
|
|
24
|
+
return targets;
|
|
25
|
+
});
|
|
26
|
+
const connect = deps.connect ?? ((options) => CDP(options));
|
|
27
|
+
const targetList = (await listTargets());
|
|
28
|
+
const target = pickTarget(targetList, runtime);
|
|
29
|
+
const client = (await connect({
|
|
30
|
+
host,
|
|
31
|
+
port: runtime.chromePort,
|
|
32
|
+
target: target?.targetId,
|
|
33
|
+
}));
|
|
34
|
+
const { Runtime, DOM } = client;
|
|
35
|
+
if (Runtime?.enable) {
|
|
36
|
+
await Runtime.enable();
|
|
37
|
+
}
|
|
38
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
39
|
+
await DOM.enable();
|
|
40
|
+
}
|
|
41
|
+
const ensureConversationOpen = async () => {
|
|
42
|
+
const { result } = await Runtime.evaluate({
|
|
43
|
+
expression: "location.href",
|
|
44
|
+
returnByValue: true,
|
|
45
|
+
});
|
|
46
|
+
const href = typeof result?.value === "string" ? result.value : "";
|
|
47
|
+
if (href.includes("/c/")) {
|
|
48
|
+
const currentId = extractConversationIdFromUrl(href);
|
|
49
|
+
if (!runtime.conversationId || (currentId && currentId === runtime.conversationId)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
54
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
55
|
+
preferProjects: true,
|
|
56
|
+
promptPreview: deps.promptPreview,
|
|
57
|
+
}, 15_000);
|
|
58
|
+
if (!opened) {
|
|
59
|
+
throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
|
|
60
|
+
}
|
|
61
|
+
await waitForLocationChange(Runtime, 15_000);
|
|
62
|
+
};
|
|
63
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
64
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
65
|
+
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
66
|
+
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
67
|
+
await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
|
|
68
|
+
await ensureConversationOpen();
|
|
69
|
+
const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
|
|
70
|
+
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
71
|
+
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, "Reattach response timed out");
|
|
72
|
+
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
73
|
+
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
74
|
+
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
75
|
+
if (client && typeof client.close === "function") {
|
|
76
|
+
try {
|
|
77
|
+
await client.close();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
88
|
+
return recoverSession(runtime, config);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
92
|
+
const resolved = resolveBrowserConfig(config ?? {});
|
|
93
|
+
const manualLogin = Boolean(resolved.manualLogin);
|
|
94
|
+
const userDataDir = manualLogin
|
|
95
|
+
? (resolved.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile"))
|
|
96
|
+
: await mkdtemp(path.join(os.tmpdir(), "oracle-reattach-"));
|
|
97
|
+
if (manualLogin) {
|
|
98
|
+
await mkdir(userDataDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
101
|
+
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
102
|
+
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
103
|
+
const { Network, Page, Runtime, DOM } = client;
|
|
104
|
+
if (Runtime?.enable) {
|
|
105
|
+
await Runtime.enable();
|
|
106
|
+
}
|
|
107
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
108
|
+
await DOM.enable();
|
|
109
|
+
}
|
|
110
|
+
if (!resolved.headless && resolved.hideWindow) {
|
|
111
|
+
await hideChromeWindow(chrome, logger);
|
|
112
|
+
}
|
|
113
|
+
let appliedCookies = 0;
|
|
114
|
+
if (!manualLogin && resolved.cookieSync) {
|
|
115
|
+
appliedCookies = await syncCookies(Network, resolved.url, resolved.chromeProfile, logger, {
|
|
116
|
+
allowErrors: resolved.allowCookieErrors,
|
|
117
|
+
filterNames: resolved.cookieNames ?? undefined,
|
|
118
|
+
inlineCookies: resolved.inlineCookies ?? undefined,
|
|
119
|
+
cookiePath: resolved.chromeCookiePath ?? undefined,
|
|
120
|
+
waitMs: resolved.cookieSyncWaitMs ?? 0,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
124
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
125
|
+
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
126
|
+
if (resolved.url !== CHATGPT_URL) {
|
|
127
|
+
await navigateToChatGPT(Page, Runtime, resolved.url, logger);
|
|
128
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
129
|
+
}
|
|
130
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
131
|
+
const conversationUrl = buildConversationUrl(runtime, resolved.url);
|
|
132
|
+
if (conversationUrl) {
|
|
133
|
+
logger(`Reopening conversation at ${conversationUrl}`);
|
|
134
|
+
await navigateToChatGPT(Page, Runtime, conversationUrl, logger);
|
|
135
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
136
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
140
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
141
|
+
preferProjects: resolved.url !== CHATGPT_URL ||
|
|
142
|
+
Boolean(runtime.tabUrl && (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes("/project"))),
|
|
143
|
+
promptPreview: deps.promptPreview,
|
|
144
|
+
}, 15_000);
|
|
145
|
+
if (!opened) {
|
|
146
|
+
throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
|
|
147
|
+
}
|
|
148
|
+
await waitForLocationChange(Runtime, 15_000);
|
|
149
|
+
}
|
|
150
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
151
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
152
|
+
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
153
|
+
const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
|
|
154
|
+
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
155
|
+
const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined);
|
|
156
|
+
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
157
|
+
const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
|
|
158
|
+
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
159
|
+
if (client && typeof client.close === "function") {
|
|
160
|
+
try {
|
|
161
|
+
await client.close();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!resolved.keepBrowser) {
|
|
168
|
+
try {
|
|
169
|
+
await chrome.kill();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
if (manualLogin) {
|
|
175
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
182
|
+
}
|
|
183
|
+
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
184
|
+
export const __test__ = {
|
|
185
|
+
pickTarget,
|
|
186
|
+
extractConversationIdFromUrl,
|
|
187
|
+
buildConversationUrl,
|
|
188
|
+
openConversationFromSidebar,
|
|
189
|
+
};
|