@steipete/oracle 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +390 -295
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +62 -48
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +156 -134
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import http from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import net from
|
|
5
|
-
import { randomBytes, randomUUID } from
|
|
6
|
-
import { spawn, spawnSync } from
|
|
7
|
-
import { mkdtemp, rm, mkdir, writeFile } from
|
|
8
|
-
import chalk from
|
|
9
|
-
import { runBrowserMode } from
|
|
10
|
-
import { getCookies } from
|
|
11
|
-
import { CHATGPT_URL } from
|
|
12
|
-
import { getCliVersion } from
|
|
13
|
-
import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from
|
|
14
|
-
import { normalizeChatgptUrl } from
|
|
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(
|
|
18
|
+
srv.on("error", (err) => reject(err));
|
|
19
19
|
srv.listen(0, () => {
|
|
20
20
|
const address = srv.address();
|
|
21
|
-
if (typeof address ===
|
|
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(
|
|
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(
|
|
35
|
+
const authToken = options.token ?? randomBytes(16).toString("hex");
|
|
36
36
|
const startedAt = Date.now();
|
|
37
|
-
const verbose = process.argv.includes(
|
|
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(
|
|
44
|
-
process.on(
|
|
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(
|
|
49
|
-
if (req.method ===
|
|
50
|
-
logger(
|
|
51
|
-
res.writeHead(200, {
|
|
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 ===
|
|
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, {
|
|
62
|
-
res.end(JSON.stringify({ error:
|
|
61
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
62
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
res.writeHead(200, {
|
|
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 !==
|
|
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, {
|
|
84
|
-
res.end(JSON.stringify({ error:
|
|
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, {
|
|
92
|
-
res.end(JSON.stringify({ error:
|
|
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
|
|
105
|
+
catch {
|
|
106
106
|
busy = false;
|
|
107
|
-
res.writeHead(400, {
|
|
108
|
-
res.end(JSON.stringify({ error:
|
|
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, {
|
|
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,
|
|
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,
|
|
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 ===
|
|
137
|
-
sendEvent({ type:
|
|
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 ??
|
|
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:
|
|
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:
|
|
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 ??
|
|
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 ===
|
|
191
|
-
throw new Error(
|
|
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(
|
|
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(),
|
|
212
|
-
const preferManualLogin = options.manualLoginDefault || process.platform ===
|
|
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 !==
|
|
216
|
-
console.log(
|
|
217
|
-
console.log(
|
|
218
|
-
console.log(
|
|
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(
|
|
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(
|
|
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, {
|
|
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(
|
|
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(
|
|
254
|
-
console.log(
|
|
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(
|
|
270
|
+
console.log("Shutting down remote service...");
|
|
269
271
|
server
|
|
270
272
|
.close()
|
|
271
|
-
.catch((error) => console.error(
|
|
273
|
+
.catch((error) => console.error("Failed to close remote server:", error))
|
|
272
274
|
.finally(() => resolve());
|
|
273
275
|
};
|
|
274
|
-
process.on(
|
|
275
|
-
process.on(
|
|
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 ===
|
|
283
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
282
284
|
}
|
|
283
|
-
return Buffer.concat(chunks).toString(
|
|
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 ??
|
|
304
|
-
const port = socket.remotePort ??
|
|
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 !==
|
|
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 ===
|
|
328
|
-
|
|
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(
|
|
338
|
+
if (addr.startsWith("127."))
|
|
331
339
|
continue;
|
|
332
|
-
if (addr.startsWith(
|
|
340
|
+
if (addr.startsWith("169.254."))
|
|
333
341
|
continue; // APIPA/link-local
|
|
334
342
|
ipv4.push(`${addr}:${port}`);
|
|
335
343
|
}
|
|
336
|
-
else if (family ===
|
|
344
|
+
else if (family === "IPv6") {
|
|
337
345
|
const addr = iface.address.toLowerCase();
|
|
338
|
-
if (addr ===
|
|
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(
|
|
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: [
|
|
357
|
-
mode:
|
|
358
|
-
chromeProfile:
|
|
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(
|
|
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(
|
|
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 ===
|
|
384
|
-
logger(
|
|
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 ===
|
|
409
|
+
if (typeof cookie.expires === "number")
|
|
402
410
|
out.expires = cookie.expires;
|
|
403
|
-
if (cookie.sameSite ===
|
|
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(
|
|
417
|
+
const verbose = process.argv.includes("--verbose") || process.env.ORACLE_SERVE_VERBOSE === "1";
|
|
410
418
|
const openers = [];
|
|
411
|
-
if (process.platform ===
|
|
412
|
-
openers.push({ cmd:
|
|
419
|
+
if (process.platform === "darwin") {
|
|
420
|
+
openers.push({ cmd: "open" });
|
|
413
421
|
}
|
|
414
|
-
else if (process.platform ===
|
|
415
|
-
openers.push({ cmd:
|
|
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:
|
|
421
|
-
openers.push({ cmd:
|
|
428
|
+
openers.push({ cmd: "wslview" });
|
|
429
|
+
openers.push({ cmd: "cmd.exe", args: ["/c", "start", "", url] });
|
|
422
430
|
}
|
|
423
|
-
openers.push({ cmd:
|
|
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:
|
|
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], {
|
|
442
|
+
const child = spawn(candidate.cmd, candidate.args ?? [url], {
|
|
443
|
+
stdio: "ignore",
|
|
444
|
+
detached: true,
|
|
445
|
+
});
|
|
435
446
|
child.unref();
|
|
436
|
-
child.once(
|
|
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(
|
|
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 !==
|
|
469
|
+
if (process.platform !== "linux")
|
|
459
470
|
return false;
|
|
460
|
-
return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes(
|
|
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 ===
|
|
477
|
+
if (process.platform === "win32") {
|
|
467
478
|
// `where` returns non-zero when the command is not found.
|
|
468
|
-
const result = spawnSync(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
503
|
-
|
|
513
|
+
"--no-first-run",
|
|
514
|
+
"--no-default-browser-check",
|
|
504
515
|
`--user-data-dir=${profileDir}`,
|
|
505
|
-
|
|
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(
|
|
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) {
|