| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- import { readdirSync } from 'fs'
- import { stat } from 'fs/promises'
- import { homedir, platform, tmpdir, userInfo } from 'os'
- import { join } from 'path'
- import { normalizeNameForMCP } from '../../services/mcp/normalization.js'
- import { logForDebugging } from '../debug.js'
- import { isFsInaccessible } from '../errors.js'
- import { execFileNoThrow } from '../execFileNoThrow.js'
- import { getPlatform } from '../platform.js'
- import { which } from '../which.js'
- export const CLAUDE_IN_CHROME_MCP_SERVER_NAME = 'claude-in-chrome'
- // Re-export ChromiumBrowser type for setup.ts
- export type { ChromiumBrowser } from './setupPortable.js'
- // Import for local use
- import type { ChromiumBrowser } from './setupPortable.js'
- type BrowserConfig = {
- name: string
- macos: {
- appName: string
- dataPath: string[]
- nativeMessagingPath: string[]
- }
- linux: {
- binaries: string[]
- dataPath: string[]
- nativeMessagingPath: string[]
- }
- windows: {
- dataPath: string[]
- registryKey: string
- useRoaming?: boolean // Opera uses Roaming instead of Local
- }
- }
- export const CHROMIUM_BROWSERS: Record<ChromiumBrowser, BrowserConfig> = {
- chrome: {
- name: 'Google Chrome',
- macos: {
- appName: 'Google Chrome',
- dataPath: ['Library', 'Application Support', 'Google', 'Chrome'],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'Google',
- 'Chrome',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- binaries: ['google-chrome', 'google-chrome-stable'],
- dataPath: ['.config', 'google-chrome'],
- nativeMessagingPath: ['.config', 'google-chrome', 'NativeMessagingHosts'],
- },
- windows: {
- dataPath: ['Google', 'Chrome', 'User Data'],
- registryKey: 'HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts',
- },
- },
- brave: {
- name: 'Brave',
- macos: {
- appName: 'Brave Browser',
- dataPath: [
- 'Library',
- 'Application Support',
- 'BraveSoftware',
- 'Brave-Browser',
- ],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'BraveSoftware',
- 'Brave-Browser',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- binaries: ['brave-browser', 'brave'],
- dataPath: ['.config', 'BraveSoftware', 'Brave-Browser'],
- nativeMessagingPath: [
- '.config',
- 'BraveSoftware',
- 'Brave-Browser',
- 'NativeMessagingHosts',
- ],
- },
- windows: {
- dataPath: ['BraveSoftware', 'Brave-Browser', 'User Data'],
- registryKey:
- 'HKCU\\Software\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts',
- },
- },
- arc: {
- name: 'Arc',
- macos: {
- appName: 'Arc',
- dataPath: ['Library', 'Application Support', 'Arc', 'User Data'],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'Arc',
- 'User Data',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- // Arc is not available on Linux
- binaries: [],
- dataPath: [],
- nativeMessagingPath: [],
- },
- windows: {
- // Arc Windows is Chromium-based
- dataPath: ['Arc', 'User Data'],
- registryKey: 'HKCU\\Software\\ArcBrowser\\Arc\\NativeMessagingHosts',
- },
- },
- chromium: {
- name: 'Chromium',
- macos: {
- appName: 'Chromium',
- dataPath: ['Library', 'Application Support', 'Chromium'],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'Chromium',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- binaries: ['chromium', 'chromium-browser'],
- dataPath: ['.config', 'chromium'],
- nativeMessagingPath: ['.config', 'chromium', 'NativeMessagingHosts'],
- },
- windows: {
- dataPath: ['Chromium', 'User Data'],
- registryKey: 'HKCU\\Software\\Chromium\\NativeMessagingHosts',
- },
- },
- edge: {
- name: 'Microsoft Edge',
- macos: {
- appName: 'Microsoft Edge',
- dataPath: ['Library', 'Application Support', 'Microsoft Edge'],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'Microsoft Edge',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- binaries: ['microsoft-edge', 'microsoft-edge-stable'],
- dataPath: ['.config', 'microsoft-edge'],
- nativeMessagingPath: [
- '.config',
- 'microsoft-edge',
- 'NativeMessagingHosts',
- ],
- },
- windows: {
- dataPath: ['Microsoft', 'Edge', 'User Data'],
- registryKey: 'HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts',
- },
- },
- vivaldi: {
- name: 'Vivaldi',
- macos: {
- appName: 'Vivaldi',
- dataPath: ['Library', 'Application Support', 'Vivaldi'],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'Vivaldi',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- binaries: ['vivaldi', 'vivaldi-stable'],
- dataPath: ['.config', 'vivaldi'],
- nativeMessagingPath: ['.config', 'vivaldi', 'NativeMessagingHosts'],
- },
- windows: {
- dataPath: ['Vivaldi', 'User Data'],
- registryKey: 'HKCU\\Software\\Vivaldi\\NativeMessagingHosts',
- },
- },
- opera: {
- name: 'Opera',
- macos: {
- appName: 'Opera',
- dataPath: ['Library', 'Application Support', 'com.operasoftware.Opera'],
- nativeMessagingPath: [
- 'Library',
- 'Application Support',
- 'com.operasoftware.Opera',
- 'NativeMessagingHosts',
- ],
- },
- linux: {
- binaries: ['opera'],
- dataPath: ['.config', 'opera'],
- nativeMessagingPath: ['.config', 'opera', 'NativeMessagingHosts'],
- },
- windows: {
- dataPath: ['Opera Software', 'Opera Stable'],
- registryKey:
- 'HKCU\\Software\\Opera Software\\Opera Stable\\NativeMessagingHosts',
- useRoaming: true, // Opera uses Roaming AppData, not Local
- },
- },
- }
- // Priority order for browser detection (most common first)
- export const BROWSER_DETECTION_ORDER: ChromiumBrowser[] = [
- 'chrome',
- 'brave',
- 'arc',
- 'edge',
- 'chromium',
- 'vivaldi',
- 'opera',
- ]
- /**
- * Get all browser data paths to check for extension installation
- */
- export function getAllBrowserDataPaths(): {
- browser: ChromiumBrowser
- path: string
- }[] {
- const platform = getPlatform()
- const home = homedir()
- const paths: { browser: ChromiumBrowser; path: string }[] = []
- for (const browserId of BROWSER_DETECTION_ORDER) {
- const config = CHROMIUM_BROWSERS[browserId]
- let dataPath: string[] | undefined
- switch (platform) {
- case 'macos':
- dataPath = config.macos.dataPath
- break
- case 'linux':
- case 'wsl':
- dataPath = config.linux.dataPath
- break
- case 'windows': {
- if (config.windows.dataPath.length > 0) {
- const appDataBase = config.windows.useRoaming
- ? join(home, 'AppData', 'Roaming')
- : join(home, 'AppData', 'Local')
- paths.push({
- browser: browserId,
- path: join(appDataBase, ...config.windows.dataPath),
- })
- }
- continue
- }
- }
- if (dataPath && dataPath.length > 0) {
- paths.push({
- browser: browserId,
- path: join(home, ...dataPath),
- })
- }
- }
- return paths
- }
- /**
- * Get native messaging host directories for all supported browsers
- */
- export function getAllNativeMessagingHostsDirs(): {
- browser: ChromiumBrowser
- path: string
- }[] {
- const platform = getPlatform()
- const home = homedir()
- const paths: { browser: ChromiumBrowser; path: string }[] = []
- for (const browserId of BROWSER_DETECTION_ORDER) {
- const config = CHROMIUM_BROWSERS[browserId]
- switch (platform) {
- case 'macos':
- if (config.macos.nativeMessagingPath.length > 0) {
- paths.push({
- browser: browserId,
- path: join(home, ...config.macos.nativeMessagingPath),
- })
- }
- break
- case 'linux':
- case 'wsl':
- if (config.linux.nativeMessagingPath.length > 0) {
- paths.push({
- browser: browserId,
- path: join(home, ...config.linux.nativeMessagingPath),
- })
- }
- break
- case 'windows':
- // Windows uses registry, not file paths for native messaging
- // We'll use a common location for the manifest file
- break
- }
- }
- return paths
- }
- /**
- * Get Windows registry keys for all supported browsers
- */
- export function getAllWindowsRegistryKeys(): {
- browser: ChromiumBrowser
- key: string
- }[] {
- const keys: { browser: ChromiumBrowser; key: string }[] = []
- for (const browserId of BROWSER_DETECTION_ORDER) {
- const config = CHROMIUM_BROWSERS[browserId]
- if (config.windows.registryKey) {
- keys.push({
- browser: browserId,
- key: config.windows.registryKey,
- })
- }
- }
- return keys
- }
- /**
- * Detect which browser to use for opening URLs
- * Returns the first available browser, or null if none found
- */
- export async function detectAvailableBrowser(): Promise<ChromiumBrowser | null> {
- const platform = getPlatform()
- for (const browserId of BROWSER_DETECTION_ORDER) {
- const config = CHROMIUM_BROWSERS[browserId]
- switch (platform) {
- case 'macos': {
- // Check if the .app bundle (a directory) exists
- const appPath = `/Applications/${config.macos.appName}.app`
- try {
- const stats = await stat(appPath)
- if (stats.isDirectory()) {
- logForDebugging(
- `[Claude in Chrome] Detected browser: ${config.name}`,
- )
- return browserId
- }
- } catch (e) {
- if (!isFsInaccessible(e)) throw e
- // App not found, continue checking
- }
- break
- }
- case 'wsl':
- case 'linux': {
- // Check if any binary exists
- for (const binary of config.linux.binaries) {
- if (await which(binary).catch(() => null)) {
- logForDebugging(
- `[Claude in Chrome] Detected browser: ${config.name}`,
- )
- return browserId
- }
- }
- break
- }
- case 'windows': {
- // Check if data path exists (indicates browser is installed)
- const home = homedir()
- if (config.windows.dataPath.length > 0) {
- const appDataBase = config.windows.useRoaming
- ? join(home, 'AppData', 'Roaming')
- : join(home, 'AppData', 'Local')
- const dataPath = join(appDataBase, ...config.windows.dataPath)
- try {
- const stats = await stat(dataPath)
- if (stats.isDirectory()) {
- logForDebugging(
- `[Claude in Chrome] Detected browser: ${config.name}`,
- )
- return browserId
- }
- } catch (e) {
- if (!isFsInaccessible(e)) throw e
- // Browser not found, continue checking
- }
- }
- break
- }
- }
- }
- return null
- }
- export function isClaudeInChromeMCPServer(name: string): boolean {
- return normalizeNameForMCP(name) === CLAUDE_IN_CHROME_MCP_SERVER_NAME
- }
- const MAX_TRACKED_TABS = 200
- const trackedTabIds = new Set<number>()
- export function trackClaudeInChromeTabId(tabId: number): void {
- if (trackedTabIds.size >= MAX_TRACKED_TABS && !trackedTabIds.has(tabId)) {
- trackedTabIds.clear()
- }
- trackedTabIds.add(tabId)
- }
- export function isTrackedClaudeInChromeTabId(tabId: number): boolean {
- return trackedTabIds.has(tabId)
- }
- export async function openInChrome(url: string): Promise<boolean> {
- const currentPlatform = getPlatform()
- // Detect the best available browser
- const browser = await detectAvailableBrowser()
- if (!browser) {
- logForDebugging('[Claude in Chrome] No compatible browser found')
- return false
- }
- const config = CHROMIUM_BROWSERS[browser]
- switch (currentPlatform) {
- case 'macos': {
- const { code } = await execFileNoThrow('open', [
- '-a',
- config.macos.appName,
- url,
- ])
- return code === 0
- }
- case 'windows': {
- // Use rundll32 to avoid cmd.exe metacharacter issues with URLs containing & | > <
- const { code } = await execFileNoThrow('rundll32', ['url,OpenURL', url])
- return code === 0
- }
- case 'wsl':
- case 'linux': {
- for (const binary of config.linux.binaries) {
- const { code } = await execFileNoThrow(binary, [url])
- if (code === 0) {
- return true
- }
- }
- return false
- }
- default:
- return false
- }
- }
- /**
- * Get the socket directory path (Unix only)
- */
- export function getSocketDir(): string {
- return `/tmp/claude-mcp-browser-bridge-${getUsername()}`
- }
- /**
- * Get the socket path (Unix) or pipe name (Windows)
- */
- export function getSecureSocketPath(): string {
- if (platform() === 'win32') {
- return `\\\\.\\pipe\\${getSocketName()}`
- }
- return join(getSocketDir(), `${process.pid}.sock`)
- }
- /**
- * Get all socket paths including PID-based sockets in the directory
- * and legacy fallback paths
- */
- export function getAllSocketPaths(): string[] {
- // Windows uses named pipes, not Unix sockets
- if (platform() === 'win32') {
- return [`\\\\.\\pipe\\${getSocketName()}`]
- }
- const paths: string[] = []
- const socketDir = getSocketDir()
- // Scan for *.sock files in the socket directory
- try {
- // eslint-disable-next-line custom-rules/no-sync-fs -- ClaudeForChromeContext.getSocketPaths (external @ant/claude-for-chrome-mcp) requires a sync () => string[] callback
- const files = readdirSync(socketDir)
- for (const file of files) {
- if (file.endsWith('.sock')) {
- paths.push(join(socketDir, file))
- }
- }
- } catch {
- // Directory may not exist yet
- }
- // Legacy fallback paths
- const legacyName = `claude-mcp-browser-bridge-${getUsername()}`
- const legacyTmpdir = join(tmpdir(), legacyName)
- const legacyTmp = `/tmp/${legacyName}`
- if (!paths.includes(legacyTmpdir)) {
- paths.push(legacyTmpdir)
- }
- if (legacyTmpdir !== legacyTmp && !paths.includes(legacyTmp)) {
- paths.push(legacyTmp)
- }
- return paths
- }
- function getSocketName(): string {
- // NOTE: This must match the one used in the Claude in Chrome MCP
- return `claude-mcp-browser-bridge-${getUsername()}`
- }
- function getUsername(): string {
- try {
- return userInfo().username || 'default'
- } catch {
- return process.env.USER || process.env.USERNAME || 'default'
- }
- }
|