v0.4.1: Chrome cookie import, config.json, session persistence
CI / test (push) Has been cancelled
style / style (push) Has been cancelled

- Add Chrome cookie importer (macOS Keychain AES-128-CBC decryption)
- Add ~/.config/browse/config.json (headless, fullscreen, stealth, preview defaults)
- Add ~/.config/browse/session.json (auto-save/restore cookies + localStorage)
- Auto-import from all browsers (Safari + Firefox + Chrome) on first launch
- Deduplicate cookies across browsers (domain+name+path key)
- Defaults: headed, fullscreen, stealth, preview all enabled
- Fix BigInt overflow for Chrome timestamps (CAST to TEXT in SQL)
This commit is contained in:
marauder-actual
2026-06-06 16:07:12 +02:00
parent 16bc55bcb9
commit 5e1375f1a1
26 changed files with 1343 additions and 31 deletions
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
+24
View File
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
import { promisify } from 'node:util';
import { webkit } from 'playwright';
const execAsync = promisify(exec);
import * as chrome from './chrome.js';
import * as firefox from './firefox.js';
import * as image from './image.js';
import * as safari from './safari.js';
@@ -866,6 +867,29 @@ export class ClaudeBrowser {
domains,
};
}
if (cmd.source === 'chrome') {
const cookies = chrome.importChromeCookies({
domain: cmd.domain,
profile: cmd.profile,
});
if (cookies.length === 0) {
return {
ok: true,
imported: 0,
source: 'chrome',
domains: [],
};
}
const playwrightCookies = cookies.map(chrome.toPlaywrightCookie);
await context.addCookies(playwrightCookies);
const domains = [...new Set(cookies.map((c) => c.domain))];
return {
ok: true,
imported: cookies.length,
source: 'chrome',
domains,
};
}
return { ok: false, error: `Unknown import source: ${cmd.source}` };
}
async executeCommand(cmd) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+52
View File
@@ -0,0 +1,52 @@
/**
* Chrome cookie importer (macOS)
*
* Reads cookies from Chrome's SQLite database and decrypts values using
* the encryption key stored in the macOS Keychain ("Chrome Safe Storage").
*
* Encryption scheme (macOS):
* - Keychain password → PBKDF2(password, salt="saltysalt", iterations=1003, keylen=16)
* - AES-128-CBC with IV = 16 bytes of 0x20 (space)
* - Encrypted values prefixed with "v10" (3 bytes)
*
* Chrome timestamps are microseconds since Jan 1, 1601 (Windows/WebKit epoch).
* Database is copied to a temp directory to avoid WAL lock conflicts.
*/
export interface ChromeCookie {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'None' | 'Lax' | 'Strict';
}
/**
* List available Chrome profiles
*/
export declare function listChromeProfiles(): {
name: string;
path: string;
}[];
/**
* Import cookies from Chrome's Cookies SQLite database
*/
export declare function importChromeCookies(options?: {
profile?: string;
domain?: string;
}): ChromeCookie[];
/**
* Convert ChromeCookie to Playwright cookie format
*/
export declare function toPlaywrightCookie(cookie: ChromeCookie): {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
};
//# sourceMappingURL=chrome.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"chrome.d.ts","sourceRoot":"","sources":["../src/chrome.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;CACrC;AA8ED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CA4BrE;AAgDD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,YAAY,EAAE,CA6CjB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CACrC,CAgBA"}
+217
View File
@@ -0,0 +1,217 @@
/**
* Chrome cookie importer (macOS)
*
* Reads cookies from Chrome's SQLite database and decrypts values using
* the encryption key stored in the macOS Keychain ("Chrome Safe Storage").
*
* Encryption scheme (macOS):
* - Keychain password → PBKDF2(password, salt="saltysalt", iterations=1003, keylen=16)
* - AES-128-CBC with IV = 16 bytes of 0x20 (space)
* - Encrypted values prefixed with "v10" (3 bytes)
*
* Chrome timestamps are microseconds since Jan 1, 1601 (Windows/WebKit epoch).
* Database is copied to a temp directory to avoid WAL lock conflicts.
*/
import { execSync } from 'node:child_process';
import { createDecipheriv, pbkdf2Sync } from 'node:crypto';
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
import { homedir, platform, tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
// Chrome epoch (Jan 1, 1601) to Unix epoch (Jan 1, 1970) offset in microseconds
const CHROME_EPOCH_OFFSET = 11644473600000000n;
/**
* Convert Chrome timestamp (microseconds since Jan 1, 1601) to Unix seconds
*/
function chromeToUnix(chromeTime) {
if (chromeTime === 0n)
return 0;
const unixMicro = chromeTime - CHROME_EPOCH_OFFSET;
return Number(unixMicro / 1000000n);
}
/**
* Get Chrome encryption key from macOS Keychain, derive AES key via PBKDF2
*/
function getDerivedKey() {
if (platform() !== 'darwin') {
throw new Error('Chrome cookie decryption is currently only supported on macOS');
}
const keychainPassword = execSync('security find-generic-password -s "Chrome Safe Storage" -w', {
encoding: 'utf-8',
}).trim();
return pbkdf2Sync(keychainPassword, 'saltysalt', 1003, 16, 'sha1');
}
/**
* Decrypt a Chrome encrypted cookie value
*/
function decryptValue(encrypted, derivedKey) {
if (encrypted.length === 0)
return '';
// Check for "v10" prefix (macOS encryption marker)
const prefix = encrypted.subarray(0, 3).toString('ascii');
if (prefix !== 'v10') {
// Not encrypted or unknown format — return as-is
return encrypted.toString('utf-8');
}
const ciphertext = encrypted.subarray(3);
if (ciphertext.length === 0)
return '';
// AES-128-CBC, IV is 16 bytes of 0x20 (space)
const iv = Buffer.alloc(16, 0x20);
const decipher = createDecipheriv('aes-128-cbc', derivedKey, iv);
try {
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return decrypted.toString('utf-8');
}
catch {
return '';
}
}
/**
* Get the Chrome user data directory for the current platform
*/
function getChromeRoot() {
const home = homedir();
switch (platform()) {
case 'darwin':
return join(home, 'Library/Application Support/Google/Chrome');
case 'linux':
return join(home, '.config/google-chrome');
case 'win32':
return join(process.env.LOCALAPPDATA || join(home, 'AppData/Local'), 'Google/Chrome/User Data');
default:
throw new Error(`Unsupported platform: ${platform()}`);
}
}
/**
* List available Chrome profiles
*/
export function listChromeProfiles() {
const root = getChromeRoot();
if (!existsSync(root))
return [];
const profiles = [];
// Check "Default" profile
const defaultCookies = join(root, 'Default', 'Cookies');
if (existsSync(defaultCookies)) {
profiles.push({ name: 'Default', path: 'Default' });
}
// Check numbered profiles (Profile 1, Profile 2, ...)
try {
const entries = readdirSync(root, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('Profile ')) {
const cookiesPath = join(root, entry.name, 'Cookies');
if (existsSync(cookiesPath)) {
profiles.push({ name: entry.name, path: entry.name });
}
}
}
}
catch {
// Ignore readdir errors
}
return profiles;
}
/**
* Safely copy the Chrome cookies database to a temp directory.
* Copies Cookies + WAL + SHM files to avoid lock conflicts.
*/
function copyDatabaseSafely(dbPath) {
if (!existsSync(dbPath)) {
throw new Error(`Chrome cookies database not found at: ${dbPath}\nMake sure Chrome has been used at least once.`);
}
const tmpDir = mkdtempSync(join(tmpdir(), 'browse-cr-'));
const dbName = 'Cookies';
try {
copyFileSync(dbPath, join(tmpDir, dbName));
for (const ext of ['-wal', '-shm']) {
const src = `${dbPath}${ext}`;
if (existsSync(src)) {
copyFileSync(src, join(tmpDir, `${dbName}${ext}`));
}
}
return { tmpDir, tmpDbPath: join(tmpDir, dbName) };
}
catch (err) {
rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
}
/**
* Convert Chrome sameSite integer to string
* -1 = unspecified (treat as None), 0 = None (was "no_restriction"), 1 = Lax, 2 = Strict
*/
function sameSiteToString(value) {
switch (value) {
case 2:
return 'Strict';
case 1:
return 'Lax';
default:
return 'None';
}
}
/**
* Import cookies from Chrome's Cookies SQLite database
*/
export function importChromeCookies(options) {
const root = getChromeRoot();
const profileDir = options?.profile || 'Default';
const dbPath = join(root, profileDir, 'Cookies');
const { tmpDir, tmpDbPath } = copyDatabaseSafely(dbPath);
try {
const derivedKey = getDerivedKey();
const db = new DatabaseSync(tmpDbPath, { readOnly: true });
let query = 'SELECT name, value, encrypted_value, host_key, path, CAST(expires_utc AS TEXT) as expires_utc_str, is_secure, is_httponly, samesite FROM cookies';
const params = [];
if (options?.domain) {
const domain = options.domain.toLowerCase();
query += ' WHERE LOWER(host_key) = ? OR LOWER(host_key) = ? OR LOWER(host_key) LIKE ?';
params.push(domain, `.${domain}`, `%.${domain}`);
}
const stmt = db.prepare(query);
const rows = params.length > 0 ? stmt.all(...params) : stmt.all();
db.close();
return rows.map((row) => {
// Use plaintext value if available, otherwise decrypt
let value = row.value;
if (!value && row.encrypted_value) {
value = decryptValue(row.encrypted_value, derivedKey);
}
return {
name: row.name,
value,
domain: row.host_key,
path: row.path,
expires: chromeToUnix(BigInt(row.expires_utc_str)),
secure: row.is_secure === 1,
httpOnly: row.is_httponly === 1,
sameSite: sameSiteToString(row.samesite),
};
});
}
finally {
rmSync(tmpDir, { recursive: true, force: true });
}
}
/**
* Convert ChromeCookie to Playwright cookie format
*/
export function toPlaywrightCookie(cookie) {
let expires = cookie.expires;
if (expires <= 0) {
expires = -1;
}
return {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
expires,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
sameSite: cookie.sameSite,
};
}
//# sourceMappingURL=chrome.js.map
+1
View File
File diff suppressed because one or more lines are too long
+96
View File
@@ -0,0 +1,96 @@
/**
* Browse configuration and session persistence
*
* Config: ~/.config/browse/config.json — user defaults for launch options
* Session: ~/.config/browse/session.json — persistent cookies, storage, last URL
*
* Config is loaded once at startup and merged under explicit tool args.
* Session is auto-saved on close and auto-restored on launch (if present).
*/
export interface BrowseConfig {
/** Launch headless (default: true) */
headless?: boolean;
/** Default viewport width (default: 1280) */
width?: number;
/** Default viewport height (default: 800) */
height?: number;
/** Launch in macOS native fullscreen (default: false) */
fullscreen?: boolean;
/** Enable preview mode — highlight elements before actions (default: false) */
preview?: boolean;
/** Preview highlight duration in ms (default: 2000) */
previewDelay?: number;
/** Enable stealth mode to reduce bot detection (default: false) */
stealth?: boolean;
/** Auto-restore session on launch (default: true) */
autoRestore?: boolean;
/** Auto-save session on close (default: true) */
autoSave?: boolean;
/** Default browser to import cookies from on first launch */
importFrom?: 'safari' | 'firefox' | 'chrome';
/** Default domain filter for cookie import */
importDomain?: string;
}
/**
* Load config from ~/.config/browse/config.json
* Returns defaults merged with user config. Missing file = all defaults.
*/
export declare function loadConfig(): Required<BrowseConfig>;
/**
* Save config to ~/.config/browse/config.json
*/
export declare function saveConfig(config: Partial<BrowseConfig>): void;
/**
* Get the config file path (for display/debugging)
*/
export declare function getConfigPath(): string;
export interface BrowseSession {
/** Last visited URL */
url?: string;
/** Page title at save time */
title?: string;
/** All cookies from the browser context */
cookies?: Array<{
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
}>;
/** localStorage key-value pairs (per origin) */
localStorage?: Record<string, string>;
/** sessionStorage key-value pairs (per origin) */
sessionStorage?: Record<string, string>;
/** ISO timestamp of last save */
savedAt?: string;
}
/**
* Load session from ~/.config/browse/session.json
* Returns null if no session file exists.
*/
export declare function loadSession(): BrowseSession | null;
/**
* Save session to ~/.config/browse/session.json
*/
export declare function saveSession(session: BrowseSession): void;
/**
* Delete the session file
*/
export declare function clearSession(): void;
/**
* Get the session file path (for display/debugging)
*/
export declare function getSessionPath(): string;
/**
* Import cookies from all available browsers, deduplicate, and save to session.json.
* Dedup key: domain + name + path. Last-write wins (Chrome > Firefox > Safari priority).
* Returns the merged cookie count.
*/
export declare function importAllToSession(): Promise<{
total: number;
sources: Record<string, number>;
}>;
//# sourceMappingURL=config.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qDAAqD;IACrD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC7C,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAsBD;;;GAGG;AACH,wBAAgB,UAAU,IAAI,QAAQ,CAAC,YAAY,CAAC,CAUnD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI,CAG9D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAID,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,OAAO,CAAC,EAAE,KAAK,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,QAAQ,EAAE,OAAO,CAAC;QAClB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;KACrC,CAAC,CAAC;IACH,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,aAAa,GAAG,IAAI,CASlD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAIxD;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAKnC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAeD;;;;GAIG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,CAAC,CAgED"}
+165
View File
@@ -0,0 +1,165 @@
/**
* Browse configuration and session persistence
*
* Config: ~/.config/browse/config.json — user defaults for launch options
* Session: ~/.config/browse/session.json — persistent cookies, storage, last URL
*
* Config is loaded once at startup and merged under explicit tool args.
* Session is auto-saved on close and auto-restored on launch (if present).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const CONFIG_DIR = join(homedir(), '.config', 'browse');
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
const SESSION_PATH = join(CONFIG_DIR, 'session.json');
const DEFAULTS = {
headless: false,
width: 1280,
height: 800,
fullscreen: true,
preview: true,
previewDelay: 2000,
stealth: true,
autoRestore: true,
autoSave: true,
importFrom: 'safari',
importDomain: '',
};
function ensureDir() {
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true });
}
}
/**
* Load config from ~/.config/browse/config.json
* Returns defaults merged with user config. Missing file = all defaults.
*/
export function loadConfig() {
if (!existsSync(CONFIG_PATH))
return { ...DEFAULTS };
try {
const raw = readFileSync(CONFIG_PATH, 'utf-8');
const user = JSON.parse(raw);
return { ...DEFAULTS, ...user };
}
catch {
return { ...DEFAULTS };
}
}
/**
* Save config to ~/.config/browse/config.json
*/
export function saveConfig(config) {
ensureDir();
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`);
}
/**
* Get the config file path (for display/debugging)
*/
export function getConfigPath() {
return CONFIG_PATH;
}
/**
* Load session from ~/.config/browse/session.json
* Returns null if no session file exists.
*/
export function loadSession() {
if (!existsSync(SESSION_PATH))
return null;
try {
const raw = readFileSync(SESSION_PATH, 'utf-8');
return JSON.parse(raw);
}
catch {
return null;
}
}
/**
* Save session to ~/.config/browse/session.json
*/
export function saveSession(session) {
ensureDir();
session.savedAt = new Date().toISOString();
writeFileSync(SESSION_PATH, `${JSON.stringify(session, null, 2)}\n`);
}
/**
* Delete the session file
*/
export function clearSession() {
if (existsSync(SESSION_PATH)) {
const { unlinkSync } = require('node:fs');
unlinkSync(SESSION_PATH);
}
}
/**
* Get the session file path (for display/debugging)
*/
export function getSessionPath() {
return SESSION_PATH;
}
/**
* Import cookies from all available browsers, deduplicate, and save to session.json.
* Dedup key: domain + name + path. Last-write wins (Chrome > Firefox > Safari priority).
* Returns the merged cookie count.
*/
export async function importAllToSession() {
const all = [];
const sources = {};
// Safari (async — binary parser)
try {
const { importSafariCookies, toPlaywrightCookie } = await import('./safari.js');
const cookies = await importSafariCookies();
const converted = cookies.map(toPlaywrightCookie);
all.push(...converted);
sources.safari = cookies.length;
}
catch {
sources.safari = 0;
}
// Firefox (sync — SQLite)
try {
const { importFirefoxCookies, listFirefoxProfiles, toPlaywrightCookie } = await import('./firefox.js');
// Try default-release first (main profile on macOS), fall back to default
const profiles = listFirefoxProfiles();
const profile = profiles.find((p) => p.name === 'default-release') ||
profiles.find((p) => p.isDefault) ||
profiles[0];
if (profile) {
const cookies = importFirefoxCookies({ profile: profile.name });
const converted = cookies.map(toPlaywrightCookie);
all.push(...converted);
sources.firefox = cookies.length;
}
else {
sources.firefox = 0;
}
}
catch {
sources.firefox = 0;
}
// Chrome (sync — SQLite + Keychain decryption, macOS only)
try {
const { importChromeCookies, toPlaywrightCookie } = await import('./chrome.js');
const cookies = importChromeCookies();
const converted = cookies.map(toPlaywrightCookie);
all.push(...converted);
sources.chrome = cookies.length;
}
catch {
sources.chrome = 0;
}
// Deduplicate: last-write wins (Chrome overwrites Firefox overwrites Safari)
const seen = new Map();
for (const cookie of all) {
const key = `${cookie.domain}|${cookie.name}|${cookie.path}`;
seen.set(key, cookie);
}
const deduped = [...seen.values()];
// Save to session.json
saveSession({
cookies: deduped,
});
return { total: deduped.length, sources };
}
//# sourceMappingURL=config.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACxD,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACpD,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AA6BtD,MAAM,QAAQ,GAA2B;IACvC,QAAQ,EAAE,KAAK;IACf,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,GAAG;IACX,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,IAAI;IACb,YAAY,EAAE,IAAI;IAClB,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,IAAI;IACd,UAAU,EAAE,QAAQ;IACpB,YAAY,EAAE,EAAE;CACjB,CAAC;AAEF,SAAS,SAAS;IAChB,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;IAErD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAC;QACtD,OAAO,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,MAA6B;IACtD,SAAS,EAAE,CAAC;IACZ,aAAa,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACrE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC;AACrB,CAAC;AA4BD;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAE3C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,OAAsB;IAChD,SAAS,EAAE,CAAC;IACZ,OAAO,CAAC,OAAO,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,aAAa,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACvE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7B,MAAM,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAC1C,UAAU,CAAC,YAAY,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,YAAY,CAAC;AACtB,CAAC;AAeD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IAItC,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,MAAM,OAAO,GAA2B,EAAE,CAAC;IAE3C,iCAAiC;IACjC,IAAI,CAAC;QACH,MAAM,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAChF,MAAM,OAAO,GAAG,MAAM,mBAAmB,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAClD,GAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QACvB,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,0BAA0B;IAC1B,IAAI,CAAC;QACH,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CACpF,cAAc,CACf,CAAC;QACF,0EAA0E;QAC1E,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;QACvC,MAAM,OAAO,GACX,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAAC;YAClD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YACjC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACd,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,oBAAoB,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YAChE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;YAClD,GAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;YACvB,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;IACtB,CAAC;IAED,2DAA2D;IAC3D,IAAI,CAAC;QACH,MAAM,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAChF,MAAM,OAAO,GAAG,mBAAmB,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAClD,GAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QACvB,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,6EAA6E;IAC7E,MAAM,IAAI,GAAG,IAAI,GAAG,EAA4B,CAAC;IACjD,KAAK,MAAM,MAAM,IAAI,GAAG,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAC7D,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACxB,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAEnC,uBAAuB;IACvB,WAAW,CAAC;QACV,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAC5C,CAAC"}
+2
View File
@@ -1,4 +1,6 @@
export { ClaudeBrowser } from './browser.js';
export { importChromeCookies, listChromeProfiles, type ChromeCookie } from './chrome.js';
export { loadConfig, saveConfig, loadSession, saveSession, clearSession, importAllToSession, getConfigPath, getSessionPath, type BrowseConfig, type BrowseSession, } from './config.js';
export { BrowserServer, startServer, type ServerOptions } from './server.js';
export { createFavicon, convert, resize, crop, compress, thumbnail, type FaviconResult, type ImageResult, type FitType, type FormatType, type ThumbnailSize, } from './image.js';
export type { BrowserOptions, BrowserCommand, CommandResponse, ElementInfo, SuccessResponse, ErrorResponse, GotoCommand, ClickCommand, TypeCommand, QueryCommand, ScreenshotCommand, UrlCommand, HtmlCommand, BackCommand, ForwardCommand, ReloadCommand, WaitCommand, NewPageCommand, CloseCommand, EvalCommand, FaviconCommand, ConvertCommand, ResizeCommand, CropCommand, CompressCommand, ThumbnailCommand, } from './types.js';
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,aAAa,EACb,OAAO,EACP,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,WAAW,EACX,eAAe,EACf,aAAa,EACb,WAAW,EACX,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,EACd,YAAY,EACZ,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,WAAW,EACX,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACzF,OAAO,EACL,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,aAAa,EACb,OAAO,EACP,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,WAAW,EACX,eAAe,EACf,aAAa,EACb,WAAW,EACX,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,EACd,YAAY,EACZ,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,WAAW,EACX,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
+2
View File
@@ -1,4 +1,6 @@
export { ClaudeBrowser } from './browser.js';
export { importChromeCookies, listChromeProfiles } from './chrome.js';
export { loadConfig, saveConfig, loadSession, saveSession, clearSession, importAllToSession, getConfigPath, getSessionPath, } from './config.js';
export { BrowserServer, startServer } from './server.js';
export { createFavicon, convert, resize, crop, compress, thumbnail, } from './image.js';
//# sourceMappingURL=index.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAsB,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,aAAa,EACb,OAAO,EACP,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,SAAS,GAMV,MAAM,YAAY,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAqB,MAAM,aAAa,CAAC;AACzF,OAAO,EACL,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,cAAc,GAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAsB,MAAM,aAAa,CAAC;AAC7E,OAAO,EACL,aAAa,EACb,OAAO,EACP,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,SAAS,GAMV,MAAM,YAAY,CAAC"}
Vendored
+97 -10
View File
@@ -6,19 +6,21 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { ClaudeBrowser } from './browser.js';
import { importAllToSession, loadConfig, loadSession, saveSession, } from './config.js';
import * as image from './image.js';
import { stderrLogger as log } from './logger.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
// Browser options configurable via launch tool
// Load user config from ~/.config/browse/config.json
const userConfig = loadConfig();
let browserOptions = {
headless: true,
width: 1280,
height: 800,
fullscreen: false,
preview: false,
previewDelay: 2000,
stealth: false,
headless: userConfig.headless,
width: userConfig.width,
height: userConfig.height,
fullscreen: userConfig.fullscreen,
preview: userConfig.preview,
previewDelay: userConfig.previewDelay,
stealth: userConfig.stealth,
};
let browser = new ClaudeBrowser(browserOptions);
let launched = false;
@@ -27,6 +29,56 @@ async function ensureLaunched() {
if (!launched) {
await browser.launch();
launched = true;
// Auto-restore session if enabled
// If no session.json exists, import all browser cookies first
if (userConfig.autoRestore) {
let session = loadSession();
if (!session) {
try {
const result = await importAllToSession();
log.command({
cmd: 'auto_import',
url: `${result.total} cookies from ${Object.entries(result.sources)
.filter(([, v]) => v > 0)
.map(([k, v]) => `${k}:${v}`)
.join(', ')}`,
});
session = loadSession();
}
catch (err) {
log.result({ cmd: 'auto_import' }, { ok: false, error: err.message });
}
}
if (session) {
try {
const context = browser.getContext();
if (context && session.cookies?.length) {
await context.addCookies(session.cookies);
}
const page = browser.getPage();
if (page && session.url && session.url !== 'about:blank') {
await page.goto(session.url, { waitUntil: 'domcontentloaded' });
await Promise.race([
page.waitForLoadState('networkidle'),
page.waitForTimeout(5000),
]).catch(() => { });
// Restore localStorage/sessionStorage after navigation
if (session.localStorage || session.sessionStorage) {
const local = session.localStorage || {};
const sessionStorage = session.sessionStorage || {};
await page.evaluate(`((data) => {
for (const [k, v] of Object.entries(data.local)) localStorage.setItem(k, v);
for (const [k, v] of Object.entries(data.session)) sessionStorage.setItem(k, v);
})({ local: ${JSON.stringify(local)}, session: ${JSON.stringify(sessionStorage)} })`);
}
}
log.command({ cmd: 'auto_restore', url: session.url });
}
catch (err) {
log.result({ cmd: 'auto_restore' }, { ok: false, error: err.message });
}
}
}
}
}
function textResult(text) {
@@ -399,6 +451,41 @@ server.tool('wait', 'Wait for a specified time in milliseconds', { ms: z.number(
// Session management
server.tool('close', 'Close the browser and end the current session', {}, withLogging('close', async () => {
if (launched) {
// Auto-save session before closing
if (userConfig.autoSave) {
try {
const page = browser.getPage();
const context = browser.getContext();
if (page && context) {
const url = page.url();
const title = await page.title();
const cookies = await context.cookies();
const storage = (await page.evaluate(`({
localStorage: Object.fromEntries(
Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i))
.filter(k => k !== null)
.map(k => [k, localStorage.getItem(k) || ''])
),
sessionStorage: Object.fromEntries(
Array.from({ length: sessionStorage.length }, (_, i) => sessionStorage.key(i))
.filter(k => k !== null)
.map(k => [k, sessionStorage.getItem(k) || ''])
),
})`));
saveSession({
url,
title,
cookies,
localStorage: storage.localStorage,
sessionStorage: storage.sessionStorage,
});
log.command({ cmd: 'auto_save', url });
}
}
catch (err) {
log.result({ cmd: 'auto_save' }, { ok: false, error: err.message });
}
}
await browser.close();
launched = false;
}
@@ -481,8 +568,8 @@ server.tool('session_restore', 'Restore a previously saved session state from a
}));
}));
// Browser import
server.tool('import', 'Import cookies from Safari or Firefox browser. Safari requires Full Disk Access permission (macOS only). Firefox works on macOS, Linux, and Windows.', {
source: z.enum(['safari', 'firefox']).describe('Browser to import from'),
server.tool('import', 'Import cookies from Safari, Firefox, or Chrome browser. Safari requires Full Disk Access (macOS). Chrome requires Keychain access (macOS). Firefox works on all platforms.', {
source: z.enum(['safari', 'firefox', 'chrome']).describe('Browser to import from'),
domain: z
.string()
.optional()
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -255,7 +255,7 @@ export interface EmulateCommand {
}
export interface ImportCommand {
cmd: 'import';
source: 'safari' | 'firefox';
source: 'safari' | 'firefox' | 'chrome';
domain?: string;
profile?: string;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@saiden/browse",
"version": "0.4.0",
"version": "0.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@saiden/browse",
"version": "0.4.0",
"version": "0.4.1",
"license": "BUSL-1.1",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
+29
View File
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
import { type Browser, type BrowserContext, type Page, type Route, webkit } from 'playwright';
const execAsync = promisify(exec);
import * as chrome from './chrome.js';
import * as firefox from './firefox.js';
import * as image from './image.js';
import * as safari from './safari.js';
@@ -997,6 +998,34 @@ export class ClaudeBrowser {
};
}
if (cmd.source === 'chrome') {
const cookies = chrome.importChromeCookies({
domain: cmd.domain,
profile: cmd.profile,
});
if (cookies.length === 0) {
return {
ok: true,
imported: 0,
source: 'chrome',
domains: [],
};
}
const playwrightCookies = cookies.map(chrome.toPlaywrightCookie);
await context.addCookies(playwrightCookies);
const domains = [...new Set(cookies.map((c) => c.domain))];
return {
ok: true,
imported: cookies.length,
source: 'chrome',
domains,
};
}
return { ok: false, error: `Unknown import source: ${cmd.source}` };
}
+270
View File
@@ -0,0 +1,270 @@
/**
* Chrome cookie importer (macOS)
*
* Reads cookies from Chrome's SQLite database and decrypts values using
* the encryption key stored in the macOS Keychain ("Chrome Safe Storage").
*
* Encryption scheme (macOS):
* - Keychain password → PBKDF2(password, salt="saltysalt", iterations=1003, keylen=16)
* - AES-128-CBC with IV = 16 bytes of 0x20 (space)
* - Encrypted values prefixed with "v10" (3 bytes)
*
* Chrome timestamps are microseconds since Jan 1, 1601 (Windows/WebKit epoch).
* Database is copied to a temp directory to avoid WAL lock conflicts.
*/
import { execSync } from 'node:child_process';
import { createDecipheriv, pbkdf2Sync } from 'node:crypto';
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
import { homedir, platform, tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
export interface ChromeCookie {
name: string;
value: string;
domain: string;
path: string;
expires: number; // Unix timestamp (seconds)
secure: boolean;
httpOnly: boolean;
sameSite: 'None' | 'Lax' | 'Strict';
}
// Chrome epoch (Jan 1, 1601) to Unix epoch (Jan 1, 1970) offset in microseconds
const CHROME_EPOCH_OFFSET = 11644473600000000n;
/**
* Convert Chrome timestamp (microseconds since Jan 1, 1601) to Unix seconds
*/
function chromeToUnix(chromeTime: bigint): number {
if (chromeTime === 0n) return 0;
const unixMicro = chromeTime - CHROME_EPOCH_OFFSET;
return Number(unixMicro / 1000000n);
}
/**
* Get Chrome encryption key from macOS Keychain, derive AES key via PBKDF2
*/
function getDerivedKey(): Buffer {
if (platform() !== 'darwin') {
throw new Error('Chrome cookie decryption is currently only supported on macOS');
}
const keychainPassword = execSync('security find-generic-password -s "Chrome Safe Storage" -w', {
encoding: 'utf-8',
}).trim();
return pbkdf2Sync(keychainPassword, 'saltysalt', 1003, 16, 'sha1');
}
/**
* Decrypt a Chrome encrypted cookie value
*/
function decryptValue(encrypted: Buffer, derivedKey: Buffer): string {
if (encrypted.length === 0) return '';
// Check for "v10" prefix (macOS encryption marker)
const prefix = encrypted.subarray(0, 3).toString('ascii');
if (prefix !== 'v10') {
// Not encrypted or unknown format — return as-is
return encrypted.toString('utf-8');
}
const ciphertext = encrypted.subarray(3);
if (ciphertext.length === 0) return '';
// AES-128-CBC, IV is 16 bytes of 0x20 (space)
const iv = Buffer.alloc(16, 0x20);
const decipher = createDecipheriv('aes-128-cbc', derivedKey, iv);
try {
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return decrypted.toString('utf-8');
} catch {
return '';
}
}
/**
* Get the Chrome user data directory for the current platform
*/
function getChromeRoot(): string {
const home = homedir();
switch (platform()) {
case 'darwin':
return join(home, 'Library/Application Support/Google/Chrome');
case 'linux':
return join(home, '.config/google-chrome');
case 'win32':
return join(
process.env.LOCALAPPDATA || join(home, 'AppData/Local'),
'Google/Chrome/User Data'
);
default:
throw new Error(`Unsupported platform: ${platform()}`);
}
}
/**
* List available Chrome profiles
*/
export function listChromeProfiles(): { name: string; path: string }[] {
const root = getChromeRoot();
if (!existsSync(root)) return [];
const profiles: { name: string; path: string }[] = [];
// Check "Default" profile
const defaultCookies = join(root, 'Default', 'Cookies');
if (existsSync(defaultCookies)) {
profiles.push({ name: 'Default', path: 'Default' });
}
// Check numbered profiles (Profile 1, Profile 2, ...)
try {
const entries = readdirSync(root, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('Profile ')) {
const cookiesPath = join(root, entry.name, 'Cookies');
if (existsSync(cookiesPath)) {
profiles.push({ name: entry.name, path: entry.name });
}
}
}
} catch {
// Ignore readdir errors
}
return profiles;
}
/**
* Safely copy the Chrome cookies database to a temp directory.
* Copies Cookies + WAL + SHM files to avoid lock conflicts.
*/
function copyDatabaseSafely(dbPath: string): { tmpDir: string; tmpDbPath: string } {
if (!existsSync(dbPath)) {
throw new Error(
`Chrome cookies database not found at: ${dbPath}\nMake sure Chrome has been used at least once.`
);
}
const tmpDir = mkdtempSync(join(tmpdir(), 'browse-cr-'));
const dbName = 'Cookies';
try {
copyFileSync(dbPath, join(tmpDir, dbName));
for (const ext of ['-wal', '-shm']) {
const src = `${dbPath}${ext}`;
if (existsSync(src)) {
copyFileSync(src, join(tmpDir, `${dbName}${ext}`));
}
}
return { tmpDir, tmpDbPath: join(tmpDir, dbName) };
} catch (err) {
rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
}
/**
* Convert Chrome sameSite integer to string
* -1 = unspecified (treat as None), 0 = None (was "no_restriction"), 1 = Lax, 2 = Strict
*/
function sameSiteToString(value: number): 'None' | 'Lax' | 'Strict' {
switch (value) {
case 2:
return 'Strict';
case 1:
return 'Lax';
default:
return 'None';
}
}
/**
* Import cookies from Chrome's Cookies SQLite database
*/
export function importChromeCookies(options?: {
profile?: string;
domain?: string;
}): ChromeCookie[] {
const root = getChromeRoot();
const profileDir = options?.profile || 'Default';
const dbPath = join(root, profileDir, 'Cookies');
const { tmpDir, tmpDbPath } = copyDatabaseSafely(dbPath);
try {
const derivedKey = getDerivedKey();
const db = new DatabaseSync(tmpDbPath, { readOnly: true });
let query =
'SELECT name, value, encrypted_value, host_key, path, CAST(expires_utc AS TEXT) as expires_utc_str, is_secure, is_httponly, samesite FROM cookies';
const params: string[] = [];
if (options?.domain) {
const domain = options.domain.toLowerCase();
query += ' WHERE LOWER(host_key) = ? OR LOWER(host_key) = ? OR LOWER(host_key) LIKE ?';
params.push(domain, `.${domain}`, `%.${domain}`);
}
const stmt = db.prepare(query);
const rows = params.length > 0 ? stmt.all(...params) : stmt.all();
db.close();
return (rows as Record<string, unknown>[]).map((row) => {
// Use plaintext value if available, otherwise decrypt
let value = row.value as string;
if (!value && row.encrypted_value) {
value = decryptValue(row.encrypted_value as Buffer, derivedKey);
}
return {
name: row.name as string,
value,
domain: row.host_key as string,
path: row.path as string,
expires: chromeToUnix(BigInt(row.expires_utc_str as string)),
secure: (row.is_secure as number) === 1,
httpOnly: (row.is_httponly as number) === 1,
sameSite: sameSiteToString(row.samesite as number),
};
});
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
}
/**
* Convert ChromeCookie to Playwright cookie format
*/
export function toPlaywrightCookie(cookie: ChromeCookie): {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
} {
let expires = cookie.expires;
if (expires <= 0) {
expires = -1;
}
return {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
expires,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
sameSite: cookie.sameSite,
};
}
+249
View File
@@ -0,0 +1,249 @@
/**
* Browse configuration and session persistence
*
* Config: ~/.config/browse/config.json — user defaults for launch options
* Session: ~/.config/browse/session.json — persistent cookies, storage, last URL
*
* Config is loaded once at startup and merged under explicit tool args.
* Session is auto-saved on close and auto-restored on launch (if present).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const CONFIG_DIR = join(homedir(), '.config', 'browse');
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
const SESSION_PATH = join(CONFIG_DIR, 'session.json');
// ── Config ──────────────────────────────────────────────────
export interface BrowseConfig {
/** Launch headless (default: true) */
headless?: boolean;
/** Default viewport width (default: 1280) */
width?: number;
/** Default viewport height (default: 800) */
height?: number;
/** Launch in macOS native fullscreen (default: false) */
fullscreen?: boolean;
/** Enable preview mode — highlight elements before actions (default: false) */
preview?: boolean;
/** Preview highlight duration in ms (default: 2000) */
previewDelay?: number;
/** Enable stealth mode to reduce bot detection (default: false) */
stealth?: boolean;
/** Auto-restore session on launch (default: true) */
autoRestore?: boolean;
/** Auto-save session on close (default: true) */
autoSave?: boolean;
/** Default browser to import cookies from on first launch */
importFrom?: 'safari' | 'firefox' | 'chrome';
/** Default domain filter for cookie import */
importDomain?: string;
}
const DEFAULTS: Required<BrowseConfig> = {
headless: false,
width: 1280,
height: 800,
fullscreen: true,
preview: true,
previewDelay: 2000,
stealth: true,
autoRestore: true,
autoSave: true,
importFrom: 'safari',
importDomain: '',
};
function ensureDir(): void {
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true });
}
}
/**
* Load config from ~/.config/browse/config.json
* Returns defaults merged with user config. Missing file = all defaults.
*/
export function loadConfig(): Required<BrowseConfig> {
if (!existsSync(CONFIG_PATH)) return { ...DEFAULTS };
try {
const raw = readFileSync(CONFIG_PATH, 'utf-8');
const user = JSON.parse(raw) as Partial<BrowseConfig>;
return { ...DEFAULTS, ...user };
} catch {
return { ...DEFAULTS };
}
}
/**
* Save config to ~/.config/browse/config.json
*/
export function saveConfig(config: Partial<BrowseConfig>): void {
ensureDir();
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`);
}
/**
* Get the config file path (for display/debugging)
*/
export function getConfigPath(): string {
return CONFIG_PATH;
}
// ── Session ─────────────────────────────────────────────────
export interface BrowseSession {
/** Last visited URL */
url?: string;
/** Page title at save time */
title?: string;
/** All cookies from the browser context */
cookies?: Array<{
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
}>;
/** localStorage key-value pairs (per origin) */
localStorage?: Record<string, string>;
/** sessionStorage key-value pairs (per origin) */
sessionStorage?: Record<string, string>;
/** ISO timestamp of last save */
savedAt?: string;
}
/**
* Load session from ~/.config/browse/session.json
* Returns null if no session file exists.
*/
export function loadSession(): BrowseSession | null {
if (!existsSync(SESSION_PATH)) return null;
try {
const raw = readFileSync(SESSION_PATH, 'utf-8');
return JSON.parse(raw) as BrowseSession;
} catch {
return null;
}
}
/**
* Save session to ~/.config/browse/session.json
*/
export function saveSession(session: BrowseSession): void {
ensureDir();
session.savedAt = new Date().toISOString();
writeFileSync(SESSION_PATH, `${JSON.stringify(session, null, 2)}\n`);
}
/**
* Delete the session file
*/
export function clearSession(): void {
if (existsSync(SESSION_PATH)) {
const { unlinkSync } = require('node:fs');
unlinkSync(SESSION_PATH);
}
}
/**
* Get the session file path (for display/debugging)
*/
export function getSessionPath(): string {
return SESSION_PATH;
}
// ── Import All ──────────────────────────────────────────────
type PlaywrightCookie = {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
};
/**
* Import cookies from all available browsers, deduplicate, and save to session.json.
* Dedup key: domain + name + path. Last-write wins (Chrome > Firefox > Safari priority).
* Returns the merged cookie count.
*/
export async function importAllToSession(): Promise<{
total: number;
sources: Record<string, number>;
}> {
const all: PlaywrightCookie[] = [];
const sources: Record<string, number> = {};
// Safari (async — binary parser)
try {
const { importSafariCookies, toPlaywrightCookie } = await import('./safari.js');
const cookies = await importSafariCookies();
const converted = cookies.map(toPlaywrightCookie);
all.push(...converted);
sources.safari = cookies.length;
} catch {
sources.safari = 0;
}
// Firefox (sync — SQLite)
try {
const { importFirefoxCookies, listFirefoxProfiles, toPlaywrightCookie } = await import(
'./firefox.js'
);
// Try default-release first (main profile on macOS), fall back to default
const profiles = listFirefoxProfiles();
const profile =
profiles.find((p) => p.name === 'default-release') ||
profiles.find((p) => p.isDefault) ||
profiles[0];
if (profile) {
const cookies = importFirefoxCookies({ profile: profile.name });
const converted = cookies.map(toPlaywrightCookie);
all.push(...converted);
sources.firefox = cookies.length;
} else {
sources.firefox = 0;
}
} catch {
sources.firefox = 0;
}
// Chrome (sync — SQLite + Keychain decryption, macOS only)
try {
const { importChromeCookies, toPlaywrightCookie } = await import('./chrome.js');
const cookies = importChromeCookies();
const converted = cookies.map(toPlaywrightCookie);
all.push(...converted);
sources.chrome = cookies.length;
} catch {
sources.chrome = 0;
}
// Deduplicate: last-write wins (Chrome overwrites Firefox overwrites Safari)
const seen = new Map<string, PlaywrightCookie>();
for (const cookie of all) {
const key = `${cookie.domain}|${cookie.name}|${cookie.path}`;
seen.set(key, cookie);
}
const deduped = [...seen.values()];
// Save to session.json
saveSession({
cookies: deduped,
});
return { total: deduped.length, sources };
}
+13
View File
@@ -1,4 +1,17 @@
export { ClaudeBrowser } from './browser.js';
export { importChromeCookies, listChromeProfiles, type ChromeCookie } from './chrome.js';
export {
loadConfig,
saveConfig,
loadSession,
saveSession,
clearSession,
importAllToSession,
getConfigPath,
getSessionPath,
type BrowseConfig,
type BrowseSession,
} from './config.js';
export { BrowserServer, startServer, type ServerOptions } from './server.js';
export {
createFavicon,
+112 -10
View File
@@ -6,21 +6,31 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { ClaudeBrowser } from './browser.js';
import {
getConfigPath,
getSessionPath,
importAllToSession,
loadConfig,
loadSession,
saveSession,
} from './config.js';
import * as image from './image.js';
import { type CommandLike, type ResultLike, stderrLogger as log } from './logger.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
// Browser options configurable via launch tool
// Load user config from ~/.config/browse/config.json
const userConfig = loadConfig();
let browserOptions = {
headless: true,
width: 1280,
height: 800,
fullscreen: false,
preview: false,
previewDelay: 2000,
stealth: false,
headless: userConfig.headless,
width: userConfig.width,
height: userConfig.height,
fullscreen: userConfig.fullscreen,
preview: userConfig.preview,
previewDelay: userConfig.previewDelay,
stealth: userConfig.stealth,
};
let browser = new ClaudeBrowser(browserOptions);
let launched = false;
@@ -30,6 +40,59 @@ async function ensureLaunched(): Promise<void> {
if (!launched) {
await browser.launch();
launched = true;
// Auto-restore session if enabled
// If no session.json exists, import all browser cookies first
if (userConfig.autoRestore) {
let session = loadSession();
if (!session) {
try {
const result = await importAllToSession();
log.command({
cmd: 'auto_import',
url: `${result.total} cookies from ${Object.entries(result.sources)
.filter(([, v]) => v > 0)
.map(([k, v]) => `${k}:${v}`)
.join(', ')}`,
});
session = loadSession();
} catch (err) {
log.result({ cmd: 'auto_import' }, { ok: false, error: (err as Error).message });
}
}
if (session) {
try {
const context = browser.getContext();
if (context && session.cookies?.length) {
await context.addCookies(session.cookies);
}
const page = browser.getPage();
if (page && session.url && session.url !== 'about:blank') {
await page.goto(session.url, { waitUntil: 'domcontentloaded' });
await Promise.race([
page.waitForLoadState('networkidle'),
page.waitForTimeout(5000),
]).catch(() => {});
// Restore localStorage/sessionStorage after navigation
if (session.localStorage || session.sessionStorage) {
const local = session.localStorage || {};
const sessionStorage = session.sessionStorage || {};
await page.evaluate(
`((data) => {
for (const [k, v] of Object.entries(data.local)) localStorage.setItem(k, v);
for (const [k, v] of Object.entries(data.session)) sessionStorage.setItem(k, v);
})({ local: ${JSON.stringify(local)}, session: ${JSON.stringify(sessionStorage)} })`
);
}
}
log.command({ cmd: 'auto_restore', url: session.url });
} catch (err) {
log.result({ cmd: 'auto_restore' }, { ok: false, error: (err as Error).message });
}
}
}
}
}
@@ -613,6 +676,45 @@ server.tool(
{},
withLogging('close', async () => {
if (launched) {
// Auto-save session before closing
if (userConfig.autoSave) {
try {
const page = browser.getPage();
const context = browser.getContext();
if (page && context) {
const url = page.url();
const title = await page.title();
const cookies = await context.cookies();
const storage = (await page.evaluate(`({
localStorage: Object.fromEntries(
Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i))
.filter(k => k !== null)
.map(k => [k, localStorage.getItem(k) || ''])
),
sessionStorage: Object.fromEntries(
Array.from({ length: sessionStorage.length }, (_, i) => sessionStorage.key(i))
.filter(k => k !== null)
.map(k => [k, sessionStorage.getItem(k) || ''])
),
})`)) as {
localStorage: Record<string, string>;
sessionStorage: Record<string, string>;
};
saveSession({
url,
title,
cookies,
localStorage: storage.localStorage,
sessionStorage: storage.sessionStorage,
});
log.command({ cmd: 'auto_save', url });
}
} catch (err) {
log.result({ cmd: 'auto_save' }, { ok: false, error: (err as Error).message });
}
}
await browser.close();
launched = false;
}
@@ -730,9 +832,9 @@ server.tool(
// Browser import
server.tool(
'import',
'Import cookies from Safari or Firefox browser. Safari requires Full Disk Access permission (macOS only). Firefox works on macOS, Linux, and Windows.',
'Import cookies from Safari, Firefox, or Chrome browser. Safari requires Full Disk Access (macOS). Chrome requires Keychain access (macOS). Firefox works on all platforms.',
{
source: z.enum(['safari', 'firefox']).describe('Browser to import from'),
source: z.enum(['safari', 'firefox', 'chrome']).describe('Browser to import from'),
domain: z
.string()
.optional()
+2 -2
View File
@@ -302,10 +302,10 @@ export interface EmulateCommand {
device: string;
}
// Browser import (Safari, Firefox)
// Browser import (Safari, Firefox, Chrome)
export interface ImportCommand {
cmd: 'import';
source: 'safari' | 'firefox';
source: 'safari' | 'firefox' | 'chrome';
domain?: string;
profile?: string;
}