source dump of claude code
at main 540 lines 14 kB view raw
1import { readdirSync } from 'fs' 2import { stat } from 'fs/promises' 3import { homedir, platform, tmpdir, userInfo } from 'os' 4import { join } from 'path' 5import { normalizeNameForMCP } from '../../services/mcp/normalization.js' 6import { logForDebugging } from '../debug.js' 7import { isFsInaccessible } from '../errors.js' 8import { execFileNoThrow } from '../execFileNoThrow.js' 9import { getPlatform } from '../platform.js' 10import { which } from '../which.js' 11 12export const CLAUDE_IN_CHROME_MCP_SERVER_NAME = 'claude-in-chrome' 13 14// Re-export ChromiumBrowser type for setup.ts 15export type { ChromiumBrowser } from './setupPortable.js' 16 17// Import for local use 18import type { ChromiumBrowser } from './setupPortable.js' 19 20type BrowserConfig = { 21 name: string 22 macos: { 23 appName: string 24 dataPath: string[] 25 nativeMessagingPath: string[] 26 } 27 linux: { 28 binaries: string[] 29 dataPath: string[] 30 nativeMessagingPath: string[] 31 } 32 windows: { 33 dataPath: string[] 34 registryKey: string 35 useRoaming?: boolean // Opera uses Roaming instead of Local 36 } 37} 38 39export const CHROMIUM_BROWSERS: Record<ChromiumBrowser, BrowserConfig> = { 40 chrome: { 41 name: 'Google Chrome', 42 macos: { 43 appName: 'Google Chrome', 44 dataPath: ['Library', 'Application Support', 'Google', 'Chrome'], 45 nativeMessagingPath: [ 46 'Library', 47 'Application Support', 48 'Google', 49 'Chrome', 50 'NativeMessagingHosts', 51 ], 52 }, 53 linux: { 54 binaries: ['google-chrome', 'google-chrome-stable'], 55 dataPath: ['.config', 'google-chrome'], 56 nativeMessagingPath: ['.config', 'google-chrome', 'NativeMessagingHosts'], 57 }, 58 windows: { 59 dataPath: ['Google', 'Chrome', 'User Data'], 60 registryKey: 'HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts', 61 }, 62 }, 63 brave: { 64 name: 'Brave', 65 macos: { 66 appName: 'Brave Browser', 67 dataPath: [ 68 'Library', 69 'Application Support', 70 'BraveSoftware', 71 'Brave-Browser', 72 ], 73 nativeMessagingPath: [ 74 'Library', 75 'Application Support', 76 'BraveSoftware', 77 'Brave-Browser', 78 'NativeMessagingHosts', 79 ], 80 }, 81 linux: { 82 binaries: ['brave-browser', 'brave'], 83 dataPath: ['.config', 'BraveSoftware', 'Brave-Browser'], 84 nativeMessagingPath: [ 85 '.config', 86 'BraveSoftware', 87 'Brave-Browser', 88 'NativeMessagingHosts', 89 ], 90 }, 91 windows: { 92 dataPath: ['BraveSoftware', 'Brave-Browser', 'User Data'], 93 registryKey: 94 'HKCU\\Software\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts', 95 }, 96 }, 97 arc: { 98 name: 'Arc', 99 macos: { 100 appName: 'Arc', 101 dataPath: ['Library', 'Application Support', 'Arc', 'User Data'], 102 nativeMessagingPath: [ 103 'Library', 104 'Application Support', 105 'Arc', 106 'User Data', 107 'NativeMessagingHosts', 108 ], 109 }, 110 linux: { 111 // Arc is not available on Linux 112 binaries: [], 113 dataPath: [], 114 nativeMessagingPath: [], 115 }, 116 windows: { 117 // Arc Windows is Chromium-based 118 dataPath: ['Arc', 'User Data'], 119 registryKey: 'HKCU\\Software\\ArcBrowser\\Arc\\NativeMessagingHosts', 120 }, 121 }, 122 chromium: { 123 name: 'Chromium', 124 macos: { 125 appName: 'Chromium', 126 dataPath: ['Library', 'Application Support', 'Chromium'], 127 nativeMessagingPath: [ 128 'Library', 129 'Application Support', 130 'Chromium', 131 'NativeMessagingHosts', 132 ], 133 }, 134 linux: { 135 binaries: ['chromium', 'chromium-browser'], 136 dataPath: ['.config', 'chromium'], 137 nativeMessagingPath: ['.config', 'chromium', 'NativeMessagingHosts'], 138 }, 139 windows: { 140 dataPath: ['Chromium', 'User Data'], 141 registryKey: 'HKCU\\Software\\Chromium\\NativeMessagingHosts', 142 }, 143 }, 144 edge: { 145 name: 'Microsoft Edge', 146 macos: { 147 appName: 'Microsoft Edge', 148 dataPath: ['Library', 'Application Support', 'Microsoft Edge'], 149 nativeMessagingPath: [ 150 'Library', 151 'Application Support', 152 'Microsoft Edge', 153 'NativeMessagingHosts', 154 ], 155 }, 156 linux: { 157 binaries: ['microsoft-edge', 'microsoft-edge-stable'], 158 dataPath: ['.config', 'microsoft-edge'], 159 nativeMessagingPath: [ 160 '.config', 161 'microsoft-edge', 162 'NativeMessagingHosts', 163 ], 164 }, 165 windows: { 166 dataPath: ['Microsoft', 'Edge', 'User Data'], 167 registryKey: 'HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts', 168 }, 169 }, 170 vivaldi: { 171 name: 'Vivaldi', 172 macos: { 173 appName: 'Vivaldi', 174 dataPath: ['Library', 'Application Support', 'Vivaldi'], 175 nativeMessagingPath: [ 176 'Library', 177 'Application Support', 178 'Vivaldi', 179 'NativeMessagingHosts', 180 ], 181 }, 182 linux: { 183 binaries: ['vivaldi', 'vivaldi-stable'], 184 dataPath: ['.config', 'vivaldi'], 185 nativeMessagingPath: ['.config', 'vivaldi', 'NativeMessagingHosts'], 186 }, 187 windows: { 188 dataPath: ['Vivaldi', 'User Data'], 189 registryKey: 'HKCU\\Software\\Vivaldi\\NativeMessagingHosts', 190 }, 191 }, 192 opera: { 193 name: 'Opera', 194 macos: { 195 appName: 'Opera', 196 dataPath: ['Library', 'Application Support', 'com.operasoftware.Opera'], 197 nativeMessagingPath: [ 198 'Library', 199 'Application Support', 200 'com.operasoftware.Opera', 201 'NativeMessagingHosts', 202 ], 203 }, 204 linux: { 205 binaries: ['opera'], 206 dataPath: ['.config', 'opera'], 207 nativeMessagingPath: ['.config', 'opera', 'NativeMessagingHosts'], 208 }, 209 windows: { 210 dataPath: ['Opera Software', 'Opera Stable'], 211 registryKey: 212 'HKCU\\Software\\Opera Software\\Opera Stable\\NativeMessagingHosts', 213 useRoaming: true, // Opera uses Roaming AppData, not Local 214 }, 215 }, 216} 217 218// Priority order for browser detection (most common first) 219export const BROWSER_DETECTION_ORDER: ChromiumBrowser[] = [ 220 'chrome', 221 'brave', 222 'arc', 223 'edge', 224 'chromium', 225 'vivaldi', 226 'opera', 227] 228 229/** 230 * Get all browser data paths to check for extension installation 231 */ 232export function getAllBrowserDataPaths(): { 233 browser: ChromiumBrowser 234 path: string 235}[] { 236 const platform = getPlatform() 237 const home = homedir() 238 const paths: { browser: ChromiumBrowser; path: string }[] = [] 239 240 for (const browserId of BROWSER_DETECTION_ORDER) { 241 const config = CHROMIUM_BROWSERS[browserId] 242 let dataPath: string[] | undefined 243 244 switch (platform) { 245 case 'macos': 246 dataPath = config.macos.dataPath 247 break 248 case 'linux': 249 case 'wsl': 250 dataPath = config.linux.dataPath 251 break 252 case 'windows': { 253 if (config.windows.dataPath.length > 0) { 254 const appDataBase = config.windows.useRoaming 255 ? join(home, 'AppData', 'Roaming') 256 : join(home, 'AppData', 'Local') 257 paths.push({ 258 browser: browserId, 259 path: join(appDataBase, ...config.windows.dataPath), 260 }) 261 } 262 continue 263 } 264 } 265 266 if (dataPath && dataPath.length > 0) { 267 paths.push({ 268 browser: browserId, 269 path: join(home, ...dataPath), 270 }) 271 } 272 } 273 274 return paths 275} 276 277/** 278 * Get native messaging host directories for all supported browsers 279 */ 280export function getAllNativeMessagingHostsDirs(): { 281 browser: ChromiumBrowser 282 path: string 283}[] { 284 const platform = getPlatform() 285 const home = homedir() 286 const paths: { browser: ChromiumBrowser; path: string }[] = [] 287 288 for (const browserId of BROWSER_DETECTION_ORDER) { 289 const config = CHROMIUM_BROWSERS[browserId] 290 291 switch (platform) { 292 case 'macos': 293 if (config.macos.nativeMessagingPath.length > 0) { 294 paths.push({ 295 browser: browserId, 296 path: join(home, ...config.macos.nativeMessagingPath), 297 }) 298 } 299 break 300 case 'linux': 301 case 'wsl': 302 if (config.linux.nativeMessagingPath.length > 0) { 303 paths.push({ 304 browser: browserId, 305 path: join(home, ...config.linux.nativeMessagingPath), 306 }) 307 } 308 break 309 case 'windows': 310 // Windows uses registry, not file paths for native messaging 311 // We'll use a common location for the manifest file 312 break 313 } 314 } 315 316 return paths 317} 318 319/** 320 * Get Windows registry keys for all supported browsers 321 */ 322export function getAllWindowsRegistryKeys(): { 323 browser: ChromiumBrowser 324 key: string 325}[] { 326 const keys: { browser: ChromiumBrowser; key: string }[] = [] 327 328 for (const browserId of BROWSER_DETECTION_ORDER) { 329 const config = CHROMIUM_BROWSERS[browserId] 330 if (config.windows.registryKey) { 331 keys.push({ 332 browser: browserId, 333 key: config.windows.registryKey, 334 }) 335 } 336 } 337 338 return keys 339} 340 341/** 342 * Detect which browser to use for opening URLs 343 * Returns the first available browser, or null if none found 344 */ 345export async function detectAvailableBrowser(): Promise<ChromiumBrowser | null> { 346 const platform = getPlatform() 347 348 for (const browserId of BROWSER_DETECTION_ORDER) { 349 const config = CHROMIUM_BROWSERS[browserId] 350 351 switch (platform) { 352 case 'macos': { 353 // Check if the .app bundle (a directory) exists 354 const appPath = `/Applications/${config.macos.appName}.app` 355 try { 356 const stats = await stat(appPath) 357 if (stats.isDirectory()) { 358 logForDebugging( 359 `[Claude in Chrome] Detected browser: ${config.name}`, 360 ) 361 return browserId 362 } 363 } catch (e) { 364 if (!isFsInaccessible(e)) throw e 365 // App not found, continue checking 366 } 367 break 368 } 369 case 'wsl': 370 case 'linux': { 371 // Check if any binary exists 372 for (const binary of config.linux.binaries) { 373 if (await which(binary).catch(() => null)) { 374 logForDebugging( 375 `[Claude in Chrome] Detected browser: ${config.name}`, 376 ) 377 return browserId 378 } 379 } 380 break 381 } 382 case 'windows': { 383 // Check if data path exists (indicates browser is installed) 384 const home = homedir() 385 if (config.windows.dataPath.length > 0) { 386 const appDataBase = config.windows.useRoaming 387 ? join(home, 'AppData', 'Roaming') 388 : join(home, 'AppData', 'Local') 389 const dataPath = join(appDataBase, ...config.windows.dataPath) 390 try { 391 const stats = await stat(dataPath) 392 if (stats.isDirectory()) { 393 logForDebugging( 394 `[Claude in Chrome] Detected browser: ${config.name}`, 395 ) 396 return browserId 397 } 398 } catch (e) { 399 if (!isFsInaccessible(e)) throw e 400 // Browser not found, continue checking 401 } 402 } 403 break 404 } 405 } 406 } 407 408 return null 409} 410 411export function isClaudeInChromeMCPServer(name: string): boolean { 412 return normalizeNameForMCP(name) === CLAUDE_IN_CHROME_MCP_SERVER_NAME 413} 414 415const MAX_TRACKED_TABS = 200 416const trackedTabIds = new Set<number>() 417 418export function trackClaudeInChromeTabId(tabId: number): void { 419 if (trackedTabIds.size >= MAX_TRACKED_TABS && !trackedTabIds.has(tabId)) { 420 trackedTabIds.clear() 421 } 422 trackedTabIds.add(tabId) 423} 424 425export function isTrackedClaudeInChromeTabId(tabId: number): boolean { 426 return trackedTabIds.has(tabId) 427} 428 429export async function openInChrome(url: string): Promise<boolean> { 430 const currentPlatform = getPlatform() 431 432 // Detect the best available browser 433 const browser = await detectAvailableBrowser() 434 435 if (!browser) { 436 logForDebugging('[Claude in Chrome] No compatible browser found') 437 return false 438 } 439 440 const config = CHROMIUM_BROWSERS[browser] 441 442 switch (currentPlatform) { 443 case 'macos': { 444 const { code } = await execFileNoThrow('open', [ 445 '-a', 446 config.macos.appName, 447 url, 448 ]) 449 return code === 0 450 } 451 case 'windows': { 452 // Use rundll32 to avoid cmd.exe metacharacter issues with URLs containing & | > < 453 const { code } = await execFileNoThrow('rundll32', ['url,OpenURL', url]) 454 return code === 0 455 } 456 case 'wsl': 457 case 'linux': { 458 for (const binary of config.linux.binaries) { 459 const { code } = await execFileNoThrow(binary, [url]) 460 if (code === 0) { 461 return true 462 } 463 } 464 return false 465 } 466 default: 467 return false 468 } 469} 470 471/** 472 * Get the socket directory path (Unix only) 473 */ 474export function getSocketDir(): string { 475 return `/tmp/claude-mcp-browser-bridge-${getUsername()}` 476} 477 478/** 479 * Get the socket path (Unix) or pipe name (Windows) 480 */ 481export function getSecureSocketPath(): string { 482 if (platform() === 'win32') { 483 return `\\\\.\\pipe\\${getSocketName()}` 484 } 485 return join(getSocketDir(), `${process.pid}.sock`) 486} 487 488/** 489 * Get all socket paths including PID-based sockets in the directory 490 * and legacy fallback paths 491 */ 492export function getAllSocketPaths(): string[] { 493 // Windows uses named pipes, not Unix sockets 494 if (platform() === 'win32') { 495 return [`\\\\.\\pipe\\${getSocketName()}`] 496 } 497 498 const paths: string[] = [] 499 const socketDir = getSocketDir() 500 501 // Scan for *.sock files in the socket directory 502 try { 503 // eslint-disable-next-line custom-rules/no-sync-fs -- ClaudeForChromeContext.getSocketPaths (external @ant/claude-for-chrome-mcp) requires a sync () => string[] callback 504 const files = readdirSync(socketDir) 505 for (const file of files) { 506 if (file.endsWith('.sock')) { 507 paths.push(join(socketDir, file)) 508 } 509 } 510 } catch { 511 // Directory may not exist yet 512 } 513 514 // Legacy fallback paths 515 const legacyName = `claude-mcp-browser-bridge-${getUsername()}` 516 const legacyTmpdir = join(tmpdir(), legacyName) 517 const legacyTmp = `/tmp/${legacyName}` 518 519 if (!paths.includes(legacyTmpdir)) { 520 paths.push(legacyTmpdir) 521 } 522 if (legacyTmpdir !== legacyTmp && !paths.includes(legacyTmp)) { 523 paths.push(legacyTmp) 524 } 525 526 return paths 527} 528 529function getSocketName(): string { 530 // NOTE: This must match the one used in the Claude in Chrome MCP 531 return `claude-mcp-browser-bridge-${getUsername()}` 532} 533 534function getUsername(): string { 535 try { 536 return userInfo().username || 'default' 537 } catch { 538 return process.env.USER || process.env.USERNAME || 'default' 539 } 540}