source dump of claude code
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 400 lines 13 kB view raw
1import { BROWSER_TOOLS } from '@ant/claude-for-chrome-mcp' 2import { chmod, mkdir, readFile, writeFile } from 'fs/promises' 3import { homedir } from 'os' 4import { join } from 'path' 5import { fileURLToPath } from 'url' 6import { 7 getIsInteractive, 8 getIsNonInteractiveSession, 9 getSessionBypassPermissionsMode, 10} from '../../bootstrap/state.js' 11import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' 12import type { ScopedMcpServerConfig } from '../../services/mcp/types.js' 13import { isInBundledMode } from '../bundledMode.js' 14import { getGlobalConfig, saveGlobalConfig } from '../config.js' 15import { logForDebugging } from '../debug.js' 16import { 17 getClaudeConfigHomeDir, 18 isEnvDefinedFalsy, 19 isEnvTruthy, 20} from '../envUtils.js' 21import { execFileNoThrowWithCwd } from '../execFileNoThrow.js' 22import { getPlatform } from '../platform.js' 23import { jsonStringify } from '../slowOperations.js' 24import { 25 CLAUDE_IN_CHROME_MCP_SERVER_NAME, 26 getAllBrowserDataPaths, 27 getAllNativeMessagingHostsDirs, 28 getAllWindowsRegistryKeys, 29 openInChrome, 30} from './common.js' 31import { getChromeSystemPrompt } from './prompt.js' 32import { isChromeExtensionInstalledPortable } from './setupPortable.js' 33 34const CHROME_EXTENSION_RECONNECT_URL = 'https://clau.de/chrome/reconnect' 35 36const NATIVE_HOST_IDENTIFIER = 'com.anthropic.claude_code_browser_extension' 37const NATIVE_HOST_MANIFEST_NAME = `${NATIVE_HOST_IDENTIFIER}.json` 38 39export function shouldEnableClaudeInChrome(chromeFlag?: boolean): boolean { 40 // Disable by default in non-interactive sessions (e.g., SDK, CI) 41 if (getIsNonInteractiveSession() && chromeFlag !== true) { 42 return false 43 } 44 45 // Check CLI flags 46 if (chromeFlag === true) { 47 return true 48 } 49 if (chromeFlag === false) { 50 return false 51 } 52 53 // Check environment variables 54 if (isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_CFC)) { 55 return true 56 } 57 if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ENABLE_CFC)) { 58 return false 59 } 60 61 // Check default config settings 62 const config = getGlobalConfig() 63 if (config.claudeInChromeDefaultEnabled !== undefined) { 64 return config.claudeInChromeDefaultEnabled 65 } 66 67 return false 68} 69 70let shouldAutoEnable: boolean | undefined = undefined 71 72export function shouldAutoEnableClaudeInChrome(): boolean { 73 if (shouldAutoEnable !== undefined) { 74 return shouldAutoEnable 75 } 76 77 shouldAutoEnable = 78 getIsInteractive() && 79 isChromeExtensionInstalled_CACHED_MAY_BE_STALE() && 80 (process.env.USER_TYPE === 'ant' || 81 getFeatureValue_CACHED_MAY_BE_STALE('tengu_chrome_auto_enable', false)) 82 83 return shouldAutoEnable 84} 85 86/** 87 * Setup Claude in Chrome MCP server and tools 88 * 89 * @returns MCP config and allowed tools, or throws an error if platform is unsupported 90 */ 91export function setupClaudeInChrome(): { 92 mcpConfig: Record<string, ScopedMcpServerConfig> 93 allowedTools: string[] 94 systemPrompt: string 95} { 96 const isNativeBuild = isInBundledMode() 97 const allowedTools = BROWSER_TOOLS.map( 98 tool => `mcp__claude-in-chrome__${tool.name}`, 99 ) 100 101 const env: Record<string, string> = {} 102 if (getSessionBypassPermissionsMode()) { 103 env.CLAUDE_CHROME_PERMISSION_MODE = 'skip_all_permission_checks' 104 } 105 const hasEnv = Object.keys(env).length > 0 106 107 if (isNativeBuild) { 108 // Create a wrapper script that calls the same binary with --chrome-native-host. This 109 // is needed because the native host manifest "path" field cannot contain arguments. 110 const execCommand = `"${process.execPath}" --chrome-native-host` 111 112 // Run asynchronously without blocking; best-effort so swallow errors 113 void createWrapperScript(execCommand) 114 .then(manifestBinaryPath => 115 installChromeNativeHostManifest(manifestBinaryPath), 116 ) 117 .catch(e => 118 logForDebugging( 119 `[Claude in Chrome] Failed to install native host: ${e}`, 120 { level: 'error' }, 121 ), 122 ) 123 124 return { 125 mcpConfig: { 126 [CLAUDE_IN_CHROME_MCP_SERVER_NAME]: { 127 type: 'stdio' as const, 128 command: process.execPath, 129 args: ['--claude-in-chrome-mcp'], 130 scope: 'dynamic' as const, 131 ...(hasEnv && { env }), 132 }, 133 }, 134 allowedTools, 135 systemPrompt: getChromeSystemPrompt(), 136 } 137 } else { 138 const __filename = fileURLToPath(import.meta.url) 139 const __dirname = join(__filename, '..') 140 const cliPath = join(__dirname, 'cli.js') 141 142 void createWrapperScript( 143 `"${process.execPath}" "${cliPath}" --chrome-native-host`, 144 ) 145 .then(manifestBinaryPath => 146 installChromeNativeHostManifest(manifestBinaryPath), 147 ) 148 .catch(e => 149 logForDebugging( 150 `[Claude in Chrome] Failed to install native host: ${e}`, 151 { level: 'error' }, 152 ), 153 ) 154 155 const mcpConfig = { 156 [CLAUDE_IN_CHROME_MCP_SERVER_NAME]: { 157 type: 'stdio' as const, 158 command: process.execPath, 159 args: [`${cliPath}`, '--claude-in-chrome-mcp'], 160 scope: 'dynamic' as const, 161 ...(hasEnv && { env }), 162 }, 163 } 164 165 return { 166 mcpConfig, 167 allowedTools, 168 systemPrompt: getChromeSystemPrompt(), 169 } 170 } 171} 172 173/** 174 * Get native messaging hosts directories for all supported browsers 175 * Returns an array of directories where the native host manifest should be installed 176 */ 177function getNativeMessagingHostsDirs(): string[] { 178 const platform = getPlatform() 179 180 if (platform === 'windows') { 181 // Windows uses a single location with registry entries pointing to it 182 const home = homedir() 183 const appData = process.env.APPDATA || join(home, 'AppData', 'Local') 184 return [join(appData, 'Claude Code', 'ChromeNativeHost')] 185 } 186 187 // macOS and Linux: return all browser native messaging directories 188 return getAllNativeMessagingHostsDirs().map(({ path }) => path) 189} 190 191export async function installChromeNativeHostManifest( 192 manifestBinaryPath: string, 193): Promise<void> { 194 const manifestDirs = getNativeMessagingHostsDirs() 195 if (manifestDirs.length === 0) { 196 throw Error('Claude in Chrome Native Host not supported on this platform') 197 } 198 199 const manifest = { 200 name: NATIVE_HOST_IDENTIFIER, 201 description: 'Claude Code Browser Extension Native Host', 202 path: manifestBinaryPath, 203 type: 'stdio', 204 allowed_origins: [ 205 `chrome-extension://fcoeoabgfenejglbffodgkkbkcdhcgfn/`, // PROD_EXTENSION_ID 206 ...(process.env.USER_TYPE === 'ant' 207 ? [ 208 'chrome-extension://dihbgbndebgnbjfmelmegjepbnkhlgni/', // DEV_EXTENSION_ID 209 'chrome-extension://dngcpimnedloihjnnfngkgjoidhnaolf/', // ANT_EXTENSION_ID 210 ] 211 : []), 212 ], 213 } 214 215 const manifestContent = jsonStringify(manifest, null, 2) 216 let anyManifestUpdated = false 217 218 // Install manifest to all browser directories 219 for (const manifestDir of manifestDirs) { 220 const manifestPath = join(manifestDir, NATIVE_HOST_MANIFEST_NAME) 221 222 // Check if content matches to avoid unnecessary writes 223 const existingContent = await readFile(manifestPath, 'utf-8').catch( 224 () => null, 225 ) 226 if (existingContent === manifestContent) { 227 continue 228 } 229 230 try { 231 await mkdir(manifestDir, { recursive: true }) 232 await writeFile(manifestPath, manifestContent) 233 logForDebugging( 234 `[Claude in Chrome] Installed native host manifest at: ${manifestPath}`, 235 ) 236 anyManifestUpdated = true 237 } catch (error) { 238 // Log but don't fail - the browser might not be installed 239 logForDebugging( 240 `[Claude in Chrome] Failed to install manifest at ${manifestPath}: ${error}`, 241 ) 242 } 243 } 244 245 // Windows requires registry entries pointing to the manifest for each browser 246 if (getPlatform() === 'windows') { 247 const manifestPath = join(manifestDirs[0]!, NATIVE_HOST_MANIFEST_NAME) 248 registerWindowsNativeHosts(manifestPath) 249 } 250 251 // Restart the native host if we have rewritten any manifest 252 if (anyManifestUpdated) { 253 void isChromeExtensionInstalled().then(isInstalled => { 254 if (isInstalled) { 255 logForDebugging( 256 `[Claude in Chrome] First-time install detected, opening reconnect page in browser`, 257 ) 258 void openInChrome(CHROME_EXTENSION_RECONNECT_URL) 259 } else { 260 logForDebugging( 261 `[Claude in Chrome] First-time install detected, but extension not installed, skipping reconnect`, 262 ) 263 } 264 }) 265 } 266} 267 268/** 269 * Register the native host in Windows registry for all supported browsers 270 */ 271function registerWindowsNativeHosts(manifestPath: string): void { 272 const registryKeys = getAllWindowsRegistryKeys() 273 274 for (const { browser, key } of registryKeys) { 275 const fullKey = `${key}\\${NATIVE_HOST_IDENTIFIER}` 276 // Use reg.exe to add the registry entry 277 // https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging 278 void execFileNoThrowWithCwd('reg', [ 279 'add', 280 fullKey, 281 '/ve', // Set the default (unnamed) value 282 '/t', 283 'REG_SZ', 284 '/d', 285 manifestPath, 286 '/f', // Force overwrite without prompt 287 ]).then(result => { 288 if (result.code === 0) { 289 logForDebugging( 290 `[Claude in Chrome] Registered native host for ${browser} in Windows registry: ${fullKey}`, 291 ) 292 } else { 293 logForDebugging( 294 `[Claude in Chrome] Failed to register native host for ${browser} in Windows registry: ${result.stderr}`, 295 ) 296 } 297 }) 298 } 299} 300 301/** 302 * Create a wrapper script in ~/.claude/chrome/ that invokes the given command. This is 303 * necessary because Chrome's native host manifest "path" field cannot contain arguments. 304 * 305 * @param command - The full command to execute (e.g., "/path/to/claude --chrome-native-host") 306 * @returns The path to the wrapper script 307 */ 308async function createWrapperScript(command: string): Promise<string> { 309 const platform = getPlatform() 310 const chromeDir = join(getClaudeConfigHomeDir(), 'chrome') 311 const wrapperPath = 312 platform === 'windows' 313 ? join(chromeDir, 'chrome-native-host.bat') 314 : join(chromeDir, 'chrome-native-host') 315 316 const scriptContent = 317 platform === 'windows' 318 ? `@echo off 319REM Chrome native host wrapper script 320REM Generated by Claude Code - do not edit manually 321${command} 322` 323 : `#!/bin/sh 324# Chrome native host wrapper script 325# Generated by Claude Code - do not edit manually 326exec ${command} 327` 328 329 // Check if content matches to avoid unnecessary writes 330 const existingContent = await readFile(wrapperPath, 'utf-8').catch(() => null) 331 if (existingContent === scriptContent) { 332 return wrapperPath 333 } 334 335 await mkdir(chromeDir, { recursive: true }) 336 await writeFile(wrapperPath, scriptContent) 337 338 if (platform !== 'windows') { 339 await chmod(wrapperPath, 0o755) 340 } 341 342 logForDebugging( 343 `[Claude in Chrome] Created Chrome native host wrapper script: ${wrapperPath}`, 344 ) 345 return wrapperPath 346} 347 348/** 349 * Get cached value of whether Chrome extension is installed. Returns 350 * from disk cache immediately, updates cache in background. 351 * 352 * Use this for sync/startup-critical paths where blocking on filesystem 353 * access is not acceptable. The value may be stale if the cache hasn't 354 * been updated recently. 355 * 356 * Only positive detections are persisted. A negative result from the 357 * filesystem scan is not cached, because it may come from a machine that 358 * shares ~/.claude.json but has no local Chrome (e.g. a remote dev 359 * environment using the bridge), and caching it would permanently poison 360 * auto-enable for every session on every machine that reads that config. 361 */ 362function isChromeExtensionInstalled_CACHED_MAY_BE_STALE(): boolean { 363 // Update cache in background without blocking 364 void isChromeExtensionInstalled().then(isInstalled => { 365 // Only persist positive detections — see docstring. The cost of a stale 366 // `true` is one silent MCP connection attempt per session; the cost of a 367 // stale `false` is auto-enable never working again without manual repair. 368 if (!isInstalled) { 369 return 370 } 371 const config = getGlobalConfig() 372 if (config.cachedChromeExtensionInstalled !== isInstalled) { 373 saveGlobalConfig(prev => ({ 374 ...prev, 375 cachedChromeExtensionInstalled: isInstalled, 376 })) 377 } 378 }) 379 380 // Return cached value immediately from disk 381 const cached = getGlobalConfig().cachedChromeExtensionInstalled 382 return cached ?? false 383} 384 385/** 386 * Detects if the Claude in Chrome extension is installed by checking the Extensions 387 * directory across all supported Chromium-based browsers and their profiles. 388 * 389 * @returns Object with isInstalled boolean and the browser where the extension was found 390 */ 391export async function isChromeExtensionInstalled(): Promise<boolean> { 392 const browserPaths = getAllBrowserDataPaths() 393 if (browserPaths.length === 0) { 394 logForDebugging( 395 `[Claude in Chrome] Unsupported platform for extension detection: ${getPlatform()}`, 396 ) 397 return false 398 } 399 return isChromeExtensionInstalledPortable(browserPaths, logForDebugging) 400}