@steipete/oracle 0.9.0 → 0.11.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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  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/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,59 +1,86 @@
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, connectToRemoteChromeTarget, listRemoteChromeTargets, } 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 { readDevToolsActivePortInfo } from "./detect.js";
12
+ import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
13
+ import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
12
14
  export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
13
15
  const recoverSession = deps.recoverSession ??
14
16
  (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
+ if (!runtime.chromePort && !runtime.chromeBrowserWSEndpoint) {
18
+ logger("No running Chrome detected; reopening browser to locate the session.");
17
19
  return recoverSession(runtime, config);
18
20
  }
19
- const host = runtime.chromeHost ?? '127.0.0.1';
20
21
  try {
22
+ const liveRuntime = (await refreshAttachRuntime(runtime).catch(() => runtime)) ?? runtime;
23
+ const host = liveRuntime.chromeHost ?? "127.0.0.1";
24
+ const port = liveRuntime.chromePort ?? inferPortFromBrowserWSEndpoint(liveRuntime.chromeBrowserWSEndpoint);
25
+ const browserWSEndpoint = liveRuntime.chromeBrowserWSEndpoint ?? undefined;
21
26
  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
+ (async () => (await listRemoteChromeTargets({
28
+ host,
29
+ port: port ?? 9222,
30
+ browserWSEndpoint,
31
+ })));
27
32
  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;
33
+ const target = pickTarget(targetList, liveRuntime);
34
+ const connection = browserWSEndpoint && !deps.connect
35
+ ? await connectToRemoteChromeTarget(host, port ?? 9222, logger, {
36
+ browserWSEndpoint,
37
+ targetId: target?.targetId ?? target?.id,
38
+ closeTargetOnDispose: false,
39
+ })
40
+ : {
41
+ client: (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
42
+ ? {
43
+ target: browserWSEndpoint,
44
+ local: true,
45
+ targetId: target?.targetId ?? target?.id,
46
+ }
47
+ : {
48
+ host,
49
+ port,
50
+ target: target?.targetId ?? target?.id,
51
+ })),
52
+ close: async () => undefined,
53
+ };
54
+ const client = connection.client;
55
+ const { Runtime, DOM, Page } = client;
35
56
  if (Runtime?.enable) {
36
57
  await Runtime.enable();
37
58
  }
38
- if (DOM && typeof DOM.enable === 'function') {
59
+ if (DOM && typeof DOM.enable === "function") {
39
60
  await DOM.enable();
40
61
  }
62
+ if (Page && typeof Page.enable === "function") {
63
+ await Page.enable();
64
+ }
41
65
  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/')) {
66
+ const { result } = await Runtime.evaluate({
67
+ expression: "location.href",
68
+ returnByValue: true,
69
+ });
70
+ const href = typeof result?.value === "string" ? result.value : "";
71
+ if (href.includes("/c/")) {
45
72
  const currentId = extractConversationIdFromUrl(href);
46
73
  if (!runtime.conversationId || (currentId && currentId === runtime.conversationId)) {
47
74
  return;
48
75
  }
49
76
  }
50
77
  const opened = await openConversationFromSidebarWithRetry(Runtime, {
51
- conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
78
+ conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
52
79
  preferProjects: true,
53
80
  promptPreview: deps.promptPreview,
54
81
  }, 15_000);
55
82
  if (!opened) {
56
- throw new Error('Unable to locate prior ChatGPT conversation in sidebar.');
83
+ throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
57
84
  }
58
85
  await waitForLocationChange(Runtime, 15_000);
59
86
  };
@@ -61,22 +88,25 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
61
88
  const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
62
89
  const timeoutMs = config?.timeoutMs ?? 120_000;
63
90
  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');
91
+ await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
65
92
  await ensureConversationOpen();
66
- const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
93
+ const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
94
+ (deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
95
+ if (config?.researchMode === "deep") {
96
+ const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
97
+ const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client), timeoutMs + 5_000, "Reattach Deep Research response timed out");
98
+ await connection.close().catch(() => undefined);
99
+ return {
100
+ answerText: researchResult.text,
101
+ answerMarkdown: researchResult.text,
102
+ };
103
+ }
67
104
  const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
68
- const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, 'Reattach response timed out');
105
+ const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, "Reattach response timed out");
69
106
  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;
107
+ const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
71
108
  const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
72
- if (client && typeof client.close === 'function') {
73
- try {
74
- await client.close();
75
- }
76
- catch {
77
- // ignore
78
- }
79
- }
109
+ await connection.close().catch(() => undefined);
80
110
  return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
81
111
  }
82
112
  catch (error) {
@@ -85,23 +115,57 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
85
115
  return recoverSession(runtime, config);
86
116
  }
87
117
  }
118
+ async function refreshAttachRuntime(runtime) {
119
+ if (!runtime.chromeProfileRoot) {
120
+ return runtime;
121
+ }
122
+ const host = runtime.chromeHost ?? "127.0.0.1";
123
+ const activePort = await readDevToolsActivePortInfo(runtime.chromeProfileRoot, {
124
+ host,
125
+ });
126
+ if (!activePort) {
127
+ return runtime;
128
+ }
129
+ return {
130
+ ...runtime,
131
+ chromeHost: host,
132
+ chromePort: activePort.port,
133
+ chromeBrowserWSEndpoint: activePort.browserWSEndpoint,
134
+ };
135
+ }
136
+ function inferPortFromBrowserWSEndpoint(browserWSEndpoint) {
137
+ if (!browserWSEndpoint) {
138
+ return undefined;
139
+ }
140
+ try {
141
+ const parsed = new URL(browserWSEndpoint);
142
+ const port = Number.parseInt(parsed.port, 10);
143
+ if (Number.isFinite(port) && port > 0) {
144
+ return port;
145
+ }
146
+ }
147
+ catch {
148
+ // ignore malformed ws endpoints and fall back to caller defaults
149
+ }
150
+ return undefined;
151
+ }
88
152
  async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
89
153
  const resolved = resolveBrowserConfig(config ?? {});
90
154
  const manualLogin = Boolean(resolved.manualLogin);
91
155
  const userDataDir = manualLogin
92
- ? resolved.manualLoginProfileDir ?? path.join(os.homedir(), '.oracle', 'browser-profile')
93
- : await mkdtemp(path.join(os.tmpdir(), 'oracle-reattach-'));
156
+ ? (resolved.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile"))
157
+ : await mkdtemp(path.join(os.tmpdir(), "oracle-reattach-"));
94
158
  if (manualLogin) {
95
159
  await mkdir(userDataDir, { recursive: true });
96
160
  }
97
161
  const chrome = await launchChrome(resolved, userDataDir, logger);
98
- const chromeHost = chrome.host ?? '127.0.0.1';
162
+ const chromeHost = chrome.host ?? "127.0.0.1";
99
163
  const client = await connectToChrome(chrome.port, logger, chromeHost);
100
164
  const { Network, Page, Runtime, DOM } = client;
101
165
  if (Runtime?.enable) {
102
166
  await Runtime.enable();
103
167
  }
104
- if (DOM && typeof DOM.enable === 'function') {
168
+ if (DOM && typeof DOM.enable === "function") {
105
169
  await DOM.enable();
106
170
  }
107
171
  if (!resolved.headless && resolved.hideWindow) {
@@ -134,48 +198,88 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
134
198
  }
135
199
  else {
136
200
  const opened = await openConversationFromSidebarWithRetry(Runtime, {
137
- conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
201
+ conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
138
202
  preferProjects: resolved.url !== CHATGPT_URL ||
139
- Boolean(runtime.tabUrl && (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes('/project'))),
203
+ Boolean(runtime.tabUrl && (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes("/project"))),
140
204
  promptPreview: deps.promptPreview,
141
205
  }, 15_000);
142
206
  if (!opened) {
143
- throw new Error('Unable to locate prior ChatGPT conversation in sidebar.');
207
+ throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
144
208
  }
145
209
  await waitForLocationChange(Runtime, 15_000);
146
210
  }
147
211
  const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
148
212
  const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
149
213
  const timeoutMs = resolved.timeoutMs ?? 120_000;
150
- const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
214
+ const cleanup = async () => {
215
+ if (client && typeof client.close === "function") {
216
+ try {
217
+ await client.close();
218
+ }
219
+ catch {
220
+ // ignore
221
+ }
222
+ }
223
+ if (!resolved.keepBrowser) {
224
+ try {
225
+ await chrome.kill();
226
+ }
227
+ catch {
228
+ // ignore
229
+ }
230
+ if (manualLogin) {
231
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
232
+ }
233
+ else {
234
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
235
+ }
236
+ }
237
+ };
238
+ const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
239
+ (deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
240
+ if (resolved.researchMode === "deep") {
241
+ const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
242
+ const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client);
243
+ await cleanup();
244
+ return {
245
+ answerText: researchResult.text,
246
+ answerMarkdown: researchResult.text,
247
+ };
248
+ }
151
249
  const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
152
250
  const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined);
153
251
  const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
154
252
  const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
155
253
  const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
156
- if (client && typeof client.close === 'function') {
157
- try {
158
- await client.close();
159
- }
160
- catch {
161
- // ignore
162
- }
254
+ await cleanup();
255
+ return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
256
+ }
257
+ async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
258
+ const preview = promptPreview?.trim();
259
+ if (!preview) {
260
+ return null;
163
261
  }
164
- if (!resolved.keepBrowser) {
165
- try {
166
- await chrome.kill();
262
+ const { result } = await Runtime.evaluate({
263
+ expression: `(() => {
264
+ const needle = ${JSON.stringify(preview.toLowerCase().replace(/\s+/g, " ").slice(0, 120))};
265
+ if (!needle) return null;
266
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
267
+ const turns = Array.from(document.querySelectorAll(${JSON.stringify(CONVERSATION_TURN_SELECTOR)}));
268
+ let matched = null;
269
+ for (const [index, node] of turns.entries()) {
270
+ const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
271
+ const isUser = attr === 'user' || Boolean(node.querySelector('[data-message-author-role="user"]'));
272
+ if (!isUser) continue;
273
+ const text = normalize(node.innerText || node.textContent || '');
274
+ if (text.length > 0 && (text.includes(needle) || needle.includes(text.slice(0, needle.length)))) {
275
+ matched = index;
167
276
  }
168
- catch {
169
- // ignore
170
- }
171
- if (manualLogin) {
172
- await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
173
- }
174
- else {
175
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
176
- }
177
- }
178
- return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
277
+ }
278
+ return matched;
279
+ })()`,
280
+ returnByValue: true,
281
+ });
282
+ return typeof result?.value === "number" ? result.value : null;
179
283
  }
180
284
  // biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
181
285
  export const __test__ = {
@@ -183,4 +287,5 @@ export const __test__ = {
183
287
  extractConversationIdFromUrl,
184
288
  buildConversationUrl,
185
289
  openConversationFromSidebar,
290
+ readPromptPreviewTurnIndex,
186
291
  };
@@ -1,22 +1,22 @@
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;
7
7
  }
8
8
  if (runtime.chromeTargetId) {
9
- const byId = targets.find((t) => t.targetId === runtime.chromeTargetId);
9
+ const byId = targets.find((t) => (t.targetId ?? t.id) === runtime.chromeTargetId);
10
10
  if (byId)
11
11
  return byId;
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
  }