v0.4.1: Chrome cookie import, config.json, session persistence
- 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:
Vendored
+1
-1
@@ -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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"}
|
||||
Vendored
+24
@@ -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) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+52
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+217
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+96
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+165
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+2
@@ -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';
|
||||
|
||||
Vendored
+1
-1
@@ -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"}
|
||||
Vendored
+2
@@ -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
|
||||
Vendored
+1
-1
@@ -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
@@ -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()
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -255,7 +255,7 @@ export interface EmulateCommand {
|
||||
}
|
||||
export interface ImportCommand {
|
||||
cmd: 'import';
|
||||
source: 'safari' | 'firefox';
|
||||
source: 'safari' | 'firefox' | 'chrome';
|
||||
domain?: string;
|
||||
profile?: string;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user