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:
Vendored
+48
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=browser.integration.test.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"browser.integration.test.d.ts","sourceRoot":"","sources":["../src/browser.integration.test.ts"],"names":[],"mappings":""}
|
||||
Vendored
+173
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+250
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=browser.test.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"browser.test.d.ts","sourceRoot":"","sources":["../src/browser.test.ts"],"names":[],"mappings":""}
|
||||
Vendored
+73
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../src/browser.test.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=cli.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
||||
+243
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+21
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+131
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"image.js","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+6
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+4
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+41
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+103
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=logger.test.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"logger.test.d.ts","sourceRoot":"","sources":["../src/logger.test.ts"],"names":[],"mappings":""}
|
||||
Vendored
+146
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=mcp.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":""}
|
||||
+536
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+20
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
Vendored
+88
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=server.test.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"server.test.d.ts","sourceRoot":"","sources":["../src/server.test.ts"],"names":[],"mappings":""}
|
||||
Vendored
+164
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+125
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
||||
Reference in New Issue
Block a user