feat(cli): add browser automation commands

This commit is contained in:
Tam Nhu Tran
2026-04-16 18:49:24 -04:00
parent b6aef885ad
commit 4e30c9b080
42 changed files with 3028 additions and 32 deletions
+136
View File
@@ -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;
+2
View File
@@ -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: [
{