init claude-code
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
import { BROWSER_TOOLS } from '@ant/claude-for-chrome-mcp'
|
||||
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 { 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 = BROWSER_TOOLS.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)
|
||||
}
|
||||
Reference in New Issue
Block a user