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