@steipete/oracle 0.8.4 → 0.8.5
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/README.md +7 -0
- package/dist/bin/oracle-cli.js +102 -9
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +13 -5
- package/dist/src/browser/chromeLifecycle.js +62 -9
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/index.js +55 -2
- 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/engine.js +17 -1
- package/dist/src/cli/options.js +14 -0
- package/dist/src/cli/runOptions.js +4 -0
- package/dist/src/mcp/tools/consult.js +80 -15
- package/dist/src/mcp/tools/sessions.js +15 -6
- package/dist/src/mcp/types.js +4 -0
- package/dist/src/mcp/utils.js +12 -2
- package/dist/src/oracle/background.js +1 -2
- package/dist/src/oracle/client.js +5 -2
- package/dist/src/oracle/files.js +2 -2
- package/dist/src/oracle/run.js +1 -0
- package/dist/src/remote/client.js +6 -5
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +28 -1
- package/dist/src/sessionManager.js +63 -5
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +13 -13
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Launcher } from 'chrome-launcher';
|
|
5
|
+
export async function detectChromeBinary() {
|
|
6
|
+
const envPath = (process.env.CHROME_PATH ?? '').trim();
|
|
7
|
+
if (envPath) {
|
|
8
|
+
const ok = await isExecutable(envPath);
|
|
9
|
+
if (ok) {
|
|
10
|
+
return { path: envPath };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const launcherDetected = Launcher.getFirstInstallation();
|
|
14
|
+
if (launcherDetected) {
|
|
15
|
+
return { path: launcherDetected };
|
|
16
|
+
}
|
|
17
|
+
const candidates = platformChromeCandidates();
|
|
18
|
+
for (const candidate of candidates.absolutePaths) {
|
|
19
|
+
if (await isExecutable(candidate)) {
|
|
20
|
+
return { path: candidate };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const fromPath = await findOnPath(candidates.binaryNames);
|
|
24
|
+
if (fromPath) {
|
|
25
|
+
return { path: fromPath };
|
|
26
|
+
}
|
|
27
|
+
return { path: null };
|
|
28
|
+
}
|
|
29
|
+
export async function detectChromeCookieDb({ profile }) {
|
|
30
|
+
const profileName = profile?.trim() ? profile.trim() : 'Default';
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const roots = platformProfileRoots();
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
const dir = path.join(root, profileName);
|
|
37
|
+
const direct = path.join(dir, 'Cookies');
|
|
38
|
+
if (await isFile(direct))
|
|
39
|
+
return direct;
|
|
40
|
+
const network = path.join(dir, 'Network', 'Cookies');
|
|
41
|
+
if (await isFile(network))
|
|
42
|
+
return network;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function platformChromeCandidates() {
|
|
47
|
+
if (process.platform === 'linux') {
|
|
48
|
+
return {
|
|
49
|
+
binaryNames: [
|
|
50
|
+
'google-chrome',
|
|
51
|
+
'google-chrome-stable',
|
|
52
|
+
'chromium',
|
|
53
|
+
'chromium-browser',
|
|
54
|
+
'brave-browser',
|
|
55
|
+
'microsoft-edge',
|
|
56
|
+
'microsoft-edge-stable',
|
|
57
|
+
],
|
|
58
|
+
absolutePaths: [
|
|
59
|
+
'/usr/bin/google-chrome',
|
|
60
|
+
'/usr/bin/google-chrome-stable',
|
|
61
|
+
'/usr/bin/google-chrome-beta',
|
|
62
|
+
'/usr/bin/google-chrome-unstable',
|
|
63
|
+
'/usr/bin/chromium',
|
|
64
|
+
'/usr/bin/chromium-browser',
|
|
65
|
+
'/usr/bin/brave-browser',
|
|
66
|
+
'/usr/bin/microsoft-edge',
|
|
67
|
+
'/usr/bin/microsoft-edge-stable',
|
|
68
|
+
'/snap/bin/chromium',
|
|
69
|
+
'/snap/bin/brave',
|
|
70
|
+
'/snap/bin/brave-browser',
|
|
71
|
+
'/snap/bin/microsoft-edge',
|
|
72
|
+
'/opt/google/chrome/chrome',
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === 'darwin') {
|
|
77
|
+
return {
|
|
78
|
+
binaryNames: [],
|
|
79
|
+
absolutePaths: [
|
|
80
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
81
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
82
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
83
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (process.platform === 'win32') {
|
|
88
|
+
const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
|
|
89
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
|
|
90
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
91
|
+
return {
|
|
92
|
+
binaryNames: [],
|
|
93
|
+
absolutePaths: [
|
|
94
|
+
path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
95
|
+
path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
96
|
+
path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
97
|
+
path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
98
|
+
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { binaryNames: [], absolutePaths: [] };
|
|
103
|
+
}
|
|
104
|
+
function platformProfileRoots() {
|
|
105
|
+
const home = os.homedir();
|
|
106
|
+
if (process.platform === 'darwin') {
|
|
107
|
+
return [
|
|
108
|
+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
|
|
109
|
+
path.join(home, 'Library', 'Application Support', 'Chromium'),
|
|
110
|
+
path.join(home, 'Library', 'Application Support', 'Microsoft Edge'),
|
|
111
|
+
path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === 'linux') {
|
|
115
|
+
return [
|
|
116
|
+
path.join(home, '.config', 'google-chrome'),
|
|
117
|
+
path.join(home, '.config', 'google-chrome-beta'),
|
|
118
|
+
path.join(home, '.config', 'google-chrome-unstable'),
|
|
119
|
+
path.join(home, '.config', 'chromium'),
|
|
120
|
+
path.join(home, '.config', 'microsoft-edge'),
|
|
121
|
+
path.join(home, '.config', 'BraveSoftware', 'Brave-Browser'),
|
|
122
|
+
// Snap Chromium profiles
|
|
123
|
+
path.join(home, 'snap', 'chromium', 'common', 'chromium'),
|
|
124
|
+
path.join(home, 'snap', 'chromium', 'current', 'chromium'),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
async function isExecutable(candidate) {
|
|
130
|
+
try {
|
|
131
|
+
const stat = await fs.stat(candidate);
|
|
132
|
+
if (!stat.isFile())
|
|
133
|
+
return false;
|
|
134
|
+
if (process.platform === 'win32')
|
|
135
|
+
return true;
|
|
136
|
+
// eslint-disable-next-line no-bitwise
|
|
137
|
+
return (stat.mode & 0o111) !== 0;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function isFile(candidate) {
|
|
144
|
+
try {
|
|
145
|
+
const stat = await fs.stat(candidate);
|
|
146
|
+
return stat.isFile();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function findOnPath(names) {
|
|
153
|
+
const rawPath = process.env.PATH ?? '';
|
|
154
|
+
const dirs = rawPath.split(path.delimiter).filter(Boolean);
|
|
155
|
+
for (const name of names) {
|
|
156
|
+
for (const dir of dirs) {
|
|
157
|
+
const candidate = path.join(dir, name);
|
|
158
|
+
if (await isExecutable(candidate)) {
|
|
159
|
+
return candidate;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import net from 'node:net';
|
|
5
5
|
import { resolveBrowserConfig } from './config.js';
|
|
6
|
-
import { launchChrome, registerTerminationHooks, hideChromeWindow,
|
|
6
|
+
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, closeRemoteChromeTarget, connectWithNewTab, closeTab, } from './chromeLifecycle.js';
|
|
7
7
|
import { syncCookies } from './cookies.js';
|
|
8
8
|
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from './pageActions.js';
|
|
9
9
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
@@ -122,6 +122,7 @@ export async function runBrowserMode(options) {
|
|
|
122
122
|
// ignore failure; cleanup still happens below
|
|
123
123
|
}
|
|
124
124
|
let client = null;
|
|
125
|
+
let isolatedTargetId = null;
|
|
125
126
|
const startedAt = Date.now();
|
|
126
127
|
let answerText = '';
|
|
127
128
|
let answerMarkdown = '';
|
|
@@ -133,7 +134,9 @@ export async function runBrowserMode(options) {
|
|
|
133
134
|
let appliedCookies = 0;
|
|
134
135
|
try {
|
|
135
136
|
try {
|
|
136
|
-
|
|
137
|
+
const connection = await connectWithNewTab(chrome.port, logger, undefined, chromeHost);
|
|
138
|
+
client = connection.client;
|
|
139
|
+
isolatedTargetId = connection.targetId ?? null;
|
|
137
140
|
}
|
|
138
141
|
catch (error) {
|
|
139
142
|
const hint = describeDevtoolsFirewallHint(chromeHost, chrome.port);
|
|
@@ -503,6 +506,14 @@ export async function runBrowserMode(options) {
|
|
|
503
506
|
})).catch(() => null);
|
|
504
507
|
answerMarkdown = copiedMarkdown ?? answerText;
|
|
505
508
|
const promptEchoMatcher = buildPromptEchoMatcher(promptText);
|
|
509
|
+
({ answerText, answerMarkdown } = await maybeRecoverLongAssistantResponse({
|
|
510
|
+
runtime: Runtime,
|
|
511
|
+
baselineTurns,
|
|
512
|
+
answerText,
|
|
513
|
+
answerMarkdown,
|
|
514
|
+
logger,
|
|
515
|
+
allowMarkdownUpdate: !copiedMarkdown,
|
|
516
|
+
}));
|
|
506
517
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
507
518
|
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
508
519
|
const finalText = typeof finalSnapshot?.text === 'string' ? finalSnapshot.text.trim() : '';
|
|
@@ -647,6 +658,9 @@ export async function runBrowserMode(options) {
|
|
|
647
658
|
catch {
|
|
648
659
|
// ignore
|
|
649
660
|
}
|
|
661
|
+
if (!effectiveKeepBrowser && isolatedTargetId && chrome?.port) {
|
|
662
|
+
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
663
|
+
}
|
|
650
664
|
removeDialogHandler?.();
|
|
651
665
|
removeTerminationHooks?.();
|
|
652
666
|
if (!effectiveKeepBrowser) {
|
|
@@ -752,6 +766,37 @@ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, time
|
|
|
752
766
|
}
|
|
753
767
|
throw new Error('Manual login mode timed out waiting for ChatGPT session; please sign in and retry.');
|
|
754
768
|
}
|
|
769
|
+
async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answerText, answerMarkdown, logger, allowMarkdownUpdate, }) {
|
|
770
|
+
// Learned: long streaming responses can still be rendering after initial capture.
|
|
771
|
+
// Add a brief delay and re-poll to catch any additional content (#71).
|
|
772
|
+
const capturedLength = answerText.trim().length;
|
|
773
|
+
if (capturedLength <= 500) {
|
|
774
|
+
return { answerText, answerMarkdown };
|
|
775
|
+
}
|
|
776
|
+
await delay(1500);
|
|
777
|
+
let bestLength = capturedLength;
|
|
778
|
+
let bestText = answerText;
|
|
779
|
+
for (let i = 0; i < 5; i++) {
|
|
780
|
+
const laterSnapshot = await readAssistantSnapshot(runtime, baselineTurns ?? undefined).catch(() => null);
|
|
781
|
+
const laterText = typeof laterSnapshot?.text === 'string' ? laterSnapshot.text.trim() : '';
|
|
782
|
+
if (laterText.length > bestLength) {
|
|
783
|
+
bestLength = laterText.length;
|
|
784
|
+
bestText = laterText;
|
|
785
|
+
await delay(800); // More content appeared, keep waiting
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
break; // Stable, stop polling
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (bestLength > capturedLength) {
|
|
792
|
+
logger(`Recovered ${bestLength - capturedLength} additional chars via delayed re-read`);
|
|
793
|
+
return {
|
|
794
|
+
answerText: bestText,
|
|
795
|
+
answerMarkdown: allowMarkdownUpdate ? bestText : answerMarkdown,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
return { answerText, answerMarkdown };
|
|
799
|
+
}
|
|
755
800
|
async function _assertNavigatedToHttp(runtime, _logger, timeoutMs = 10_000) {
|
|
756
801
|
const deadline = Date.now() + timeoutMs;
|
|
757
802
|
let lastUrl = '';
|
|
@@ -1013,6 +1058,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1013
1058
|
},
|
|
1014
1059
|
}).catch(() => null);
|
|
1015
1060
|
answerMarkdown = copiedMarkdown ?? answerText;
|
|
1061
|
+
({ answerText, answerMarkdown } = await maybeRecoverLongAssistantResponse({
|
|
1062
|
+
runtime: Runtime,
|
|
1063
|
+
baselineTurns,
|
|
1064
|
+
answerText,
|
|
1065
|
+
answerMarkdown,
|
|
1066
|
+
logger,
|
|
1067
|
+
allowMarkdownUpdate: !copiedMarkdown,
|
|
1068
|
+
}));
|
|
1016
1069
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
1017
1070
|
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1018
1071
|
const finalText = typeof finalSnapshot?.text === 'string' ? finalSnapshot.text.trim() : '';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadUserConfig } from '../../config.js';
|
|
5
|
+
import { resolveRemoteServiceConfig } from '../../remote/remoteServiceConfig.js';
|
|
6
|
+
export async function runBridgeClaudeConfig(options) {
|
|
7
|
+
const { config: userConfig } = await loadUserConfig();
|
|
8
|
+
const resolved = resolveRemoteServiceConfig({
|
|
9
|
+
cliHost: undefined,
|
|
10
|
+
cliToken: undefined,
|
|
11
|
+
userConfig,
|
|
12
|
+
env: process.env,
|
|
13
|
+
});
|
|
14
|
+
const snippet = formatClaudeMcpConfig({
|
|
15
|
+
oracleHomeDir: process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle-local'),
|
|
16
|
+
browserProfileDir: process.env.ORACLE_BROWSER_PROFILE_DIR ??
|
|
17
|
+
path.join(os.homedir(), '.oracle-local', 'browser-profile'),
|
|
18
|
+
remoteHost: resolved.host,
|
|
19
|
+
remoteToken: resolved.token,
|
|
20
|
+
includeToken: Boolean(options.printToken),
|
|
21
|
+
});
|
|
22
|
+
console.log(snippet);
|
|
23
|
+
if (!options.printToken) {
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(chalk.dim('Tip: rerun with --print-token to include ORACLE_REMOTE_TOKEN in the snippet.'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function formatClaudeMcpConfig({ oracleHomeDir, browserProfileDir, remoteHost, remoteToken, includeToken, }) {
|
|
29
|
+
const env = {};
|
|
30
|
+
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
31
|
+
env['ORACLE_ENGINE'] = 'browser';
|
|
32
|
+
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
33
|
+
env['ORACLE_HOME_DIR'] = oracleHomeDir;
|
|
34
|
+
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
35
|
+
env['ORACLE_BROWSER_PROFILE_DIR'] = browserProfileDir;
|
|
36
|
+
if (remoteHost) {
|
|
37
|
+
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
38
|
+
env['ORACLE_REMOTE_HOST'] = remoteHost;
|
|
39
|
+
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
40
|
+
env['ORACLE_REMOTE_TOKEN'] = includeToken ? remoteToken ?? '<YOUR_TOKEN>' : '<YOUR_TOKEN>';
|
|
41
|
+
}
|
|
42
|
+
// Claude Code supports project-scoped `.mcp.json` config files:
|
|
43
|
+
// https://docs.anthropic.com/en/docs/claude-code/mcp
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
mcpServers: {
|
|
46
|
+
oracle: {
|
|
47
|
+
type: 'stdio',
|
|
48
|
+
command: 'oracle-mcp',
|
|
49
|
+
args: [],
|
|
50
|
+
env,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}, null, 2);
|
|
54
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { configPath as defaultConfigPath } from '../../config.js';
|
|
5
|
+
import { parseBridgeConnectionString, readBridgeConnectionArtifact, looksLikePath, } from '../../bridge/connection.js';
|
|
6
|
+
import { readUserConfigFile, writeUserConfigFile } from '../../bridge/userConfigFile.js';
|
|
7
|
+
import { checkRemoteHealth } from '../../remote/health.js';
|
|
8
|
+
export async function runBridgeClient(options) {
|
|
9
|
+
const connectRaw = options.connect?.trim();
|
|
10
|
+
if (!connectRaw) {
|
|
11
|
+
throw new Error('Missing --connect. Provide a connection string or a bridge-connection.json path.');
|
|
12
|
+
}
|
|
13
|
+
const { remoteHost, remoteToken, tunnel } = await resolveConnection(connectRaw);
|
|
14
|
+
if (options.test !== false) {
|
|
15
|
+
const health = await checkRemoteHealth({ host: remoteHost, token: remoteToken, timeoutMs: 5000 });
|
|
16
|
+
if (!health.ok) {
|
|
17
|
+
const suffix = health.statusCode ? ` (HTTP ${health.statusCode})` : '';
|
|
18
|
+
throw new Error(`Remote service health check failed: ${health.error ?? 'unknown error'}${suffix}`);
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.green(`Remote service OK (${remoteHost})${health.version ? ` — oracle ${health.version}` : ''}`));
|
|
21
|
+
}
|
|
22
|
+
const configFilePath = options.config?.trim() || defaultConfigPath();
|
|
23
|
+
if (options.writeConfig !== false) {
|
|
24
|
+
const { config } = await readUserConfigFile(configFilePath);
|
|
25
|
+
const next = { ...config, browser: { ...(config.browser ?? {}) } };
|
|
26
|
+
next.browser = { ...(next.browser ?? {}) };
|
|
27
|
+
next.browser.remoteHost = remoteHost;
|
|
28
|
+
next.browser.remoteToken = remoteToken;
|
|
29
|
+
if (tunnel) {
|
|
30
|
+
next.browser.remoteViaSshReverseTunnel = {
|
|
31
|
+
ssh: tunnel.ssh,
|
|
32
|
+
remotePort: tunnel.remotePort,
|
|
33
|
+
localPort: tunnel.localPort,
|
|
34
|
+
identity: tunnel.identity,
|
|
35
|
+
extraArgs: tunnel.extraArgs,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
await writeUserConfigFile(configFilePath, next);
|
|
39
|
+
console.log(chalk.green(`Wrote remote config to ${configFilePath}`));
|
|
40
|
+
}
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('Next:');
|
|
43
|
+
console.log(chalk.dim(`- oracle --engine browser -p "hello" --file README.md`));
|
|
44
|
+
if (options.printEnv) {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('# Optional env overrides (paste into your shell):');
|
|
47
|
+
console.log(`export ORACLE_ENGINE=browser`);
|
|
48
|
+
console.log(`export ORACLE_REMOTE_HOST=${shellQuote(remoteHost)}`);
|
|
49
|
+
console.log(`export ORACLE_REMOTE_TOKEN=${shellQuote(remoteToken)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function resolveConnection(input) {
|
|
53
|
+
if (input.includes('://')) {
|
|
54
|
+
return { ...parseBridgeConnectionString(input) };
|
|
55
|
+
}
|
|
56
|
+
const resolvedPath = looksLikePath(input) ? path.resolve(process.cwd(), input) : null;
|
|
57
|
+
if (resolvedPath) {
|
|
58
|
+
const stat = await fs.stat(resolvedPath).catch(() => null);
|
|
59
|
+
if (stat?.isFile()) {
|
|
60
|
+
const artifact = await readBridgeConnectionArtifact(resolvedPath);
|
|
61
|
+
return { remoteHost: artifact.remoteHost, remoteToken: artifact.remoteToken, tunnel: artifact.tunnel };
|
|
62
|
+
}
|
|
63
|
+
if (stat) {
|
|
64
|
+
throw new Error(`--connect points to ${resolvedPath}, but it is not a file.`);
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Connection artifact not found at ${resolvedPath}`);
|
|
67
|
+
}
|
|
68
|
+
return { ...parseBridgeConnectionString(input) };
|
|
69
|
+
}
|
|
70
|
+
function shellQuote(value) {
|
|
71
|
+
// Single-quote for POSIX shells; safe for tokens/host strings.
|
|
72
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadUserConfig } from '../../config.js';
|
|
3
|
+
import { resolveRemoteServiceConfig } from '../../remote/remoteServiceConfig.js';
|
|
4
|
+
export async function runBridgeCodexConfig(options) {
|
|
5
|
+
const { config: userConfig } = await loadUserConfig();
|
|
6
|
+
const resolved = resolveRemoteServiceConfig({
|
|
7
|
+
cliHost: undefined,
|
|
8
|
+
cliToken: undefined,
|
|
9
|
+
userConfig,
|
|
10
|
+
env: process.env,
|
|
11
|
+
});
|
|
12
|
+
const snippet = formatCodexMcpSnippet({
|
|
13
|
+
remoteHost: resolved.host,
|
|
14
|
+
remoteToken: resolved.token,
|
|
15
|
+
includeToken: Boolean(options.printToken),
|
|
16
|
+
});
|
|
17
|
+
console.log(snippet);
|
|
18
|
+
if (!options.printToken) {
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log(chalk.dim('Tip: rerun with --print-token to include ORACLE_REMOTE_TOKEN in the snippet.'));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function formatCodexMcpSnippet({ remoteHost, remoteToken, includeToken, }) {
|
|
24
|
+
const hostValue = remoteHost ?? '127.0.0.1:9473';
|
|
25
|
+
const tokenValue = includeToken ? remoteToken ?? '<YOUR_TOKEN>' : '<YOUR_TOKEN>';
|
|
26
|
+
return [
|
|
27
|
+
'# ~/.codex/config.toml',
|
|
28
|
+
'',
|
|
29
|
+
'[mcp.servers.oracle]',
|
|
30
|
+
'command = "oracle-mcp"',
|
|
31
|
+
'args = []',
|
|
32
|
+
`env = { ORACLE_ENGINE = "browser", ORACLE_REMOTE_HOST = "${escapeTomlString(hostValue)}", ORACLE_REMOTE_TOKEN = "${escapeTomlString(tokenValue)}" }`,
|
|
33
|
+
'',
|
|
34
|
+
'# If you prefer npx:',
|
|
35
|
+
'# [mcp.servers.oracle]',
|
|
36
|
+
'# command = "npx"',
|
|
37
|
+
'# args = ["-y", "@steipete/oracle", "oracle-mcp"]',
|
|
38
|
+
`# env = { ORACLE_ENGINE = "browser", ORACLE_REMOTE_HOST = "${escapeTomlString(hostValue)}", ORACLE_REMOTE_TOKEN = "${escapeTomlString(tokenValue)}" }`,
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
function escapeTomlString(value) {
|
|
42
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
43
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getCliVersion } from '../../version.js';
|
|
4
|
+
import { loadUserConfig } from '../../config.js';
|
|
5
|
+
import { resolveRemoteServiceConfig } from '../../remote/remoteServiceConfig.js';
|
|
6
|
+
import { checkTcpConnection, checkRemoteHealth } from '../../remote/health.js';
|
|
7
|
+
import { detectChromeBinary, detectChromeCookieDb } from '../../browser/detect.js';
|
|
8
|
+
import { formatCodexMcpSnippet } from './codexConfig.js';
|
|
9
|
+
export async function runBridgeDoctor(_options) {
|
|
10
|
+
const { config: userConfig, path: configPath, loaded } = await loadUserConfig();
|
|
11
|
+
const version = getCliVersion();
|
|
12
|
+
const resolvedRemote = resolveRemoteServiceConfig({
|
|
13
|
+
cliHost: undefined,
|
|
14
|
+
cliToken: undefined,
|
|
15
|
+
userConfig,
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
18
|
+
const lines = [];
|
|
19
|
+
const fail = [];
|
|
20
|
+
const warn = [];
|
|
21
|
+
lines.push(chalk.bold('Bridge doctor'));
|
|
22
|
+
lines.push(chalk.dim(`OS: ${process.platform} ${os.release()} (${process.arch})`));
|
|
23
|
+
lines.push(chalk.dim(`Node: ${process.version}`));
|
|
24
|
+
lines.push(chalk.dim(`Oracle: ${version}`));
|
|
25
|
+
lines.push(chalk.dim(`Config: ${loaded ? configPath : '(missing)'}`));
|
|
26
|
+
if (userConfig.engine) {
|
|
27
|
+
lines.push(chalk.dim(`Default engine: ${userConfig.engine}`));
|
|
28
|
+
}
|
|
29
|
+
if (userConfig.model) {
|
|
30
|
+
lines.push(chalk.dim(`Default model: ${userConfig.model}`));
|
|
31
|
+
}
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push(chalk.bold('Browser mode'));
|
|
34
|
+
if (resolvedRemote.host) {
|
|
35
|
+
lines.push(`Remote service: ${chalk.green('configured')}`);
|
|
36
|
+
lines.push(chalk.dim(`remoteHost: ${resolvedRemote.host} (${resolvedRemote.sources.host})`));
|
|
37
|
+
lines.push(chalk.dim(`remoteToken: ${resolvedRemote.token ? 'set' : 'missing'} (${resolvedRemote.sources.token})`));
|
|
38
|
+
const tcp = await checkTcpConnection(resolvedRemote.host, 2000);
|
|
39
|
+
if (tcp.ok) {
|
|
40
|
+
lines.push(chalk.dim(`TCP connect: ${chalk.green('ok')}`));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
fail.push(`Cannot reach ${resolvedRemote.host} (${tcp.error ?? 'unknown error'}).`);
|
|
44
|
+
lines.push(chalk.dim(`TCP connect: ${chalk.red(`failed (${tcp.error ?? 'unknown error'})`)}`));
|
|
45
|
+
}
|
|
46
|
+
if (!resolvedRemote.token) {
|
|
47
|
+
fail.push('Remote token is missing. Run `oracle bridge client --connect <...> --write-config` or set ORACLE_REMOTE_TOKEN.');
|
|
48
|
+
}
|
|
49
|
+
else if (tcp.ok) {
|
|
50
|
+
const health = await checkRemoteHealth({ host: resolvedRemote.host, token: resolvedRemote.token, timeoutMs: 5000 });
|
|
51
|
+
if (health.ok) {
|
|
52
|
+
const meta = health.version ? `oracle ${health.version}` : 'ok';
|
|
53
|
+
lines.push(chalk.dim(`Auth (/health): ${chalk.green(meta)}`));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const detail = health.error ?? 'unknown error';
|
|
57
|
+
fail.push(`Remote auth failed: ${detail}`);
|
|
58
|
+
const suffix = health.statusCode ? `HTTP ${health.statusCode}` : 'network';
|
|
59
|
+
lines.push(chalk.dim(`Auth (/health): ${chalk.red(`${suffix} (${detail})`)}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push(`Remote service: ${chalk.yellow('not configured')}`);
|
|
65
|
+
const chrome = await detectChromeBinary();
|
|
66
|
+
if (chrome.path) {
|
|
67
|
+
lines.push(chalk.dim(`Chrome: ${chalk.green(chrome.path)}`));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
fail.push('No Chrome installation detected. Install Chrome/Chromium or set --browser-chrome-path.');
|
|
71
|
+
lines.push(chalk.dim(`Chrome: ${chalk.red('not found')}`));
|
|
72
|
+
}
|
|
73
|
+
if (process.platform === 'win32') {
|
|
74
|
+
warn.push('Cookie sync is disabled on Windows; use --browser-manual-login or run browser automation on another host.');
|
|
75
|
+
lines.push(chalk.dim('Cookies: (cookie sync disabled on Windows)'));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const cookieDb = await detectChromeCookieDb({ profile: 'Default' });
|
|
79
|
+
if (cookieDb) {
|
|
80
|
+
lines.push(chalk.dim(`Cookies DB: ${chalk.green(cookieDb)}`));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
warn.push('Chrome cookies DB not detected. You may need --browser-cookie-path or --browser-manual-login.');
|
|
84
|
+
lines.push(chalk.dim(`Cookies DB: ${chalk.yellow('not found')}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(chalk.bold('Codex MCP'));
|
|
90
|
+
lines.push(formatCodexMcpSnippet({ remoteHost: resolvedRemote.host, remoteToken: resolvedRemote.token, includeToken: false }));
|
|
91
|
+
if (warn.length) {
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(chalk.yellowBright('Warnings:'));
|
|
94
|
+
for (const message of warn) {
|
|
95
|
+
lines.push(chalk.yellow(`- ${message}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (fail.length) {
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push(chalk.redBright('Problems:'));
|
|
101
|
+
for (const message of fail) {
|
|
102
|
+
lines.push(chalk.red(`- ${message}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.log(lines.join('\n'));
|
|
106
|
+
process.exitCode = fail.length ? 1 : 0;
|
|
107
|
+
}
|