common.ts 14 KB

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