import fs from "node:fs"; import path from "node:path"; import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_PROFILE_NAME } from "./constants.js"; function decoratedMarkerPath(userDataDir: string) { return path.join(userDataDir, ".clawd-profile-decorated"); } function safeReadJson(filePath: string): Record | null { try { if (!fs.existsSync(filePath)) return null; const raw = fs.readFileSync(filePath, "utf-8"); const parsed = JSON.parse(raw) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null; return parsed as Record; } catch { return null; } } function safeWriteJson(filePath: string, data: Record) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } function setDeep(obj: Record, keys: string[], value: unknown) { let node: Record = obj; for (const key of keys.slice(0, -1)) { const next = node[key]; if (typeof next !== "object" || next === null || Array.isArray(next)) { node[key] = {}; } node = node[key] as Record; } node[keys[keys.length - 1] ?? ""] = value; } function parseHexRgbToSignedArgbInt(hex: string): number | null { const cleaned = hex.trim().replace(/^#/, ""); if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null; const rgb = Number.parseInt(cleaned, 16); const argbUnsigned = (0xff << 24) | rgb; // Chrome stores colors as signed 32-bit ints (SkColor). return argbUnsigned > 0x7fffffff ? argbUnsigned - 0x1_0000_0000 : argbUnsigned; } export function isProfileDecorated( userDataDir: string, desiredName: string, desiredColorHex: string, ): boolean { const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex); const localStatePath = path.join(userDataDir, "Local State"); const preferencesPath = path.join(userDataDir, "Default", "Preferences"); const localState = safeReadJson(localStatePath); const profile = localState?.profile; const infoCache = typeof profile === "object" && profile !== null && !Array.isArray(profile) ? (profile as Record).info_cache : null; const info = typeof infoCache === "object" && infoCache !== null && !Array.isArray(infoCache) && typeof (infoCache as Record).Default === "object" && (infoCache as Record).Default !== null && !Array.isArray((infoCache as Record).Default) ? ((infoCache as Record).Default as Record) : null; const prefs = safeReadJson(preferencesPath); const browserTheme = (() => { const browser = prefs?.browser; const theme = typeof browser === "object" && browser !== null && !Array.isArray(browser) ? (browser as Record).theme : null; return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? (theme as Record) : null; })(); const autogeneratedTheme = (() => { const autogenerated = prefs?.autogenerated; const theme = typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated) ? (autogenerated as Record).theme : null; return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? (theme as Record) : null; })(); const nameOk = typeof info?.name === "string" ? info.name === desiredName : true; if (desiredColorInt == null) { // If the user provided a non-#RRGGBB value, we can only do best-effort. return nameOk; } const localSeedOk = typeof info?.profile_color_seed === "number" ? info.profile_color_seed === desiredColorInt : false; const prefOk = (typeof browserTheme?.user_color2 === "number" && browserTheme.user_color2 === desiredColorInt) || (typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt); return nameOk && localSeedOk && prefOk; } /** * Best-effort profile decoration (name + lobster-orange). Chrome preference keys * vary by version; we keep this conservative and idempotent. */ export function decorateClawdProfile( userDataDir: string, opts?: { name?: string; color?: string }, ) { const desiredName = opts?.name ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME; const desiredColor = (opts?.color ?? DEFAULT_CLAWD_BROWSER_COLOR).toUpperCase(); const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor); const localStatePath = path.join(userDataDir, "Local State"); const preferencesPath = path.join(userDataDir, "Default", "Preferences"); const localState = safeReadJson(localStatePath) ?? {}; // Common-ish shape: profile.info_cache.Default setDeep(localState, ["profile", "info_cache", "Default", "name"], desiredName); setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], desiredName); setDeep(localState, ["profile", "info_cache", "Default", "user_name"], desiredName); // Color keys are best-effort (Chrome changes these frequently). setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], desiredColor); setDeep(localState, ["profile", "info_cache", "Default", "user_color"], desiredColor); if (desiredColorInt != null) { // These are the fields Chrome actually uses for profile/avatar tinting. setDeep( localState, ["profile", "info_cache", "Default", "profile_color_seed"], desiredColorInt, ); setDeep( localState, ["profile", "info_cache", "Default", "profile_highlight_color"], desiredColorInt, ); setDeep( localState, ["profile", "info_cache", "Default", "default_avatar_fill_color"], desiredColorInt, ); setDeep( localState, ["profile", "info_cache", "Default", "default_avatar_stroke_color"], desiredColorInt, ); } safeWriteJson(localStatePath, localState); const prefs = safeReadJson(preferencesPath) ?? {}; setDeep(prefs, ["profile", "name"], desiredName); setDeep(prefs, ["profile", "profile_color"], desiredColor); setDeep(prefs, ["profile", "user_color"], desiredColor); if (desiredColorInt != null) { // Chrome refresh stores the autogenerated theme in these prefs (SkColor ints). setDeep(prefs, ["autogenerated", "theme", "color"], desiredColorInt); // User-selected browser theme color (pref name: browser.theme.user_color2). setDeep(prefs, ["browser", "theme", "user_color2"], desiredColorInt); } safeWriteJson(preferencesPath, prefs); try { fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8"); } catch { // ignore } }