@steipete/oracle 0.8.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -1,29 +1,29 @@
1
- import http from 'node:http';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import net from 'node:net';
5
- import { randomBytes, randomUUID } from 'node:crypto';
6
- import { spawn, spawnSync } from 'node:child_process';
7
- import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
8
- import chalk from 'chalk';
9
- import { runBrowserMode } from '../browserMode.js';
10
- import { getCookies } from '@steipete/sweet-cookie';
11
- import { CHATGPT_URL } from '../browser/constants.js';
12
- import { getCliVersion } from '../version.js';
13
- import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from '../browser/profileState.js';
14
- import { normalizeChatgptUrl } from '../browser/utils.js';
1
+ import http from "node:http";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import net from "node:net";
5
+ import { randomBytes, randomUUID } from "node:crypto";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
8
+ import chalk from "chalk";
9
+ import { runBrowserMode } from "../browserMode.js";
10
+ import { getCookies } from "@steipete/sweet-cookie";
11
+ import { CHATGPT_URL } from "../browser/constants.js";
12
+ import { getCliVersion } from "../version.js";
13
+ import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
14
+ import { normalizeChatgptUrl } from "../browser/utils.js";
15
15
  async function findAvailablePort() {
16
16
  return await new Promise((resolve, reject) => {
17
17
  const srv = net.createServer();
18
- srv.on('error', (err) => reject(err));
18
+ srv.on("error", (err) => reject(err));
19
19
  srv.listen(0, () => {
20
20
  const address = srv.address();
21
- if (typeof address === 'object' && address?.port) {
21
+ if (typeof address === "object" && address?.port) {
22
22
  const port = address.port;
23
23
  srv.close(() => resolve(port));
24
24
  }
25
25
  else {
26
- srv.close(() => reject(new Error('Unable to allocate port')));
26
+ srv.close(() => reject(new Error("Unable to allocate port")));
27
27
  }
28
28
  });
29
29
  });
@@ -32,37 +32,37 @@ export async function createRemoteServer(options = {}, deps = {}) {
32
32
  const runBrowser = deps.runBrowser ?? runBrowserMode;
33
33
  const server = http.createServer();
34
34
  const logger = options.logger ?? console.log;
35
- const authToken = options.token ?? randomBytes(16).toString('hex');
35
+ const authToken = options.token ?? randomBytes(16).toString("hex");
36
36
  const startedAt = Date.now();
37
- const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
37
+ const verbose = process.argv.includes("--verbose") || process.env.ORACLE_SERVE_VERBOSE === "1";
38
38
  const color = process.stdout.isTTY
39
39
  ? (formatter, msg) => formatter(msg)
40
40
  : (_formatter, msg) => msg;
41
41
  // Single-flight guard: remote Chrome can only host one run at a time, so we serialize requests.
42
42
  let busy = false;
43
- if (!process.listenerCount('unhandledRejection')) {
44
- process.on('unhandledRejection', (reason) => {
43
+ if (!process.listenerCount("unhandledRejection")) {
44
+ process.on("unhandledRejection", (reason) => {
45
45
  logger(`Unhandled promise rejection in remote server: ${reason instanceof Error ? reason.message : String(reason)}`);
46
46
  });
47
47
  }
48
- server.on('request', async (req, res) => {
49
- if (req.method === 'GET' && req.url === '/status') {
50
- logger('[serve] Health check /status');
51
- res.writeHead(200, { 'Content-Type': 'application/json' });
48
+ server.on("request", async (req, res) => {
49
+ if (req.method === "GET" && req.url === "/status") {
50
+ logger("[serve] Health check /status");
51
+ res.writeHead(200, { "Content-Type": "application/json" });
52
52
  res.end(JSON.stringify({ ok: true }));
53
53
  return;
54
54
  }
55
- if (req.method === 'GET' && req.url === '/health') {
56
- const authHeader = req.headers.authorization ?? '';
55
+ if (req.method === "GET" && req.url === "/health") {
56
+ const authHeader = req.headers.authorization ?? "";
57
57
  if (authHeader !== `Bearer ${authToken}`) {
58
58
  if (verbose) {
59
59
  logger(`[serve] Unauthorized /health attempt from ${formatSocket(req)} (missing/invalid token)`);
60
60
  }
61
- res.writeHead(401, { 'Content-Type': 'application/json' });
62
- res.end(JSON.stringify({ error: 'unauthorized' }));
61
+ res.writeHead(401, { "Content-Type": "application/json" });
62
+ res.end(JSON.stringify({ error: "unauthorized" }));
63
63
  return;
64
64
  }
65
- res.writeHead(200, { 'Content-Type': 'application/json' });
65
+ res.writeHead(200, { "Content-Type": "application/json" });
66
66
  res.end(JSON.stringify({
67
67
  ok: true,
68
68
  version: getCliVersion(),
@@ -70,26 +70,26 @@ export async function createRemoteServer(options = {}, deps = {}) {
70
70
  }));
71
71
  return;
72
72
  }
73
- if (req.method !== 'POST' || req.url !== '/runs') {
73
+ if (req.method !== "POST" || req.url !== "/runs") {
74
74
  res.statusCode = 404;
75
75
  res.end();
76
76
  return;
77
77
  }
78
- const authHeader = req.headers.authorization ?? '';
78
+ const authHeader = req.headers.authorization ?? "";
79
79
  if (authHeader !== `Bearer ${authToken}`) {
80
80
  if (verbose) {
81
81
  logger(`[serve] Unauthorized /runs attempt from ${formatSocket(req)} (missing/invalid token)`);
82
82
  }
83
- res.writeHead(401, { 'Content-Type': 'application/json' });
84
- res.end(JSON.stringify({ error: 'unauthorized' }));
83
+ res.writeHead(401, { "Content-Type": "application/json" });
84
+ res.end(JSON.stringify({ error: "unauthorized" }));
85
85
  return;
86
86
  }
87
87
  if (busy) {
88
88
  if (verbose) {
89
89
  logger(`[serve] Busy: rejecting new run from ${formatSocket(req)} while another run is active`);
90
90
  }
91
- res.writeHead(409, { 'Content-Type': 'application/json' });
92
- res.end(JSON.stringify({ error: 'busy' }));
91
+ res.writeHead(409, { "Content-Type": "application/json" });
92
+ res.end(JSON.stringify({ error: "busy" }));
93
93
  return;
94
94
  }
95
95
  busy = true;
@@ -102,18 +102,18 @@ export async function createRemoteServer(options = {}, deps = {}) {
102
102
  payload.browserConfig.url = normalizeChatgptUrl(payload.browserConfig.url, CHATGPT_URL);
103
103
  }
104
104
  }
105
- catch (_error) {
105
+ catch {
106
106
  busy = false;
107
- res.writeHead(400, { 'Content-Type': 'application/json' });
108
- res.end(JSON.stringify({ error: 'invalid_request' }));
107
+ res.writeHead(400, { "Content-Type": "application/json" });
108
+ res.end(JSON.stringify({ error: "invalid_request" }));
109
109
  return;
110
110
  }
111
- res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });
111
+ res.writeHead(200, { "Content-Type": "application/x-ndjson" });
112
112
  const runId = randomUUID();
113
113
  logger(`[serve] Accepted run ${runId} from ${formatSocket(req)} (prompt ${payload?.prompt?.length ?? 0} chars)`);
114
114
  // Each run gets an isolated temp dir so attachments/logs don't collide.
115
115
  const runDir = await mkdtemp(path.join(os.tmpdir(), `oracle-serve-${runId}-`));
116
- const attachmentDir = path.join(runDir, 'attachments');
116
+ const attachmentDir = path.join(runDir, "attachments");
117
117
  await mkdir(attachmentDir, { recursive: true });
118
118
  const sendEvent = (event) => {
119
119
  res.write(`${JSON.stringify(event)}\n`);
@@ -124,7 +124,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
124
124
  for (const [index, attachment] of attachmentsPayload.entries()) {
125
125
  const safeName = sanitizeName(attachment.fileName ?? `attachment-${index + 1}`);
126
126
  const filePath = path.join(attachmentDir, safeName);
127
- await writeFile(filePath, Buffer.from(attachment.contentBase64, 'base64'));
127
+ await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
128
128
  attachments.push({
129
129
  path: filePath,
130
130
  displayPath: attachment.displayPath,
@@ -133,8 +133,8 @@ export async function createRemoteServer(options = {}, deps = {}) {
133
133
  }
134
134
  // Reuse the existing browser logger surface so clients see the same log stream.
135
135
  const automationLogger = ((message) => {
136
- if (typeof message === 'string') {
137
- sendEvent({ type: 'log', message });
136
+ if (typeof message === "string") {
137
+ sendEvent({ type: "log", message });
138
138
  }
139
139
  });
140
140
  automationLogger.verbose = Boolean(payload.options.verbose);
@@ -153,7 +153,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
153
153
  payload.browserConfig.manualLoginProfileDir = options.manualLoginProfileDir;
154
154
  payload.browserConfig.keepBrowser = true;
155
155
  if (verbose) {
156
- logger(`[serve] Enforcing manual-login profile at ${options.manualLoginProfileDir ?? 'default'} for remote run ${runId}`);
156
+ logger(`[serve] Enforcing manual-login profile at ${options.manualLoginProfileDir ?? "default"} for remote run ${runId}`);
157
157
  }
158
158
  }
159
159
  const result = await runBrowser({
@@ -164,12 +164,12 @@ export async function createRemoteServer(options = {}, deps = {}) {
164
164
  heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
165
165
  verbose: payload.options.verbose,
166
166
  });
167
- sendEvent({ type: 'result', result: sanitizeResult(result) });
167
+ sendEvent({ type: "result", result: sanitizeResult(result) });
168
168
  logger(`[serve] Run ${runId} completed in ${Date.now() - runStartedAt}ms`);
169
169
  }
170
170
  catch (error) {
171
171
  const message = error instanceof Error ? error.message : String(error);
172
- sendEvent({ type: 'error', message });
172
+ sendEvent({ type: "error", message });
173
173
  logger(`[serve] Run ${runId} failed after ${Date.now() - runStartedAt}ms: ${message}`);
174
174
  }
175
175
  finally {
@@ -184,19 +184,19 @@ export async function createRemoteServer(options = {}, deps = {}) {
184
184
  }
185
185
  });
186
186
  await new Promise((resolve) => {
187
- server.listen(options.port ?? 0, options.host ?? '0.0.0.0', () => resolve());
187
+ server.listen(options.port ?? 0, options.host ?? "0.0.0.0", () => resolve());
188
188
  });
189
189
  const address = server.address();
190
- if (!address || typeof address === 'string') {
191
- throw new Error('Unable to determine server address.');
190
+ if (!address || typeof address === "string") {
191
+ throw new Error("Unable to determine server address.");
192
192
  }
193
193
  const reachable = formatReachableAddresses(address.address, address.port);
194
194
  const primary = reachable[0] ?? `${address.address}:${address.port}`;
195
195
  const extras = reachable.slice(1);
196
- const also = extras.length ? `, also [${extras.join(', ')}]` : '';
196
+ const also = extras.length ? `, also [${extras.join(", ")}]` : "";
197
197
  logger(color(chalk.cyanBright.bold, `Listening at ${primary}${also}`));
198
198
  logger(color(chalk.yellowBright, `Access token: ${authToken}`));
199
- logger('Leave this terminal running; press Ctrl+C to stop oracle serve.');
199
+ logger("Leave this terminal running; press Ctrl+C to stop oracle serve.");
200
200
  return {
201
201
  port: address.port,
202
202
  token: authToken,
@@ -208,14 +208,14 @@ export async function createRemoteServer(options = {}, deps = {}) {
208
208
  };
209
209
  }
210
210
  export async function serveRemote(options = {}) {
211
- const manualProfileDir = options.manualLoginProfileDir ?? path.join(os.homedir(), '.oracle', 'browser-profile');
212
- const preferManualLogin = options.manualLoginDefault || process.platform === 'win32' || isWsl();
211
+ const manualProfileDir = options.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile");
212
+ const preferManualLogin = options.manualLoginDefault || process.platform === "win32" || isWsl();
213
213
  let cookies = null;
214
214
  let opened = false;
215
- if (isWsl() && process.env.ORACLE_ALLOW_WSL_SERVE !== '1') {
216
- console.log('WSL detected. For reliable browser automation, run `oracle serve` from Windows PowerShell/Command Prompt so we can use your Windows Chrome profile.');
217
- console.log('If you want to stay in WSL anyway, set ORACLE_ALLOW_WSL_SERVE=1 and ensure a Linux Chrome is installed, then rerun.');
218
- console.log('Alternatively, start Windows Chrome with --remote-debugging-port=9222 and use `--remote-chrome <windows-ip>:9222`.');
215
+ if (isWsl() && process.env.ORACLE_ALLOW_WSL_SERVE !== "1") {
216
+ console.log("WSL detected. For reliable browser automation, run `oracle serve` from Windows PowerShell/Command Prompt so we can use your Windows Chrome profile.");
217
+ console.log("If you want to stay in WSL anyway, set ORACLE_ALLOW_WSL_SERVE=1 and ensure a Linux Chrome is installed, then rerun.");
218
+ console.log("Alternatively, start Windows Chrome with --remote-debugging-port=9222 and use `--remote-chrome <windows-ip>:9222`.");
219
219
  return;
220
220
  }
221
221
  if (!preferManualLogin) {
@@ -225,7 +225,7 @@ export async function serveRemote(options = {}) {
225
225
  opened = result.opened;
226
226
  }
227
227
  if (!cookies || cookies.length === 0) {
228
- console.log('No ChatGPT cookies detected on this host.');
228
+ console.log("No ChatGPT cookies detected on this host.");
229
229
  if (preferManualLogin) {
230
230
  await mkdir(manualProfileDir, { recursive: true });
231
231
  console.log(`Cookie extraction is unavailable on this platform. Using manual-login Chrome profile at ${manualProfileDir}. Remote runs will reuse this profile; sign in once when the browser opens.`);
@@ -233,11 +233,13 @@ export async function serveRemote(options = {}) {
233
233
  if (existingPort) {
234
234
  const reachable = await verifyDevToolsReachable({ port: existingPort });
235
235
  if (reachable.ok) {
236
- console.log('Detected an existing automation Chrome session; will reuse it for manual login.');
236
+ console.log("Detected an existing automation Chrome session; will reuse it for manual login.");
237
237
  }
238
238
  else {
239
239
  console.log(`Found stale DevToolsActivePort (port ${existingPort}, ${reachable.error}); launching a fresh manual-login Chrome.`);
240
- await cleanupStaleProfileState(manualProfileDir, console.log, { lockRemovalMode: 'never' });
240
+ await cleanupStaleProfileState(manualProfileDir, console.log, {
241
+ lockRemovalMode: "never",
242
+ });
241
243
  void launchManualLoginChrome(manualProfileDir, CHATGPT_URL, console.log);
242
244
  }
243
245
  }
@@ -246,12 +248,12 @@ export async function serveRemote(options = {}) {
246
248
  }
247
249
  }
248
250
  else if (opened) {
249
- console.log('Opened chatgpt.com for login. Sign in, then restart `oracle serve` to continue.');
251
+ console.log("Opened chatgpt.com for login. Sign in, then restart `oracle serve` to continue.");
250
252
  return;
251
253
  }
252
254
  else {
253
- console.log('Please open https://chatgpt.com/ in this host\'s browser and sign in; then rerun.');
254
- console.log('Tip: install xdg-utils (xdg-open) to enable automatic browser opening on Linux/WSL.');
255
+ console.log("Please open https://chatgpt.com/ in this host's browser and sign in; then rerun.");
256
+ console.log("Tip: install xdg-utils (xdg-open) to enable automatic browser opening on Linux/WSL.");
255
257
  return;
256
258
  }
257
259
  }
@@ -265,25 +267,25 @@ export async function serveRemote(options = {}) {
265
267
  });
266
268
  await new Promise((resolve) => {
267
269
  const shutdown = () => {
268
- console.log('Shutting down remote service...');
270
+ console.log("Shutting down remote service...");
269
271
  server
270
272
  .close()
271
- .catch((error) => console.error('Failed to close remote server:', error))
273
+ .catch((error) => console.error("Failed to close remote server:", error))
272
274
  .finally(() => resolve());
273
275
  };
274
- process.on('SIGINT', shutdown);
275
- process.on('SIGTERM', shutdown);
276
+ process.on("SIGINT", shutdown);
277
+ process.on("SIGTERM", shutdown);
276
278
  });
277
279
  }
278
280
  async function readRequestBody(req) {
279
281
  const chunks = [];
280
282
  for await (const chunk of req) {
281
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
283
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
282
284
  }
283
- return Buffer.concat(chunks).toString('utf8');
285
+ return Buffer.concat(chunks).toString("utf8");
284
286
  }
285
287
  function sanitizeName(raw) {
286
- return raw.replace(/[^a-zA-Z0-9._-]/g, '_');
288
+ return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
287
289
  }
288
290
  function sanitizeResult(result) {
289
291
  return {
@@ -300,15 +302,15 @@ function sanitizeResult(result) {
300
302
  }
301
303
  function formatSocket(req) {
302
304
  const socket = req.socket;
303
- const host = socket.remoteAddress ?? 'unknown';
304
- const port = socket.remotePort ?? '0';
305
+ const host = socket.remoteAddress ?? "unknown";
306
+ const port = socket.remotePort ?? "0";
305
307
  return `${host}:${port}`;
306
308
  }
307
309
  function formatReachableAddresses(bindAddress, port) {
308
310
  const ipv4 = [];
309
311
  const ipv6 = [];
310
- if (bindAddress && bindAddress !== '::' && bindAddress !== '0.0.0.0') {
311
- if (bindAddress.includes(':')) {
312
+ if (bindAddress && bindAddress !== "::" && bindAddress !== "0.0.0.0") {
313
+ if (bindAddress.includes(":")) {
312
314
  ipv6.push(`[${bindAddress}]:${port}`);
313
315
  }
314
316
  else {
@@ -324,18 +326,24 @@ function formatReachableAddresses(bindAddress, port) {
324
326
  const iface = entry;
325
327
  if (!iface || iface.internal)
326
328
  continue;
327
- const family = typeof iface.family === 'string' ? iface.family : iface.family === 4 ? 'IPv4' : iface.family === 6 ? 'IPv6' : '';
328
- if (family === 'IPv4') {
329
+ const family = typeof iface.family === "string"
330
+ ? iface.family
331
+ : iface.family === 4
332
+ ? "IPv4"
333
+ : iface.family === 6
334
+ ? "IPv6"
335
+ : "";
336
+ if (family === "IPv4") {
329
337
  const addr = iface.address;
330
- if (addr.startsWith('127.'))
338
+ if (addr.startsWith("127."))
331
339
  continue;
332
- if (addr.startsWith('169.254.'))
340
+ if (addr.startsWith("169.254."))
333
341
  continue; // APIPA/link-local
334
342
  ipv4.push(`${addr}:${port}`);
335
343
  }
336
- else if (family === 'IPv6') {
344
+ else if (family === "IPv6") {
337
345
  const addr = iface.address.toLowerCase();
338
- if (addr === '::1' || addr.startsWith('fe80:'))
346
+ if (addr === "::1" || addr.startsWith("fe80:"))
339
347
  continue; // loopback/link-local
340
348
  ipv6.push(`[${iface.address}]:${port}`);
341
349
  }
@@ -350,20 +358,20 @@ function formatReachableAddresses(bindAddress, port) {
350
358
  }
351
359
  async function loadLocalChatgptCookies(logger, targetUrl) {
352
360
  try {
353
- logger('Loading ChatGPT cookies from this host\'s Chrome profile...');
361
+ logger("Loading ChatGPT cookies from this host's Chrome profile...");
354
362
  const { cookies: rawCookies, warnings } = await getCookies({
355
363
  url: targetUrl,
356
- browsers: ['chrome'],
357
- mode: 'merge',
358
- chromeProfile: 'Default',
364
+ browsers: ["chrome"],
365
+ mode: "merge",
366
+ chromeProfile: "Default",
359
367
  timeoutMs: 5_000,
360
368
  });
361
369
  if (warnings.length) {
362
- logger(`Cookie warnings:\n- ${warnings.join('\n- ')}`);
370
+ logger(`Cookie warnings:\n- ${warnings.join("\n- ")}`);
363
371
  }
364
372
  const cookies = rawCookies.map(toCdpCookie).filter((c) => Boolean(c));
365
373
  if (!cookies || cookies.length === 0) {
366
- logger('No local ChatGPT cookies found on this host. Please log in once; opening ChatGPT...');
374
+ logger("No local ChatGPT cookies found on this host. Please log in once; opening ChatGPT...");
367
375
  const opened = triggerLocalLoginPrompt(logger, targetUrl);
368
376
  return { cookies: null, opened };
369
377
  }
@@ -380,8 +388,8 @@ async function loadLocalChatgptCookies(logger, targetUrl) {
380
388
  else {
381
389
  logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
382
390
  }
383
- if (process.platform === 'linux' && isWsl()) {
384
- logger('WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.');
391
+ if (process.platform === "linux" && isWsl()) {
392
+ logger("WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.");
385
393
  }
386
394
  const opened = triggerLocalLoginPrompt(logger, targetUrl);
387
395
  return { cookies: null, opened };
@@ -394,46 +402,49 @@ function toCdpCookie(cookie) {
394
402
  name: cookie.name,
395
403
  value: cookie.value,
396
404
  domain: cookie.domain,
397
- path: cookie.path ?? '/',
405
+ path: cookie.path ?? "/",
398
406
  secure: cookie.secure ?? true,
399
407
  httpOnly: cookie.httpOnly ?? false,
400
408
  };
401
- if (typeof cookie.expires === 'number')
409
+ if (typeof cookie.expires === "number")
402
410
  out.expires = cookie.expires;
403
- if (cookie.sameSite === 'Lax' || cookie.sameSite === 'Strict' || cookie.sameSite === 'None') {
411
+ if (cookie.sameSite === "Lax" || cookie.sameSite === "Strict" || cookie.sameSite === "None") {
404
412
  out.sameSite = cookie.sameSite;
405
413
  }
406
414
  return out;
407
415
  }
408
416
  function triggerLocalLoginPrompt(logger, url) {
409
- const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
417
+ const verbose = process.argv.includes("--verbose") || process.env.ORACLE_SERVE_VERBOSE === "1";
410
418
  const openers = [];
411
- if (process.platform === 'darwin') {
412
- openers.push({ cmd: 'open' });
419
+ if (process.platform === "darwin") {
420
+ openers.push({ cmd: "open" });
413
421
  }
414
- else if (process.platform === 'win32') {
415
- openers.push({ cmd: 'start' });
422
+ else if (process.platform === "win32") {
423
+ openers.push({ cmd: "start" });
416
424
  }
417
425
  else {
418
426
  if (isWsl()) {
419
427
  // Prefer wslview when available, then fall back to Windows start.exe to open in the host browser.
420
- openers.push({ cmd: 'wslview' });
421
- openers.push({ cmd: 'cmd.exe', args: ['/c', 'start', '', url] });
428
+ openers.push({ cmd: "wslview" });
429
+ openers.push({ cmd: "cmd.exe", args: ["/c", "start", "", url] });
422
430
  }
423
- openers.push({ cmd: 'xdg-open' });
431
+ openers.push({ cmd: "xdg-open" });
424
432
  }
425
433
  // Add a cross-platform, low-friction fallback when nothing above is available.
426
- openers.push({ cmd: 'sensible-browser' });
434
+ openers.push({ cmd: "sensible-browser" });
427
435
  try {
428
436
  // Fire and forget; user completes login in the opened browser window.
429
437
  if (verbose) {
430
- logger(`[serve] Login opener candidates: ${openers.map((o) => o.cmd).join(', ')}`);
438
+ logger(`[serve] Login opener candidates: ${openers.map((o) => o.cmd).join(", ")}`);
431
439
  }
432
440
  const candidate = openers.find((opener) => canSpawn(opener.cmd));
433
441
  if (candidate) {
434
- const child = spawn(candidate.cmd, candidate.args ?? [url], { stdio: 'ignore', detached: true });
442
+ const child = spawn(candidate.cmd, candidate.args ?? [url], {
443
+ stdio: "ignore",
444
+ detached: true,
445
+ });
435
446
  child.unref();
436
- child.once('error', (error) => {
447
+ child.once("error", (error) => {
437
448
  if (verbose) {
438
449
  logger(`[serve] Opener ${candidate.cmd} failed: ${error instanceof Error ? error.message : String(error)}`);
439
450
  }
@@ -441,12 +452,12 @@ function triggerLocalLoginPrompt(logger, url) {
441
452
  });
442
453
  logger(`Opened ${url} locally via ${candidate.cmd}. Please sign in; subsequent runs will reuse the session.`);
443
454
  if (verbose && candidate.args) {
444
- logger(`[serve] Opener args: ${candidate.args.join(' ')}`);
455
+ logger(`[serve] Opener args: ${candidate.args.join(" ")}`);
445
456
  }
446
457
  return true;
447
458
  }
448
459
  if (verbose) {
449
- logger('[serve] No available opener found; prompting manual login.');
460
+ logger("[serve] No available opener found; prompting manual login.");
450
461
  }
451
462
  return false;
452
463
  }
@@ -455,24 +466,24 @@ function triggerLocalLoginPrompt(logger, url) {
455
466
  }
456
467
  }
457
468
  function isWsl() {
458
- if (process.platform !== 'linux')
469
+ if (process.platform !== "linux")
459
470
  return false;
460
- return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes('microsoft'));
471
+ return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes("microsoft"));
461
472
  }
462
473
  function canSpawn(cmd) {
463
474
  if (!cmd)
464
475
  return false;
465
476
  try {
466
- if (process.platform === 'win32') {
477
+ if (process.platform === "win32") {
467
478
  // `where` returns non-zero when the command is not found.
468
- const result = spawnSync('where', [cmd], { stdio: 'ignore' });
479
+ const result = spawnSync("where", [cmd], { stdio: "ignore" });
469
480
  return result.status === 0;
470
481
  }
471
482
  // `command -v` is a shell builtin; run through sh. Fallback to `which`.
472
- const shResult = spawnSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' });
483
+ const shResult = spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
473
484
  if (shResult.status === 0)
474
485
  return true;
475
- const whichResult = spawnSync('which', [cmd], { stdio: 'ignore' });
486
+ const whichResult = spawnSync("which", [cmd], { stdio: "ignore" });
476
487
  return whichResult.status === 0;
477
488
  }
478
489
  catch {
@@ -488,7 +499,7 @@ async function launchManualLoginChrome(profileDir, url, logger) {
488
499
  }
489
500
  }, timeoutMs);
490
501
  try {
491
- const chromeLauncher = await import('chrome-launcher');
502
+ const chromeLauncher = await import("chrome-launcher");
492
503
  const { launch } = chromeLauncher;
493
504
  const debugPort = await findAvailablePort();
494
505
  logger(`Planned manual-login Chrome DevTools port: ${debugPort}`);
@@ -499,10 +510,10 @@ async function launchManualLoginChrome(profileDir, url, logger) {
499
510
  userDataDir: profileDir,
500
511
  startingUrl: url,
501
512
  chromeFlags: [
502
- '--no-first-run',
503
- '--no-default-browser-check',
513
+ "--no-first-run",
514
+ "--no-default-browser-check",
504
515
  `--user-data-dir=${profileDir}`,
505
- '--remote-allow-origins=*',
516
+ "--remote-allow-origins=*",
506
517
  `--remote-debugging-port=${debugPort}`, // ensure DevToolsActivePort is written even on Windows
507
518
  ],
508
519
  });
@@ -517,11 +528,11 @@ async function launchManualLoginChrome(profileDir, url, logger) {
517
528
  logger(`If needed, DevTools JSON at http://127.0.0.1:${chosenPort}/json/version`);
518
529
  }
519
530
  else {
520
- logger('Warning: unable to determine manual-login Chrome DevTools port. Remote runs may fail to attach.');
531
+ logger("Warning: unable to determine manual-login Chrome DevTools port. Remote runs may fail to attach.");
521
532
  }
522
533
  finished = true;
523
534
  clearTimeout(timeout);
524
- const portInfo = chosenPort ? ` (DevTools port ${chosenPort})` : '';
535
+ const portInfo = chosenPort ? ` (DevTools port ${chosenPort})` : "";
525
536
  logger(`Opened Chrome with manual-login profile at ${profileDir}${portInfo}. Complete login, then rerun remote sessions.`);
526
537
  }
527
538
  catch (error) {