@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.
Files changed (35) hide show
  1. package/README.md +7 -0
  2. package/dist/bin/oracle-cli.js +102 -9
  3. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  4. package/dist/src/bridge/connection.js +103 -0
  5. package/dist/src/bridge/userConfigFile.js +28 -0
  6. package/dist/src/browser/actions/assistantResponse.js +13 -5
  7. package/dist/src/browser/chromeLifecycle.js +62 -9
  8. package/dist/src/browser/detect.js +164 -0
  9. package/dist/src/browser/index.js +55 -2
  10. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  11. package/dist/src/cli/bridge/client.js +73 -0
  12. package/dist/src/cli/bridge/codexConfig.js +43 -0
  13. package/dist/src/cli/bridge/doctor.js +107 -0
  14. package/dist/src/cli/bridge/host.js +259 -0
  15. package/dist/src/cli/engine.js +17 -1
  16. package/dist/src/cli/options.js +14 -0
  17. package/dist/src/cli/runOptions.js +4 -0
  18. package/dist/src/mcp/tools/consult.js +80 -15
  19. package/dist/src/mcp/tools/sessions.js +15 -6
  20. package/dist/src/mcp/types.js +4 -0
  21. package/dist/src/mcp/utils.js +12 -2
  22. package/dist/src/oracle/background.js +1 -2
  23. package/dist/src/oracle/client.js +5 -2
  24. package/dist/src/oracle/files.js +2 -2
  25. package/dist/src/oracle/run.js +1 -0
  26. package/dist/src/remote/client.js +6 -5
  27. package/dist/src/remote/health.js +113 -0
  28. package/dist/src/remote/remoteServiceConfig.js +31 -0
  29. package/dist/src/remote/server.js +28 -1
  30. package/dist/src/sessionManager.js +63 -5
  31. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  32. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  33. package/package.json +13 -13
  34. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  35. 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, connectToChrome, connectToRemoteChrome, closeRemoteChromeTarget, } from './chromeLifecycle.js';
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
- client = await connectToChrome(chrome.port, logger, chromeHost);
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
+ }