@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
@@ -5,14 +5,15 @@
5
5
  * - Verifies the DevTools /json/version endpoint responds.
6
6
  * - Prints a WSL-friendly firewall hint if the port is unreachable.
7
7
  */
8
- import { setTimeout as sleep } from 'node:timers/promises';
9
- import { launch } from 'chrome-launcher';
10
- import os from 'node:os';
11
- import { readFileSync } from 'node:fs';
8
+ import { setTimeout as sleep } from "node:timers/promises";
9
+ import { launch } from "chrome-launcher";
10
+ import os from "node:os";
11
+ import { readFileSync } from "node:fs";
12
12
  const DEFAULT_PORT = 45871;
13
- const port = normalizePort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT) ?? DEFAULT_PORT;
13
+ const port = normalizePort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT) ??
14
+ DEFAULT_PORT;
14
15
  const hostHint = resolveWslHost();
15
- const targetHost = hostHint ?? '127.0.0.1';
16
+ const targetHost = hostHint ?? "127.0.0.1";
16
17
  function normalizePort(raw) {
17
18
  if (!raw)
18
19
  return null;
@@ -22,18 +23,18 @@ function normalizePort(raw) {
22
23
  return value;
23
24
  }
24
25
  function isWsl() {
25
- if (process.platform !== 'linux')
26
+ if (process.platform !== "linux")
26
27
  return false;
27
28
  if (process.env.WSL_DISTRO_NAME)
28
29
  return true;
29
- return os.release().toLowerCase().includes('microsoft');
30
+ return os.release().toLowerCase().includes("microsoft");
30
31
  }
31
32
  function resolveWslHost() {
32
33
  if (!isWsl())
33
34
  return null;
34
35
  try {
35
- const resolv = readFileSync('/etc/resolv.conf', 'utf8');
36
- for (const line of resolv.split('\n')) {
36
+ const resolv = readFileSync("/etc/resolv.conf", "utf8");
37
+ for (const line of resolv.split("\n")) {
37
38
  const match = line.match(/^nameserver\s+([0-9.]+)/);
38
39
  if (match?.[1])
39
40
  return match[1];
@@ -49,19 +50,21 @@ function firewallHint(host, devtoolsPort) {
49
50
  return null;
50
51
  return [
51
52
  `DevTools port ${host}:${devtoolsPort} is blocked from WSL.`,
52
- '',
53
- 'PowerShell (admin):',
53
+ "",
54
+ "PowerShell (admin):",
54
55
  `New-NetFirewallRule -DisplayName 'Chrome DevTools ${devtoolsPort}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${devtoolsPort}`,
55
56
  "New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
56
- '',
57
- 'Re-run ./runner pnpm test:browser after adding the rule.',
58
- ].join('\n');
57
+ "",
58
+ "Re-run ./runner pnpm test:browser after adding the rule.",
59
+ ].join("\n");
59
60
  }
60
61
  async function fetchVersion(host, devtoolsPort) {
61
62
  const controller = new AbortController();
62
63
  const timer = setTimeout(() => controller.abort(), 5000);
63
64
  try {
64
- const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, { signal: controller.signal });
65
+ const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, {
66
+ signal: controller.signal,
67
+ });
65
68
  if (!res.ok)
66
69
  return false;
67
70
  const json = (await res.json());
@@ -78,7 +81,7 @@ async function main() {
78
81
  console.log(`[browser-test] launching Chrome on ${targetHost}:${port} (headful)…`);
79
82
  const chrome = await launch({
80
83
  port,
81
- chromeFlags: ['--remote-debugging-address=0.0.0.0'],
84
+ chromeFlags: ["--remote-debugging-address=0.0.0.0"],
82
85
  });
83
86
  let ok = await fetchVersion(targetHost, chrome.port);
84
87
  if (!ok) {
@@ -98,6 +101,6 @@ async function main() {
98
101
  process.exit(1);
99
102
  }
100
103
  main().catch((error) => {
101
- console.error('[browser-test] Unexpected failure:', error instanceof Error ? error.message : String(error));
104
+ console.error("[browser-test] Unexpected failure:", error instanceof Error ? error.message : String(error));
102
105
  process.exit(1);
103
106
  });
@@ -8,30 +8,30 @@
8
8
  * Then run this script:
9
9
  * npx tsx scripts/test-remote-chrome.ts <remote-host> [port]
10
10
  */
11
- import CDP from 'chrome-remote-interface';
11
+ import CDP from "chrome-remote-interface";
12
12
  async function main() {
13
- const host = process.argv[2] || 'localhost';
14
- const port = parseInt(process.argv[3] || '9222', 10);
13
+ const host = process.argv[2] || "localhost";
14
+ const port = parseInt(process.argv[3] || "9222", 10);
15
15
  console.log(`Attempting to connect to Chrome at ${host}:${port}...`);
16
16
  try {
17
17
  // Test connection
18
18
  const client = await CDP({ host, port });
19
- console.log('✓ Connected to Chrome DevTools Protocol');
19
+ console.log("✓ Connected to Chrome DevTools Protocol");
20
20
  const { Network, Page, Runtime } = client;
21
21
  // Enable domains
22
22
  await Promise.all([Network.enable(), Page.enable()]);
23
- console.log('✓ Enabled Network and Page domains');
23
+ console.log("✓ Enabled Network and Page domains");
24
24
  // Get browser version info
25
25
  const version = await CDP.Version({ host, port });
26
26
  console.log(`✓ Browser: ${version.Browser}`);
27
- console.log(`✓ Protocol: ${version['Protocol-Version']}`);
27
+ console.log(`✓ Protocol: ${version["Protocol-Version"]}`);
28
28
  // Navigate to ChatGPT
29
- console.log('\nNavigating to ChatGPT...');
30
- await Page.navigate({ url: 'https://chatgpt.com/' });
29
+ console.log("\nNavigating to ChatGPT...");
30
+ await Page.navigate({ url: "https://chatgpt.com/" });
31
31
  await Page.loadEventFired();
32
- console.log('✓ Page loaded');
32
+ console.log("✓ Page loaded");
33
33
  // Check current URL
34
- const evalResult = await Runtime.evaluate({ expression: 'window.location.href' });
34
+ const evalResult = await Runtime.evaluate({ expression: "window.location.href" });
35
35
  console.log(`✓ Current URL: ${evalResult.result.value}`);
36
36
  // Check if logged in (look for specific elements)
37
37
  const checkLogin = await Runtime.evaluate({
@@ -49,19 +49,19 @@ async function main() {
49
49
  });
50
50
  console.log(`✓ Login status: ${JSON.stringify(checkLogin.result.value)}`);
51
51
  await client.close();
52
- console.log('\n✓ POC successful! Remote Chrome connection works.');
53
- console.log('\nTo use Oracle with remote Chrome, you would need to:');
54
- console.log('1. Ensure cookies are loaded in remote Chrome');
55
- console.log('2. Configure Oracle with --remote-chrome <host:port> to use this instance');
56
- console.log('3. Ensure Oracle skips local Chrome launch when --remote-chrome is specified');
52
+ console.log("\n✓ POC successful! Remote Chrome connection works.");
53
+ console.log("\nTo use Oracle with remote Chrome, you would need to:");
54
+ console.log("1. Ensure cookies are loaded in remote Chrome");
55
+ console.log("2. Configure Oracle with --remote-chrome <host:port> to use this instance");
56
+ console.log("3. Ensure Oracle skips local Chrome launch when --remote-chrome is specified");
57
57
  }
58
58
  catch (error) {
59
- console.error('✗ Connection failed:', error instanceof Error ? error.message : error);
60
- console.log('\nTroubleshooting:');
61
- console.log('1. Ensure Chrome is running on remote machine with:');
59
+ console.error("✗ Connection failed:", error instanceof Error ? error.message : error);
60
+ console.log("\nTroubleshooting:");
61
+ console.log("1. Ensure Chrome is running on remote machine with:");
62
62
  console.log(` google-chrome --remote-debugging-port=${port} --remote-debugging-address=0.0.0.0`);
63
- console.log('2. Check firewall allows connections to port', port);
64
- console.log('3. Verify network connectivity to', host);
63
+ console.log("2. Check firewall allows connections to port", port);
64
+ console.log("3. Verify network connectivity to", host);
65
65
  process.exit(1);
66
66
  }
67
67
  }
@@ -1,9 +1,9 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
3
  export function normalizeHostPort(hostname, port) {
4
4
  const trimmed = hostname.trim();
5
- const unwrapped = trimmed.startsWith('[') && trimmed.endsWith(']') ? trimmed.slice(1, -1) : trimmed;
6
- if (unwrapped.includes(':')) {
5
+ const unwrapped = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
6
+ if (unwrapped.includes(":")) {
7
7
  return `[${unwrapped}]:${port}`;
8
8
  }
9
9
  return `${unwrapped}:${port}`;
@@ -11,7 +11,7 @@ export function normalizeHostPort(hostname, port) {
11
11
  export function parseHostPort(raw) {
12
12
  const target = raw.trim();
13
13
  if (!target) {
14
- throw new Error('Expected host:port but received an empty value.');
14
+ throw new Error("Expected host:port but received an empty value.");
15
15
  }
16
16
  const ipv6Match = target.match(/^\[(.+)]:(\d+)$/);
17
17
  let hostname;
@@ -21,43 +21,43 @@ export function parseHostPort(raw) {
21
21
  portSegment = ipv6Match[2]?.trim();
22
22
  }
23
23
  else {
24
- const lastColon = target.lastIndexOf(':');
24
+ const lastColon = target.lastIndexOf(":");
25
25
  if (lastColon === -1) {
26
26
  throw new Error(`Invalid host:port format: ${target}. Expected host:port (IPv6 must use [host]:port notation).`);
27
27
  }
28
28
  hostname = target.slice(0, lastColon).trim();
29
29
  portSegment = target.slice(lastColon + 1).trim();
30
- if (hostname.includes(':')) {
30
+ if (hostname.includes(":")) {
31
31
  throw new Error(`Invalid host:port format: ${target}. Wrap IPv6 addresses in brackets, e.g. "[2001:db8::1]:9473".`);
32
32
  }
33
33
  }
34
34
  if (!hostname) {
35
35
  throw new Error(`Invalid host:port format: ${target}. Host portion is missing.`);
36
36
  }
37
- const port = Number.parseInt(portSegment ?? '', 10);
37
+ const port = Number.parseInt(portSegment ?? "", 10);
38
38
  if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
39
- throw new Error(`Invalid port: "${portSegment ?? ''}". Expected a number between 1 and 65535.`);
39
+ throw new Error(`Invalid port: "${portSegment ?? ""}". Expected a number between 1 and 65535.`);
40
40
  }
41
41
  return { hostname, port };
42
42
  }
43
43
  export function parseBridgeConnectionString(input) {
44
44
  const raw = input.trim();
45
45
  if (!raw) {
46
- throw new Error('Missing connection string.');
46
+ throw new Error("Missing connection string.");
47
47
  }
48
48
  let url;
49
49
  try {
50
- url = raw.includes('://') ? new URL(raw) : new URL(`oracle+tcp://${raw}`);
50
+ url = raw.includes("://") ? new URL(raw) : new URL(`oracle+tcp://${raw}`);
51
51
  }
52
52
  catch (error) {
53
53
  throw new Error(`Invalid connection string: ${error instanceof Error ? error.message : String(error)}`);
54
54
  }
55
55
  const hostname = url.hostname?.trim();
56
- const port = Number.parseInt(url.port ?? '', 10);
56
+ const port = Number.parseInt(url.port ?? "", 10);
57
57
  if (!hostname || !Number.isFinite(port) || port <= 0 || port > 65_535) {
58
58
  throw new Error(`Invalid connection string host: ${raw}. Expected host:port.`);
59
59
  }
60
- const token = url.searchParams.get('token')?.trim() ?? '';
60
+ const token = url.searchParams.get("token")?.trim() ?? "";
61
61
  if (!token) {
62
62
  throw new Error('Connection string is missing token. Expected "?token=...".');
63
63
  }
@@ -74,11 +74,11 @@ export function formatBridgeConnectionString(connection, options = {}) {
74
74
  return `${base}?${params.toString()}`;
75
75
  }
76
76
  export function looksLikePath(value) {
77
- return value.includes('/') || value.includes('\\') || value.endsWith('.json');
77
+ return value.includes("/") || value.includes("\\") || value.endsWith(".json");
78
78
  }
79
79
  export async function readBridgeConnectionArtifact(filePath) {
80
80
  const resolved = path.resolve(process.cwd(), filePath);
81
- const raw = await fs.readFile(resolved, 'utf8');
81
+ const raw = await fs.readFile(resolved, "utf8");
82
82
  let parsed;
83
83
  try {
84
84
  parsed = JSON.parse(raw);
@@ -86,15 +86,15 @@ export async function readBridgeConnectionArtifact(filePath) {
86
86
  catch (error) {
87
87
  throw new Error(`Failed to parse connection artifact JSON at ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
88
88
  }
89
- if (!parsed || typeof parsed !== 'object') {
89
+ if (!parsed || typeof parsed !== "object") {
90
90
  throw new Error(`Invalid connection artifact at ${resolved}: expected an object.`);
91
91
  }
92
92
  const remoteHost = parsed.remoteHost;
93
93
  const remoteToken = parsed.remoteToken;
94
- if (typeof remoteHost !== 'string' || remoteHost.trim().length === 0) {
94
+ if (typeof remoteHost !== "string" || remoteHost.trim().length === 0) {
95
95
  throw new Error(`Invalid connection artifact at ${resolved}: remoteHost is missing.`);
96
96
  }
97
- if (typeof remoteToken !== 'string' || remoteToken.trim().length === 0) {
97
+ if (typeof remoteToken !== "string" || remoteToken.trim().length === 0) {
98
98
  throw new Error(`Invalid connection artifact at ${resolved}: remoteToken is missing.`);
99
99
  }
100
100
  // Validate host formatting early so downstream checks don't crash.
@@ -1,15 +1,15 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import JSON5 from 'json5';
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import JSON5 from "json5";
4
4
  export async function readUserConfigFile(configPath) {
5
5
  try {
6
- const raw = await fs.readFile(configPath, 'utf8');
6
+ const raw = await fs.readFile(configPath, "utf8");
7
7
  const parsed = JSON5.parse(raw);
8
8
  return { config: parsed ?? {}, loaded: true };
9
9
  }
10
10
  catch (error) {
11
11
  const code = error.code;
12
- if (code === 'ENOENT') {
12
+ if (code === "ENOENT") {
13
13
  return { config: {}, loaded: false };
14
14
  }
15
15
  throw new Error(`Failed to read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
@@ -20,9 +20,9 @@ export async function writeUserConfigFile(configPath, config) {
20
20
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
21
21
  const contents = `${JSON.stringify(config, null, 2)}\n`;
22
22
  const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
23
- await fs.writeFile(tempPath, contents, { encoding: 'utf8', mode: 0o600 });
23
+ await fs.writeFile(tempPath, contents, { encoding: "utf8", mode: 0o600 });
24
24
  await fs.rename(tempPath, configPath);
25
- if (process.platform !== 'win32') {
25
+ if (process.platform !== "win32") {
26
26
  await fs.chmod(configPath, 0o600).catch(() => undefined);
27
27
  }
28
28
  }
@@ -0,0 +1,224 @@
1
+ export function isProjectChatgptUrl(url) {
2
+ return /\/project(?:[/?#]|$)/i.test(url ?? "");
3
+ }
4
+ export function resolveBrowserArchiveDecision({ mode = "auto", chatgptUrl, conversationUrl, researchMode, followUpCount, }) {
5
+ if (mode === "never") {
6
+ return { mode, shouldArchive: false, reason: "disabled" };
7
+ }
8
+ if (!conversationUrl) {
9
+ return { mode, shouldArchive: false, reason: "missing-conversation-url" };
10
+ }
11
+ if (mode === "always") {
12
+ return { mode, shouldArchive: true, reason: "forced" };
13
+ }
14
+ if (isProjectChatgptUrl(chatgptUrl) || isProjectChatgptUrl(conversationUrl)) {
15
+ return { mode, shouldArchive: false, reason: "project-conversation" };
16
+ }
17
+ if (researchMode === "deep") {
18
+ return { mode, shouldArchive: false, reason: "deep-research" };
19
+ }
20
+ if ((followUpCount ?? 0) > 0) {
21
+ return { mode, shouldArchive: false, reason: "multi-turn" };
22
+ }
23
+ return { mode, shouldArchive: true, reason: "successful-one-shot" };
24
+ }
25
+ export async function archiveChatGptConversation(Runtime, logger, { mode, conversationUrl, }) {
26
+ const evaluated = await Runtime.evaluate({
27
+ expression: buildArchiveConversationExpression(),
28
+ awaitPromise: true,
29
+ returnByValue: true,
30
+ });
31
+ const value = evaluated.result?.value;
32
+ const resolvedUrl = value?.conversationUrl ?? conversationUrl ?? undefined;
33
+ if (value?.status === "archived") {
34
+ logger("[browser] Archived ChatGPT conversation after saving local artifacts.");
35
+ return { mode, attempted: true, archived: true, conversationUrl: resolvedUrl };
36
+ }
37
+ const reason = value?.status === "skipped" ? value.reason : "archive-failed";
38
+ const error = value?.status === "failed" ? value.error : undefined;
39
+ logger(`[browser] ChatGPT archive skipped (${error ?? reason}).`);
40
+ return {
41
+ mode,
42
+ attempted: true,
43
+ archived: false,
44
+ reason,
45
+ conversationUrl: resolvedUrl,
46
+ error,
47
+ };
48
+ }
49
+ export function buildArchiveConversationExpressionForTest() {
50
+ return buildArchiveConversationExpression();
51
+ }
52
+ function buildArchiveConversationExpression() {
53
+ return `(() => {
54
+ const conversationUrl = typeof location === 'object' ? location.href : null;
55
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
56
+ const normalize = (value) =>
57
+ String(value ?? '')
58
+ .replace(/\\s+/g, ' ')
59
+ .trim()
60
+ .toLowerCase();
61
+ const isVisible = (element) => {
62
+ if (!element || !(element instanceof HTMLElement)) return false;
63
+ const rect = element.getBoundingClientRect();
64
+ const style = getComputedStyle(element);
65
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
66
+ };
67
+ const labelFor = (element) =>
68
+ normalize([
69
+ element.getAttribute?.('aria-label'),
70
+ element.getAttribute?.('title'),
71
+ element.textContent,
72
+ ].filter(Boolean).join(' '));
73
+ const click = (element) => {
74
+ const rect = element.getBoundingClientRect();
75
+ const eventInit = {
76
+ bubbles: true,
77
+ cancelable: true,
78
+ view: window,
79
+ clientX: rect.left + rect.width / 2,
80
+ clientY: rect.top + rect.height / 2,
81
+ button: 0,
82
+ };
83
+ if (typeof PointerEvent === 'function') {
84
+ element.dispatchEvent(new PointerEvent('pointerdown', {
85
+ ...eventInit,
86
+ buttons: 1,
87
+ pointerId: 1,
88
+ pointerType: 'mouse',
89
+ isPrimary: true,
90
+ }));
91
+ }
92
+ element.dispatchEvent(new MouseEvent('mousedown', { ...eventInit, buttons: 1 }));
93
+ if (typeof PointerEvent === 'function') {
94
+ element.dispatchEvent(new PointerEvent('pointerup', {
95
+ ...eventInit,
96
+ buttons: 0,
97
+ pointerId: 1,
98
+ pointerType: 'mouse',
99
+ isPrimary: true,
100
+ }));
101
+ }
102
+ element.dispatchEvent(new MouseEvent('mouseup', { ...eventInit, buttons: 0 }));
103
+ element.dispatchEvent(new MouseEvent('click', { ...eventInit, buttons: 0 }));
104
+ };
105
+ const findConversationMenuButton = () => {
106
+ const buttons = Array.from(document.querySelectorAll('button,[role="button"]'))
107
+ .filter((element) => element instanceof HTMLElement && isVisible(element));
108
+ const labelled = buttons
109
+ .map((element) => ({ element, label: labelFor(element), rect: element.getBoundingClientRect() }))
110
+ .filter(({ label }) =>
111
+ label.includes('more') ||
112
+ label.includes('conversation options') ||
113
+ label.includes('open menu') ||
114
+ label.includes('więcej') ||
115
+ label.includes('opcje')
116
+ );
117
+ const headerCandidates = labelled
118
+ .filter(({ rect }) => rect.top < 180 && rect.right > window.innerWidth - 420)
119
+ .sort((a, b) => b.rect.right - a.rect.right);
120
+ return (headerCandidates[0] ?? labelled[0])?.element ?? null;
121
+ };
122
+ const visibleMenuCandidates = () => {
123
+ const menuRoots = Array.from(document.querySelectorAll('[role="menu"]'))
124
+ .filter((element) => element instanceof HTMLElement && isVisible(element));
125
+ const roots = menuRoots.length > 0 ? menuRoots : [document];
126
+ return roots.flatMap((root) =>
127
+ Array.from(root.querySelectorAll('[role="menuitem"],[role="option"],button,div[tabindex],a')),
128
+ ).filter((element) => element instanceof HTMLElement && isVisible(element));
129
+ };
130
+ const findArchiveMenuItem = () => {
131
+ const candidates = visibleMenuCandidates();
132
+ return candidates.find((element) => {
133
+ const label = labelFor(element);
134
+ if (!label) return false;
135
+ if (label.includes('unarchive') || label.includes('restore')) return false;
136
+ return label.includes('archive') || label.includes('archiwizuj');
137
+ }) ?? null;
138
+ };
139
+ const findArchiveConfirmationButton = () => {
140
+ const candidates = Array.from(document.querySelectorAll('[role="dialog"] button,[role="dialog"] [role="button"]'))
141
+ .filter((element) => element instanceof HTMLElement && isVisible(element));
142
+ return candidates.find((element) => {
143
+ const label = labelFor(element);
144
+ if (!label) return false;
145
+ if (label.includes('unarchive') || label.includes('restore')) return false;
146
+ return label === 'archive' || label === 'archiwizuj' || label.includes('archive conversation');
147
+ }) ?? null;
148
+ };
149
+ const hasUnarchiveMenuItem = () => {
150
+ const candidates = visibleMenuCandidates();
151
+ return candidates.some((element) => {
152
+ const label = labelFor(element);
153
+ return (
154
+ label.includes('unarchive') ||
155
+ label.includes('restore') ||
156
+ label.includes('przywróć') ||
157
+ label.includes('przywroc')
158
+ );
159
+ });
160
+ };
161
+ const hasArchiveConfirmation = () => {
162
+ const visibleText = Array.from(document.querySelectorAll('[role="status"],[role="alert"],[data-testid*="toast"],[class*="toast"],[class*="snackbar"]'))
163
+ .filter((element) => element instanceof HTMLElement && isVisible(element))
164
+ .map((element) => labelFor(element))
165
+ .join(' ');
166
+ return (
167
+ visibleText.includes('archived') ||
168
+ visibleText.includes('conversation archived') ||
169
+ visibleText.includes('chat archived') ||
170
+ visibleText.includes('zarchiwizowano') ||
171
+ visibleText.includes('archiwum')
172
+ );
173
+ };
174
+ const waitForArchiveConfirmation = async () => {
175
+ const deadline = Date.now() + 3000;
176
+ while (Date.now() < deadline) {
177
+ if (conversationUrl && location.href !== conversationUrl) return true;
178
+ if (hasArchiveConfirmation()) return true;
179
+ await sleep(150);
180
+ }
181
+ return false;
182
+ };
183
+ const verifyArchivedStateFromMenu = async () => {
184
+ const menuButton = findConversationMenuButton();
185
+ if (!menuButton) return false;
186
+ click(menuButton);
187
+ await sleep(300);
188
+ const archived = hasUnarchiveMenuItem();
189
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
190
+ return archived;
191
+ };
192
+ return (async () => {
193
+ const menuButton = findConversationMenuButton();
194
+ if (!menuButton) {
195
+ return { status: 'skipped', reason: 'conversation-menu-not-found', conversationUrl };
196
+ }
197
+ click(menuButton);
198
+ await sleep(350);
199
+ const archiveItem = findArchiveMenuItem();
200
+ if (!archiveItem) {
201
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
202
+ return { status: 'skipped', reason: 'archive-menu-item-not-found', conversationUrl };
203
+ }
204
+ click(archiveItem);
205
+ await sleep(350);
206
+ const confirmButton = findArchiveConfirmationButton();
207
+ if (confirmButton) {
208
+ click(confirmButton);
209
+ await sleep(500);
210
+ }
211
+ if (await waitForArchiveConfirmation()) {
212
+ return { status: 'archived', conversationUrl };
213
+ }
214
+ if (await verifyArchivedStateFromMenu()) {
215
+ return { status: 'archived', conversationUrl };
216
+ }
217
+ return { status: 'skipped', reason: 'archive-not-confirmed', conversationUrl };
218
+ })().catch((error) => ({
219
+ status: 'failed',
220
+ error: error instanceof Error ? error.message : String(error),
221
+ conversationUrl,
222
+ }));
223
+ })()`;
224
+ }