feat(browser): add remote-capable profiles

Co-authored-by: James Groat <james@groat.com>
This commit is contained in:
Peter Steinberger
2026-01-04 03:32:40 +00:00
parent 0e75aa2716
commit 12ba32c724
30 changed files with 2102 additions and 298 deletions

View File

@@ -1,24 +1,36 @@
import type { BrowserConfig } from "../config/config.js";
import type { BrowserConfig, BrowserProfileConfig } from "../config/config.js";
import {
DEFAULT_CLAWD_BROWSER_COLOR,
DEFAULT_CLAWD_BROWSER_CONTROL_URL,
DEFAULT_CLAWD_BROWSER_ENABLED,
DEFAULT_CLAWD_BROWSER_PROFILE_NAME,
} from "./constants.js";
import { CDP_PORT_RANGE_START } from "./profiles.js";
export type ResolvedBrowserConfig = {
enabled: boolean;
controlUrl: string;
controlHost: string;
controlPort: number;
cdpUrl: string;
cdpProtocol: "http" | "https";
cdpHost: string;
cdpPort: number;
cdpIsLoopback: boolean;
color: string;
executablePath?: string;
headless: boolean;
noSandbox: boolean;
attachOnly: boolean;
defaultProfile: string;
profiles: Record<string, BrowserProfileConfig>;
};
export type ResolvedBrowserProfile = {
name: string;
cdpPort: number;
cdpUrl: string;
cdpHost: string;
cdpIsLoopback: boolean;
color: string;
};
function isLoopbackHost(host: string) {
@@ -42,7 +54,7 @@ function normalizeHexColor(raw: string | undefined) {
return normalized.toUpperCase();
}
function parseHttpUrl(raw: string, label: string) {
export function parseHttpUrl(raw: string, label: string) {
const trimmed = raw.trim();
const parsed = new URL(trimmed);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
@@ -69,6 +81,24 @@ function parseHttpUrl(raw: string, label: string) {
};
}
/**
* Ensure the default "clawd" profile exists in the profiles map.
* Auto-creates it with the legacy CDP port (from browser.cdpUrl) or first port if missing.
*/
function ensureDefaultProfile(
profiles: Record<string, BrowserProfileConfig> | undefined,
defaultColor: string,
legacyCdpPort?: number,
): Record<string, BrowserProfileConfig> {
const result = { ...profiles };
if (!result[DEFAULT_CLAWD_BROWSER_PROFILE_NAME]) {
result[DEFAULT_CLAWD_BROWSER_PROFILE_NAME] = {
cdpPort: legacyCdpPort ?? CDP_PORT_RANGE_START,
color: defaultColor,
};
}
return result;
}
export function resolveBrowserConfig(
cfg: BrowserConfig | undefined,
): ResolvedBrowserConfig {
@@ -78,6 +108,7 @@ export function resolveBrowserConfig(
"browser.controlUrl",
);
const controlPort = controlInfo.port;
const defaultColor = normalizeHexColor(cfg?.color);
const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
let cdpInfo:
@@ -105,26 +136,77 @@ export function resolveBrowserConfig(
};
}
const cdpPort = cdpInfo.port;
const headless = cfg?.headless === true;
const noSandbox = cfg?.noSandbox === true;
const attachOnly = cfg?.attachOnly === true;
const executablePath = cfg?.executablePath?.trim() || undefined;
const defaultProfile =
cfg?.defaultProfile ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME;
// Use legacy cdpUrl port for backward compatibility when no profiles configured
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
const profiles = ensureDefaultProfile(
cfg?.profiles,
defaultColor,
legacyCdpPort,
);
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
return {
enabled,
controlUrl: controlInfo.normalized,
controlHost: controlInfo.parsed.hostname,
controlPort,
cdpUrl: cdpInfo.normalized,
cdpProtocol,
cdpHost: cdpInfo.parsed.hostname,
cdpPort,
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
color: normalizeHexColor(cfg?.color),
color: defaultColor,
executablePath,
headless,
noSandbox,
attachOnly,
defaultProfile,
profiles,
};
}
/**
* Resolve a profile by name from the config.
* Returns null if the profile doesn't exist.
*/
export function resolveProfile(
resolved: ResolvedBrowserConfig,
profileName: string,
): ResolvedBrowserProfile | null {
const profile = resolved.profiles[profileName];
if (!profile) return null;
const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
let cdpHost = resolved.cdpHost;
let cdpPort = profile.cdpPort ?? 0;
let cdpUrl = "";
if (rawProfileUrl) {
const parsed = parseHttpUrl(
rawProfileUrl,
`browser.profiles.${profileName}.cdpUrl`,
);
cdpHost = parsed.parsed.hostname;
cdpPort = parsed.port;
cdpUrl = parsed.normalized;
} else if (cdpPort) {
cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`;
} else {
throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`);
}
return {
name: profileName,
cdpPort,
cdpUrl,
cdpHost,
cdpIsLoopback: isLoopbackHost(cdpHost),
color: profile.color,
};
}