Download src/utils/claudeInChrome/setup.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/claudeInChrome/setup.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/claudeInChrome/setup.ts
-
curl -L -o setup.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/claudeInChrome/setup.ts
13 kB
| import { chmod, mkdir, readFile, writeFile } from 'fs/promises' | |
| import { homedir } from 'os' | |
| import { join } from 'path' | |
| import { fileURLToPath } from 'url' | |
| import { | |
| getIsInteractive, | |
| getIsNonInteractiveSession, | |
| getSessionBypassPermissionsMode, | |
| } from '../../bootstrap/state.js' | |
| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' | |
| import type { ScopedMcpServerConfig } from '../../services/mcp/types.js' | |
| import { isInBundledMode } from '../bundledMode.js' | |
| import { getGlobalConfig, saveGlobalConfig } from '../config.js' | |
| import { logForDebugging } from '../debug.js' | |
| import { | |
| getClaudeConfigHomeDir, | |
| isEnvDefinedFalsy, | |
| isEnvTruthy, | |
| } from '../envUtils.js' | |
| import { execFileNoThrowWithCwd } from '../execFileNoThrow.js' | |
| import { getPlatform } from '../platform.js' | |
| import { jsonStringify } from '../slowOperations.js' | |
| import { | |
| CLAUDE_IN_CHROME_MCP_SERVER_NAME, | |
| getAllBrowserDataPaths, | |
| getAllNativeMessagingHostsDirs, | |
| getAllWindowsRegistryKeys, | |
| openInChrome, | |
| } from './common.js' | |
| import { getChromeBrowserTools } from './package.js' | |
| import { getChromeSystemPrompt } from './prompt.js' | |
| import { isChromeExtensionInstalledPortable } from './setupPortable.js' | |
| const CHROME_EXTENSION_RECONNECT_URL = 'https://clau.de/chrome/reconnect' | |
| const NATIVE_HOST_IDENTIFIER = 'com.anthropic.claude_code_browser_extension' | |
| const NATIVE_HOST_MANIFEST_NAME = `${NATIVE_HOST_IDENTIFIER}.json` | |
| export function shouldEnableClaudeInChrome(chromeFlag?: boolean): boolean { | |
| // Disable by default in non-interactive sessions (e.g., SDK, CI) | |
| if (getIsNonInteractiveSession() && chromeFlag !== true) { | |
| return false | |
| } | |
| // Check CLI flags | |
| if (chromeFlag === true) { | |
| return true | |
| } | |
| if (chromeFlag === false) { | |
| return false | |
| } | |
| // Check environment variables | |
| if (isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_CFC)) { | |
| return true | |
| } | |
| if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ENABLE_CFC)) { | |
| return false | |
| } | |
| // Check default config settings | |
| const config = getGlobalConfig() | |
| if (config.claudeInChromeDefaultEnabled !== undefined) { | |
| return config.claudeInChromeDefaultEnabled | |
| } | |
| return false | |
| } | |
| let shouldAutoEnable: boolean | undefined = undefined | |
| export function shouldAutoEnableClaudeInChrome(): boolean { | |
| if (shouldAutoEnable !== undefined) { | |
| return shouldAutoEnable | |
| } | |
| shouldAutoEnable = | |
| getIsInteractive() && | |
| isChromeExtensionInstalled_CACHED_MAY_BE_STALE() && | |
| (process.env.USER_TYPE === 'ant' || | |
| getFeatureValue_CACHED_MAY_BE_STALE('tengu_chrome_auto_enable', false)) | |
| return shouldAutoEnable | |
| } | |
| /** | |
| * Setup Claude in Chrome MCP server and tools | |
| * | |
| * @returns MCP config and allowed tools, or throws an error if platform is unsupported | |
| */ | |
| export function setupClaudeInChrome(): { | |
| mcpConfig: Record<string, ScopedMcpServerConfig> | |
| allowedTools: string[] | |
| systemPrompt: string | |
| } { | |
| const isNativeBuild = isInBundledMode() | |
| const allowedTools = getChromeBrowserTools().map( | |
| tool => `mcp__claude-in-chrome__${tool.name}`, | |
| ) | |
| const env: Record<string, string> = {} | |
| if (getSessionBypassPermissionsMode()) { | |
| env.CLAUDE_CHROME_PERMISSION_MODE = 'skip_all_permission_checks' | |
| } | |
| const hasEnv = Object.keys(env).length > 0 | |
| if (isNativeBuild) { | |
| // Create a wrapper script that calls the same binary with --chrome-native-host. This | |
| // is needed because the native host manifest "path" field cannot contain arguments. | |
| const execCommand = `"${process.execPath}" --chrome-native-host` | |
| // Run asynchronously without blocking; best-effort so swallow errors | |
| void createWrapperScript(execCommand) | |
| .then(manifestBinaryPath => | |
| installChromeNativeHostManifest(manifestBinaryPath), | |
| ) | |
| .catch(e => | |
| logForDebugging( | |
| `[Claude in Chrome] Failed to install native host: ${e}`, | |
| { level: 'error' }, | |
| ), | |
| ) | |
| return { | |
| mcpConfig: { | |
| [CLAUDE_IN_CHROME_MCP_SERVER_NAME]: { | |
| type: 'stdio' as const, | |
| command: process.execPath, | |
| args: ['--claude-in-chrome-mcp'], | |
| scope: 'dynamic' as const, | |
| ...(hasEnv && { env }), | |
| }, | |
| }, | |
| allowedTools, | |
| systemPrompt: getChromeSystemPrompt(), | |
| } | |
| } else { | |
| const __filename = fileURLToPath(import.meta.url) | |
| const __dirname = join(__filename, '..') | |
| const cliPath = join(__dirname, 'cli.js') | |
| void createWrapperScript( | |
| `"${process.execPath}" "${cliPath}" --chrome-native-host`, | |
| ) | |
| .then(manifestBinaryPath => | |
| installChromeNativeHostManifest(manifestBinaryPath), | |
| ) | |
| .catch(e => | |
| logForDebugging( | |
| `[Claude in Chrome] Failed to install native host: ${e}`, | |
| { level: 'error' }, | |
| ), | |
| ) | |
| const mcpConfig = { | |
| [CLAUDE_IN_CHROME_MCP_SERVER_NAME]: { | |
| type: 'stdio' as const, | |
| command: process.execPath, | |
| args: [`${cliPath}`, '--claude-in-chrome-mcp'], | |
| scope: 'dynamic' as const, | |
| ...(hasEnv && { env }), | |
| }, | |
| } | |
| return { | |
| mcpConfig, | |
| allowedTools, | |
| systemPrompt: getChromeSystemPrompt(), | |
| } | |
| } | |
| } | |
| /** | |
| * Get native messaging hosts directories for all supported browsers | |
| * Returns an array of directories where the native host manifest should be installed | |
| */ | |
| function getNativeMessagingHostsDirs(): string[] { | |
| const platform = getPlatform() | |
| if (platform === 'windows') { | |
| // Windows uses a single location with registry entries pointing to it | |
| const home = homedir() | |
| const appData = process.env.APPDATA || join(home, 'AppData', 'Local') | |
| return [join(appData, 'Claude Code', 'ChromeNativeHost')] | |
| } | |
| // macOS and Linux: return all browser native messaging directories | |
| return getAllNativeMessagingHostsDirs().map(({ path }) => path) | |
| } | |
| export async function installChromeNativeHostManifest( | |
| manifestBinaryPath: string, | |
| ): Promise<void> { | |
| const manifestDirs = getNativeMessagingHostsDirs() | |
| if (manifestDirs.length === 0) { | |
| throw Error('Claude in Chrome Native Host not supported on this platform') | |
| } | |
| const manifest = { | |
| name: NATIVE_HOST_IDENTIFIER, | |
| description: 'Claude Code Browser Extension Native Host', | |
| path: manifestBinaryPath, | |
| type: 'stdio', | |
| allowed_origins: [ | |
| `chrome-extension://fcoeoabgfenejglbffodgkkbkcdhcgfn/`, // PROD_EXTENSION_ID | |
| ...(process.env.USER_TYPE === 'ant' | |
| ? [ | |
| 'chrome-extension://dihbgbndebgnbjfmelmegjepbnkhlgni/', // DEV_EXTENSION_ID | |
| 'chrome-extension://dngcpimnedloihjnnfngkgjoidhnaolf/', // ANT_EXTENSION_ID | |
| ] | |
| : []), | |
| ], | |
| } | |
| const manifestContent = jsonStringify(manifest, null, 2) | |
| let anyManifestUpdated = false | |
| // Install manifest to all browser directories | |
| for (const manifestDir of manifestDirs) { | |
| const manifestPath = join(manifestDir, NATIVE_HOST_MANIFEST_NAME) | |
| // Check if content matches to avoid unnecessary writes | |
| const existingContent = await readFile(manifestPath, 'utf-8').catch( | |
| () => null, | |
| ) | |
| if (existingContent === manifestContent) { | |
| continue | |
| } | |
| try { | |
| await mkdir(manifestDir, { recursive: true }) | |
| await writeFile(manifestPath, manifestContent) | |
| logForDebugging( | |
| `[Claude in Chrome] Installed native host manifest at: ${manifestPath}`, | |
| ) | |
| anyManifestUpdated = true | |
| } catch (error) { | |
| // Log but don't fail - the browser might not be installed | |
| logForDebugging( | |
| `[Claude in Chrome] Failed to install manifest at ${manifestPath}: ${error}`, | |
| ) | |
| } | |
| } | |
| // Windows requires registry entries pointing to the manifest for each browser | |
| if (getPlatform() === 'windows') { | |
| const manifestPath = join(manifestDirs[0]!, NATIVE_HOST_MANIFEST_NAME) | |
| registerWindowsNativeHosts(manifestPath) | |
| } | |
| // Restart the native host if we have rewritten any manifest | |
| if (anyManifestUpdated) { | |
| void isChromeExtensionInstalled().then(isInstalled => { | |
| if (isInstalled) { | |
| logForDebugging( | |
| `[Claude in Chrome] First-time install detected, opening reconnect page in browser`, | |
| ) | |
| void openInChrome(CHROME_EXTENSION_RECONNECT_URL) | |
| } else { | |
| logForDebugging( | |
| `[Claude in Chrome] First-time install detected, but extension not installed, skipping reconnect`, | |
| ) | |
| } | |
| }) | |
| } | |
| } | |
| /** | |
| * Register the native host in Windows registry for all supported browsers | |
| */ | |
| function registerWindowsNativeHosts(manifestPath: string): void { | |
| const registryKeys = getAllWindowsRegistryKeys() | |
| for (const { browser, key } of registryKeys) { | |
| const fullKey = `${key}\\${NATIVE_HOST_IDENTIFIER}` | |
| // Use reg.exe to add the registry entry | |
| // https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging | |
| void execFileNoThrowWithCwd('reg', [ | |
| 'add', | |
| fullKey, | |
| '/ve', // Set the default (unnamed) value | |
| '/t', | |
| 'REG_SZ', | |
| '/d', | |
| manifestPath, | |
| '/f', // Force overwrite without prompt | |
| ]).then(result => { | |
| if (result.code === 0) { | |
| logForDebugging( | |
| `[Claude in Chrome] Registered native host for ${browser} in Windows registry: ${fullKey}`, | |
| ) | |
| } else { | |
| logForDebugging( | |
| `[Claude in Chrome] Failed to register native host for ${browser} in Windows registry: ${result.stderr}`, | |
| ) | |
| } | |
| }) | |
| } | |
| } | |
| /** | |
| * Create a wrapper script in ~/.claude/chrome/ that invokes the given command. This is | |
| * necessary because Chrome's native host manifest "path" field cannot contain arguments. | |
| * | |
| * @param command - The full command to execute (e.g., "/path/to/claude --chrome-native-host") | |
| * @returns The path to the wrapper script | |
| */ | |
| async function createWrapperScript(command: string): Promise<string> { | |
| const platform = getPlatform() | |
| const chromeDir = join(getClaudeConfigHomeDir(), 'chrome') | |
| const wrapperPath = | |
| platform === 'windows' | |
| ? join(chromeDir, 'chrome-native-host.bat') | |
| : join(chromeDir, 'chrome-native-host') | |
| const scriptContent = | |
| platform === 'windows' | |
| ? `@echo off | |
| REM Chrome native host wrapper script | |
| REM Generated by Claude Code - do not edit manually | |
| ${command} | |
| ` | |
| : `#!/bin/sh | |
| # Chrome native host wrapper script | |
| # Generated by Claude Code - do not edit manually | |
| exec ${command} | |
| ` | |
| // Check if content matches to avoid unnecessary writes | |
| const existingContent = await readFile(wrapperPath, 'utf-8').catch(() => null) | |
| if (existingContent === scriptContent) { | |
| return wrapperPath | |
| } | |
| await mkdir(chromeDir, { recursive: true }) | |
| await writeFile(wrapperPath, scriptContent) | |
| if (platform !== 'windows') { | |
| await chmod(wrapperPath, 0o755) | |
| } | |
| logForDebugging( | |
| `[Claude in Chrome] Created Chrome native host wrapper script: ${wrapperPath}`, | |
| ) | |
| return wrapperPath | |
| } | |
| /** | |
| * Get cached value of whether Chrome extension is installed. Returns | |
| * from disk cache immediately, updates cache in background. | |
| * | |
| * Use this for sync/startup-critical paths where blocking on filesystem | |
| * access is not acceptable. The value may be stale if the cache hasn't | |
| * been updated recently. | |
| * | |
| * Only positive detections are persisted. A negative result from the | |
| * filesystem scan is not cached, because it may come from a machine that | |
| * shares ~/.claude.json but has no local Chrome (e.g. a remote dev | |
| * environment using the bridge), and caching it would permanently poison | |
| * auto-enable for every session on every machine that reads that config. | |
| */ | |
| function isChromeExtensionInstalled_CACHED_MAY_BE_STALE(): boolean { | |
| // Update cache in background without blocking | |
| void isChromeExtensionInstalled().then(isInstalled => { | |
| // Only persist positive detections — see docstring. The cost of a stale | |
| // `true` is one silent MCP connection attempt per session; the cost of a | |
| // stale `false` is auto-enable never working again without manual repair. | |
| if (!isInstalled) { | |
| return | |
| } | |
| const config = getGlobalConfig() | |
| if (config.cachedChromeExtensionInstalled !== isInstalled) { | |
| saveGlobalConfig(prev => ({ | |
| ...prev, | |
| cachedChromeExtensionInstalled: isInstalled, | |
| })) | |
| } | |
| }) | |
| // Return cached value immediately from disk | |
| const cached = getGlobalConfig().cachedChromeExtensionInstalled | |
| return cached ?? false | |
| } | |
| /** | |
| * Detects if the Claude in Chrome extension is installed by checking the Extensions | |
| * directory across all supported Chromium-based browsers and their profiles. | |
| * | |
| * @returns Object with isInstalled boolean and the browser where the extension was found | |
| */ | |
| export async function isChromeExtensionInstalled(): Promise<boolean> { | |
| const browserPaths = getAllBrowserDataPaths() | |
| if (browserPaths.length === 0) { | |
| logForDebugging( | |
| `[Claude in Chrome] Unsupported platform for extension detection: ${getPlatform()}`, | |
| ) | |
| return false | |
| } | |
| return isChromeExtensionInstalledPortable(browserPaths, logForDebugging) | |
| } | |