@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,15 +1,21 @@
1
- export const CHATGPT_URL = 'https://chatgpt.com/';
2
- export const DEFAULT_MODEL_TARGET = 'GPT-5.4 Pro';
3
- export const DEFAULT_MODEL_STRATEGY = 'select';
4
- export const COOKIE_URLS = ['https://chatgpt.com', 'https://chat.openai.com', 'https://atlas.openai.com'];
1
+ export const CHATGPT_URL = "https://chatgpt.com/";
2
+ export const DEFAULT_MODEL_TARGET = "GPT-5.5 Pro";
3
+ export const DEFAULT_MODEL_STRATEGY = "select";
4
+ export const COOKIE_URLS = [
5
+ "https://chatgpt.com",
6
+ "https://chat.openai.com",
7
+ "https://atlas.openai.com",
8
+ ];
5
9
  export const INPUT_SELECTORS = [
6
10
  'textarea[data-id="prompt-textarea"]',
7
11
  'textarea[placeholder*="Send a message"]',
12
+ 'textarea[aria-label="Chat with ChatGPT"]',
8
13
  'textarea[aria-label="Message ChatGPT"]',
9
- 'textarea:not([disabled])',
14
+ "textarea:not([disabled])",
10
15
  'textarea[name="prompt-textarea"]',
11
- '#prompt-textarea',
12
- '.ProseMirror',
16
+ "#prompt-textarea",
17
+ ".ProseMirror",
18
+ '[contenteditable="true"][role="textbox"]',
13
19
  '[contenteditable="true"][data-virtualkeyboard="true"]',
14
20
  ];
15
21
  export const ANSWER_SELECTORS = [
@@ -24,12 +30,12 @@ export const ANSWER_SELECTORS = [
24
30
  '[data-turn="assistant"]',
25
31
  ];
26
32
  export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"], section[data-testid^="conversation-turn"], ' +
27
- 'article[data-message-author-role], div[data-message-author-role], section[data-message-author-role], ' +
28
- 'article[data-turn], div[data-turn], section[data-turn]';
33
+ "article[data-message-author-role], div[data-message-author-role], section[data-message-author-role], " +
34
+ "article[data-turn], div[data-turn], section[data-turn]";
29
35
  export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]';
30
36
  export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
31
- export const CLOUDFLARE_TITLE = 'just a moment';
32
- export const PROMPT_PRIMARY_SELECTOR = '#prompt-textarea';
37
+ export const CLOUDFLARE_TITLE = "just a moment";
38
+ export const PROMPT_PRIMARY_SELECTOR = "#prompt-textarea";
33
39
  export const PROMPT_FALLBACK_SELECTOR = 'textarea[name="prompt-textarea"]';
34
40
  export const FILE_INPUT_SELECTORS = [
35
41
  'form input[type="file"]:not([accept])',
@@ -65,7 +71,14 @@ export const SEND_BUTTON_SELECTORS = [
65
71
  'button[aria-label*="Send"]',
66
72
  ];
67
73
  export const SEND_BUTTON_SELECTOR = SEND_BUTTON_SELECTORS[0];
68
- export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"]';
74
+ export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"], button.__composer-pill[aria-haspopup="menu"]';
75
+ export const COMPOSER_MODEL_SIGNAL_SELECTOR = '[data-testid="composer-footer-actions"]';
69
76
  export const COPY_BUTTON_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
70
77
  // Action buttons that only appear once a turn has finished rendering.
78
+ export const DEEP_RESEARCH_PLUS_BUTTON = '[data-testid="composer-plus-btn"]';
79
+ export const DEEP_RESEARCH_DROPDOWN_ITEM_TEXT = "Deep research";
80
+ export const DEEP_RESEARCH_PILL_LABEL = "Deep research";
81
+ export const DEEP_RESEARCH_POLL_INTERVAL_MS = 5_000;
82
+ export const DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS = 70_000;
83
+ export const DEEP_RESEARCH_DEFAULT_TIMEOUT_MS = 2_400_000;
71
84
  export const FINISHED_ACTIONS_SELECTOR = 'button[data-testid="copy-turn-action-button"], button[data-testid="good-response-turn-action-button"], button[data-testid="bad-response-turn-action-button"], button[aria-label="Share"]';
@@ -0,0 +1,81 @@
1
+ export function describeBrowserControlPlan(config = {}) {
2
+ const guidance = [];
3
+ const tabRef = String(config.browserTabRef ?? "").trim();
4
+ const reusesExistingTab = tabRef.length > 0;
5
+ if (config.attachRunning) {
6
+ guidance.push(reusesExistingTab
7
+ ? `Oracle reuses the matching ChatGPT tab (${tabRef}) and leaves the existing browser process alone.`
8
+ : "Oracle opens a dedicated tab and leaves the existing browser process alone.");
9
+ if (config.keepBrowser) {
10
+ guidance.push("The browser stays open because Oracle did not launch it.");
11
+ }
12
+ return {
13
+ mode: "attach-running",
14
+ launchesChrome: false,
15
+ mayFocusWindow: true,
16
+ summary: reusesExistingTab
17
+ ? "attach to an already-running local Chrome tab"
18
+ : "attach to an already-running local Chrome session",
19
+ guidance,
20
+ };
21
+ }
22
+ if (config.remoteChrome) {
23
+ guidance.push(reusesExistingTab
24
+ ? `Oracle reuses the matching ChatGPT tab (${tabRef}) in the configured remote Chrome session.`
25
+ : "Oracle opens a dedicated tab in the configured remote Chrome session.");
26
+ guidance.push("Local Chrome launch, cookie copy, and window hiding flags are skipped.");
27
+ return {
28
+ mode: "remote-chrome",
29
+ launchesChrome: false,
30
+ mayFocusWindow: false,
31
+ summary: reusesExistingTab
32
+ ? "reuse an existing remote Chrome tab"
33
+ : "reuse an existing remote Chrome session",
34
+ guidance,
35
+ };
36
+ }
37
+ if (config.headless) {
38
+ guidance.push("Headless mode avoids visible UI but may be blocked by ChatGPT or Cloudflare.");
39
+ return {
40
+ mode: "headless",
41
+ launchesChrome: true,
42
+ mayFocusWindow: false,
43
+ summary: "launch headless Chrome",
44
+ guidance,
45
+ };
46
+ }
47
+ if (config.hideWindow) {
48
+ guidance.push("Chrome may briefly focus while launching before Oracle hides it.");
49
+ guidance.push("For the calmest shared-desktop flow, prefer --browser-attach-running or --remote-chrome.");
50
+ return {
51
+ mode: "hidden-window",
52
+ launchesChrome: true,
53
+ mayFocusWindow: true,
54
+ summary: "launch Chrome and hide the window after startup",
55
+ guidance,
56
+ };
57
+ }
58
+ guidance.push(config.manualLogin
59
+ ? "Manual-login mode may show the persistent Oracle Chrome profile for sign-in or automation."
60
+ : "A visible automation Chrome window may take focus while Oracle controls ChatGPT.");
61
+ guidance.push("Use --browser-hide-window, --browser-attach-running, or --remote-chrome to reduce desktop disruption.");
62
+ if (config.keepBrowser) {
63
+ guidance.push("Chrome will remain open after the run because --browser-keep-browser is enabled.");
64
+ }
65
+ return {
66
+ mode: "visible-window",
67
+ launchesChrome: true,
68
+ mayFocusWindow: true,
69
+ summary: "launch visible Chrome",
70
+ guidance,
71
+ };
72
+ }
73
+ export function formatBrowserControlPlan(plan, label = "browser") {
74
+ const risk = plan.mayFocusWindow
75
+ ? "may focus/control the browser UI"
76
+ : "does not use a visible local browser window";
77
+ return [
78
+ `[${label}] Browser control: ${plan.summary}; ${risk}.`,
79
+ ...plan.guidance.map((entry) => `[${label}] Browser guidance: ${entry}`),
80
+ ];
81
+ }
@@ -1,6 +1,6 @@
1
- import { COOKIE_URLS } from './constants.js';
2
- import { delay } from './utils.js';
3
- import { getCookies } from '@steipete/sweet-cookie';
1
+ import { COOKIE_URLS } from "./constants.js";
2
+ import { delay } from "./utils.js";
3
+ import { getCookies } from "@steipete/sweet-cookie";
4
4
  export class ChromeCookieSyncError extends Error {
5
5
  }
6
6
  export async function syncCookies(Network, url, profile, logger, options = {}) {
@@ -55,7 +55,7 @@ async function readChromeCookiesWithWait(url, profile, filterNames, cookiePath,
55
55
  return cookies;
56
56
  }
57
57
  const waitLabel = waitMs >= 1000 ? `${Math.round(waitMs / 1000)}s` : `${waitMs}ms`;
58
- const message = firstError instanceof Error ? firstError.message : String(firstError ?? '');
58
+ const message = firstError instanceof Error ? firstError.message : String(firstError ?? "");
59
59
  if (firstError) {
60
60
  logger(`[cookies] Cookie read failed (${message}); waiting ${waitLabel} then retrying once.`);
61
61
  }
@@ -68,27 +68,27 @@ async function readChromeCookiesWithWait(url, profile, filterNames, cookiePath,
68
68
  async function readChromeCookies(url, profile, filterNames, cookiePath) {
69
69
  const origins = Array.from(new Set([stripQuery(url), ...COOKIE_URLS]));
70
70
  const chromeProfile = cookiePath ?? profile ?? undefined;
71
- const timeoutMs = readDuration('ORACLE_COOKIE_LOAD_TIMEOUT_MS', 5_000);
71
+ const timeoutMs = readDuration("ORACLE_COOKIE_LOAD_TIMEOUT_MS", 5_000);
72
72
  // Learned: read from multiple origins to capture auth cookies that land on chat.openai.com + atlas.
73
73
  const { cookies, warnings } = await getCookies({
74
74
  url,
75
75
  origins,
76
76
  names: filterNames?.length ? filterNames : undefined,
77
- browsers: ['chrome'],
78
- mode: 'merge',
77
+ browsers: ["chrome"],
78
+ mode: "merge",
79
79
  chromeProfile,
80
80
  timeoutMs,
81
81
  });
82
- if (process.env.ORACLE_DEBUG_COOKIES === '1' && warnings.length) {
82
+ if (process.env.ORACLE_DEBUG_COOKIES === "1" && warnings.length) {
83
83
  // eslint-disable-next-line no-console
84
- console.log(`[cookies] sweet-cookie warnings:\n- ${warnings.join('\n- ')}`);
84
+ console.log(`[cookies] sweet-cookie warnings:\n- ${warnings.join("\n- ")}`);
85
85
  }
86
86
  const merged = new Map();
87
87
  for (const cookie of cookies) {
88
88
  const normalized = toCdpCookie(cookie);
89
89
  if (!normalized)
90
90
  continue;
91
- const key = `${normalized.domain ?? ''}:${normalized.name}`;
91
+ const key = `${normalized.domain ?? ""}:${normalized.name}`;
92
92
  if (!merged.has(key))
93
93
  merged.set(key, normalized);
94
94
  }
@@ -102,10 +102,10 @@ function normalizeInlineCookies(rawCookies, fallbackHost) {
102
102
  // Learned: inline cookies may omit url/domain; default to current host with a safe path.
103
103
  const normalized = {
104
104
  name: cookie.name,
105
- value: cookie.value ?? '',
105
+ value: cookie.value ?? "",
106
106
  url: cookie.url,
107
107
  domain: cookie.domain ?? fallbackHost,
108
- path: cookie.path ?? '/',
108
+ path: cookie.path ?? "/",
109
109
  expires: normalizeExpiration(cookie.expires),
110
110
  secure: cookie.secure ?? true,
111
111
  httpOnly: cookie.httpOnly ?? false,
@@ -125,13 +125,13 @@ function toCdpCookie(cookie) {
125
125
  name: cookie.name,
126
126
  value: cookie.value,
127
127
  domain: cookie.domain,
128
- path: cookie.path ?? '/',
128
+ path: cookie.path ?? "/",
129
129
  secure: cookie.secure ?? true,
130
130
  httpOnly: cookie.httpOnly ?? false,
131
131
  };
132
- if (typeof cookie.expires === 'number')
132
+ if (typeof cookie.expires === "number")
133
133
  out.expires = cookie.expires;
134
- if (cookie.sameSite === 'Lax' || cookie.sameSite === 'Strict' || cookie.sameSite === 'None') {
134
+ if (cookie.sameSite === "Lax" || cookie.sameSite === "Strict" || cookie.sameSite === "None") {
135
135
  out.sameSite = cookie.sameSite;
136
136
  }
137
137
  return out;
@@ -139,10 +139,10 @@ function toCdpCookie(cookie) {
139
139
  function attachUrl(cookie, fallbackUrl) {
140
140
  const cookieWithUrl = { ...cookie };
141
141
  if (!cookieWithUrl.url) {
142
- if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
142
+ if (!cookieWithUrl.domain || cookieWithUrl.domain === "localhost") {
143
143
  cookieWithUrl.url = fallbackUrl;
144
144
  }
145
- else if (!cookieWithUrl.domain.startsWith('.')) {
145
+ else if (!cookieWithUrl.domain.startsWith(".")) {
146
146
  cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
147
147
  }
148
148
  }
@@ -156,8 +156,8 @@ function attachUrl(cookie, fallbackUrl) {
156
156
  function stripQuery(url) {
157
157
  try {
158
158
  const parsed = new URL(url);
159
- parsed.hash = '';
160
- parsed.search = '';
159
+ parsed.hash = "";
160
+ parsed.search = "";
161
161
  return parsed.toString();
162
162
  }
163
163
  catch {
@@ -1,9 +1,9 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { Launcher } from 'chrome-launcher';
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { Launcher } from "chrome-launcher";
5
5
  export async function detectChromeBinary() {
6
- const envPath = (process.env.CHROME_PATH ?? '').trim();
6
+ const envPath = (process.env.CHROME_PATH ?? "").trim();
7
7
  if (envPath) {
8
8
  const ok = await isExecutable(envPath);
9
9
  if (ok) {
@@ -14,7 +14,7 @@ export async function detectChromeBinary() {
14
14
  if (launcherDetected) {
15
15
  return { path: launcherDetected };
16
16
  }
17
- const candidates = platformChromeCandidates();
17
+ const candidates = platformChromeCandidates(process.platform, os.homedir());
18
18
  for (const candidate of candidates.absolutePaths) {
19
19
  if (await isExecutable(candidate)) {
20
20
  return { path: candidate };
@@ -26,112 +26,234 @@ export async function detectChromeBinary() {
26
26
  }
27
27
  return { path: null };
28
28
  }
29
- export async function detectChromeCookieDb({ profile }) {
30
- const profileName = profile?.trim() ? profile.trim() : 'Default';
31
- if (process.platform === 'win32') {
29
+ export async function detectChromeCookieDb({ profile, }) {
30
+ const profileName = profile?.trim() ? profile.trim() : "Default";
31
+ if (process.platform === "win32") {
32
32
  return null;
33
33
  }
34
- const roots = platformProfileRoots();
34
+ const roots = resolveAttachRunningProfileRoots();
35
35
  for (const root of roots) {
36
- const dir = path.join(root, profileName);
37
- const direct = path.join(dir, 'Cookies');
36
+ const dir = path.join(root.root, profileName);
37
+ const direct = path.join(dir, "Cookies");
38
38
  if (await isFile(direct))
39
39
  return direct;
40
- const network = path.join(dir, 'Network', 'Cookies');
40
+ const network = path.join(dir, "Network", "Cookies");
41
41
  if (await isFile(network))
42
42
  return network;
43
43
  }
44
44
  return null;
45
45
  }
46
- function platformChromeCandidates() {
47
- if (process.platform === 'linux') {
46
+ export function resolveAttachRunningProfileRoots(platform = process.platform, homeDir = os.homedir()) {
47
+ if (platform === "darwin") {
48
+ return [
49
+ {
50
+ family: "chrome",
51
+ root: path.join(homeDir, "Library", "Application Support", "Google", "Chrome"),
52
+ },
53
+ {
54
+ family: "chromium",
55
+ root: path.join(homeDir, "Library", "Application Support", "Chromium"),
56
+ },
57
+ {
58
+ family: "edge",
59
+ root: path.join(homeDir, "Library", "Application Support", "Microsoft Edge"),
60
+ },
61
+ {
62
+ family: "brave",
63
+ root: path.join(homeDir, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
64
+ },
65
+ ];
66
+ }
67
+ if (platform === "linux") {
68
+ return [
69
+ { family: "chrome", root: path.join(homeDir, ".config", "google-chrome") },
70
+ { family: "chromium", root: path.join(homeDir, ".config", "chromium") },
71
+ { family: "edge", root: path.join(homeDir, ".config", "microsoft-edge") },
72
+ {
73
+ family: "brave",
74
+ root: path.join(homeDir, ".config", "BraveSoftware", "Brave-Browser"),
75
+ },
76
+ { family: "chromium", root: path.join(homeDir, "snap", "chromium", "common", "chromium") },
77
+ { family: "chromium", root: path.join(homeDir, "snap", "chromium", "current", "chromium") },
78
+ ];
79
+ }
80
+ if (platform === "win32") {
81
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
82
+ return [
83
+ {
84
+ family: "chrome",
85
+ root: path.join(localAppData, "Google", "Chrome", "User Data"),
86
+ },
87
+ {
88
+ family: "chromium",
89
+ root: path.join(localAppData, "Chromium", "User Data"),
90
+ },
91
+ {
92
+ family: "edge",
93
+ root: path.join(localAppData, "Microsoft", "Edge", "User Data"),
94
+ },
95
+ {
96
+ family: "brave",
97
+ root: path.join(localAppData, "BraveSoftware", "Brave-Browser", "User Data"),
98
+ },
99
+ ];
100
+ }
101
+ return [];
102
+ }
103
+ export function resolveDevToolsActivePortDiscoveryRoots(platform = process.platform, homeDir = os.homedir()) {
104
+ if (platform === "darwin") {
105
+ return [path.join(homeDir, "Library", "Application Support")];
106
+ }
107
+ if (platform === "linux") {
108
+ return [path.join(homeDir, ".config"), path.join(homeDir, "snap")];
109
+ }
110
+ if (platform === "win32") {
111
+ return [process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local")];
112
+ }
113
+ return [];
114
+ }
115
+ export function inferAttachRunningBrowserFamily(chromePath) {
116
+ const normalized = chromePath?.trim().toLowerCase();
117
+ if (!normalized) {
118
+ return null;
119
+ }
120
+ if (normalized.includes("microsoft edge") || normalized.includes("msedge")) {
121
+ return "edge";
122
+ }
123
+ if (normalized.includes("brave")) {
124
+ return "brave";
125
+ }
126
+ if (normalized.includes("chromium")) {
127
+ return "chromium";
128
+ }
129
+ if (normalized.includes("chrome")) {
130
+ return "chrome";
131
+ }
132
+ return null;
133
+ }
134
+ export function parseDevToolsActivePort(raw, options = {}) {
135
+ const host = formatWebSocketHost(options.host ?? "127.0.0.1");
136
+ const [rawPort, rawBrowserPath] = raw.split(/\r?\n/u);
137
+ const port = Number.parseInt(rawPort?.trim() ?? "", 10);
138
+ if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
139
+ throw new Error("DevToolsActivePort did not contain a valid port.");
140
+ }
141
+ const browserPath = rawBrowserPath?.trim() || "/devtools/browser";
142
+ const normalizedPath = browserPath.startsWith("/") ? browserPath : `/${browserPath}`;
143
+ return {
144
+ port,
145
+ browserWSEndpoint: `ws://${host}:${port}${normalizedPath}`,
146
+ };
147
+ }
148
+ export async function readDevToolsActivePortInfo(profileRoot, options = {}) {
149
+ const candidates = [
150
+ path.join(profileRoot, "DevToolsActivePort"),
151
+ path.join(profileRoot, "Default", "DevToolsActivePort"),
152
+ ];
153
+ for (const candidate of candidates) {
154
+ try {
155
+ const raw = await fs.readFile(candidate, "utf8");
156
+ const parsed = parseDevToolsActivePort(raw, options);
157
+ return { ...parsed, path: candidate };
158
+ }
159
+ catch {
160
+ // ignore missing/unreadable candidates
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+ export async function discoverDevToolsActivePortCandidates(options = {}) {
166
+ const { host, platform = process.platform, homeDir = os.homedir(), maxDepth = 6 } = options;
167
+ const roots = resolveDevToolsActivePortDiscoveryRoots(platform, homeDir);
168
+ const candidates = [];
169
+ const seenPaths = new Set();
170
+ for (const root of roots) {
171
+ await walkForDevToolsActivePort(root, maxDepth, async (candidatePath, stat) => {
172
+ if (seenPaths.has(candidatePath)) {
173
+ return;
174
+ }
175
+ seenPaths.add(candidatePath);
176
+ try {
177
+ const raw = await fs.readFile(candidatePath, "utf8");
178
+ const parsed = parseDevToolsActivePort(raw, { host });
179
+ candidates.push({
180
+ ...parsed,
181
+ path: candidatePath,
182
+ profileRoot: deriveDevToolsProfileRoot(candidatePath),
183
+ mtimeMs: Number(stat.mtimeMs),
184
+ });
185
+ }
186
+ catch {
187
+ // ignore unreadable or malformed DevToolsActivePort files
188
+ }
189
+ });
190
+ }
191
+ return candidates;
192
+ }
193
+ function platformChromeCandidates(platform = process.platform, homeDir = os.homedir()) {
194
+ if (platform === "linux") {
48
195
  return {
49
196
  binaryNames: [
50
- 'google-chrome',
51
- 'google-chrome-stable',
52
- 'chromium',
53
- 'chromium-browser',
54
- 'brave-browser',
55
- 'microsoft-edge',
56
- 'microsoft-edge-stable',
197
+ "google-chrome",
198
+ "google-chrome-stable",
199
+ "chromium",
200
+ "chromium-browser",
201
+ "brave-browser",
202
+ "microsoft-edge",
203
+ "microsoft-edge-stable",
57
204
  ],
58
205
  absolutePaths: [
59
- '/usr/bin/google-chrome',
60
- '/usr/bin/google-chrome-stable',
61
- '/usr/bin/google-chrome-beta',
62
- '/usr/bin/google-chrome-unstable',
63
- '/usr/bin/chromium',
64
- '/usr/bin/chromium-browser',
65
- '/usr/bin/brave-browser',
66
- '/usr/bin/microsoft-edge',
67
- '/usr/bin/microsoft-edge-stable',
68
- '/snap/bin/chromium',
69
- '/snap/bin/brave',
70
- '/snap/bin/brave-browser',
71
- '/snap/bin/microsoft-edge',
72
- '/opt/google/chrome/chrome',
206
+ "/usr/bin/google-chrome",
207
+ "/usr/bin/google-chrome-stable",
208
+ "/usr/bin/google-chrome-beta",
209
+ "/usr/bin/google-chrome-unstable",
210
+ "/usr/bin/chromium",
211
+ "/usr/bin/chromium-browser",
212
+ "/usr/bin/brave-browser",
213
+ "/usr/bin/microsoft-edge",
214
+ "/usr/bin/microsoft-edge-stable",
215
+ "/snap/bin/chromium",
216
+ "/snap/bin/brave",
217
+ "/snap/bin/brave-browser",
218
+ "/snap/bin/microsoft-edge",
219
+ "/opt/google/chrome/chrome",
73
220
  ],
74
221
  };
75
222
  }
76
- if (process.platform === 'darwin') {
223
+ if (platform === "darwin") {
77
224
  return {
78
225
  binaryNames: [],
79
226
  absolutePaths: [
80
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
81
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
82
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
83
- '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
227
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
228
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
229
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
230
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
84
231
  ],
85
232
  };
86
233
  }
87
- if (process.platform === 'win32') {
88
- const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
89
- const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
90
- const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
234
+ if (platform === "win32") {
235
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
236
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
237
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
91
238
  return {
92
239
  binaryNames: [],
93
240
  absolutePaths: [
94
- path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
95
- path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
96
- path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
97
- path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
98
- path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
241
+ path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
242
+ path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
243
+ path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
244
+ path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
245
+ path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
99
246
  ],
100
247
  };
101
248
  }
102
249
  return { binaryNames: [], absolutePaths: [] };
103
250
  }
104
- function platformProfileRoots() {
105
- const home = os.homedir();
106
- if (process.platform === 'darwin') {
107
- return [
108
- path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
109
- path.join(home, 'Library', 'Application Support', 'Chromium'),
110
- path.join(home, 'Library', 'Application Support', 'Microsoft Edge'),
111
- path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'),
112
- ];
113
- }
114
- if (process.platform === 'linux') {
115
- return [
116
- path.join(home, '.config', 'google-chrome'),
117
- path.join(home, '.config', 'google-chrome-beta'),
118
- path.join(home, '.config', 'google-chrome-unstable'),
119
- path.join(home, '.config', 'chromium'),
120
- path.join(home, '.config', 'microsoft-edge'),
121
- path.join(home, '.config', 'BraveSoftware', 'Brave-Browser'),
122
- // Snap Chromium profiles
123
- path.join(home, 'snap', 'chromium', 'common', 'chromium'),
124
- path.join(home, 'snap', 'chromium', 'current', 'chromium'),
125
- ];
126
- }
127
- return [];
128
- }
129
251
  async function isExecutable(candidate) {
130
252
  try {
131
253
  const stat = await fs.stat(candidate);
132
254
  if (!stat.isFile())
133
255
  return false;
134
- if (process.platform === 'win32')
256
+ if (process.platform === "win32")
135
257
  return true;
136
258
  // eslint-disable-next-line no-bitwise
137
259
  return (stat.mode & 0o111) !== 0;
@@ -150,7 +272,7 @@ async function isFile(candidate) {
150
272
  }
151
273
  }
152
274
  async function findOnPath(names) {
153
- const rawPath = process.env.PATH ?? '';
275
+ const rawPath = process.env.PATH ?? "";
154
276
  const dirs = rawPath.split(path.delimiter).filter(Boolean);
155
277
  for (const name of names) {
156
278
  for (const dir of dirs) {
@@ -162,3 +284,54 @@ async function findOnPath(names) {
162
284
  }
163
285
  return null;
164
286
  }
287
+ function deriveDevToolsProfileRoot(activePortPath) {
288
+ const parentDir = path.dirname(activePortPath);
289
+ if (path.basename(parentDir).toLowerCase() === "default") {
290
+ return path.dirname(parentDir);
291
+ }
292
+ return parentDir;
293
+ }
294
+ function formatWebSocketHost(host) {
295
+ if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
296
+ return `[${host}]`;
297
+ }
298
+ return host;
299
+ }
300
+ async function walkForDevToolsActivePort(root, maxDepth, onFile) {
301
+ const stack = [{ dir: root, depth: 0 }];
302
+ while (stack.length > 0) {
303
+ const current = stack.pop();
304
+ if (!current) {
305
+ continue;
306
+ }
307
+ let entries;
308
+ try {
309
+ entries = await fs.readdir(current.dir, { withFileTypes: true });
310
+ }
311
+ catch {
312
+ continue;
313
+ }
314
+ for (const entry of entries) {
315
+ const candidatePath = path.join(current.dir, entry.name);
316
+ if (entry.isSymbolicLink()) {
317
+ continue;
318
+ }
319
+ if (entry.isFile()) {
320
+ if (entry.name !== "DevToolsActivePort") {
321
+ continue;
322
+ }
323
+ try {
324
+ const stat = await fs.stat(candidatePath);
325
+ await onFile(candidatePath, stat);
326
+ }
327
+ catch {
328
+ // ignore unreadable candidates
329
+ }
330
+ continue;
331
+ }
332
+ if (entry.isDirectory() && current.depth < maxDepth) {
333
+ stack.push({ dir: candidatePath, depth: current.depth + 1 });
334
+ }
335
+ }
336
+ }
337
+ }