@steipete/oracle 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +390 -295
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +62 -48
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +156 -134
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
|
@@ -1,43 +1,50 @@
|
|
|
1
|
-
import { joinSelectors } from
|
|
1
|
+
import { joinSelectors } from "../providerDomFlow.js";
|
|
2
2
|
const UI_TIMEOUT_MS = 60_000;
|
|
3
3
|
const RESPONSE_TIMEOUT_MS = 10 * 60_000;
|
|
4
4
|
export const GEMINI_DEEP_THINK_SELECTORS = {
|
|
5
|
-
input: [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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"],
|
|
11
18
|
uploadMenuItem: ['[role="menuitem"]'],
|
|
12
|
-
uploadTrigger: [
|
|
13
|
-
uploaderContainer: [
|
|
14
|
-
uploaderElement: [
|
|
15
|
-
userTurnAttachment: [
|
|
16
|
-
responseTurn: [
|
|
17
|
-
responseText: [
|
|
18
|
-
responseComplete: [
|
|
19
|
-
userQuery: [
|
|
20
|
-
userQueryText: [
|
|
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"],
|
|
21
28
|
spinner: ['[role="progressbar"]'],
|
|
22
|
-
thoughtsToggle: [
|
|
23
|
-
thoughtsContent: [
|
|
24
|
-
hasThoughts: [
|
|
29
|
+
thoughtsToggle: [".thoughts-header-button", '[data-test-id="thoughts-header-button"]'],
|
|
30
|
+
thoughtsContent: ["model-thoughts", '[data-test-id="model-thoughts"]'],
|
|
31
|
+
hasThoughts: [".has-thoughts"],
|
|
25
32
|
};
|
|
26
33
|
function asSelectorLiteral(selectors) {
|
|
27
34
|
return JSON.stringify(joinSelectors(selectors));
|
|
28
35
|
}
|
|
29
36
|
function readTimeouts(ctx) {
|
|
30
37
|
const state = ctx.state;
|
|
31
|
-
const uiTimeoutMs = typeof state?.inputTimeoutMs ===
|
|
38
|
+
const uiTimeoutMs = typeof state?.inputTimeoutMs === "number" && Number.isFinite(state.inputTimeoutMs)
|
|
32
39
|
? Math.max(1_000, state.inputTimeoutMs)
|
|
33
40
|
: UI_TIMEOUT_MS;
|
|
34
|
-
const responseTimeoutMs = typeof state?.timeoutMs ===
|
|
41
|
+
const responseTimeoutMs = typeof state?.timeoutMs === "number" && Number.isFinite(state.timeoutMs)
|
|
35
42
|
? Math.max(1_000, state.timeoutMs)
|
|
36
43
|
: RESPONSE_TIMEOUT_MS;
|
|
37
44
|
return { uiTimeoutMs, responseTimeoutMs };
|
|
38
45
|
}
|
|
39
46
|
async function waitForUi(ctx) {
|
|
40
|
-
ctx.log?.(
|
|
47
|
+
ctx.log?.("[gemini-web] Waiting for Gemini UI to load...");
|
|
41
48
|
const inputSelector = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.input);
|
|
42
49
|
const { uiTimeoutMs } = readTimeouts(ctx);
|
|
43
50
|
const uiDeadline = Date.now() + uiTimeoutMs;
|
|
@@ -64,9 +71,9 @@ async function waitForUi(ctx) {
|
|
|
64
71
|
}
|
|
65
72
|
if (!uiReady) {
|
|
66
73
|
if (sawLoginRedirect) {
|
|
67
|
-
throw new Error(
|
|
74
|
+
throw new Error("Gemini is showing a sign-in flow. Please sign in in Chrome and retry.");
|
|
68
75
|
}
|
|
69
|
-
throw new Error(
|
|
76
|
+
throw new Error("Timed out waiting for Gemini UI prompt input to become ready.");
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
async function selectMode(ctx) {
|
|
@@ -79,8 +86,8 @@ async function selectMode(ctx) {
|
|
|
79
86
|
}
|
|
80
87
|
return 'not-found';
|
|
81
88
|
})()`);
|
|
82
|
-
if (toolsClickResult !==
|
|
83
|
-
throw new Error(
|
|
89
|
+
if (toolsClickResult !== "clicked") {
|
|
90
|
+
throw new Error("Unable to open Gemini tools menu; Deep Think toggle is not accessible.");
|
|
84
91
|
}
|
|
85
92
|
await ctx.delay(1_000);
|
|
86
93
|
const deepThinkItemSelectors = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.toolsMenuItem);
|
|
@@ -94,7 +101,7 @@ async function selectMode(ctx) {
|
|
|
94
101
|
}
|
|
95
102
|
return 'not-found';
|
|
96
103
|
})()`);
|
|
97
|
-
if (deepThinkClickResult !==
|
|
104
|
+
if (deepThinkClickResult !== "clicked") {
|
|
98
105
|
throw new Error('Unable to select "Deep Think" from Gemini tools menu.');
|
|
99
106
|
}
|
|
100
107
|
await ctx.delay(1_500);
|
|
@@ -107,11 +114,11 @@ async function selectMode(ctx) {
|
|
|
107
114
|
return label.includes('deep think') || text.includes('deep think');
|
|
108
115
|
})()`);
|
|
109
116
|
if (!deepThinkActive) {
|
|
110
|
-
throw new Error(
|
|
117
|
+
throw new Error("Deep Think did not appear selected after clicking the tools menu item.");
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
async function typePrompt(ctx) {
|
|
114
|
-
ctx.log?.(
|
|
121
|
+
ctx.log?.("[gemini-web] Typing prompt...");
|
|
115
122
|
const inputSelector = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.input);
|
|
116
123
|
const typeResult = await ctx.evaluate(`(() => {
|
|
117
124
|
const editor = document.querySelector(${inputSelector});
|
|
@@ -127,13 +134,13 @@ async function typePrompt(ctx) {
|
|
|
127
134
|
const typed = (editor.textContent || '').trim().length > 0;
|
|
128
135
|
return typed ? 'typed' : 'empty';
|
|
129
136
|
})()`);
|
|
130
|
-
if (typeResult !==
|
|
131
|
-
throw new Error(`Failed to type Gemini prompt (status=${typeResult ??
|
|
137
|
+
if (typeResult !== "typed") {
|
|
138
|
+
throw new Error(`Failed to type Gemini prompt (status=${typeResult ?? "unknown"}).`);
|
|
132
139
|
}
|
|
133
140
|
await ctx.delay(500);
|
|
134
141
|
}
|
|
135
142
|
async function submitPrompt(ctx) {
|
|
136
|
-
ctx.log?.(
|
|
143
|
+
ctx.log?.("[gemini-web] Sending prompt...");
|
|
137
144
|
const inputSelector = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.input);
|
|
138
145
|
const sendButtonSelectors = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.sendButton);
|
|
139
146
|
const sendResult = await ctx.evaluate(`(() => {
|
|
@@ -150,12 +157,12 @@ async function submitPrompt(ctx) {
|
|
|
150
157
|
}
|
|
151
158
|
return 'not-found';
|
|
152
159
|
})()`);
|
|
153
|
-
if (sendResult !==
|
|
154
|
-
throw new Error(
|
|
160
|
+
if (sendResult !== "clicked" && sendResult !== "enter") {
|
|
161
|
+
throw new Error("Failed to submit prompt in Gemini Deep Think mode (send control not found).");
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
async function waitForResponse(ctx) {
|
|
158
|
-
ctx.log?.(
|
|
165
|
+
ctx.log?.("[gemini-web] Waiting for Deep Think response (this may take a while)...");
|
|
159
166
|
const responseTurnSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.responseTurn);
|
|
160
167
|
const responseTextSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.responseText);
|
|
161
168
|
const responseCompleteSel = asSelectorLiteral(GEMINI_DEEP_THINK_SELECTORS.responseComplete);
|
|
@@ -163,7 +170,7 @@ async function waitForResponse(ctx) {
|
|
|
163
170
|
const { responseTimeoutMs } = readTimeouts(ctx);
|
|
164
171
|
const responseDeadline = Date.now() + responseTimeoutMs;
|
|
165
172
|
let lastLog = 0;
|
|
166
|
-
let responseText =
|
|
173
|
+
let responseText = "";
|
|
167
174
|
while (Date.now() < responseDeadline) {
|
|
168
175
|
const payload = await ctx.evaluate(`(() => {
|
|
169
176
|
const turns = document.querySelectorAll(${responseTurnSel});
|
|
@@ -187,14 +194,14 @@ async function waitForResponse(ctx) {
|
|
|
187
194
|
return JSON.stringify({ status: 'generating' });
|
|
188
195
|
})()`);
|
|
189
196
|
try {
|
|
190
|
-
const parsed = JSON.parse(payload ??
|
|
191
|
-
if (parsed.status ===
|
|
197
|
+
const parsed = JSON.parse(payload ?? "{}");
|
|
198
|
+
if (parsed.status === "done" && typeof parsed.text === "string" && parsed.text.length > 0) {
|
|
192
199
|
responseText = parsed.text;
|
|
193
200
|
break;
|
|
194
201
|
}
|
|
195
202
|
const now = Date.now();
|
|
196
203
|
if (now - lastLog > 10_000) {
|
|
197
|
-
ctx.log?.(`[gemini-web] Deep Think still generating... (${parsed.status ??
|
|
204
|
+
ctx.log?.(`[gemini-web] Deep Think still generating... (${parsed.status ?? "unknown"})`);
|
|
198
205
|
lastLog = now;
|
|
199
206
|
}
|
|
200
207
|
}
|
|
@@ -217,7 +224,7 @@ async function extractThoughts(ctx) {
|
|
|
217
224
|
toggle.click();
|
|
218
225
|
return 'clicked';
|
|
219
226
|
})()`);
|
|
220
|
-
if (thinkResult !==
|
|
227
|
+
if (thinkResult !== "clicked") {
|
|
221
228
|
return null;
|
|
222
229
|
}
|
|
223
230
|
await ctx.delay(1_500);
|
|
@@ -232,10 +239,12 @@ async function extractThoughts(ctx) {
|
|
|
232
239
|
}
|
|
233
240
|
return full;
|
|
234
241
|
})()`);
|
|
235
|
-
return typeof extractedThoughts ===
|
|
242
|
+
return typeof extractedThoughts === "string" && extractedThoughts.length > 0
|
|
243
|
+
? extractedThoughts
|
|
244
|
+
: null;
|
|
236
245
|
}
|
|
237
246
|
export const geminiDeepThinkDomProvider = {
|
|
238
|
-
providerName:
|
|
247
|
+
providerName: "gemini-web",
|
|
239
248
|
waitForUi,
|
|
240
249
|
selectMode,
|
|
241
250
|
typePrompt,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { chatgptDomProvider } from
|
|
2
|
-
export { geminiDeepThinkDomProvider, GEMINI_DEEP_THINK_SELECTORS, } from
|
|
1
|
+
export { chatgptDomProvider } from "./chatgptDomProvider.js";
|
|
2
|
+
export { geminiDeepThinkDomProvider, GEMINI_DEEP_THINK_SELECTORS, } from "./geminiDeepThinkDomProvider.js";
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import CDP from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { mkdtemp, mkdir, rm } from
|
|
5
|
-
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from
|
|
6
|
-
import { launchChrome, connectToChrome, hideChromeWindow } from
|
|
7
|
-
import { resolveBrowserConfig } from
|
|
8
|
-
import { syncCookies } from
|
|
9
|
-
import { CHATGPT_URL } from
|
|
10
|
-
import { cleanupStaleProfileState } from
|
|
11
|
-
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from
|
|
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, CONVERSATION_TURN_SELECTOR } 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
12
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
13
13
|
const recoverSession = deps.recoverSession ??
|
|
14
14
|
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
15
15
|
if (!runtime.chromePort) {
|
|
16
|
-
logger(
|
|
16
|
+
logger("No running Chrome detected; reopening browser to locate the session.");
|
|
17
17
|
return recoverSession(runtime, config);
|
|
18
18
|
}
|
|
19
|
-
const host = runtime.chromeHost ??
|
|
19
|
+
const host = runtime.chromeHost ?? "127.0.0.1";
|
|
20
20
|
try {
|
|
21
21
|
const listTargets = deps.listTargets ??
|
|
22
22
|
(async () => {
|
|
@@ -35,25 +35,28 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
35
35
|
if (Runtime?.enable) {
|
|
36
36
|
await Runtime.enable();
|
|
37
37
|
}
|
|
38
|
-
if (DOM && typeof DOM.enable ===
|
|
38
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
39
39
|
await DOM.enable();
|
|
40
40
|
}
|
|
41
41
|
const ensureConversationOpen = async () => {
|
|
42
|
-
const { result } = await Runtime.evaluate({
|
|
43
|
-
|
|
44
|
-
|
|
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/")) {
|
|
45
48
|
const currentId = extractConversationIdFromUrl(href);
|
|
46
49
|
if (!runtime.conversationId || (currentId && currentId === runtime.conversationId)) {
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
51
|
-
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ??
|
|
54
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
52
55
|
preferProjects: true,
|
|
53
56
|
promptPreview: deps.promptPreview,
|
|
54
57
|
}, 15_000);
|
|
55
58
|
if (!opened) {
|
|
56
|
-
throw new Error(
|
|
59
|
+
throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
|
|
57
60
|
}
|
|
58
61
|
await waitForLocationChange(Runtime, 15_000);
|
|
59
62
|
};
|
|
@@ -61,15 +64,16 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
61
64
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
62
65
|
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
63
66
|
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
64
|
-
await withTimeout(Runtime.evaluate({ expression:
|
|
67
|
+
await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
|
|
65
68
|
await ensureConversationOpen();
|
|
66
|
-
const minTurnIndex = await
|
|
69
|
+
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
70
|
+
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
67
71
|
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
68
|
-
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000,
|
|
72
|
+
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, "Reattach response timed out");
|
|
69
73
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
70
|
-
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000,
|
|
74
|
+
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
71
75
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
72
|
-
if (client && typeof client.close ===
|
|
76
|
+
if (client && typeof client.close === "function") {
|
|
73
77
|
try {
|
|
74
78
|
await client.close();
|
|
75
79
|
}
|
|
@@ -89,19 +93,19 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
89
93
|
const resolved = resolveBrowserConfig(config ?? {});
|
|
90
94
|
const manualLogin = Boolean(resolved.manualLogin);
|
|
91
95
|
const userDataDir = manualLogin
|
|
92
|
-
? resolved.manualLoginProfileDir ?? path.join(os.homedir(),
|
|
93
|
-
: await mkdtemp(path.join(os.tmpdir(),
|
|
96
|
+
? (resolved.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile"))
|
|
97
|
+
: await mkdtemp(path.join(os.tmpdir(), "oracle-reattach-"));
|
|
94
98
|
if (manualLogin) {
|
|
95
99
|
await mkdir(userDataDir, { recursive: true });
|
|
96
100
|
}
|
|
97
101
|
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
98
|
-
const chromeHost = chrome.host ??
|
|
102
|
+
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
99
103
|
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
100
104
|
const { Network, Page, Runtime, DOM } = client;
|
|
101
105
|
if (Runtime?.enable) {
|
|
102
106
|
await Runtime.enable();
|
|
103
107
|
}
|
|
104
|
-
if (DOM && typeof DOM.enable ===
|
|
108
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
105
109
|
await DOM.enable();
|
|
106
110
|
}
|
|
107
111
|
if (!resolved.headless && resolved.hideWindow) {
|
|
@@ -134,26 +138,27 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
134
138
|
}
|
|
135
139
|
else {
|
|
136
140
|
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
137
|
-
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ??
|
|
141
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
138
142
|
preferProjects: resolved.url !== CHATGPT_URL ||
|
|
139
|
-
Boolean(runtime.tabUrl && (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes(
|
|
143
|
+
Boolean(runtime.tabUrl && (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes("/project"))),
|
|
140
144
|
promptPreview: deps.promptPreview,
|
|
141
145
|
}, 15_000);
|
|
142
146
|
if (!opened) {
|
|
143
|
-
throw new Error(
|
|
147
|
+
throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
|
|
144
148
|
}
|
|
145
149
|
await waitForLocationChange(Runtime, 15_000);
|
|
146
150
|
}
|
|
147
151
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
148
152
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
149
153
|
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
150
|
-
const minTurnIndex = await
|
|
154
|
+
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
155
|
+
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
151
156
|
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
152
157
|
const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined);
|
|
153
158
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
154
159
|
const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
|
|
155
160
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
156
|
-
if (client && typeof client.close ===
|
|
161
|
+
if (client && typeof client.close === "function") {
|
|
157
162
|
try {
|
|
158
163
|
await client.close();
|
|
159
164
|
}
|
|
@@ -169,7 +174,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
169
174
|
// ignore
|
|
170
175
|
}
|
|
171
176
|
if (manualLogin) {
|
|
172
|
-
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode:
|
|
177
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
173
178
|
}
|
|
174
179
|
else {
|
|
175
180
|
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -177,10 +182,38 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
177
182
|
}
|
|
178
183
|
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
179
184
|
}
|
|
185
|
+
async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
|
|
186
|
+
const preview = promptPreview?.trim();
|
|
187
|
+
if (!preview) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const { result } = await Runtime.evaluate({
|
|
191
|
+
expression: `(() => {
|
|
192
|
+
const needle = ${JSON.stringify(preview.toLowerCase().replace(/\s+/g, " ").slice(0, 120))};
|
|
193
|
+
if (!needle) return null;
|
|
194
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
195
|
+
const turns = Array.from(document.querySelectorAll(${JSON.stringify(CONVERSATION_TURN_SELECTOR)}));
|
|
196
|
+
let matched = null;
|
|
197
|
+
for (const [index, node] of turns.entries()) {
|
|
198
|
+
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
199
|
+
const isUser = attr === 'user' || Boolean(node.querySelector('[data-message-author-role="user"]'));
|
|
200
|
+
if (!isUser) continue;
|
|
201
|
+
const text = normalize(node.innerText || node.textContent || '');
|
|
202
|
+
if (text.length > 0 && (text.includes(needle) || needle.includes(text.slice(0, needle.length)))) {
|
|
203
|
+
matched = index;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return matched;
|
|
207
|
+
})()`,
|
|
208
|
+
returnByValue: true,
|
|
209
|
+
});
|
|
210
|
+
return typeof result?.value === "number" ? result.value : null;
|
|
211
|
+
}
|
|
180
212
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
181
213
|
export const __test__ = {
|
|
182
214
|
pickTarget,
|
|
183
215
|
extractConversationIdFromUrl,
|
|
184
216
|
buildConversationUrl,
|
|
185
217
|
openConversationFromSidebar,
|
|
218
|
+
readPromptPreviewTurnIndex,
|
|
186
219
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CONVERSATION_TURN_SELECTOR } from
|
|
2
|
-
import { delay } from
|
|
3
|
-
import { readAssistantSnapshot } from
|
|
1
|
+
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
import { delay } from "./utils.js";
|
|
3
|
+
import { readAssistantSnapshot } from "./pageActions.js";
|
|
4
4
|
export function pickTarget(targets, runtime) {
|
|
5
5
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
6
6
|
return undefined;
|
|
@@ -12,11 +12,11 @@ export function pickTarget(targets, runtime) {
|
|
|
12
12
|
}
|
|
13
13
|
if (runtime.tabUrl) {
|
|
14
14
|
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
15
|
-
targets.find((t) => runtime.tabUrl.startsWith(t.url ||
|
|
15
|
+
targets.find((t) => runtime.tabUrl.startsWith(t.url || ""));
|
|
16
16
|
if (byUrl)
|
|
17
17
|
return byUrl;
|
|
18
18
|
}
|
|
19
|
-
return targets.find((t) => t.type ===
|
|
19
|
+
return targets.find((t) => t.type === "page") ?? targets[0];
|
|
20
20
|
}
|
|
21
21
|
export function extractConversationIdFromUrl(url) {
|
|
22
22
|
if (!url)
|
|
@@ -26,7 +26,7 @@ export function extractConversationIdFromUrl(url) {
|
|
|
26
26
|
}
|
|
27
27
|
export function buildConversationUrl(runtime, baseUrl) {
|
|
28
28
|
if (runtime.tabUrl) {
|
|
29
|
-
if (runtime.tabUrl.includes(
|
|
29
|
+
if (runtime.tabUrl.includes("/c/")) {
|
|
30
30
|
return runtime.tabUrl;
|
|
31
31
|
}
|
|
32
32
|
return null;
|
|
@@ -37,8 +37,8 @@ export function buildConversationUrl(runtime, baseUrl) {
|
|
|
37
37
|
}
|
|
38
38
|
try {
|
|
39
39
|
const base = new URL(baseUrl);
|
|
40
|
-
const pathRoot = base.pathname.replace(/\/$/,
|
|
41
|
-
const prefix = pathRoot ===
|
|
40
|
+
const pathRoot = base.pathname.replace(/\/$/, "");
|
|
41
|
+
const prefix = pathRoot === "/" ? "" : pathRoot;
|
|
42
42
|
return `${base.origin}${prefix}/c/${conversationId}`;
|
|
43
43
|
}
|
|
44
44
|
catch {
|
|
@@ -193,7 +193,7 @@ export async function openConversationFromSidebarWithRetry(Runtime, options, tim
|
|
|
193
193
|
}
|
|
194
194
|
export async function waitForPromptPreview(Runtime, promptPreview, timeoutMs) {
|
|
195
195
|
const needleFull = promptPreview.trim().toLowerCase().slice(0, 120);
|
|
196
|
-
const needleShort = needleFull.replace(/\\s*\\d{4,}\\s*$/,
|
|
196
|
+
const needleShort = needleFull.replace(/\\s*\\d{4,}\\s*$/, "").trim();
|
|
197
197
|
const needles = Array.from(new Set([needleFull, needleShort].filter(Boolean)));
|
|
198
198
|
if (needles.length === 0)
|
|
199
199
|
return false;
|
|
@@ -241,10 +241,10 @@ export async function waitForPromptPreview(Runtime, promptPreview, timeoutMs) {
|
|
|
241
241
|
}
|
|
242
242
|
export async function waitForLocationChange(Runtime, timeoutMs) {
|
|
243
243
|
const start = Date.now();
|
|
244
|
-
let lastHref =
|
|
244
|
+
let lastHref = "";
|
|
245
245
|
while (Date.now() - start < timeoutMs) {
|
|
246
|
-
const { result } = await Runtime.evaluate({ expression:
|
|
247
|
-
const href = typeof result?.value ===
|
|
246
|
+
const { result } = await Runtime.evaluate({ expression: "location.href", returnByValue: true });
|
|
247
|
+
const href = typeof result?.value === "string" ? result.value : "";
|
|
248
248
|
if (lastHref && href !== lastHref) {
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
@@ -259,9 +259,9 @@ export async function readConversationTurnIndex(Runtime, logger) {
|
|
|
259
259
|
expression: `document.querySelectorAll(${selectorLiteral}).length`,
|
|
260
260
|
returnByValue: true,
|
|
261
261
|
});
|
|
262
|
-
const raw = typeof result?.value ===
|
|
262
|
+
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
263
263
|
if (!Number.isFinite(raw)) {
|
|
264
|
-
throw new Error(
|
|
264
|
+
throw new Error("Turn count not numeric");
|
|
265
265
|
}
|
|
266
266
|
return Math.max(0, Math.floor(raw) - 1);
|
|
267
267
|
}
|
|
@@ -273,14 +273,19 @@ export async function readConversationTurnIndex(Runtime, logger) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
function normalizeForComparison(text) {
|
|
276
|
-
return String(text ||
|
|
276
|
+
return String(text || "")
|
|
277
|
+
.toLowerCase()
|
|
278
|
+
.replace(/\\s+/g, " ")
|
|
279
|
+
.trim();
|
|
277
280
|
}
|
|
278
281
|
export function buildPromptEchoMatcher(promptPreview) {
|
|
279
|
-
const normalizedPrompt = normalizeForComparison(promptPreview ??
|
|
282
|
+
const normalizedPrompt = normalizeForComparison(promptPreview ?? "");
|
|
280
283
|
if (!normalizedPrompt) {
|
|
281
284
|
return null;
|
|
282
285
|
}
|
|
283
|
-
const promptPrefix = normalizedPrompt.length >= 80
|
|
286
|
+
const promptPrefix = normalizedPrompt.length >= 80
|
|
287
|
+
? normalizedPrompt.slice(0, Math.min(200, normalizedPrompt.length))
|
|
288
|
+
: "";
|
|
284
289
|
const minFragment = Math.min(40, normalizedPrompt.length);
|
|
285
290
|
return {
|
|
286
291
|
isEcho: (text) => {
|
|
@@ -294,11 +299,11 @@ export function buildPromptEchoMatcher(promptPreview) {
|
|
|
294
299
|
if (normalized.length >= minFragment && normalizedPrompt.startsWith(normalized)) {
|
|
295
300
|
return true;
|
|
296
301
|
}
|
|
297
|
-
if (normalized.includes(
|
|
298
|
-
const marker = normalized.includes(
|
|
302
|
+
if (normalized.includes("…") || normalized.includes("...")) {
|
|
303
|
+
const marker = normalized.includes("…") ? "…" : "...";
|
|
299
304
|
const [prefixRaw, suffixRaw] = normalized.split(marker);
|
|
300
|
-
const prefix = prefixRaw?.trim() ??
|
|
301
|
-
const suffix = suffixRaw?.trim() ??
|
|
305
|
+
const prefix = prefixRaw?.trim() ?? "";
|
|
306
|
+
const suffix = suffixRaw?.trim() ?? "";
|
|
302
307
|
if (!prefix && !suffix)
|
|
303
308
|
return false;
|
|
304
309
|
if (prefix && !normalizedPrompt.includes(prefix))
|
|
@@ -316,13 +321,13 @@ export async function recoverPromptEcho(Runtime, answer, matcher, logger, minTur
|
|
|
316
321
|
if (!matcher || !matcher.isEcho(answer.text)) {
|
|
317
322
|
return answer;
|
|
318
323
|
}
|
|
319
|
-
logger(
|
|
324
|
+
logger("Detected prompt echo while reattaching; waiting for assistant response...");
|
|
320
325
|
const deadline = Date.now() + Math.min(timeoutMs, 15_000);
|
|
321
326
|
let bestText = null;
|
|
322
327
|
let stableCount = 0;
|
|
323
328
|
while (Date.now() < deadline) {
|
|
324
329
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex ?? undefined).catch(() => null);
|
|
325
|
-
const text = typeof snapshot?.text ===
|
|
330
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
326
331
|
if (!text || matcher.isEcho(text)) {
|
|
327
332
|
await delay(300);
|
|
328
333
|
continue;
|
|
@@ -340,7 +345,7 @@ export async function recoverPromptEcho(Runtime, answer, matcher, logger, minTur
|
|
|
340
345
|
await delay(300);
|
|
341
346
|
}
|
|
342
347
|
if (bestText) {
|
|
343
|
-
logger(
|
|
348
|
+
logger("Recovered assistant response after prompt echo during reattach");
|
|
344
349
|
return { ...answer, text: bestText };
|
|
345
350
|
}
|
|
346
351
|
return answer;
|
|
@@ -375,8 +380,8 @@ export function alignPromptEchoPair(answerText, answerMarkdown, matcher, logger,
|
|
|
375
380
|
}
|
|
376
381
|
export function alignPromptEchoMarkdown(answerText, answerMarkdown, matcher, logger) {
|
|
377
382
|
const aligned = alignPromptEchoPair(answerText, answerMarkdown, matcher, logger, {
|
|
378
|
-
text:
|
|
379
|
-
markdown:
|
|
383
|
+
text: "Aligned prompt-echo text to copied markdown during reattach",
|
|
384
|
+
markdown: "Aligned prompt-echo markdown to response text during reattach",
|
|
380
385
|
});
|
|
381
386
|
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
382
387
|
}
|