@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.
Files changed (177) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +61 -48
  3. package/dist/bin/oracle-cli.js +455 -402
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +275 -117
  22. package/dist/src/browser/actions/navigation.js +161 -137
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +390 -295
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +1 -1
  40. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  42. package/dist/src/browser/providers/index.js +2 -2
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +62 -48
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +2 -2
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +3 -3
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +7 -7
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +127 -106
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +32 -28
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +95 -81
  77. package/dist/src/cli/sessionLineage.js +6 -2
  78. package/dist/src/cli/sessionRunner.js +103 -93
  79. package/dist/src/cli/sessionTable.js +26 -23
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +139 -128
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  86. package/dist/src/gemini-web/client.js +76 -70
  87. package/dist/src/gemini-web/executionMode.js +6 -8
  88. package/dist/src/gemini-web/executor.js +98 -93
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +51 -47
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +50 -41
  99. package/dist/src/oracle/config.js +96 -66
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +55 -46
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -33
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +16 -13
  112. package/dist/src/oracle/run.js +156 -134
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +77 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/README.md +2 -0
  127. package/package.json +66 -62
  128. package/vendor/oracle-notifier/README.md +2 -0
  129. package/dist/markdansi/types/index.js +0 -4
  130. package/dist/oracle/bin/oracle-cli.js +0 -472
  131. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  132. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  133. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  134. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  135. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  136. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  137. package/dist/oracle/src/browser/config.js +0 -33
  138. package/dist/oracle/src/browser/constants.js +0 -40
  139. package/dist/oracle/src/browser/cookies.js +0 -210
  140. package/dist/oracle/src/browser/domDebug.js +0 -36
  141. package/dist/oracle/src/browser/index.js +0 -331
  142. package/dist/oracle/src/browser/pageActions.js +0 -5
  143. package/dist/oracle/src/browser/prompt.js +0 -88
  144. package/dist/oracle/src/browser/promptSummary.js +0 -20
  145. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  146. package/dist/oracle/src/browser/types.js +0 -1
  147. package/dist/oracle/src/browser/utils.js +0 -62
  148. package/dist/oracle/src/browserMode.js +0 -1
  149. package/dist/oracle/src/cli/browserConfig.js +0 -44
  150. package/dist/oracle/src/cli/dryRun.js +0 -59
  151. package/dist/oracle/src/cli/engine.js +0 -17
  152. package/dist/oracle/src/cli/errorUtils.js +0 -9
  153. package/dist/oracle/src/cli/help.js +0 -70
  154. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  155. package/dist/oracle/src/cli/options.js +0 -103
  156. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  157. package/dist/oracle/src/cli/rootAlias.js +0 -30
  158. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  159. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  160. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  161. package/dist/oracle/src/heartbeat.js +0 -43
  162. package/dist/oracle/src/oracle/client.js +0 -48
  163. package/dist/oracle/src/oracle/config.js +0 -29
  164. package/dist/oracle/src/oracle/errors.js +0 -101
  165. package/dist/oracle/src/oracle/files.js +0 -220
  166. package/dist/oracle/src/oracle/format.js +0 -33
  167. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  168. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  169. package/dist/oracle/src/oracle/request.js +0 -48
  170. package/dist/oracle/src/oracle/run.js +0 -444
  171. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  172. package/dist/oracle/src/oracle/types.js +0 -1
  173. package/dist/oracle/src/oracle.js +0 -9
  174. package/dist/oracle/src/sessionManager.js +0 -205
  175. package/dist/oracle/src/version.js +0 -39
  176. package/dist/scripts/chrome/browser-tools.js +0 -295
  177. package/dist/src/browser/profileSync.js +0 -141
@@ -1,43 +1,50 @@
1
- import { joinSelectors } from '../providerDomFlow.js';
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: ['rich-textarea .ql-editor', '[role="textbox"][aria-label*="prompt" i]', 'div[contenteditable="true"]'],
6
- sendButton: ['button.send-button', 'button[aria-label="Send message"]'],
7
- toolsButton: ['button.toolbox-drawer-button', 'button[aria-label="Tools"]'],
8
- toolsMenuItem: ['[role="menuitemcheckbox"]', '.toolbox-drawer-item-list-button'],
9
- deepThinkActive: ['.toolbox-drawer-item-deselect-button', 'button[aria-label*="Deselect Deep Think"]'],
10
- uploadButton: ['button[aria-label="Open upload file menu"]', '.upload-card-button'],
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: ['.hidden-local-file-upload-button', '.hidden-local-upload-button'],
13
- uploaderContainer: ['.uploader-button-container', '.file-uploader'],
14
- uploaderElement: ['uploader.upload-button'],
15
- userTurnAttachment: ['.file-preview-container'],
16
- responseTurn: ['model-response'],
17
- responseText: ['message-content', '.model-response-text message-content'],
18
- responseComplete: ['.response-footer.complete'],
19
- userQuery: ['user-query'],
20
- userQueryText: ['user-query-content', '.query-text'],
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: ['.thoughts-header-button', '[data-test-id="thoughts-header-button"]'],
23
- thoughtsContent: ['model-thoughts', '[data-test-id="model-thoughts"]'],
24
- hasThoughts: ['.has-thoughts'],
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 === 'number' && Number.isFinite(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 === 'number' && Number.isFinite(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?.('[gemini-web] Waiting for Gemini UI to load...');
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('Gemini is showing a sign-in flow. Please sign in in Chrome and retry.');
74
+ throw new Error("Gemini is showing a sign-in flow. Please sign in in Chrome and retry.");
68
75
  }
69
- throw new Error('Timed out waiting for Gemini UI prompt input to become ready.');
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 !== 'clicked') {
83
- throw new Error('Unable to open Gemini tools menu; Deep Think toggle is not accessible.');
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 !== 'clicked') {
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('Deep Think did not appear selected after clicking the tools menu item.');
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?.('[gemini-web] Typing prompt...');
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 !== 'typed') {
131
- throw new Error(`Failed to type Gemini prompt (status=${typeResult ?? 'unknown'}).`);
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?.('[gemini-web] Sending prompt...');
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 !== 'clicked' && sendResult !== 'enter') {
154
- throw new Error('Failed to submit prompt in Gemini Deep Think mode (send control not found).');
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?.('[gemini-web] Waiting for Deep Think response (this may take a while)...');
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 === 'done' && typeof parsed.text === 'string' && parsed.text.length > 0) {
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 ?? 'unknown'})`);
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 !== 'clicked') {
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 === 'string' && extractedThoughts.length > 0 ? extractedThoughts : null;
242
+ return typeof extractedThoughts === "string" && extractedThoughts.length > 0
243
+ ? extractedThoughts
244
+ : null;
236
245
  }
237
246
  export const geminiDeepThinkDomProvider = {
238
- providerName: 'gemini-web',
247
+ providerName: "gemini-web",
239
248
  waitForUi,
240
249
  selectMode,
241
250
  typePrompt,
@@ -1,2 +1,2 @@
1
- export { chatgptDomProvider } from './chatgptDomProvider.js';
2
- export { geminiDeepThinkDomProvider, GEMINI_DEEP_THINK_SELECTORS, } from './geminiDeepThinkDomProvider.js';
1
+ export { chatgptDomProvider } from "./chatgptDomProvider.js";
2
+ export { geminiDeepThinkDomProvider, GEMINI_DEEP_THINK_SELECTORS, } from "./geminiDeepThinkDomProvider.js";
@@ -1,22 +1,22 @@
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';
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('No running Chrome detected; reopening browser to locate the session.');
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 ?? '127.0.0.1';
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 === 'function') {
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({ expression: 'location.href', returnByValue: true });
43
- const href = typeof result?.value === 'string' ? result.value : '';
44
- if (href.includes('/c/')) {
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('Unable to locate prior ChatGPT conversation in sidebar.');
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: '1+1', returnByValue: true }), pingTimeoutMs, 'Reattach target did not respond');
67
+ await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
65
68
  await ensureConversationOpen();
66
- const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
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, 'Reattach response timed out');
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, 'Reattach markdown capture timed out')) ?? recovered.text;
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 === 'function') {
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(), '.oracle', 'browser-profile')
93
- : await mkdtemp(path.join(os.tmpdir(), 'oracle-reattach-'));
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 ?? '127.0.0.1';
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 === 'function') {
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('/project'))),
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('Unable to locate prior ChatGPT conversation in sidebar.');
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 readConversationTurnIndex(Runtime, logger);
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 === 'function') {
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: 'never' }).catch(() => undefined);
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 './constants.js';
2
- import { delay } from './utils.js';
3
- import { readAssistantSnapshot } from './pageActions.js';
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 === 'page') ?? targets[0];
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('/c/')) {
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 === '/' ? '' : 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*$/, '').trim();
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: 'location.href', returnByValue: true });
247
- const href = typeof result?.value === 'string' ? 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 === 'number' ? result.value : Number(result?.value);
262
+ const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
263
263
  if (!Number.isFinite(raw)) {
264
- throw new Error('Turn count not numeric');
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 || '').toLowerCase().replace(/\\s+/g, ' ').trim();
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 ? normalizedPrompt.slice(0, Math.min(200, normalizedPrompt.length)) : '';
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('') || 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('Detected prompt echo while reattaching; waiting for assistant response...');
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 === 'string' ? snapshot.text.trim() : '';
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('Recovered assistant response after prompt echo during reattach');
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: 'Aligned prompt-echo text to copied markdown during reattach',
379
- markdown: 'Aligned prompt-echo markdown to response text during reattach',
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
  }