mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 18:16:08 +00:00
feat(cli): add browser automation commands
This commit is contained in:
+24
-14
@@ -39,11 +39,15 @@ import {
|
||||
import {
|
||||
appendBrowserToolArgs,
|
||||
ensureBrowserMcpOrThrow,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
resolveBrowserRuntimeEnv,
|
||||
resolveConfiguredBrowserProfileDir,
|
||||
syncBrowserMcpToConfigDir,
|
||||
} from './utils/browser';
|
||||
import { getGlobalEnvConfig, getOfficialChannelsConfig } from './config/unified-config-loader';
|
||||
import {
|
||||
getBrowserConfig,
|
||||
getGlobalEnvConfig,
|
||||
getOfficialChannelsConfig,
|
||||
} from './config/unified-config-loader';
|
||||
import {
|
||||
ensureProfileHooks as ensureImageAnalyzerHooks,
|
||||
removeImageAnalysisProfileHook,
|
||||
@@ -135,7 +139,7 @@ const CODEX_NATIVE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--version', '-v
|
||||
function resolveCodexRuntimeConfigOverrides(
|
||||
target: ReturnType<typeof resolveTargetType>
|
||||
): string[] {
|
||||
if (target !== 'codex') {
|
||||
if (target !== 'codex' || !getBrowserConfig().codex.enabled) {
|
||||
return [];
|
||||
}
|
||||
return buildCodexBrowserMcpOverrides();
|
||||
@@ -1054,13 +1058,13 @@ async function main(): Promise<void> {
|
||||
const imageAnalysisMcpReady =
|
||||
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
|
||||
let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv'];
|
||||
const browserProfileDir =
|
||||
const browserAttachConfig =
|
||||
resolvedTarget === 'claude'
|
||||
? resolveConfiguredBrowserProfileDir(process.env.CCS_BROWSER_PROFILE_DIR)
|
||||
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
||||
: undefined;
|
||||
if (resolvedTarget === 'claude') {
|
||||
ensureWebSearchMcpOrThrow();
|
||||
if (browserProfileDir) {
|
||||
if (browserAttachConfig?.enabled) {
|
||||
ensureBrowserMcpOrThrow();
|
||||
}
|
||||
}
|
||||
@@ -1087,7 +1091,7 @@ async function main(): Promise<void> {
|
||||
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
|
||||
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
||||
if (
|
||||
browserProfileDir &&
|
||||
browserAttachConfig?.enabled &&
|
||||
inheritedClaudeConfigDir &&
|
||||
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
||||
) {
|
||||
@@ -1297,10 +1301,13 @@ async function main(): Promise<void> {
|
||||
|
||||
// Explicitly inject effective settings env vars so stale ANTHROPIC_*
|
||||
// values from prior sessions cannot leak into the active profile.
|
||||
if (browserProfileDir) {
|
||||
if (browserAttachConfig?.enabled) {
|
||||
browserRuntimeEnv = {
|
||||
...(await resolveBrowserRuntimeEnv({
|
||||
profileDir: browserProfileDir,
|
||||
profileDir: browserAttachConfig.userDataDir,
|
||||
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
|
||||
? String(browserAttachConfig.devtoolsPort)
|
||||
: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1465,17 +1472,20 @@ async function main(): Promise<void> {
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
||||
};
|
||||
let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv'];
|
||||
const browserProfileDir =
|
||||
const browserAttachConfig =
|
||||
resolvedTarget === 'claude'
|
||||
? resolveConfiguredBrowserProfileDir(process.env.CCS_BROWSER_PROFILE_DIR)
|
||||
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
||||
: undefined;
|
||||
|
||||
if (resolvedTarget === 'claude') {
|
||||
if (browserProfileDir) {
|
||||
if (browserAttachConfig?.enabled) {
|
||||
ensureBrowserMcpOrThrow();
|
||||
browserRuntimeEnv = {
|
||||
...(await resolveBrowserRuntimeEnv({
|
||||
profileDir: browserProfileDir,
|
||||
profileDir: browserAttachConfig.userDataDir,
|
||||
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
|
||||
? String(browserAttachConfig.devtoolsPort)
|
||||
: undefined,
|
||||
})),
|
||||
};
|
||||
Object.assign(envVars, browserRuntimeEnv);
|
||||
@@ -1495,7 +1505,7 @@ async function main(): Promise<void> {
|
||||
if (defaultContinuityInheritance.claudeConfigDir) {
|
||||
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
|
||||
if (
|
||||
browserProfileDir &&
|
||||
browserAttachConfig?.enabled &&
|
||||
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
|
||||
) {
|
||||
throw new Error(
|
||||
|
||||
@@ -66,11 +66,15 @@ import {
|
||||
import {
|
||||
appendBrowserToolArgs,
|
||||
ensureBrowserMcpOrThrow,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
resolveBrowserRuntimeEnv,
|
||||
resolveConfiguredBrowserProfileDir,
|
||||
syncBrowserMcpToConfigDir,
|
||||
} from '../../utils/browser';
|
||||
import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/unified-config-loader';
|
||||
import {
|
||||
getBrowserConfig,
|
||||
loadOrCreateUnifiedConfig,
|
||||
getThinkingConfig,
|
||||
} from '../../config/unified-config-loader';
|
||||
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
|
||||
import {
|
||||
isKiroAuthMethod,
|
||||
@@ -260,8 +264,8 @@ export async function execClaudeWithCLIProxy(
|
||||
// Setup first-class CCS WebSearch runtime
|
||||
ensureWebSearchMcpOrThrow();
|
||||
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
|
||||
const browserProfileDir = resolveConfiguredBrowserProfileDir(process.env.CCS_BROWSER_PROFILE_DIR);
|
||||
if (browserProfileDir) {
|
||||
const browserAttachConfig = getEffectiveClaudeBrowserAttachConfig(getBrowserConfig());
|
||||
if (browserAttachConfig.enabled) {
|
||||
ensureBrowserMcpOrThrow();
|
||||
}
|
||||
displayWebSearchStatus();
|
||||
@@ -1050,7 +1054,7 @@ export async function execClaudeWithCLIProxy(
|
||||
|
||||
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
||||
if (
|
||||
browserProfileDir &&
|
||||
browserAttachConfig.enabled &&
|
||||
inheritedClaudeConfigDir &&
|
||||
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
||||
) {
|
||||
@@ -1153,10 +1157,13 @@ export async function execClaudeWithCLIProxy(
|
||||
}
|
||||
|
||||
// 11. Build final environment with all proxy chains
|
||||
const browserRuntimeEnv = browserProfileDir
|
||||
const browserRuntimeEnv = browserAttachConfig.enabled
|
||||
? {
|
||||
...(await resolveBrowserRuntimeEnv({
|
||||
profileDir: browserProfileDir,
|
||||
profileDir: browserAttachConfig.userDataDir,
|
||||
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
|
||||
? String(browserAttachConfig.devtoolsPort)
|
||||
: undefined,
|
||||
})),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { getBrowserStatus, type BrowserStatusPayload } from '../utils/browser';
|
||||
import { color, dim, header, initUI, subheader } from '../utils/ui';
|
||||
|
||||
type HelpWriter = (line: string) => void;
|
||||
|
||||
function currentPlatform(): 'darwin' | 'linux' | 'win32' {
|
||||
if (process.platform === 'darwin') return 'darwin';
|
||||
if (process.platform === 'win32') return 'win32';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
function summarizeBrowserHealth(status: BrowserStatusPayload): {
|
||||
label: 'ready' | 'partial' | 'action required';
|
||||
exitCode: 0 | 1;
|
||||
} {
|
||||
const claudeNeedsAttention = status.claude.enabled && status.claude.state !== 'ready';
|
||||
if (claudeNeedsAttention) {
|
||||
return { label: 'action required', exitCode: 1 };
|
||||
}
|
||||
|
||||
if (status.codex.enabled && status.codex.state !== 'enabled') {
|
||||
return { label: 'partial', exitCode: 0 };
|
||||
}
|
||||
|
||||
return { label: 'ready', exitCode: 0 };
|
||||
}
|
||||
|
||||
function writeCommandTable(writeLine: HelpWriter): void {
|
||||
writeLine(subheader('Commands'));
|
||||
writeLine(
|
||||
` ${color('ccs browser status', 'command')} Show Claude attach and Codex browser readiness`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs browser doctor', 'command')} Explain what is missing and how to fix it`
|
||||
);
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
function writeIntro(writeLine: HelpWriter): void {
|
||||
writeLine(' Claude Browser Attach reuses a local Chrome session for Claude-target launches.');
|
||||
writeLine(
|
||||
' Codex Browser Tools inject managed Playwright MCP overrides into Codex-target launches.'
|
||||
);
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
function writeClaudeStatus(
|
||||
status: BrowserStatusPayload['claude'],
|
||||
writeLine: HelpWriter,
|
||||
includeLaunchGuidance: boolean
|
||||
): void {
|
||||
writeLine(subheader('Claude Browser Attach'));
|
||||
writeLine(` State: ${status.state}`);
|
||||
writeLine(` Enabled: ${status.enabled ? 'yes' : 'no'}`);
|
||||
writeLine(` Source: ${status.source}${status.overrideActive ? ' (env override active)' : ''}`);
|
||||
writeLine(` User data dir: ${status.effectiveUserDataDir}`);
|
||||
writeLine(` DevTools port: ${status.devtoolsPort}`);
|
||||
writeLine(` Managed MCP: ${status.managedMcpServerName}`);
|
||||
writeLine(` Managed path: ${status.managedMcpServerPath}`);
|
||||
if (status.runtimeEnv?.CCS_BROWSER_DEVTOOLS_HTTP_URL) {
|
||||
writeLine(` DevTools endpoint: ${status.runtimeEnv.CCS_BROWSER_DEVTOOLS_HTTP_URL}`);
|
||||
}
|
||||
writeLine(` Detail: ${status.detail}`);
|
||||
writeLine(` Next step: ${status.nextStep}`);
|
||||
if (includeLaunchGuidance && status.enabled && status.state !== 'ready') {
|
||||
writeLine(
|
||||
` Launch command (${currentPlatform()}): ${status.launchCommands[currentPlatform()]}`
|
||||
);
|
||||
}
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
function writeCodexStatus(status: BrowserStatusPayload['codex'], writeLine: HelpWriter): void {
|
||||
writeLine(subheader('Codex Browser Tools'));
|
||||
writeLine(` State: ${status.state}`);
|
||||
writeLine(` Enabled: ${status.enabled ? 'yes' : 'no'}`);
|
||||
writeLine(` Managed server: ${status.serverName}`);
|
||||
writeLine(` Supports overrides: ${status.supportsConfigOverrides ? 'yes' : 'no'}`);
|
||||
writeLine(` Codex binary: ${status.binaryPath || 'not detected'}`);
|
||||
if (status.version) {
|
||||
writeLine(` Codex version: ${status.version}`);
|
||||
}
|
||||
writeLine(` Detail: ${status.detail}`);
|
||||
writeLine(` Next step: ${status.nextStep}`);
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
export async function showBrowserHelp(writeLine: HelpWriter = console.log): Promise<void> {
|
||||
await initUI();
|
||||
writeLine(header('CCS Browser Help'));
|
||||
writeLine('');
|
||||
writeIntro(writeLine);
|
||||
writeLine(subheader('Usage'));
|
||||
writeLine(` ${color('ccs browser <status|doctor>', 'command')}`);
|
||||
writeLine(` ${color('ccs help browser', 'command')}`);
|
||||
writeLine('');
|
||||
writeCommandTable(writeLine);
|
||||
writeLine(subheader('What Each Lane Does'));
|
||||
writeLine(' Claude Browser Attach expects a Chrome user-data dir and remote debugging port.');
|
||||
writeLine(' Codex Browser Tools depend on a Codex build that supports --config overrides.');
|
||||
writeLine('');
|
||||
writeLine(subheader('Examples'));
|
||||
writeLine(` ${color('ccs browser status', 'command')} ${dim('# Quick readiness snapshot')}`);
|
||||
writeLine(
|
||||
` ${color('ccs browser doctor', 'command')} ${dim('# Detailed troubleshooting output')}`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs config', 'command')} ${dim('# Open Settings > Browser in the dashboard')}`
|
||||
);
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
export async function handleBrowserCommand(
|
||||
args: string[],
|
||||
writeLine: HelpWriter = console.log
|
||||
): Promise<void> {
|
||||
const subcommand = args[0];
|
||||
if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
||||
await showBrowserHelp(writeLine);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand !== 'status' && subcommand !== 'doctor') {
|
||||
await initUI();
|
||||
writeLine(color(`Unknown browser subcommand: ${subcommand}`, 'error'));
|
||||
writeLine('');
|
||||
writeLine(` ${dim('Supported subcommands: status, doctor')}`);
|
||||
writeLine('');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await initUI();
|
||||
const status = await getBrowserStatus();
|
||||
|
||||
writeLine(header(`ccs browser ${subcommand}`));
|
||||
writeLine('');
|
||||
writeIntro(writeLine);
|
||||
|
||||
if (subcommand === 'doctor') {
|
||||
const summary = summarizeBrowserHealth(status);
|
||||
writeLine(subheader('Overall'));
|
||||
writeLine(` Claude Browser Attach: ${status.claude.title}`);
|
||||
writeLine(` Codex Browser Tools: ${status.codex.title}`);
|
||||
writeLine(` Result: ${summary.label}`);
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
writeClaudeStatus(status.claude, writeLine, subcommand === 'doctor');
|
||||
writeCodexStatus(status.codex, writeLine);
|
||||
|
||||
if (subcommand === 'doctor') {
|
||||
process.exitCode = summarizeBrowserHealth(status).exitCode;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { COPILOT_SUBCOMMANDS } from '../copilot/constants';
|
||||
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
|
||||
|
||||
export type HelpTopicName = 'profiles' | 'providers' | 'kiro' | 'completion' | 'targets';
|
||||
export type HelpTopicName =
|
||||
| 'profiles'
|
||||
| 'providers'
|
||||
| 'kiro'
|
||||
| 'browser'
|
||||
| 'completion'
|
||||
| 'targets';
|
||||
|
||||
export interface HelpTopicEntry {
|
||||
name: HelpTopicName;
|
||||
@@ -25,6 +31,7 @@ export const ROOT_HELP_TOPICS: readonly HelpTopicEntry[] = [
|
||||
{ name: 'profiles', summary: 'Account profiles, API profiles, and CLIProxy variants' },
|
||||
{ name: 'providers', summary: 'Built-in OAuth providers and runtime shortcuts' },
|
||||
{ name: 'kiro', summary: 'Kiro auth methods, IDC flags, and callback guidance' },
|
||||
{ name: 'browser', summary: 'Claude Browser Attach and Codex Browser Tools guidance' },
|
||||
{ name: 'completion', summary: 'Shell completion install, refresh, and testing' },
|
||||
{ name: 'targets', summary: 'Claude, Droid, and Codex target routing' },
|
||||
] as const;
|
||||
@@ -114,6 +121,12 @@ export const ROOT_COMMAND_CATALOG: readonly RootCommandEntry[] = [
|
||||
group: 'runtime',
|
||||
visibility: 'public',
|
||||
},
|
||||
{
|
||||
name: 'browser',
|
||||
summary: 'Inspect Claude Browser Attach and Codex Browser Tools readiness',
|
||||
group: 'runtime',
|
||||
visibility: 'public',
|
||||
},
|
||||
{
|
||||
name: 'copilot',
|
||||
summary: 'Run or manage the GitHub Copilot bridge',
|
||||
@@ -307,6 +320,7 @@ export const COMMAND_FLAG_SUGGESTIONS: Readonly<Record<string, readonly string[]
|
||||
config: ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'],
|
||||
cursor: ['--help', '-h'],
|
||||
doctor: ['--fix', '-f', '--help', '-h'],
|
||||
browser: ['status', 'doctor', '--help', '-h'],
|
||||
docker: ['--help', '-h', '--host'],
|
||||
env: ['--format', '--shell', '--ide', '--help', '-h'],
|
||||
migrate: MIGRATE_FLAGS,
|
||||
|
||||
@@ -249,7 +249,7 @@ export async function handleHelpCommand(writeLine: HelpWriter = console.log): Pr
|
||||
|
||||
writeLine(header(`CCS CLI v${packageJson.version}`));
|
||||
writeLine('');
|
||||
writeLine(' Claude profile switching, provider routing, and compatible runtime bridges.');
|
||||
writeLine(' Claude profile switching, provider routing, runtime bridges, and browser tooling.');
|
||||
writeLine('');
|
||||
|
||||
writeLine(subheader('Usage'));
|
||||
@@ -279,6 +279,7 @@ export async function handleHelpCommand(writeLine: HelpWriter = console.log): Pr
|
||||
[
|
||||
{ name: 'ccs help profiles', summary: getTopicSummary('profiles') },
|
||||
{ name: 'ccs help providers', summary: getTopicSummary('providers') },
|
||||
{ name: 'ccs help browser', summary: getTopicSummary('browser') },
|
||||
{ name: 'ccs help completion', summary: getTopicSummary('completion') },
|
||||
{ name: 'ccs help targets', summary: getTopicSummary('targets') },
|
||||
{ name: 'ccs api --help', summary: 'Deep help for API profile lifecycle commands' },
|
||||
@@ -328,6 +329,10 @@ export async function handleHelpRoute(
|
||||
await showTargetsHelp(writeLine);
|
||||
return;
|
||||
}
|
||||
if (topic === 'browser') {
|
||||
await (await import('./browser-command')).showBrowserHelp(writeLine);
|
||||
return;
|
||||
}
|
||||
if (topic === 'completion') {
|
||||
const { showShellCompletionHelp } = await import('./shell-completion-command');
|
||||
showShellCompletionHelp(writeLine);
|
||||
@@ -342,6 +347,7 @@ export async function handleHelpRoute(
|
||||
await new AuthCommands().showHelp();
|
||||
},
|
||||
cleanup: async () => (await import('./cleanup-command')).handleCleanupCommand(['--help']),
|
||||
browser: async () => (await import('./browser-command')).showBrowserHelp(writeLine),
|
||||
cliproxy: async () => (await import('./cliproxy/help-subcommand')).showHelp(),
|
||||
copilot: async () =>
|
||||
process.exit(await (await import('./copilot-command')).handleCopilotCommand(['--help'])),
|
||||
|
||||
@@ -105,6 +105,13 @@ export const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
|
||||
await handleSyncCommand();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'browser',
|
||||
handle: async (args) => {
|
||||
const { handleBrowserCommand } = await import('./browser-command');
|
||||
await handleBrowserCommand(args);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cleanup',
|
||||
aliases: ['--cleanup'],
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DEFAULT_THINKING_CONFIG,
|
||||
DEFAULT_OFFICIAL_CHANNELS_CONFIG,
|
||||
DEFAULT_DASHBOARD_AUTH_CONFIG,
|
||||
DEFAULT_BROWSER_CONFIG,
|
||||
DEFAULT_IMAGE_ANALYSIS_CONFIG,
|
||||
DEFAULT_LOGGING_CONFIG,
|
||||
} from './unified-config-types';
|
||||
@@ -34,6 +35,7 @@ import type {
|
||||
OfficialChannelsConfig,
|
||||
OfficialChannelId,
|
||||
DashboardAuthConfig,
|
||||
BrowserConfig,
|
||||
ImageAnalysisConfig,
|
||||
LoggingConfig,
|
||||
CursorConfig,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
normalizeOfficialChannelIds,
|
||||
resolveLegacyDiscordSelection,
|
||||
} from '../channels/official-channels-runtime';
|
||||
import { getRecommendedBrowserUserDataDir } from '../utils/browser/browser-settings';
|
||||
import { canonicalizeImageAnalysisConfig } from '../utils/hooks/image-analysis-backend-resolver';
|
||||
import { normalizeSearxngBaseUrl } from '../utils/websearch/types';
|
||||
|
||||
@@ -54,6 +57,32 @@ const CONFIG_JSON = 'config.json';
|
||||
const CONFIG_LOCK = 'config.yaml.lock';
|
||||
const LOCK_STALE_MS = 5000; // Lock is stale after 5 seconds
|
||||
|
||||
function normalizeBrowserDevtoolsPort(value: number | undefined): number {
|
||||
if (!Number.isFinite(value)) {
|
||||
return DEFAULT_BROWSER_CONFIG.claude.devtools_port;
|
||||
}
|
||||
|
||||
const port = Math.floor(value as number);
|
||||
if (port < 1 || port > 65535) {
|
||||
return DEFAULT_BROWSER_CONFIG.claude.devtools_port;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
function canonicalizeBrowserConfig(config?: BrowserConfig): BrowserConfig {
|
||||
return {
|
||||
claude: {
|
||||
enabled: config?.claude?.enabled ?? DEFAULT_BROWSER_CONFIG.claude.enabled,
|
||||
user_data_dir: config?.claude?.user_data_dir?.trim() || getRecommendedBrowserUserDataDir(),
|
||||
devtools_port: normalizeBrowserDevtoolsPort(config?.claude?.devtools_port),
|
||||
},
|
||||
codex: {
|
||||
enabled: config?.codex?.enabled ?? DEFAULT_BROWSER_CONFIG.codex.enabled,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to unified config.yaml
|
||||
*/
|
||||
@@ -592,6 +621,7 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
||||
partial.dashboard_auth?.session_timeout_hours ??
|
||||
DEFAULT_DASHBOARD_AUTH_CONFIG.session_timeout_hours,
|
||||
},
|
||||
browser: canonicalizeBrowserConfig(partial.browser),
|
||||
// Image analysis config - enabled by default for CLIProxy providers
|
||||
image_analysis: canonicalizeImageAnalysisConfig({
|
||||
enabled: partial.image_analysis?.enabled ?? DEFAULT_IMAGE_ANALYSIS_CONFIG.enabled,
|
||||
@@ -916,6 +946,23 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Browser automation section
|
||||
if (config.browser) {
|
||||
lines.push('# ----------------------------------------------------------------------------');
|
||||
lines.push('# Browser Automation: Claude browser attach and Codex browser tooling');
|
||||
lines.push('# Claude attach reuses a running Chrome/Chromium session with remote debugging.');
|
||||
lines.push('# Codex tooling controls whether CCS injects Playwright MCP overrides.');
|
||||
lines.push('#');
|
||||
lines.push('# claude.user_data_dir should point at the Chrome user-data directory for the');
|
||||
lines.push('# dedicated attach session. claude.devtools_port is the expected debugging port.');
|
||||
lines.push('# Configure via: Settings > Browser or `ccs browser ...`.');
|
||||
lines.push('# ----------------------------------------------------------------------------');
|
||||
lines.push(
|
||||
yaml.dump({ browser: config.browser }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim()
|
||||
);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Image analysis section
|
||||
if (config.image_analysis) {
|
||||
lines.push('# ----------------------------------------------------------------------------');
|
||||
@@ -1334,6 +1381,15 @@ export function getDashboardAuthConfig(): DashboardAuthConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser automation configuration.
|
||||
* Returns canonicalized defaults if not configured.
|
||||
*/
|
||||
export function getBrowserConfig(): BrowserConfig {
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
return canonicalizeBrowserConfig(config.browser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image_analysis configuration.
|
||||
* Returns defaults if not configured.
|
||||
|
||||
@@ -812,6 +812,40 @@ export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = {
|
||||
session_timeout_hours: 24,
|
||||
};
|
||||
|
||||
/**
|
||||
* Browser automation configuration.
|
||||
* Controls Claude browser attach and Codex browser tooling.
|
||||
*/
|
||||
export interface BrowserClaudeConfig {
|
||||
/** Enable Claude browser attach (default: false) */
|
||||
enabled: boolean;
|
||||
/** Chrome user-data directory used for attach mode */
|
||||
user_data_dir: string;
|
||||
/** DevTools port used for attach mode (default: 9222) */
|
||||
devtools_port: number;
|
||||
}
|
||||
|
||||
export interface BrowserCodexConfig {
|
||||
/** Enable Codex browser tooling injection (default: true) */
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface BrowserConfig {
|
||||
claude: BrowserClaudeConfig;
|
||||
codex: BrowserCodexConfig;
|
||||
}
|
||||
|
||||
export const DEFAULT_BROWSER_CONFIG: BrowserConfig = {
|
||||
claude: {
|
||||
enabled: false,
|
||||
user_data_dir: '',
|
||||
devtools_port: 9222,
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Image analysis configuration.
|
||||
* Routes image/PDF files through CLIProxy for vision analysis.
|
||||
@@ -895,6 +929,8 @@ export interface UnifiedConfig {
|
||||
channels?: OfficialChannelsConfig;
|
||||
/** Dashboard authentication configuration (optional) */
|
||||
dashboard_auth?: DashboardAuthConfig;
|
||||
/** Browser automation configuration */
|
||||
browser?: BrowserConfig;
|
||||
/** Image analysis configuration (vision via CLIProxy) */
|
||||
image_analysis?: ImageAnalysisConfig;
|
||||
}
|
||||
@@ -1041,6 +1077,10 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
||||
thinking: { ...DEFAULT_THINKING_CONFIG },
|
||||
channels: { ...DEFAULT_OFFICIAL_CHANNELS_CONFIG },
|
||||
dashboard_auth: { ...DEFAULT_DASHBOARD_AUTH_CONFIG },
|
||||
browser: {
|
||||
claude: { ...DEFAULT_BROWSER_CONFIG.claude },
|
||||
codex: { ...DEFAULT_BROWSER_CONFIG.codex },
|
||||
},
|
||||
image_analysis: { ...DEFAULT_IMAGE_ANALYSIS_CONFIG },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as path from 'path';
|
||||
import type { BrowserConfig } from '../../config/unified-config-types';
|
||||
import { getCcsDir } from '../config-manager';
|
||||
import { expandPath } from '../helpers';
|
||||
|
||||
export type BrowserOverrideSource = 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
|
||||
|
||||
export interface EffectiveClaudeBrowserAttachConfig {
|
||||
enabled: boolean;
|
||||
source: 'config' | BrowserOverrideSource;
|
||||
overrideActive: boolean;
|
||||
userDataDir: string;
|
||||
devtoolsPort: number;
|
||||
hasExplicitDevtoolsPort: boolean;
|
||||
}
|
||||
|
||||
export function getRecommendedBrowserUserDataDir(): string {
|
||||
return path.join(getCcsDir(), 'browser', 'chrome-user-data');
|
||||
}
|
||||
|
||||
export function resolveBrowserUserDataDir(value?: string): string | undefined {
|
||||
return value?.trim() ? expandPath(value) : undefined;
|
||||
}
|
||||
|
||||
export function getBrowserAttachOverride(env: NodeJS.ProcessEnv = process.env): {
|
||||
userDataDir?: string;
|
||||
devtoolsPort?: number;
|
||||
source?: BrowserOverrideSource;
|
||||
} {
|
||||
const explicitUserDataDir = resolveBrowserUserDataDir(env.CCS_BROWSER_USER_DATA_DIR);
|
||||
if (explicitUserDataDir) {
|
||||
return {
|
||||
userDataDir: explicitUserDataDir,
|
||||
devtoolsPort: parseDevtoolsPort(env.CCS_BROWSER_DEVTOOLS_PORT),
|
||||
source: 'CCS_BROWSER_USER_DATA_DIR',
|
||||
};
|
||||
}
|
||||
|
||||
const legacyProfileDir = resolveBrowserUserDataDir(env.CCS_BROWSER_PROFILE_DIR);
|
||||
if (legacyProfileDir) {
|
||||
return {
|
||||
userDataDir: legacyProfileDir,
|
||||
devtoolsPort: parseDevtoolsPort(env.CCS_BROWSER_DEVTOOLS_PORT),
|
||||
source: 'CCS_BROWSER_PROFILE_DIR',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getEffectiveClaudeBrowserAttachConfig(
|
||||
config: BrowserConfig,
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): EffectiveClaudeBrowserAttachConfig {
|
||||
const override = getBrowserAttachOverride(env);
|
||||
const configUserDataDir =
|
||||
resolveBrowserUserDataDir(config.claude.user_data_dir) ?? getRecommendedBrowserUserDataDir();
|
||||
const configPort = normalizeDevtoolsPort(config.claude.devtools_port);
|
||||
|
||||
if (override.userDataDir) {
|
||||
return {
|
||||
enabled: true,
|
||||
source: override.source as BrowserOverrideSource,
|
||||
overrideActive: true,
|
||||
userDataDir: override.userDataDir,
|
||||
devtoolsPort: override.devtoolsPort ?? configPort,
|
||||
hasExplicitDevtoolsPort: override.devtoolsPort !== undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: config.claude.enabled,
|
||||
source: 'config',
|
||||
overrideActive: false,
|
||||
userDataDir: configUserDataDir,
|
||||
devtoolsPort: configPort,
|
||||
hasExplicitDevtoolsPort: true,
|
||||
};
|
||||
}
|
||||
|
||||
function parseDevtoolsPort(value?: string): number | undefined {
|
||||
if (!value?.trim() || !/^\d+$/.test(value.trim())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return normalizeDevtoolsPort(Number.parseInt(value.trim(), 10));
|
||||
}
|
||||
|
||||
function normalizeDevtoolsPort(value: number | undefined): number {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 9222;
|
||||
}
|
||||
|
||||
const port = Math.floor(value as number);
|
||||
if (port < 1 || port > 65535) {
|
||||
return 9222;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { getBrowserConfig } from '../../config/unified-config-loader';
|
||||
import { getCodexBinaryInfo } from '../../targets/codex-detector';
|
||||
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
||||
import { getBrowserMcpServerName, getBrowserMcpServerPath } from './mcp-installer';
|
||||
import {
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
getRecommendedBrowserUserDataDir,
|
||||
} from './browser-settings';
|
||||
|
||||
export interface BrowserLaunchCommands {
|
||||
darwin: string;
|
||||
linux: string;
|
||||
win32: string;
|
||||
}
|
||||
|
||||
export interface ClaudeBrowserStatus {
|
||||
enabled: boolean;
|
||||
source: 'config' | 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
|
||||
overrideActive: boolean;
|
||||
state: 'disabled' | 'path_missing' | 'browser_not_running' | 'endpoint_unreachable' | 'ready';
|
||||
title: string;
|
||||
detail: string;
|
||||
nextStep: string;
|
||||
effectiveUserDataDir: string;
|
||||
recommendedUserDataDir: string;
|
||||
devtoolsPort: number;
|
||||
managedMcpServerName: string;
|
||||
managedMcpServerPath: string;
|
||||
launchCommands: BrowserLaunchCommands;
|
||||
runtimeEnv?: BrowserRuntimeEnv;
|
||||
}
|
||||
|
||||
export interface CodexBrowserStatus {
|
||||
enabled: boolean;
|
||||
state: 'disabled' | 'enabled' | 'unsupported_build';
|
||||
title: string;
|
||||
detail: string;
|
||||
nextStep: string;
|
||||
serverName: string;
|
||||
supportsConfigOverrides: boolean;
|
||||
binaryPath: string | null;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface BrowserStatusPayload {
|
||||
claude: ClaudeBrowserStatus;
|
||||
codex: CodexBrowserStatus;
|
||||
}
|
||||
|
||||
export async function getBrowserStatus(): Promise<BrowserStatusPayload> {
|
||||
const browserConfig = getBrowserConfig();
|
||||
return {
|
||||
claude: await buildClaudeBrowserStatus(browserConfig),
|
||||
codex: buildCodexBrowserStatus(browserConfig),
|
||||
};
|
||||
}
|
||||
|
||||
async function buildClaudeBrowserStatus(
|
||||
browserConfig = getBrowserConfig()
|
||||
): Promise<ClaudeBrowserStatus> {
|
||||
const effective = getEffectiveClaudeBrowserAttachConfig(browserConfig);
|
||||
const launchCommands = buildLaunchCommands(effective.userDataDir, effective.devtoolsPort);
|
||||
const base: Omit<ClaudeBrowserStatus, 'state' | 'title' | 'detail' | 'nextStep'> = {
|
||||
enabled: effective.enabled,
|
||||
source: effective.source,
|
||||
overrideActive: effective.overrideActive,
|
||||
effectiveUserDataDir: effective.userDataDir,
|
||||
recommendedUserDataDir: getRecommendedBrowserUserDataDir(),
|
||||
devtoolsPort: effective.devtoolsPort,
|
||||
managedMcpServerName: getBrowserMcpServerName(),
|
||||
managedMcpServerPath: getBrowserMcpServerPath(),
|
||||
launchCommands,
|
||||
};
|
||||
|
||||
if (!effective.enabled) {
|
||||
return {
|
||||
...base,
|
||||
state: 'disabled',
|
||||
title: 'Claude Browser Attach is disabled.',
|
||||
detail:
|
||||
'CCS will not provision the managed browser MCP runtime for Claude launches until this lane is enabled.',
|
||||
nextStep:
|
||||
'Enable Claude Browser Attach in Settings > Browser or in ~/.ccs/config.yaml, then rerun `ccs browser doctor`.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const runtimeEnv = await resolveBrowserRuntimeEnv({
|
||||
profileDir: effective.userDataDir,
|
||||
devtoolsPort: effective.hasExplicitDevtoolsPort ? String(effective.devtoolsPort) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
...base,
|
||||
state: 'ready',
|
||||
title: 'Claude Browser Attach is ready.',
|
||||
detail:
|
||||
'CCS can reach the configured Chrome DevTools endpoint for the current attach session.',
|
||||
nextStep: 'Launch a Claude-target CCS session to use the managed browser MCP runtime.',
|
||||
runtimeEnv,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = (error as Error).message;
|
||||
if (message.includes('Chrome profile directory is invalid')) {
|
||||
return {
|
||||
...base,
|
||||
state: 'path_missing',
|
||||
title: 'Claude Browser Attach path is missing.',
|
||||
detail: message,
|
||||
nextStep: `Create or choose a Chrome user-data directory, then launch Chrome with attach mode enabled. Example: ${launchCommands[platformKey()]}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (message.includes('Chrome reuse metadata')) {
|
||||
return {
|
||||
...base,
|
||||
state: 'browser_not_running',
|
||||
title: 'Claude Browser Attach could not find a running browser session.',
|
||||
detail: message,
|
||||
nextStep: `Start Chrome with remote debugging and the configured user-data dir. Example: ${launchCommands[platformKey()]}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
state: 'endpoint_unreachable',
|
||||
title: 'Claude Browser Attach could not reach the DevTools endpoint.',
|
||||
detail: message,
|
||||
nextStep: `Restart the attach browser session or confirm the configured port. Example: ${launchCommands[platformKey()]}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildCodexBrowserStatus(browserConfig = getBrowserConfig()): CodexBrowserStatus {
|
||||
if (!browserConfig.codex.enabled) {
|
||||
return {
|
||||
enabled: false,
|
||||
state: 'disabled',
|
||||
title: 'Codex Browser Tools are disabled.',
|
||||
detail: 'CCS will not inject Playwright MCP browser tooling into Codex-target launches.',
|
||||
nextStep:
|
||||
'Enable Codex Browser Tools in Settings > Browser to restore the managed Codex browser path.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: false,
|
||||
binaryPath: null,
|
||||
};
|
||||
}
|
||||
|
||||
const binaryInfo = getCodexBinaryInfo({ includeVersion: true, includeFeatures: true });
|
||||
const supportsConfigOverrides = Boolean(binaryInfo?.features?.includes('config-overrides'));
|
||||
if (!binaryInfo || !supportsConfigOverrides) {
|
||||
return {
|
||||
enabled: true,
|
||||
state: 'unsupported_build',
|
||||
title: 'Codex Browser Tools need a Codex build with --config override support.',
|
||||
detail: binaryInfo
|
||||
? `Detected Codex at ${binaryInfo.path}, but it does not advertise --config overrides.`
|
||||
: 'No Codex binary was detected, so CCS cannot confirm managed browser override support.',
|
||||
nextStep: 'Install or upgrade Codex, then rerun browser status/doctor.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides,
|
||||
binaryPath: binaryInfo?.path ?? null,
|
||||
version: binaryInfo?.version,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
state: 'enabled',
|
||||
title: 'Codex Browser Tools are enabled.',
|
||||
detail: 'CCS can inject the managed Playwright MCP overrides into Codex-target launches.',
|
||||
nextStep: 'Use a Codex-target CCS launch to access browser tools.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides,
|
||||
binaryPath: binaryInfo.path,
|
||||
version: binaryInfo.version,
|
||||
};
|
||||
}
|
||||
|
||||
function buildLaunchCommands(userDataDir: string, devtoolsPort: number): BrowserLaunchCommands {
|
||||
const quotedPath = JSON.stringify(userDataDir);
|
||||
return {
|
||||
darwin: `open -na "Google Chrome" --args --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`,
|
||||
linux: `google-chrome --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`,
|
||||
win32: `chrome.exe --remote-debugging-port=${devtoolsPort} --user-data-dir=${quotedPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
function platformKey(): keyof BrowserLaunchCommands {
|
||||
if (process.platform === 'darwin') return 'darwin';
|
||||
if (process.platform === 'win32') return 'win32';
|
||||
return 'linux';
|
||||
}
|
||||
@@ -17,9 +17,22 @@ export {
|
||||
|
||||
export { appendBrowserToolArgs } from './claude-tool-args';
|
||||
|
||||
export {
|
||||
getRecommendedBrowserUserDataDir,
|
||||
getBrowserAttachOverride,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
} from './browser-settings';
|
||||
|
||||
export {
|
||||
resolveBrowserRuntimeEnv,
|
||||
resolveDefaultChromeUserDataDir,
|
||||
resolveConfiguredBrowserProfileDir,
|
||||
} from './chrome-reuse';
|
||||
export type { BrowserReuseOptions, BrowserRuntimeEnv } from './chrome-reuse';
|
||||
|
||||
export { getBrowserStatus } from './browser-status';
|
||||
export type {
|
||||
BrowserStatusPayload,
|
||||
ClaudeBrowserStatus,
|
||||
CodexBrowserStatus,
|
||||
} from './browser-status';
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { getBrowserConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import { getBrowserStatus } from '../../utils/browser';
|
||||
import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware';
|
||||
|
||||
const router = Router();
|
||||
const BROWSER_LOCAL_ACCESS_ERROR =
|
||||
'Browser endpoints require localhost access when dashboard auth is disabled.';
|
||||
|
||||
interface BrowserRouteBody {
|
||||
claude?: {
|
||||
enabled?: boolean;
|
||||
userDataDir?: string;
|
||||
devtoolsPort?: number;
|
||||
};
|
||||
codex?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
function isValidDevtoolsPort(value: number): boolean {
|
||||
return Number.isInteger(value) && value >= 1 && value <= 65535;
|
||||
}
|
||||
|
||||
router.use((req: Request, res: Response, next) => {
|
||||
if (requireLocalAccessWhenAuthDisabled(req, res, BROWSER_LOCAL_ACCESS_ERROR)) {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const config = getBrowserConfig();
|
||||
const status = await getBrowserStatus();
|
||||
res.json({
|
||||
config: toBrowserRouteConfig(config),
|
||||
status,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
res.json(await getBrowserStatus());
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/', async (req: Request, res: Response): Promise<void> => {
|
||||
if (
|
||||
req.body === null ||
|
||||
req.body === undefined ||
|
||||
typeof req.body !== 'object' ||
|
||||
Array.isArray(req.body)
|
||||
) {
|
||||
res.status(400).json({ error: 'Invalid request body. Must be an object.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { claude, codex } = req.body as BrowserRouteBody;
|
||||
if (claude && (typeof claude !== 'object' || Array.isArray(claude))) {
|
||||
res.status(400).json({ error: 'Invalid value for claude. Must be an object.' });
|
||||
return;
|
||||
}
|
||||
if (codex && (typeof codex !== 'object' || Array.isArray(codex))) {
|
||||
res.status(400).json({ error: 'Invalid value for codex. Must be an object.' });
|
||||
return;
|
||||
}
|
||||
if (claude?.enabled !== undefined && typeof claude.enabled !== 'boolean') {
|
||||
res.status(400).json({ error: 'Invalid value for claude.enabled. Must be a boolean.' });
|
||||
return;
|
||||
}
|
||||
if (claude?.userDataDir !== undefined && typeof claude.userDataDir !== 'string') {
|
||||
res.status(400).json({ error: 'Invalid value for claude.userDataDir. Must be a string.' });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
claude?.devtoolsPort !== undefined &&
|
||||
(typeof claude.devtoolsPort !== 'number' || !isValidDevtoolsPort(claude.devtoolsPort))
|
||||
) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid value for claude.devtoolsPort. Must be an integer between 1 and 65535.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (codex?.enabled !== undefined && typeof codex.enabled !== 'boolean') {
|
||||
res.status(400).json({ error: 'Invalid value for codex.enabled. Must be a boolean.' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const current = getBrowserConfig();
|
||||
mutateUnifiedConfig((config) => {
|
||||
config.browser = {
|
||||
claude: {
|
||||
enabled: claude?.enabled ?? current.claude.enabled,
|
||||
user_data_dir: claude?.userDataDir?.trim() || current.claude.user_data_dir,
|
||||
devtools_port: claude?.devtoolsPort ?? current.claude.devtools_port,
|
||||
},
|
||||
codex: {
|
||||
enabled: codex?.enabled ?? current.codex.enabled,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const config = getBrowserConfig();
|
||||
const status = await getBrowserStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
browser: {
|
||||
config: toBrowserRouteConfig(config),
|
||||
status,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
function toBrowserRouteConfig(config: ReturnType<typeof getBrowserConfig>) {
|
||||
return {
|
||||
claude: {
|
||||
enabled: config.claude.enabled,
|
||||
userDataDir: config.claude.user_data_dir,
|
||||
devtoolsPort: config.claude.devtools_port,
|
||||
},
|
||||
codex: {
|
||||
enabled: config.codex.enabled,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -19,6 +19,7 @@ import settingsRoutes from './settings-routes';
|
||||
import channelsRoutes from './channels-routes';
|
||||
import websearchRoutes from './websearch-routes';
|
||||
import imageAnalysisRoutes from './image-analysis-routes';
|
||||
import browserRoutes from './browser-routes';
|
||||
import cliproxyAuthRoutes from './cliproxy-auth-routes';
|
||||
import cliproxyStatsRoutes from './cliproxy-stats-routes';
|
||||
import cliproxyRoutingRoutes from './cliproxy-routing-routes';
|
||||
@@ -97,6 +98,7 @@ apiRoutes.use('/cliproxy/openai-compat', providerRoutes);
|
||||
|
||||
// ==================== WebSearch ====================
|
||||
apiRoutes.use('/websearch', websearchRoutes);
|
||||
apiRoutes.use('/browser', browserRoutes);
|
||||
apiRoutes.use('/image-analysis', imageAnalysisRoutes);
|
||||
|
||||
// ==================== Copilot ====================
|
||||
|
||||
@@ -114,6 +114,7 @@ const COMPATIBLE_CLI_DOCS_REGISTRY: Record<string, CompatibleCliDocsRegistryEntr
|
||||
'CLI --profile selects a named [profiles.<name>] overlay on top of base config',
|
||||
'CCS-backed Codex launches may apply transient -c overrides and CCS_CODEX_API_KEY',
|
||||
'Official docs treat model_providers, mcp_servers, features, and project trust as schema-backed config surfaces',
|
||||
'CCS-managed browser tooling for Codex should be configured from Settings > Browser, not by editing the ccs_browser MCP entry directly',
|
||||
],
|
||||
links: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user