541 lines
14 KiB
TypeScript
541 lines
14 KiB
TypeScript
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'
|
|
}
|
|
}
|