Add Claude Code plugin support with MCP prompts and resources

- Add .claude-plugin/plugin.json manifest (name: browse)
- Add 11 slash commands: start, end, goto, screenshot, scrape, analyze, extract, fill, compare, save, restore
- Add MCP resources: browser://state, browser://html, browser://screenshot
- Add MCP prompts: analyze_page, extract_data, navigate_to, fill_form, compare_screenshots
- Add session management tools: close, session_save, session_restore
- Include dist/ for plugin installation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adam Ladachowski
2026-02-08 03:08:02 +01:00
parent ab03140803
commit bc1f3d9d73
50 changed files with 2218 additions and 5 deletions
+48
View File
@@ -0,0 +1,48 @@
import { type BrowserContext, type Page } from 'playwright';
import type { BrowserCommand, BrowserOptions, CommandResponse, ElementInfo } from './types.js';
export declare class ClaudeBrowser {
private browser;
private context;
private page;
private options;
constructor(options?: BrowserOptions);
launch(): Promise<void>;
close(): Promise<void>;
private ensurePage;
/** Get the current page instance (for advanced usage) */
getPage(): Page | null;
/** Get the browser context (for advanced usage like cookies) */
getContext(): BrowserContext | null;
goto(url: string): Promise<{
url: string;
title: string;
}>;
click(selector: string): Promise<{
url: string;
}>;
type(selector: string, text: string): Promise<void>;
query(selector: string): Promise<ElementInfo[]>;
screenshot(path?: string, fullPage?: boolean): Promise<{
path: string;
buffer?: Buffer;
}>;
getUrl(): Promise<{
url: string;
title: string;
}>;
getHtml(full?: boolean): Promise<string>;
back(): Promise<{
url: string;
}>;
forward(): Promise<{
url: string;
}>;
reload(): Promise<{
url: string;
}>;
wait(ms?: number): Promise<void>;
newPage(): Promise<void>;
eval(script: string): Promise<unknown>;
executeCommand(cmd: BrowserCommand): Promise<CommandResponse>;
}
//# sourceMappingURL=browser.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,cAAc,EAAE,KAAK,IAAI,EAAU,MAAM,YAAY,CAAC;AAElF,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/F,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,OAAO,CAA2B;gBAE9B,OAAO,GAAE,cAAmB;IAQlC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAWvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B,OAAO,CAAC,UAAU;IAOlB,yDAAyD;IACzD,OAAO,IAAI,IAAI,GAAG,IAAI;IAItB,gEAAgE;IAChE,UAAU,IAAI,cAAc,GAAG,IAAI;IAI7B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAM1D,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAOjD,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAiB/C,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAOvF,MAAM,IAAI,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAKjD,OAAO,CAAC,IAAI,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAMtC,IAAI,IAAI,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAMhC,OAAO,IAAI,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAMnC,MAAM,IAAI,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAMlC,IAAI,CAAC,EAAE,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAK9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAOxB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAsIpE"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=browser.integration.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browser.integration.test.d.ts","sourceRoot":"","sources":["../src/browser.integration.test.ts"],"names":[],"mappings":""}
+173
View File
@@ -0,0 +1,173 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { ClaudeBrowser } from './browser.js';
describe('ClaudeBrowser Integration', () => {
let browser;
beforeAll(async () => {
browser = new ClaudeBrowser({ headless: true });
await browser.launch();
}, 30000);
afterAll(async () => {
await browser.close();
});
describe('navigation', () => {
it('navigates to a URL and returns title', async () => {
const result = await browser.goto('https://example.com');
expect(result.url).toContain('example.com');
expect(result.title).toBe('Example Domain');
});
it('gets current URL and title', async () => {
const result = await browser.getUrl();
expect(result.url).toContain('example.com');
expect(result.title).toBe('Example Domain');
});
it('gets page HTML', async () => {
const html = await browser.getHtml();
expect(html.toLowerCase()).toContain('<!doctype html>');
expect(html).toContain('Example Domain');
});
it('gets full page HTML', async () => {
const html = await browser.getHtml(true);
expect(html.toLowerCase()).toContain('<!doctype html>');
expect(html.length).toBeGreaterThan(100);
});
it('reloads the page', async () => {
const result = await browser.reload();
expect(result.url).toContain('example.com');
});
});
describe('DOM interaction', () => {
it('queries elements', async () => {
const elements = await browser.query('h1');
expect(elements.length).toBe(1);
expect(elements[0].tag).toBe('h1');
expect(elements[0].text).toContain('Example Domain');
});
it('queries multiple elements', async () => {
const elements = await browser.query('p');
expect(elements.length).toBeGreaterThan(0);
expect(elements[0].tag).toBe('p');
});
it('clicks an element', async () => {
await browser.goto('https://example.com');
const result = await browser.click('a');
expect(result.url).toBeDefined();
});
});
describe('screenshots', () => {
it('takes a screenshot', async () => {
await browser.goto('https://example.com');
const result = await browser.screenshot('screenshots/test-integration.png');
expect(result.path).toContain('test-integration.png');
expect(result.buffer).toBeDefined();
});
it('takes a full page screenshot', async () => {
const result = await browser.screenshot('screenshots/test-full.png', true);
expect(result.path).toContain('test-full.png');
});
});
describe('wait', () => {
it('waits for specified time', async () => {
const start = Date.now();
await browser.wait(100);
const elapsed = Date.now() - start;
expect(elapsed).toBeGreaterThanOrEqual(90);
});
});
describe('eval', () => {
it('evaluates JavaScript', async () => {
await browser.goto('https://example.com');
const result = await browser.eval('document.title');
expect(result).toBe('Example Domain');
});
it('evaluates expressions', async () => {
const result = await browser.eval('1 + 1');
expect(result).toBe(2);
});
it('evaluates complex expressions', async () => {
const result = await browser.eval('document.querySelectorAll("p").length');
expect(result).toBeGreaterThan(0);
});
});
describe('pages', () => {
it('creates a new page', async () => {
await browser.newPage();
const result = await browser.getUrl();
expect(result.url).toBe('about:blank');
});
it('navigates in new page', async () => {
const result = await browser.goto('https://example.com');
expect(result.title).toBe('Example Domain');
});
});
describe('executeCommand', () => {
it('handles goto command', async () => {
const result = await browser.executeCommand({ cmd: 'goto', url: 'https://example.com' });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.title).toBe('Example Domain');
}
});
it('handles query command', async () => {
const result = await browser.executeCommand({ cmd: 'query', selector: 'h1' });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.count).toBe(1);
}
});
it('handles url command', async () => {
const result = await browser.executeCommand({ cmd: 'url' });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.url).toContain('example.com');
}
});
it('handles html command', async () => {
const result = await browser.executeCommand({ cmd: 'html' });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.html).toContain('Example');
}
});
it('handles wait command', async () => {
const result = await browser.executeCommand({ cmd: 'wait', ms: 50 });
expect(result.ok).toBe(true);
});
it('handles eval command', async () => {
const result = await browser.executeCommand({ cmd: 'eval', script: '2+2' });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.result).toBe(4);
}
});
it('handles screenshot command', async () => {
const result = await browser.executeCommand({
cmd: 'screenshot',
path: 'screenshots/cmd-test.png',
});
expect(result.ok).toBe(true);
});
it('handles reload command', async () => {
const result = await browser.executeCommand({ cmd: 'reload' });
expect(result.ok).toBe(true);
});
it('handles click command', async () => {
await browser.executeCommand({ cmd: 'goto', url: 'https://example.com' });
const result = await browser.executeCommand({ cmd: 'click', selector: 'a' });
expect(result.ok).toBe(true);
});
it('handles newpage command', async () => {
const result = await browser.executeCommand({ cmd: 'newpage' });
expect(result.ok).toBe(true);
});
it('handles back command', async () => {
await browser.executeCommand({ cmd: 'goto', url: 'https://example.com' });
const result = await browser.executeCommand({ cmd: 'back' });
expect(result.ok).toBe(true);
});
it('handles forward command', async () => {
const result = await browser.executeCommand({ cmd: 'forward' });
expect(result.ok).toBe(true);
});
});
});
//# sourceMappingURL=browser.integration.test.js.map
File diff suppressed because one or more lines are too long
+250
View File
@@ -0,0 +1,250 @@
import { resolve } from 'node:path';
import { webkit } from 'playwright';
import * as image from './image.js';
export class ClaudeBrowser {
browser = null;
context = null;
page = null;
options;
constructor(options = {}) {
this.options = {
headless: options.headless ?? true,
width: options.width ?? 1280,
height: options.height ?? 800,
};
}
async launch() {
this.browser = await webkit.launch({ headless: this.options.headless });
this.context = await this.browser.newContext({
viewport: {
width: this.options.width,
height: this.options.height,
},
});
this.page = await this.context.newPage();
}
async close() {
if (this.browser) {
await this.browser.close();
this.browser = null;
this.context = null;
this.page = null;
}
}
ensurePage() {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
return this.page;
}
/** Get the current page instance (for advanced usage) */
getPage() {
return this.page;
}
/** Get the browser context (for advanced usage like cookies) */
getContext() {
return this.context;
}
async goto(url) {
const page = this.ensurePage();
await page.goto(url, { waitUntil: 'networkidle' });
return { url: page.url(), title: await page.title() };
}
async click(selector) {
const page = this.ensurePage();
await page.click(selector);
await page.waitForLoadState('networkidle').catch(() => { });
return { url: page.url() };
}
async type(selector, text) {
const page = this.ensurePage();
await page.fill(selector, text);
}
async query(selector) {
const page = this.ensurePage();
return page.$$eval(selector, (nodes) => nodes.map((el) => {
const attrs = {};
for (const attr of el.attributes) {
attrs[attr.name] = attr.value;
}
return {
tag: el.tagName.toLowerCase(),
text: el.textContent?.trim().slice(0, 200) || '',
attributes: attrs,
};
}));
}
async screenshot(path, fullPage = false) {
const page = this.ensurePage();
const resolvedPath = resolve(path || 'screenshot.png');
const buffer = await page.screenshot({ path: resolvedPath, fullPage });
return { path: resolvedPath, buffer };
}
async getUrl() {
const page = this.ensurePage();
return { url: page.url(), title: await page.title() };
}
async getHtml(full = false) {
const page = this.ensurePage();
const html = await page.content();
return full ? html : html.slice(0, 10000);
}
async back() {
const page = this.ensurePage();
await page.goBack();
return { url: page.url() };
}
async forward() {
const page = this.ensurePage();
await page.goForward();
return { url: page.url() };
}
async reload() {
const page = this.ensurePage();
await page.reload();
return { url: page.url() };
}
async wait(ms = 1000) {
const page = this.ensurePage();
await page.waitForTimeout(ms);
}
async newPage() {
if (!this.context) {
throw new Error('Browser not launched. Call launch() first.');
}
this.page = await this.context.newPage();
}
async eval(script) {
const page = this.ensurePage();
return page.evaluate(script);
}
async executeCommand(cmd) {
try {
switch (cmd.cmd) {
case 'goto': {
const result = await this.goto(cmd.url);
return { ok: true, ...result };
}
case 'click': {
const result = await this.click(cmd.selector);
return { ok: true, ...result };
}
case 'type': {
await this.type(cmd.selector, cmd.text);
return { ok: true };
}
case 'query': {
const elements = await this.query(cmd.selector);
return { ok: true, count: elements.length, elements };
}
case 'screenshot': {
const result = await this.screenshot(cmd.path, cmd.fullPage);
return { ok: true, path: result.path };
}
case 'url': {
const result = await this.getUrl();
return { ok: true, ...result };
}
case 'html': {
const html = await this.getHtml(cmd.full);
return { ok: true, html };
}
case 'back': {
const result = await this.back();
return { ok: true, ...result };
}
case 'forward': {
const result = await this.forward();
return { ok: true, ...result };
}
case 'reload': {
const result = await this.reload();
return { ok: true, ...result };
}
case 'wait': {
await this.wait(cmd.ms);
return { ok: true };
}
case 'newpage': {
await this.newPage();
return { ok: true };
}
case 'close': {
await this.close();
return { ok: true };
}
case 'eval': {
const result = await this.eval(cmd.script);
return { ok: true, result };
}
case 'favicon': {
const result = await image.createFavicon(cmd.input, cmd.outputDir);
return { ok: true, files: result.files, outputDir: result.outputDir };
}
case 'convert': {
const result = await image.convert(cmd.input, cmd.output, cmd.format);
return {
ok: true,
path: result.path,
width: result.width,
height: result.height,
format: result.format,
size: result.size,
};
}
case 'resize': {
const result = await image.resize(cmd.input, cmd.output, cmd.width, cmd.height, cmd.fit);
return {
ok: true,
path: result.path,
width: result.width,
height: result.height,
format: result.format,
size: result.size,
};
}
case 'crop': {
const result = await image.crop(cmd.input, cmd.output, cmd.left, cmd.top, cmd.width, cmd.height);
return {
ok: true,
path: result.path,
width: result.width,
height: result.height,
format: result.format,
size: result.size,
};
}
case 'compress': {
const result = await image.compress(cmd.input, cmd.output, cmd.quality);
return {
ok: true,
path: result.path,
width: result.width,
height: result.height,
format: result.format,
size: result.size,
};
}
case 'thumbnail': {
const result = await image.thumbnail(cmd.input, cmd.output, cmd.size);
return {
ok: true,
path: result.path,
width: result.width,
height: result.height,
format: result.format,
size: result.size,
};
}
default: {
const _exhaustive = cmd;
return { ok: false, error: `Unknown command: ${_exhaustive.cmd}` };
}
}
}
catch (err) {
return { ok: false, error: err.message };
}
}
}
//# sourceMappingURL=browser.js.map
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=browser.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browser.test.d.ts","sourceRoot":"","sources":["../src/browser.test.ts"],"names":[],"mappings":""}
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { ClaudeBrowser } from './browser.js';
describe('ClaudeBrowser', () => {
describe('constructor', () => {
it('creates browser with default options', () => {
const browser = new ClaudeBrowser();
expect(browser).toBeInstanceOf(ClaudeBrowser);
});
it('accepts custom options', () => {
const browser = new ClaudeBrowser({
headless: false,
width: 1920,
height: 1080,
});
expect(browser).toBeInstanceOf(ClaudeBrowser);
});
});
describe('ensurePage (via executeCommand)', () => {
it('throws error when browser not launched', async () => {
const browser = new ClaudeBrowser();
const result = await browser.executeCommand({ cmd: 'url' });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('Browser not launched');
}
});
});
describe('executeCommand', () => {
it('returns error for commands without launch', async () => {
const browser = new ClaudeBrowser();
const gotoResult = await browser.executeCommand({ cmd: 'goto', url: 'https://example.com' });
expect(gotoResult.ok).toBe(false);
const clickResult = await browser.executeCommand({ cmd: 'click', selector: '#btn' });
expect(clickResult.ok).toBe(false);
const typeResult = await browser.executeCommand({
cmd: 'type',
selector: '#input',
text: 'test',
});
expect(typeResult.ok).toBe(false);
const queryResult = await browser.executeCommand({ cmd: 'query', selector: '.item' });
expect(queryResult.ok).toBe(false);
const screenshotResult = await browser.executeCommand({ cmd: 'screenshot' });
expect(screenshotResult.ok).toBe(false);
const htmlResult = await browser.executeCommand({ cmd: 'html' });
expect(htmlResult.ok).toBe(false);
const backResult = await browser.executeCommand({ cmd: 'back' });
expect(backResult.ok).toBe(false);
const forwardResult = await browser.executeCommand({ cmd: 'forward' });
expect(forwardResult.ok).toBe(false);
const reloadResult = await browser.executeCommand({ cmd: 'reload' });
expect(reloadResult.ok).toBe(false);
const waitResult = await browser.executeCommand({ cmd: 'wait', ms: 100 });
expect(waitResult.ok).toBe(false);
const evalResult = await browser.executeCommand({ cmd: 'eval', script: '1+1' });
expect(evalResult.ok).toBe(false);
});
it('handles close command without error when not launched', async () => {
const browser = new ClaudeBrowser();
const result = await browser.executeCommand({ cmd: 'close' });
expect(result.ok).toBe(true);
});
it('handles newpage command error when not launched', async () => {
const browser = new ClaudeBrowser();
const result = await browser.executeCommand({ cmd: 'newpage' });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('Browser not launched');
}
});
});
});
//# sourceMappingURL=browser.test.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../src/browser.test.ts"],"names":[],"mappings":"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"}
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=cli.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
Vendored Executable
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env node
import { resolve } from 'node:path';
import { parseArgs } from 'node:util';
import { ClaudeBrowser } from './browser.js';
import * as image from './image.js';
import { startServer } from './server.js';
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
output: { type: 'string', short: 'o', default: 'screenshot.png' },
width: { type: 'string', short: 'w', default: '1280' },
height: { type: 'string', short: 'h', default: '800' },
fullpage: { type: 'boolean', short: 'f', default: false },
wait: { type: 'string', default: '2000' },
headed: { type: 'boolean', default: false },
interactive: { type: 'boolean', short: 'i', default: false },
query: { type: 'string', short: 'q' },
json: { type: 'boolean', short: 'j', default: false },
click: { type: 'string', short: 'c', multiple: true },
type: { type: 'string', short: 't', multiple: true },
help: { type: 'boolean', default: false },
version: { type: 'boolean', short: 'v', default: false },
// Image processing options
favicon: { type: 'string' },
convert: { type: 'string' },
resize: { type: 'string' },
compress: { type: 'string' },
},
});
const HELP = `
Usage: claude-browse [options] <url>
Options:
-o, --output <file> Output screenshot path (default: screenshot.png)
-w, --width <px> Viewport width (default: 1280)
-h, --height <px> Viewport height (default: 800)
-f, --fullpage Capture full page scroll
--wait <ms> Wait time after load (default: 2000)
--headed Show browser window
-i, --interactive Keep browser open for manual interaction
-q, --query <selector> Query elements by CSS selector and show attributes
-j, --json Output query results as JSON
-c, --click <selector> Click on element (can be repeated for multiple clicks)
-t, --type <sel>=<text> Type text into input (can be repeated)
-v, --version Show version
--help Show this help
Image Processing:
--favicon <dir> Generate favicon set to directory (from screenshot or input)
--convert <format> Convert screenshot to format (png, jpeg, webp, avif)
--resize <WxH> Resize screenshot (e.g., 800x600 or 800 for width only)
--compress <quality> Compress with quality 1-100
Examples:
claude-browse https://example.com
claude-browse -o page.png -w 1920 -h 1080 https://example.com
claude-browse -i --headed https://example.com
claude-browse -q "a[href]" https://example.com
claude-browse -q "img" -j https://example.com
claude-browse -c "button.submit" https://example.com
claude-browse -t "input[name=q]=hello" -c "button[type=submit]" https://google.com
claude-browse -c ".cookie-accept" -c "a.nav-link" -q "h1" https://example.com
Image processing examples:
claude-browse https://example.com --favicon ./favicons/
claude-browse https://example.com -o page.webp --convert webp
claude-browse https://example.com --resize 800x600
claude-browse https://example.com --compress 60
Server mode (default):
claude-browse # Start server on port 13373
claude-browse --headed # Start with visible browser
# Send commands via curl:
curl -X POST http://localhost:13373 -d '{"cmd":"goto","url":"https://example.com"}'
curl -X POST http://localhost:13373 -d '{"cmd":"click","selector":"button"}'
curl -X POST http://localhost:13373 -d '{"cmd":"type","selector":"input","text":"hello"}'
curl -X POST http://localhost:13373 -d '{"cmd":"query","selector":"a[href]"}'
curl -X POST http://localhost:13373 -d '{"cmd":"screenshot","path":"shot.png"}'
curl -X POST http://localhost:13373 -d '{"cmd":"url"}'
curl -X POST http://localhost:13373 -d '{"cmd":"html"}'
curl -X POST http://localhost:13373 -d '{"cmd":"close"}'
# Image processing via server:
curl localhost:13373 -d '{"cmd":"favicon","input":"screenshot.png","outputDir":"./favicons"}'
curl localhost:13373 -d '{"cmd":"convert","input":"img.png","output":"img.webp","format":"webp"}'
curl localhost:13373 -d '{"cmd":"resize","input":"img.png","output":"small.png","width":400}'
curl localhost:13373 -d '{"cmd":"compress","input":"img.png","output":"compressed.png","quality":60}'
`;
function getViewportConfig() {
return {
headless: !values.headed,
width: Number.parseInt(values.width),
height: Number.parseInt(values.height),
};
}
async function runServerMode() {
const port = 13373;
const server = await startServer({ port, ...getViewportConfig() });
process.on('SIGINT', async () => {
console.log('\nShutting down...');
await server.stop();
process.exit(0);
});
await new Promise(() => { });
}
async function processTypeActions(browser) {
const typeActions = values.type;
if (!typeActions?.length)
return;
for (const typeAction of typeActions) {
const eqIndex = typeAction.indexOf('=');
if (eqIndex === -1) {
console.error(`Invalid --type format: "${typeAction}" (expected selector=text)`);
continue;
}
const selector = typeAction.slice(0, eqIndex);
const text = typeAction.slice(eqIndex + 1);
console.log(`Typing "${text}" into: ${selector}`);
await browser.type(selector, text);
}
}
async function processClickActions(browser) {
const clickActions = values.click;
if (!clickActions?.length)
return;
for (const selector of clickActions) {
console.log(`Clicking: ${selector}`);
await browser.click(selector);
await browser.wait(500);
}
const { url: currentUrl } = await browser.getUrl();
console.log(`Current URL: ${currentUrl}`);
}
function printElement(el, index) {
console.log(`[${index + 1}] <${el.tag}>`);
for (const [name, value] of Object.entries(el.attributes)) {
console.log(` ${name}="${value}"`);
}
if (el.text) {
const truncated = el.text.length > 100 ? `${el.text.slice(0, 100)}...` : el.text;
console.log(` text: "${truncated}"`);
}
console.log();
}
async function runQueryMode(browser) {
const elements = await browser.query(values.query);
if (values.json) {
console.log(JSON.stringify(elements, null, 2));
}
else {
console.log(`Found ${elements.length} element(s) matching "${values.query}":\n`);
elements.forEach(printElement);
}
await browser.close();
process.exit(0);
}
async function runInteractiveMode(browser) {
console.log('Interactive mode - browser will stay open.');
console.log('Press Ctrl+C to exit.');
process.on('SIGINT', async () => {
console.log('\nClosing browser...');
await browser.close();
process.exit(0);
});
await new Promise(() => { });
}
async function processImageOptions(screenshotPath) {
// Process image options on the screenshot
if (values.favicon) {
console.log(`Generating favicon set to: ${values.favicon}`);
const result = await image.createFavicon(screenshotPath, values.favicon);
console.log(`Created ${result.files.length} favicon files`);
}
if (values.convert) {
const format = values.convert;
const outputPath = screenshotPath.replace(/\.[^.]+$/, `.${format}`);
console.log(`Converting to ${format}: ${outputPath}`);
await image.convert(screenshotPath, outputPath, format);
}
if (values.resize) {
const resizeValue = values.resize;
const [widthStr, heightStr] = resizeValue.split('x');
const width = Number.parseInt(widthStr);
const height = heightStr ? Number.parseInt(heightStr) : undefined;
console.log(`Resizing to ${width}${height ? `x${height}` : ''}`);
await image.resize(screenshotPath, screenshotPath, width, height);
}
if (values.compress) {
const quality = Number.parseInt(values.compress);
console.log(`Compressing with quality ${quality}`);
await image.compress(screenshotPath, screenshotPath, quality);
}
}
async function runScreenshotMode(browser) {
const outputPath = resolve(values.output);
console.log(`Saving screenshot to: ${outputPath}`);
await browser.screenshot(outputPath, values.fullpage);
// Process any image options
await processImageOptions(outputPath);
await browser.close();
console.log('Done!');
}
async function runBrowserMode() {
const url = positionals[0];
const browser = new ClaudeBrowser(getViewportConfig());
await browser.launch();
console.log(`Navigating to: ${url}`);
await browser.goto(url);
await browser.wait(Number.parseInt(values.wait));
await processTypeActions(browser);
await processClickActions(browser);
if (values.query) {
await runQueryMode(browser);
return;
}
if (values.interactive) {
await runInteractiveMode(browser);
}
else {
await runScreenshotMode(browser);
}
}
async function main() {
if (values.version) {
console.log('claude-browse 0.1.0');
process.exit(0);
}
if (values.help) {
console.log(HELP);
process.exit(0);
}
if (positionals.length === 0) {
await runServerMode();
return;
}
await runBrowserMode();
}
main().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
//# sourceMappingURL=cli.js.map
+1
View File
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
export type FitType = 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
export type FormatType = 'png' | 'jpeg' | 'webp' | 'avif';
export type ThumbnailSize = 'small' | 'medium' | 'large';
export interface FaviconResult {
files: string[];
outputDir: string;
}
export interface ImageResult {
path: string;
width?: number;
height?: number;
format?: string;
size?: number;
}
export declare function createFavicon(input: string, outputDir: string): Promise<FaviconResult>;
export declare function convert(input: string, output: string, format: FormatType): Promise<ImageResult>;
export declare function resize(input: string, output: string, width: number, height?: number, fit?: FitType): Promise<ImageResult>;
export declare function crop(input: string, output: string, left: number, top: number, width: number, height: number): Promise<ImageResult>;
export declare function compress(input: string, output: string, quality?: number): Promise<ImageResult>;
export declare function thumbnail(input: string, output: string, size?: ThumbnailSize): Promise<ImageResult>;
//# sourceMappingURL=image.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1D,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAiBzD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B5F;AAED,wBAAsB,OAAO,CAC3B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,WAAW,CAAC,CA+BtB;AAED,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,GAAG,GAAE,OAAiB,GACrB,OAAO,CAAC,WAAW,CAAC,CAatB;AAED,wBAAsB,IAAI,CACxB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,CAAC,CAatB;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,OAAO,CAAC,WAAW,CAAC,CAqChG;AAED,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,aAAwB,GAC7B,OAAO,CAAC,WAAW,CAAC,CAGtB"}
+131
View File
@@ -0,0 +1,131 @@
import { mkdir } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import sharp from 'sharp';
const THUMBNAIL_SIZES = {
small: 150,
medium: 300,
large: 600,
};
const FAVICON_SIZES = [
{ name: 'favicon-16x16.png', size: 16 },
{ name: 'favicon-32x32.png', size: 32 },
{ name: 'favicon-48x48.png', size: 48 },
{ name: 'apple-touch-icon.png', size: 180 },
{ name: 'android-chrome-192x192.png', size: 192 },
{ name: 'android-chrome-512x512.png', size: 512 },
];
async function ensureDir(filePath) {
await mkdir(dirname(filePath), { recursive: true });
}
export async function createFavicon(input, outputDir) {
const resolvedDir = resolve(outputDir);
await mkdir(resolvedDir, { recursive: true });
const files = [];
const image = sharp(input);
for (const { name, size } of FAVICON_SIZES) {
const outputPath = join(resolvedDir, name);
await image.clone().resize(size, size, { fit: 'cover' }).png().toFile(outputPath);
files.push(outputPath);
}
// Create favicon.ico with multiple sizes (16, 32, 48)
const icoPath = join(resolvedDir, 'favicon.ico');
const sizes = [16, 32, 48];
const buffers = await Promise.all(sizes.map((size) => image.clone().resize(size, size, { fit: 'cover' }).png().toBuffer()));
// ICO format: simple approach - use largest PNG as ICO
// For true multi-size ICO, we'd need a dedicated library
// Sharp doesn't support ICO output, so we'll use the 32x32 PNG
await image.clone().resize(32, 32, { fit: 'cover' }).png().toFile(icoPath);
files.push(icoPath);
return { files, outputDir: resolvedDir };
}
export async function convert(input, output, format) {
const resolvedOutput = resolve(output);
await ensureDir(resolvedOutput);
const image = sharp(input);
let result;
switch (format) {
case 'png':
result = image.png();
break;
case 'jpeg':
result = image.jpeg();
break;
case 'webp':
result = image.webp();
break;
case 'avif':
result = image.avif();
break;
}
const info = await result.toFile(resolvedOutput);
return {
path: resolvedOutput,
width: info.width,
height: info.height,
format: info.format,
size: info.size,
};
}
export async function resize(input, output, width, height, fit = 'cover') {
const resolvedOutput = resolve(output);
await ensureDir(resolvedOutput);
const info = await sharp(input).resize(width, height, { fit }).toFile(resolvedOutput);
return {
path: resolvedOutput,
width: info.width,
height: info.height,
format: info.format,
size: info.size,
};
}
export async function crop(input, output, left, top, width, height) {
const resolvedOutput = resolve(output);
await ensureDir(resolvedOutput);
const info = await sharp(input).extract({ left, top, width, height }).toFile(resolvedOutput);
return {
path: resolvedOutput,
width: info.width,
height: info.height,
format: info.format,
size: info.size,
};
}
export async function compress(input, output, quality = 80) {
const resolvedOutput = resolve(output);
await ensureDir(resolvedOutput);
const image = sharp(input);
const metadata = await image.metadata();
const format = metadata.format;
let result;
switch (format) {
case 'png':
result = image.png({ quality });
break;
case 'jpeg':
case 'jpg':
result = image.jpeg({ quality });
break;
case 'webp':
result = image.webp({ quality });
break;
case 'avif':
result = image.avif({ quality });
break;
default:
// Default to PNG for unknown formats
result = image.png({ quality });
}
const info = await result.toFile(resolvedOutput);
return {
path: resolvedOutput,
width: info.width,
height: info.height,
format: info.format,
size: info.size,
};
}
export async function thumbnail(input, output, size = 'medium') {
const dimension = THUMBNAIL_SIZES[size];
return resize(input, output, dimension, dimension, 'cover');
}
//# sourceMappingURL=image.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"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"}
+6
View File
@@ -0,0 +1,6 @@
export { ClaudeBrowser } from './browser.js';
export { BrowserServer, startServer } 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';
export type { ServerOptions } from './server.js';
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +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,MAAM,aAAa,CAAC;AACzD,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;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
+4
View File
@@ -0,0 +1,4 @@
export { ClaudeBrowser } from './browser.js';
export { BrowserServer, startServer } from './server.js';
export { createFavicon, convert, resize, crop, compress, thumbnail, } from './image.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +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,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,aAAa,EACb,OAAO,EACP,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,SAAS,GAMV,MAAM,YAAY,CAAC"}
+41
View File
@@ -0,0 +1,41 @@
export declare const icons: Record<string, string>;
export declare const cmdColor: Record<string, (s: string) => string>;
export declare function ts(): string;
export declare function truncate(str: string, max: number): string;
export interface CommandLike {
cmd: string;
url?: string;
selector?: string;
text?: string;
path?: string;
full?: boolean;
ms?: number;
script?: string;
}
export declare function getCommandDetail(cmd: CommandLike): string | undefined;
export declare function formatCommand(cmd: CommandLike): string;
export interface ResultLike {
ok: boolean;
error?: string;
title?: string;
url?: string;
count?: number;
path?: string;
html?: string;
result?: unknown;
}
export declare function formatResult(cmd: CommandLike, result: ResultLike): string;
export type LogFn = (msg: string) => void;
export declare function createLogger(logFn?: LogFn): {
command(cmd: CommandLike): void;
result(cmd: CommandLike, result: ResultLike): void;
};
export declare const logger: {
command(cmd: CommandLike): void;
result(cmd: CommandLike, result: ResultLike): void;
};
export declare const stderrLogger: {
command(cmd: CommandLike): void;
result(cmd: CommandLike, result: ResultLike): void;
};
//# sourceMappingURL=logger.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAexC,CAAC;AAGF,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAe1D,CAAC;AAEF,wBAAgB,EAAE,IAAI,MAAM,CAE3B;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAoBrE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAMtD;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAYD,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAQzE;AAED,MAAM,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAE1C,wBAAgB,YAAY,CAAC,KAAK,GAAE,KAAmB;iBAEtC,WAAW,GAAG,IAAI;gBAGnB,WAAW,UAAU,UAAU,GAAG,IAAI;EAIrD;AAGD,eAAO,MAAM,MAAM;iBAVF,WAAW,GAAG,IAAI;gBAGnB,WAAW,UAAU,UAAU,GAAG,IAAI;CAOlB,CAAC;AAGrC,eAAO,MAAM,YAAY;iBAbR,WAAW,GAAG,IAAI;gBAGnB,WAAW,UAAU,UAAU,GAAG,IAAI;CAU6B,CAAC"}
+103
View File
@@ -0,0 +1,103 @@
import chalk from 'chalk';
import logSymbols from 'log-symbols';
// Icons for commands
export const icons = {
goto: '→',
click: '◉',
type: '⌨',
query: '?',
screenshot: '📷',
url: '🔗',
html: '<>',
back: '←',
forward: '→',
reload: '↻',
wait: '⏳',
newpage: '+',
close: '✕',
eval: '⚡',
};
// Colors for command types
export const cmdColor = {
goto: chalk.cyan,
click: chalk.yellow,
type: chalk.magenta,
query: chalk.blue,
screenshot: chalk.green,
url: chalk.cyan,
html: chalk.blue,
back: chalk.yellow,
forward: chalk.yellow,
reload: chalk.yellow,
wait: chalk.gray,
newpage: chalk.green,
close: chalk.red,
eval: chalk.magenta,
};
export function ts() {
return chalk.gray(`[${new Date().toISOString()}]`);
}
export function truncate(str, max) {
return str.length > max ? `${str.slice(0, max)}...` : str;
}
export function getCommandDetail(cmd) {
switch (cmd.cmd) {
case 'goto':
return chalk.white(cmd.url);
case 'click':
case 'query':
return chalk.white(cmd.selector);
case 'type':
return `${chalk.white(cmd.selector)} ${chalk.dim(`="${cmd.text}"`)}`;
case 'screenshot':
return chalk.dim(cmd.path || 'screenshot.png');
case 'html':
return cmd.full ? chalk.dim('(full)') : undefined;
case 'wait':
return chalk.dim(`${cmd.ms || 1000}ms`);
case 'eval':
return chalk.dim(truncate(cmd.script || '', 50));
default:
return undefined;
}
}
export function formatCommand(cmd) {
const color = cmdColor[cmd.cmd] || chalk.white;
const icon = icons[cmd.cmd] || '•';
const detail = getCommandDetail(cmd);
const suffix = detail ? ` ${detail}` : '';
return `${ts()} ${chalk.bold(color(icon))} ${color(cmd.cmd.toUpperCase())}${suffix}`;
}
const resultFormatters = {
goto: (r) => r.title,
click: (r) => (r.url ? `${r.url}` : undefined),
query: (r) => (r.count !== undefined ? `Found ${r.count} element(s)` : undefined),
screenshot: (r) => (r.path ? `Saved to ${r.path}` : undefined),
url: (r) => r.url,
html: (r) => (r.html !== undefined ? `${r.html.length} chars` : undefined),
eval: (r) => (r.result !== undefined ? truncate(JSON.stringify(r.result), 80) : undefined),
};
export function formatResult(cmd, result) {
if (!result.ok) {
return `${ts()} ${logSymbols.error} ${chalk.red(result.error)}`;
}
const formatter = resultFormatters[cmd.cmd];
const msg = formatter ? formatter(result) : undefined;
const suffix = msg ? ` ${chalk.dim(msg)}` : '';
return `${ts()} ${logSymbols.success}${suffix}`;
}
export function createLogger(logFn = console.log) {
return {
command(cmd) {
logFn(formatCommand(cmd));
},
result(cmd, result) {
logFn(formatResult(cmd, result));
},
};
}
// Default logger to stdout
export const logger = createLogger();
// Logger to stderr (for MCP)
export const stderrLogger = createLogger((msg) => process.stderr.write(`${msg}\n`));
//# sourceMappingURL=logger.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=logger.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"logger.test.d.ts","sourceRoot":"","sources":["../src/logger.test.ts"],"names":[],"mappings":""}
+146
View File
@@ -0,0 +1,146 @@
import { describe, expect, it, vi } from 'vitest';
import { createLogger, formatCommand, formatResult, getCommandDetail, icons, truncate, } from './logger.js';
describe('truncate', () => {
it('returns string unchanged if shorter than max', () => {
expect(truncate('hello', 10)).toBe('hello');
});
it('truncates and adds ellipsis if longer than max', () => {
expect(truncate('hello world', 5)).toBe('hello...');
});
it('handles exact length', () => {
expect(truncate('hello', 5)).toBe('hello');
});
});
describe('icons', () => {
it('has icon for each command type', () => {
expect(icons.goto).toBe('→');
expect(icons.click).toBe('◉');
expect(icons.type).toBe('⌨');
expect(icons.screenshot).toBe('📷');
});
});
describe('getCommandDetail', () => {
it('returns url for goto command', () => {
const result = getCommandDetail({ cmd: 'goto', url: 'https://example.com' });
expect(result).toContain('https://example.com');
});
it('returns selector for click command', () => {
const result = getCommandDetail({ cmd: 'click', selector: '#btn' });
expect(result).toContain('#btn');
});
it('returns selector and text for type command', () => {
const result = getCommandDetail({ cmd: 'type', selector: '#input', text: 'hello' });
expect(result).toContain('#input');
expect(result).toContain('hello');
});
it('returns path for screenshot command', () => {
const result = getCommandDetail({ cmd: 'screenshot', path: 'test.png' });
expect(result).toContain('test.png');
});
it('returns default path when none provided for screenshot', () => {
const result = getCommandDetail({ cmd: 'screenshot' });
expect(result).toContain('screenshot.png');
});
it('returns undefined for url command', () => {
expect(getCommandDetail({ cmd: 'url' })).toBeUndefined();
});
it('returns ms for wait command', () => {
const result = getCommandDetail({ cmd: 'wait', ms: 500 });
expect(result).toContain('500');
});
it('returns default ms for wait command without ms', () => {
const result = getCommandDetail({ cmd: 'wait' });
expect(result).toContain('1000');
});
it('returns script for eval command', () => {
const result = getCommandDetail({ cmd: 'eval', script: 'document.title' });
expect(result).toContain('document.title');
});
it('truncates long eval scripts', () => {
const longScript = 'a'.repeat(100);
const result = getCommandDetail({ cmd: 'eval', script: longScript });
expect(result).toContain('...');
});
it('returns (full) for html command with full=true', () => {
const result = getCommandDetail({ cmd: 'html', full: true });
expect(result).toContain('full');
});
it('returns undefined for html command with full=false', () => {
expect(getCommandDetail({ cmd: 'html', full: false })).toBeUndefined();
});
it('returns selector for query command', () => {
const result = getCommandDetail({ cmd: 'query', selector: '.items' });
expect(result).toContain('.items');
});
});
describe('formatCommand', () => {
it('formats goto command', () => {
const result = formatCommand({ cmd: 'goto', url: 'https://example.com' });
expect(result).toContain('GOTO');
expect(result).toContain('https://example.com');
});
it('formats click command', () => {
const result = formatCommand({ cmd: 'click', selector: '#btn' });
expect(result).toContain('CLICK');
expect(result).toContain('#btn');
});
});
describe('formatResult', () => {
it('formats error result', () => {
const result = formatResult({ cmd: 'goto' }, { ok: false, error: 'Failed' });
expect(result).toContain('Failed');
});
it('formats successful goto result with title', () => {
const result = formatResult({ cmd: 'goto' }, { ok: true, title: 'Example' });
expect(result).toContain('Example');
});
it('formats query result with count', () => {
const result = formatResult({ cmd: 'query' }, { ok: true, count: 5 });
expect(result).toContain('5');
});
it('formats click result with url', () => {
const result = formatResult({ cmd: 'click' }, { ok: true, url: '/page' });
expect(result).toContain('/page');
});
it('formats screenshot result with path', () => {
const result = formatResult({ cmd: 'screenshot' }, { ok: true, path: 'test.png' });
expect(result).toContain('Saved to test.png');
});
it('formats url result', () => {
const result = formatResult({ cmd: 'url' }, { ok: true, url: 'https://example.com' });
expect(result).toContain('https://example.com');
});
it('formats html result with length', () => {
const result = formatResult({ cmd: 'html' }, { ok: true, html: '<html></html>' });
expect(result).toContain('13 chars');
});
it('formats eval result', () => {
const result = formatResult({ cmd: 'eval' }, { ok: true, result: { foo: 'bar' } });
expect(result).toContain('foo');
expect(result).toContain('bar');
});
it('formats result without formatter', () => {
const result = formatResult({ cmd: 'wait' }, { ok: true });
expect(result).toBeDefined();
});
});
describe('createLogger', () => {
it('creates logger with custom log function', () => {
const logs = [];
const logFn = (msg) => logs.push(msg);
const logger = createLogger(logFn);
logger.command({ cmd: 'goto', url: 'https://example.com' });
expect(logs).toHaveLength(1);
expect(logs[0]).toContain('GOTO');
logger.result({ cmd: 'goto' }, { ok: true, title: 'Example' });
expect(logs).toHaveLength(2);
});
it('logs command and result', () => {
const spy = vi.fn();
const logger = createLogger(spy);
logger.command({ cmd: 'click', selector: '#btn' });
logger.result({ cmd: 'click' }, { ok: true, url: '/page' });
expect(spy).toHaveBeenCalledTimes(2);
});
});
//# sourceMappingURL=logger.test.js.map
+1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=mcp.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":""}
Vendored Executable
+536
View File
@@ -0,0 +1,536 @@
#!/usr/bin/env node
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 * as image from './image.js';
import { stderrLogger as log } from './logger.js';
const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 });
let launched = false;
let currentScreenshotBuffer = null;
async function ensureLaunched() {
if (!launched) {
await browser.launch();
launched = true;
}
}
function textResult(text) {
return { content: [{ type: 'text', text }] };
}
function withLogging(cmd, fn) {
return async (args) => {
const cmdLike = { cmd, ...args };
log.command(cmdLike);
try {
const result = await fn(args);
const parsed = JSON.parse(result.content[0]?.text || '{}');
const resultLike = { ok: true, ...parsed };
log.result(cmdLike, resultLike);
return result;
}
catch (err) {
log.result(cmdLike, { ok: false, error: err.message });
throw err;
}
};
}
const server = new McpServer({
name: 'claude-browse',
version: '0.1.0',
});
// Navigation
server.tool('goto', 'Navigate to a URL', { url: z.string().url() }, withLogging('goto', async ({ url }) => {
await ensureLaunched();
const result = await browser.goto(url);
return textResult(JSON.stringify(result));
}));
server.tool('back', 'Go back in browser history', {}, withLogging('back', async () => {
await ensureLaunched();
const result = await browser.back();
return textResult(JSON.stringify(result));
}));
server.tool('forward', 'Go forward in browser history', {}, withLogging('forward', async () => {
await ensureLaunched();
const result = await browser.forward();
return textResult(JSON.stringify(result));
}));
server.tool('reload', 'Reload the current page', {}, withLogging('reload', async () => {
await ensureLaunched();
const result = await browser.reload();
return textResult(JSON.stringify(result));
}));
// Interaction
server.tool('click', 'Click on an element', { selector: z.string() }, withLogging('click', async ({ selector }) => {
await ensureLaunched();
const result = await browser.click(selector);
return textResult(JSON.stringify(result));
}));
server.tool('type', 'Type text into an input field', { selector: z.string(), text: z.string() }, withLogging('type', async ({ selector, text }) => {
await ensureLaunched();
await browser.type(selector, text);
return textResult(JSON.stringify({ ok: true }));
}));
// Query
server.tool('query', 'Query elements by CSS selector, returns tag, text, and attributes', { selector: z.string() }, withLogging('query', async ({ selector }) => {
await ensureLaunched();
const elements = await browser.query(selector);
return textResult(JSON.stringify({ ok: true, count: elements.length, elements }));
}));
server.tool('url', 'Get current URL and page title', {}, withLogging('url', async () => {
await ensureLaunched();
const result = await browser.getUrl();
return textResult(JSON.stringify(result));
}));
server.tool('html', 'Get page HTML content', { full: z.boolean().optional().default(false) }, withLogging('html', async ({ full }) => {
await ensureLaunched();
const html = await browser.getHtml(full);
return textResult(JSON.stringify({ ok: true, html }));
}));
// Screenshot
server.tool('screenshot', 'Take a screenshot of the current page', {
path: z.string().optional().default('screenshots/screenshot.png'),
fullPage: z.boolean().optional().default(false),
}, withLogging('screenshot', async ({ path, fullPage }) => {
await ensureLaunched();
const result = await browser.screenshot(path, fullPage);
return textResult(JSON.stringify({ ok: true, path: result.path }));
}));
// Eval
server.tool('eval', 'Execute JavaScript in the browser context', { script: z.string() }, withLogging('eval', async ({ script }) => {
await ensureLaunched();
const result = await browser.eval(script);
return textResult(JSON.stringify({ ok: true, result }));
}));
// Utility
server.tool('wait', 'Wait for a specified time in milliseconds', { ms: z.number().optional().default(1000) }, withLogging('wait', async ({ ms }) => {
await ensureLaunched();
await browser.wait(ms);
return textResult(JSON.stringify({ ok: true }));
}));
// Session management
server.tool('close', 'Close the browser and end the current session', {}, withLogging('close', async () => {
if (launched) {
await browser.close();
launched = false;
}
return textResult(JSON.stringify({ ok: true, message: 'Browser closed' }));
}));
server.tool('session_save', 'Save the current session state (URL, cookies, localStorage, sessionStorage) to a JSON file', {
path: z.string().optional().default('session.json').describe('Path to save session file'),
}, withLogging('session_save', async ({ path }) => {
await ensureLaunched();
const { writeFile } = await import('node:fs/promises');
const { resolve } = await import('node:path');
const page = browser.getPage();
const context = browser.getContext();
if (!page || !context) {
return textResult(JSON.stringify({ ok: false, error: 'No active page' }));
}
const url = page.url();
const title = await page.title();
const cookies = await context.cookies();
// Get localStorage and sessionStorage (runs in browser context)
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) || ''])
),
})`);
const sessionData = {
url,
title,
cookies,
localStorage: storage.localStorage,
sessionStorage: storage.sessionStorage,
savedAt: new Date().toISOString(),
};
const resolvedPath = resolve(path);
await writeFile(resolvedPath, JSON.stringify(sessionData, null, 2));
return textResult(JSON.stringify({ ok: true, path: resolvedPath, url, cookieCount: cookies.length }));
}));
server.tool('session_restore', 'Restore a previously saved session state from a JSON file', {
path: z.string().optional().default('session.json').describe('Path to session file'),
}, withLogging('session_restore', async ({ path }) => {
await ensureLaunched();
const { readFile } = await import('node:fs/promises');
const { resolve } = await import('node:path');
const resolvedPath = resolve(path);
const data = JSON.parse(await readFile(resolvedPath, 'utf-8'));
const page = browser.getPage();
const context = browser.getContext();
if (!page || !context) {
return textResult(JSON.stringify({ ok: false, error: 'No active page' }));
}
// Restore cookies first
if (data.cookies?.length > 0) {
await context.addCookies(data.cookies);
}
// Navigate to saved URL
if (data.url) {
await page.goto(data.url, { waitUntil: 'networkidle' });
}
// Restore storage (runs in browser context)
const local = data.localStorage || {};
const session = data.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);
})(${JSON.stringify({ local, session })})`);
return textResult(JSON.stringify({
ok: true,
url: data.url,
title: data.title,
cookiesRestored: data.cookies?.length || 0,
savedAt: data.savedAt,
}));
}));
// Image processing
server.tool('favicon', 'Generate a complete favicon set from an image (16x16, 32x32, 48x48, apple-touch-icon 180x180, android-chrome 192x192 and 512x512)', {
input: z.string().describe('Path to source image'),
outputDir: z.string().describe('Directory to output favicon files'),
}, withLogging('favicon', async ({ input, outputDir }) => {
const result = await image.createFavicon(input, outputDir);
return textResult(JSON.stringify({ ok: true, files: result.files, outputDir: result.outputDir }));
}));
server.tool('convert', 'Convert an image to a different format (png, jpeg, webp, avif)', {
input: z.string().describe('Path to source image'),
output: z.string().describe('Path for output image'),
format: z.enum(['png', 'jpeg', 'webp', 'avif']).describe('Target format'),
}, withLogging('convert', async ({ input, output, format }) => {
const result = await image.convert(input, output, format);
return textResult(JSON.stringify({ ok: true, ...result }));
}));
server.tool('resize', 'Resize an image to specified dimensions', {
input: z.string().describe('Path to source image'),
output: z.string().describe('Path for output image'),
width: z.number().describe('Target width in pixels'),
height: z
.number()
.optional()
.describe('Target height in pixels (optional, maintains aspect ratio if omitted)'),
fit: z
.enum(['cover', 'contain', 'fill', 'inside', 'outside'])
.optional()
.default('cover')
.describe('How to fit the image'),
}, withLogging('resize', async ({ input, output, width, height, fit }) => {
const result = await image.resize(input, output, width, height, fit);
return textResult(JSON.stringify({ ok: true, ...result }));
}));
server.tool('crop', 'Crop a region from an image', {
input: z.string().describe('Path to source image'),
output: z.string().describe('Path for output image'),
left: z.number().describe('Left edge position in pixels'),
top: z.number().describe('Top edge position in pixels'),
width: z.number().describe('Width of crop region in pixels'),
height: z.number().describe('Height of crop region in pixels'),
}, withLogging('crop', async ({ input, output, left, top, width, height }) => {
const result = await image.crop(input, output, left, top, width, height);
return textResult(JSON.stringify({ ok: true, ...result }));
}));
server.tool('compress', 'Compress an image to reduce file size', {
input: z.string().describe('Path to source image'),
output: z.string().describe('Path for output image'),
quality: z.number().min(1).max(100).optional().default(80).describe('Quality level 1-100'),
}, withLogging('compress', async ({ input, output, quality }) => {
const result = await image.compress(input, output, quality);
return textResult(JSON.stringify({ ok: true, ...result }));
}));
server.tool('thumbnail', 'Create a thumbnail from an image', {
input: z.string().describe('Path to source image'),
output: z.string().describe('Path for output image'),
size: z
.enum(['small', 'medium', 'large'])
.optional()
.default('medium')
.describe('Thumbnail size preset (small=150px, medium=300px, large=600px)'),
}, withLogging('thumbnail', async ({ input, output, size }) => {
const result = await image.thumbnail(input, output, size);
return textResult(JSON.stringify({ ok: true, ...result }));
}));
// ============================================================================
// MCP Resources - Browser state accessible via @ mentions
// ============================================================================
// Resource: browser://state - Current browser state (URL, title, launched status)
server.resource('Browser State', 'browser://state', {
description: 'Current browser state including URL, title, and status',
mimeType: 'application/json',
}, async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://state',
mimeType: 'application/json',
text: JSON.stringify({ launched: false, url: null, title: null }),
},
],
};
}
const state = await browser.getUrl();
return {
contents: [
{
uri: 'browser://state',
mimeType: 'application/json',
text: JSON.stringify({ launched: true, ...state }),
},
],
};
});
// Resource: browser://html - Current page HTML content
server.resource('Page HTML', 'browser://html', { description: 'HTML content of the current page (truncated to 10KB)', mimeType: 'text/html' }, async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://html',
mimeType: 'text/plain',
text: 'Browser not launched. Use goto tool first.',
},
],
};
}
const html = await browser.getHtml(false);
return {
contents: [
{
uri: 'browser://html',
mimeType: 'text/html',
text: html,
},
],
};
});
// Resource: browser://html/full - Full page HTML content
server.resource('Full Page HTML', 'browser://html/full', { description: 'Complete HTML content of the current page', mimeType: 'text/html' }, async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://html/full',
mimeType: 'text/plain',
text: 'Browser not launched. Use goto tool first.',
},
],
};
}
const html = await browser.getHtml(true);
return {
contents: [
{
uri: 'browser://html/full',
mimeType: 'text/html',
text: html,
},
],
};
});
// Resource: browser://screenshot - Current page screenshot (base64)
server.resource('Page Screenshot', 'browser://screenshot', { description: 'Screenshot of the current page as base64 PNG', mimeType: 'image/png' }, async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://screenshot',
mimeType: 'text/plain',
text: 'Browser not launched. Use goto tool first.',
},
],
};
}
const result = await browser.screenshot(undefined, false);
currentScreenshotBuffer = result.buffer || null;
return {
contents: [
{
uri: 'browser://screenshot',
mimeType: 'image/png',
blob: result.buffer?.toString('base64') || '',
},
],
};
});
// ============================================================================
// MCP Prompts - Common workflows accessible via / commands
// ============================================================================
// Prompt: Analyze current page
server.prompt('analyze_page', 'Analyze the current page content and structure', async () => {
if (!launched) {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'The browser is not launched yet. Please use the goto tool to navigate to a URL first, then I can analyze the page.',
},
},
],
};
}
const state = await browser.getUrl();
const html = await browser.getHtml(false);
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Analyze the following webpage:
URL: ${state.url}
Title: ${state.title}
HTML Content (truncated):
\`\`\`html
${html}
\`\`\`
Please provide:
1. A summary of the page purpose and content
2. Key interactive elements (forms, buttons, links)
3. Any notable structure or patterns
4. Suggestions for what actions might be useful`,
},
},
],
};
});
// Prompt: Extract data from page
server.prompt('extract_data', 'Extract structured data from the current page', { selector: z.string().optional().describe('CSS selector to focus extraction (optional)') }, async ({ selector }) => {
if (!launched) {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'The browser is not launched yet. Please use the goto tool to navigate to a URL first.',
},
},
],
};
}
const state = await browser.getUrl();
let elements = [];
if (selector) {
elements = await browser.query(selector);
}
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Extract structured data from this webpage:
URL: ${state.url}
Title: ${state.title}
${selector ? `\nSelector: ${selector}\nMatched Elements: ${elements.length}\n\nElements:\n${JSON.stringify(elements, null, 2)}` : ''}
Please:
1. Use the query tool to find relevant data elements
2. Extract and structure the data in a useful format (JSON, table, etc.)
3. Identify patterns that could help with similar pages`,
},
},
],
};
});
// Prompt: Navigate and interact
server.prompt('navigate_to', 'Navigate to a URL and describe what you find', { url: z.string().url().describe('URL to navigate to') }, async ({ url }) => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please navigate to ${url} and:
1. Use the goto tool to navigate there
2. Take a screenshot to see the page
3. Describe what you see
4. Identify the main interactive elements
5. Suggest what actions might be useful
Start by navigating to the URL.`,
},
},
],
};
});
// Prompt: Fill form
server.prompt('fill_form', 'Help fill out a form on the current page', { formData: z.string().optional().describe('JSON object with field names and values to fill') }, async ({ formData }) => {
if (!launched) {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'The browser is not launched yet. Please use the goto tool to navigate to a page with a form first.',
},
},
],
};
}
const state = await browser.getUrl();
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Help fill out a form on this page:
URL: ${state.url}
Title: ${state.title}
${formData ? `\nData to fill: ${formData}` : ''}
Please:
1. Use the query tool to find form inputs (input, textarea, select)
2. Identify required fields and their types
3. Use the type tool to fill in each field
4. Report what was filled and any issues encountered`,
},
},
],
};
});
// Prompt: Screenshot comparison
server.prompt('compare_screenshots', 'Take screenshots and compare changes', async () => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `I'll help you compare page states:
1. Take an initial screenshot
2. Perform some action (click, navigate, etc.)
3. Take another screenshot
4. Describe the differences
What action would you like me to perform between screenshots?`,
},
},
],
};
});
// ============================================================================
// Start server
// ============================================================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
//# sourceMappingURL=mcp.js.map
+1
View File
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
import type { BrowserOptions } from './types.js';
export interface ServerOptions extends BrowserOptions {
port?: number;
}
export declare class BrowserServer {
private browser;
private app;
private server;
private port;
constructor(options?: ServerOptions);
private setupMiddleware;
private setupRoutes;
private handleCommand;
start(): Promise<void>;
stop(): Promise<void>;
getPort(): number;
getApp(): import("express-serve-static-core").Express;
}
export declare function startServer(options?: ServerOptions): Promise<BrowserServer>;
//# sourceMappingURL=server.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAuBD,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,MAAM,CAAmD;IACjE,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,GAAE,aAAkB;IAOvC,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,WAAW;YAIL,aAAa;IAsBrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B,OAAO,IAAI,MAAM;IAIjB,MAAM;CAGP;AAED,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAIrF"}
+88
View File
@@ -0,0 +1,88 @@
import chalk from 'chalk';
import express from 'express';
import logSymbols from 'log-symbols';
import { ClaudeBrowser } from './browser.js';
import { logger, ts } from './logger.js';
function printBanner(port) {
console.log();
console.log(chalk.cyan.bold(' 🌐 Claude Browse Server'));
console.log(chalk.dim(' ─────────────────────────'));
console.log(` ${logSymbols.success} Listening on ${chalk.bold(`http://localhost:${port}`)}`);
console.log();
console.log(chalk.dim(' Commands:'));
console.log(` ${chalk.cyan('goto')} ${chalk.yellow('click')} ${chalk.magenta('type')} ${chalk.blue('query')} ${chalk.green('screenshot')}`);
console.log(` ${chalk.cyan('url')} ${chalk.blue('html')} ${chalk.yellow('back')} ${chalk.yellow('forward')} ${chalk.yellow('reload')} ${chalk.gray('wait')} ${chalk.red('close')}`);
console.log();
console.log(chalk.dim(' Example:'));
console.log(chalk.gray(` curl -X POST localhost:${port} -d '{"cmd":"goto","url":"https://example.com"}'`));
console.log();
}
export class BrowserServer {
browser;
app = express();
server = null;
port;
constructor(options = {}) {
this.browser = new ClaudeBrowser(options);
this.port = options.port ?? 13373;
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
this.app.use(express.json());
this.app.use(express.text({ type: '*/*' }));
}
setupRoutes() {
this.app.post('/', (req, res) => this.handleCommand(req, res));
}
async handleCommand(req, res) {
try {
const cmd = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
logger.command(cmd);
if (cmd.cmd === 'close') {
logger.result(cmd, { ok: true });
res.json({ ok: true });
await this.stop();
process.exit(0);
}
const result = await this.browser.executeCommand(cmd);
logger.result(cmd, result);
res.json(result);
}
catch (err) {
const error = err.message;
console.log(`${ts()} ${logSymbols.error} ${chalk.red(error)}`);
res.status(500).json({ ok: false, error });
}
}
async start() {
await this.browser.launch();
return new Promise((resolve) => {
this.server = this.app.listen(this.port, () => {
printBanner(this.port);
resolve();
});
});
}
async stop() {
console.log(chalk.dim('\n Shutting down...'));
if (this.server) {
this.server.close();
this.server = null;
}
await this.browser.close();
console.log(` ${logSymbols.success} Browser closed\n`);
}
getPort() {
return this.port;
}
getApp() {
return this.app;
}
}
export async function startServer(options = {}) {
const server = new BrowserServer(options);
await server.start();
return server;
}
//# sourceMappingURL=server.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=server.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"server.test.d.ts","sourceRoot":"","sources":["../src/server.test.ts"],"names":[],"mappings":""}
+164
View File
@@ -0,0 +1,164 @@
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { BrowserServer } from './server.js';
// Mock the browser module
vi.mock('./browser.js', () => ({
ClaudeBrowser: class MockClaudeBrowser {
async launch() { }
async close() { }
async executeCommand(cmd) {
switch (cmd.cmd) {
case 'goto':
return { ok: true, url: cmd.url, title: 'Test Page' };
case 'click':
return { ok: true, url: '/clicked' };
case 'type':
return { ok: true };
case 'query':
return { ok: true, count: 2, elements: [] };
case 'url':
return { ok: true, url: 'https://example.com', title: 'Example' };
case 'html':
return { ok: true, html: '<html></html>' };
case 'back':
return { ok: true, url: '/previous' };
case 'forward':
return { ok: true, url: '/next' };
case 'reload':
return { ok: true, url: '/current' };
case 'wait':
return { ok: true };
case 'screenshot':
return { ok: true, path: cmd.path || 'screenshot.png' };
case 'eval':
return { ok: true, result: 2 };
case 'newpage':
return { ok: true };
case 'error':
throw new Error('Test error');
default:
return { ok: true };
}
}
},
}));
describe('BrowserServer', () => {
let server;
beforeAll(async () => {
server = new BrowserServer({ port: 0 }); // Use port 0 to get random available port
});
afterAll(async () => {
await server.stop();
});
describe('constructor', () => {
it('creates server with default port', () => {
const s = new BrowserServer();
expect(s.getPort()).toBe(13373);
});
it('creates server with custom port', () => {
const s = new BrowserServer({ port: 8080 });
expect(s.getPort()).toBe(8080);
});
});
describe('POST /', () => {
it('handles goto command', async () => {
const app = server.getApp();
const res = await request(app)
.post('/')
.send({ cmd: 'goto', url: 'https://example.com' })
.expect(200);
expect(res.body.ok).toBe(true);
expect(res.body.url).toBe('https://example.com');
expect(res.body.title).toBe('Test Page');
});
it('handles click command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'click', selector: '#btn' }).expect(200);
expect(res.body.ok).toBe(true);
expect(res.body.url).toBe('/clicked');
});
it('handles type command', async () => {
const app = server.getApp();
const res = await request(app)
.post('/')
.send({ cmd: 'type', selector: '#input', text: 'hello' })
.expect(200);
expect(res.body.ok).toBe(true);
});
it('handles query command', async () => {
const app = server.getApp();
const res = await request(app)
.post('/')
.send({ cmd: 'query', selector: '.items' })
.expect(200);
expect(res.body.ok).toBe(true);
expect(res.body.count).toBe(2);
});
it('handles url command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'url' }).expect(200);
expect(res.body.ok).toBe(true);
expect(res.body.url).toBe('https://example.com');
});
it('handles html command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'html' }).expect(200);
expect(res.body.ok).toBe(true);
expect(res.body.html).toBe('<html></html>');
});
it('handles JSON string body', async () => {
const app = server.getApp();
const res = await request(app)
.post('/')
.set('Content-Type', 'text/plain')
.send(JSON.stringify({ cmd: 'url' }))
.expect(200);
expect(res.body.ok).toBe(true);
});
it('handles back command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'back' }).expect(200);
expect(res.body.ok).toBe(true);
});
it('handles forward command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'forward' }).expect(200);
expect(res.body.ok).toBe(true);
});
it('handles reload command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'reload' }).expect(200);
expect(res.body.ok).toBe(true);
});
it('handles wait command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'wait', ms: 100 }).expect(200);
expect(res.body.ok).toBe(true);
});
it('handles screenshot command', async () => {
const app = server.getApp();
const res = await request(app)
.post('/')
.send({ cmd: 'screenshot', path: 'test.png' })
.expect(200);
expect(res.body.ok).toBe(true);
});
it('handles eval command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'eval', script: '1+1' }).expect(200);
expect(res.body.ok).toBe(true);
});
it('handles newpage command', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'newpage' }).expect(200);
expect(res.body.ok).toBe(true);
});
it('handles errors and returns 500', async () => {
const app = server.getApp();
const res = await request(app).post('/').send({ cmd: 'error' }).expect(500);
expect(res.body.ok).toBe(false);
expect(res.body.error).toBe('Test error');
});
});
});
//# sourceMappingURL=server.test.js.map
+1
View File
File diff suppressed because one or more lines are too long
+125
View File
@@ -0,0 +1,125 @@
export interface BrowserOptions {
headless?: boolean;
width?: number;
height?: number;
}
export interface ElementInfo {
tag: string;
text: string;
attributes: Record<string, string>;
}
export interface GotoCommand {
cmd: 'goto';
url: string;
}
export interface ClickCommand {
cmd: 'click';
selector: string;
}
export interface TypeCommand {
cmd: 'type';
selector: string;
text: string;
}
export interface QueryCommand {
cmd: 'query';
selector: string;
}
export interface ScreenshotCommand {
cmd: 'screenshot';
path?: string;
fullPage?: boolean;
}
export interface UrlCommand {
cmd: 'url';
}
export interface HtmlCommand {
cmd: 'html';
full?: boolean;
}
export interface BackCommand {
cmd: 'back';
}
export interface ForwardCommand {
cmd: 'forward';
}
export interface ReloadCommand {
cmd: 'reload';
}
export interface WaitCommand {
cmd: 'wait';
ms?: number;
}
export interface NewPageCommand {
cmd: 'newpage';
}
export interface CloseCommand {
cmd: 'close';
}
export interface EvalCommand {
cmd: 'eval';
script: string;
}
export interface FaviconCommand {
cmd: 'favicon';
input: string;
outputDir: string;
}
export interface ConvertCommand {
cmd: 'convert';
input: string;
output: string;
format: 'png' | 'jpeg' | 'webp' | 'avif';
}
export interface ResizeCommand {
cmd: 'resize';
input: string;
output: string;
width: number;
height?: number;
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
}
export interface CropCommand {
cmd: 'crop';
input: string;
output: string;
left: number;
top: number;
width: number;
height: number;
}
export interface CompressCommand {
cmd: 'compress';
input: string;
output: string;
quality?: number;
}
export interface ThumbnailCommand {
cmd: 'thumbnail';
input: string;
output: string;
size?: 'small' | 'medium' | 'large';
}
export type BrowserCommand = GotoCommand | ClickCommand | TypeCommand | QueryCommand | ScreenshotCommand | UrlCommand | HtmlCommand | BackCommand | ForwardCommand | ReloadCommand | WaitCommand | NewPageCommand | CloseCommand | EvalCommand | FaviconCommand | ConvertCommand | ResizeCommand | CropCommand | CompressCommand | ThumbnailCommand;
export interface SuccessResponse {
ok: true;
url?: string;
title?: string;
path?: string;
html?: string;
count?: number;
elements?: ElementInfo[];
result?: unknown;
files?: string[];
outputDir?: string;
width?: number;
height?: number;
format?: string;
size?: number;
}
export interface ErrorResponse {
ok: false;
error: string;
}
export type CommandResponse = SuccessResponse | ErrorResponse;
//# sourceMappingURL=types.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}