💬 Commit message: Update 2026-02-06 21:32:05, 5 files, 447 lines
📁 Files changed: 5 📝 Lines changed: 447 • settings.local.json • cli.ts • logger.ts • mcp.ts • server.ts
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enabledMcpjsonServers": [
|
||||||
|
"claude-browse"
|
||||||
|
],
|
||||||
|
"enableAllProjectMcpServers": true
|
||||||
|
}
|
||||||
+18
-20
@@ -19,7 +19,6 @@ const { values, positionals } = parseArgs({
|
|||||||
json: { type: 'boolean', short: 'j', default: false },
|
json: { type: 'boolean', short: 'j', default: false },
|
||||||
click: { type: 'string', short: 'c', multiple: true },
|
click: { type: 'string', short: 'c', multiple: true },
|
||||||
type: { type: 'string', short: 't', multiple: true },
|
type: { type: 'string', short: 't', multiple: true },
|
||||||
serve: { type: 'string', short: 's' },
|
|
||||||
help: { type: 'boolean', default: false },
|
help: { type: 'boolean', default: false },
|
||||||
version: { type: 'boolean', short: 'v', default: false },
|
version: { type: 'boolean', short: 'v', default: false },
|
||||||
},
|
},
|
||||||
@@ -40,7 +39,6 @@ Options:
|
|||||||
-j, --json Output query results as JSON
|
-j, --json Output query results as JSON
|
||||||
-c, --click <selector> Click on element (can be repeated for multiple clicks)
|
-c, --click <selector> Click on element (can be repeated for multiple clicks)
|
||||||
-t, --type <sel>=<text> Type text into input (can be repeated)
|
-t, --type <sel>=<text> Type text into input (can be repeated)
|
||||||
-s, --serve <port> Start browser server on port (default: 3000)
|
|
||||||
-v, --version Show version
|
-v, --version Show version
|
||||||
--help Show this help
|
--help Show this help
|
||||||
|
|
||||||
@@ -54,19 +52,19 @@ Examples:
|
|||||||
claude-browse -t "input[name=q]=hello" -c "button[type=submit]" https://google.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
|
claude-browse -c ".cookie-accept" -c "a.nav-link" -q "h1" https://example.com
|
||||||
|
|
||||||
Server mode:
|
Server mode (default):
|
||||||
claude-browse -s 3000 # Start server on port 3000
|
claude-browse # Start server on port 13373
|
||||||
claude-browse -s 3000 --headed # Start with visible browser
|
claude-browse --headed # Start with visible browser
|
||||||
|
|
||||||
# Send commands via curl:
|
# Send commands via curl:
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"goto","url":"https://example.com"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"goto","url":"https://example.com"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"click","selector":"button"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"click","selector":"button"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"type","selector":"input","text":"hello"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"type","selector":"input","text":"hello"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"query","selector":"a[href]"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"query","selector":"a[href]"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"screenshot","path":"shot.png"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"screenshot","path":"shot.png"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"url"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"url"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"html"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"html"}'
|
||||||
curl -X POST http://localhost:3000 -d '{"cmd":"close"}'
|
curl -X POST http://localhost:13373 -d '{"cmd":"close"}'
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function getViewportConfig() {
|
function getViewportConfig() {
|
||||||
@@ -78,7 +76,7 @@ function getViewportConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runServerMode(): Promise<void> {
|
async function runServerMode(): Promise<void> {
|
||||||
const port = Number.parseInt(values.serve as string) || 3000;
|
const port = 13373;
|
||||||
const server = await startServer({ port, ...getViewportConfig() });
|
const server = await startServer({ port, ...getViewportConfig() });
|
||||||
|
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
@@ -197,16 +195,16 @@ async function main(): Promise<void> {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.serve) {
|
if (values.help) {
|
||||||
await runServerMode();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.help || positionals.length === 0) {
|
|
||||||
console.log(HELP);
|
console.log(HELP);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (positionals.length === 0) {
|
||||||
|
await runServerMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await runBrowserMode();
|
await runBrowserMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import logSymbols from 'log-symbols';
|
||||||
|
|
||||||
|
// Icons for commands
|
||||||
|
export const icons: Record<string, string> = {
|
||||||
|
goto: '→',
|
||||||
|
click: '◉',
|
||||||
|
type: '⌨',
|
||||||
|
query: '?',
|
||||||
|
screenshot: '📷',
|
||||||
|
url: '🔗',
|
||||||
|
html: '<>',
|
||||||
|
back: '←',
|
||||||
|
forward: '→',
|
||||||
|
reload: '↻',
|
||||||
|
wait: '⏳',
|
||||||
|
newpage: '+',
|
||||||
|
close: '✕',
|
||||||
|
eval: '⚡',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Colors for command types
|
||||||
|
export const cmdColor: Record<string, (s: string) => string> = {
|
||||||
|
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(): string {
|
||||||
|
return chalk.gray(`[${new Date().toISOString()}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncate(str: string, max: number): string {
|
||||||
|
return str.length > max ? `${str.slice(0, max)}...` : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandLike {
|
||||||
|
cmd: string;
|
||||||
|
url?: string;
|
||||||
|
selector?: string;
|
||||||
|
text?: string;
|
||||||
|
path?: string;
|
||||||
|
full?: boolean;
|
||||||
|
ms?: number;
|
||||||
|
script?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommandDetail(cmd: CommandLike): string | undefined {
|
||||||
|
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: CommandLike): string {
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResultLike {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
count?: number;
|
||||||
|
path?: string;
|
||||||
|
html?: string;
|
||||||
|
result?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultFormatters: Record<string, (r: ResultLike) => string | undefined> = {
|
||||||
|
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: CommandLike, result: ResultLike): string {
|
||||||
|
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 type LogFn = (msg: string) => void;
|
||||||
|
|
||||||
|
export function createLogger(logFn: LogFn = console.log) {
|
||||||
|
return {
|
||||||
|
command(cmd: CommandLike): void {
|
||||||
|
logFn(formatCommand(cmd));
|
||||||
|
},
|
||||||
|
result(cmd: CommandLike, result: ResultLike): void {
|
||||||
|
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`));
|
||||||
+105
-48
@@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ClaudeBrowser } from './browser.js';
|
import { ClaudeBrowser } from './browser.js';
|
||||||
|
import { type CommandLike, type ResultLike, stderrLogger as log } from './logger.js';
|
||||||
|
|
||||||
const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 });
|
const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 });
|
||||||
let launched = false;
|
let launched = false;
|
||||||
@@ -14,52 +15,103 @@ async function ensureLaunched(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ToolResult = { content: [{ type: 'text'; text: string }] };
|
||||||
|
|
||||||
|
function textResult(text: string): ToolResult {
|
||||||
|
return { content: [{ type: 'text' as const, text }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function withLogging<T extends Record<string, unknown>>(
|
||||||
|
cmd: string,
|
||||||
|
fn: (args: T) => Promise<ToolResult>
|
||||||
|
): (args: T) => Promise<ToolResult> {
|
||||||
|
return async (args: T) => {
|
||||||
|
const cmdLike: CommandLike = { cmd, ...args };
|
||||||
|
log.command(cmdLike);
|
||||||
|
try {
|
||||||
|
const result = await fn(args);
|
||||||
|
const parsed = JSON.parse(result.content[0]?.text || '{}');
|
||||||
|
const resultLike: ResultLike = { ok: true, ...parsed };
|
||||||
|
log.result(cmdLike, resultLike);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
log.result(cmdLike, { ok: false, error: (err as Error).message });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: 'claude-browse',
|
name: 'claude-browse',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
server.tool('goto', 'Navigate to a URL', { url: z.string().url() }, async ({ url }) => {
|
server.tool(
|
||||||
await ensureLaunched();
|
'goto',
|
||||||
const result = await browser.goto(url);
|
'Navigate to a URL',
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
{ 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', {}, async () => {
|
server.tool(
|
||||||
await ensureLaunched();
|
'back',
|
||||||
const result = await browser.back();
|
'Go back in browser history',
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
{},
|
||||||
});
|
withLogging('back', async () => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.back();
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
server.tool('forward', 'Go forward in browser history', {}, async () => {
|
server.tool(
|
||||||
await ensureLaunched();
|
'forward',
|
||||||
const result = await browser.forward();
|
'Go forward in browser history',
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
{},
|
||||||
});
|
withLogging('forward', async () => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.forward();
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
server.tool('reload', 'Reload the current page', {}, async () => {
|
server.tool(
|
||||||
await ensureLaunched();
|
'reload',
|
||||||
const result = await browser.reload();
|
'Reload the current page',
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
{},
|
||||||
});
|
withLogging('reload', async () => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.reload();
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Interaction
|
// Interaction
|
||||||
server.tool('click', 'Click on an element', { selector: z.string() }, async ({ selector }) => {
|
server.tool(
|
||||||
await ensureLaunched();
|
'click',
|
||||||
const result = await browser.click(selector);
|
'Click on an element',
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
{ selector: z.string() },
|
||||||
});
|
withLogging('click', async ({ selector }) => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.click(selector);
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
'type',
|
'type',
|
||||||
'Type text into an input field',
|
'Type text into an input field',
|
||||||
{ selector: z.string(), text: z.string() },
|
{ selector: z.string(), text: z.string() },
|
||||||
async ({ selector, text }) => {
|
withLogging('type', async ({ selector, text }) => {
|
||||||
await ensureLaunched();
|
await ensureLaunched();
|
||||||
await browser.type(selector, text);
|
await browser.type(selector, text);
|
||||||
return { content: [{ type: 'text', text: 'ok' }] };
|
return textResult(JSON.stringify({ ok: true }));
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
@@ -67,28 +119,33 @@ server.tool(
|
|||||||
'query',
|
'query',
|
||||||
'Query elements by CSS selector, returns tag, text, and attributes',
|
'Query elements by CSS selector, returns tag, text, and attributes',
|
||||||
{ selector: z.string() },
|
{ selector: z.string() },
|
||||||
async ({ selector }) => {
|
withLogging('query', async ({ selector }) => {
|
||||||
await ensureLaunched();
|
await ensureLaunched();
|
||||||
const elements = await browser.query(selector);
|
const elements = await browser.query(selector);
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(elements, null, 2) }] };
|
return textResult(JSON.stringify({ ok: true, count: elements.length, elements }));
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool('url', 'Get current URL and page title', {}, async () => {
|
server.tool(
|
||||||
await ensureLaunched();
|
'url',
|
||||||
const result = await browser.getUrl();
|
'Get current URL and page title',
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
{},
|
||||||
});
|
withLogging('url', async () => {
|
||||||
|
await ensureLaunched();
|
||||||
|
const result = await browser.getUrl();
|
||||||
|
return textResult(JSON.stringify(result));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
'html',
|
'html',
|
||||||
'Get page HTML content',
|
'Get page HTML content',
|
||||||
{ full: z.boolean().optional().default(false) },
|
{ full: z.boolean().optional().default(false) },
|
||||||
async ({ full }) => {
|
withLogging('html', async ({ full }) => {
|
||||||
await ensureLaunched();
|
await ensureLaunched();
|
||||||
const html = await browser.getHtml(full);
|
const html = await browser.getHtml(full);
|
||||||
return { content: [{ type: 'text', text: html }] };
|
return textResult(JSON.stringify({ ok: true, html }));
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Screenshot
|
// Screenshot
|
||||||
@@ -99,11 +156,11 @@ server.tool(
|
|||||||
path: z.string().optional().default('screenshots/screenshot.png'),
|
path: z.string().optional().default('screenshots/screenshot.png'),
|
||||||
fullPage: z.boolean().optional().default(false),
|
fullPage: z.boolean().optional().default(false),
|
||||||
},
|
},
|
||||||
async ({ path, fullPage }) => {
|
withLogging('screenshot', async ({ path, fullPage }) => {
|
||||||
await ensureLaunched();
|
await ensureLaunched();
|
||||||
const result = await browser.screenshot(path, fullPage);
|
const result = await browser.screenshot(path, fullPage);
|
||||||
return { content: [{ type: 'text', text: `Screenshot saved to ${result.path}` }] };
|
return textResult(JSON.stringify({ ok: true, path: result.path }));
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Eval
|
// Eval
|
||||||
@@ -111,11 +168,11 @@ server.tool(
|
|||||||
'eval',
|
'eval',
|
||||||
'Execute JavaScript in the browser context',
|
'Execute JavaScript in the browser context',
|
||||||
{ script: z.string() },
|
{ script: z.string() },
|
||||||
async ({ script }) => {
|
withLogging('eval', async ({ script }) => {
|
||||||
await ensureLaunched();
|
await ensureLaunched();
|
||||||
const result = await browser.eval(script);
|
const result = await browser.eval(script);
|
||||||
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
return textResult(JSON.stringify({ ok: true, result }));
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
@@ -123,11 +180,11 @@ server.tool(
|
|||||||
'wait',
|
'wait',
|
||||||
'Wait for a specified time in milliseconds',
|
'Wait for a specified time in milliseconds',
|
||||||
{ ms: z.number().optional().default(1000) },
|
{ ms: z.number().optional().default(1000) },
|
||||||
async ({ ms }) => {
|
withLogging('wait', async ({ ms }) => {
|
||||||
await ensureLaunched();
|
await ensureLaunched();
|
||||||
await browser.wait(ms);
|
await browser.wait(ms);
|
||||||
return { content: [{ type: 'text', text: 'ok' }] };
|
return textResult(JSON.stringify({ ok: true }));
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
|
|||||||
+6
-107
@@ -2,114 +2,13 @@ import chalk from 'chalk';
|
|||||||
import express, { type Request, type Response } from 'express';
|
import express, { type Request, type Response } from 'express';
|
||||||
import logSymbols from 'log-symbols';
|
import logSymbols from 'log-symbols';
|
||||||
import { ClaudeBrowser } from './browser.js';
|
import { ClaudeBrowser } from './browser.js';
|
||||||
import type { BrowserCommand, BrowserOptions, CommandResponse } from './types.js';
|
import { logger, ts } from './logger.js';
|
||||||
|
import type { BrowserCommand, BrowserOptions } from './types.js';
|
||||||
|
|
||||||
export interface ServerOptions extends BrowserOptions {
|
export interface ServerOptions extends BrowserOptions {
|
||||||
port?: number;
|
port?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icons for commands
|
|
||||||
const icons = {
|
|
||||||
goto: '→',
|
|
||||||
click: '◉',
|
|
||||||
type: '⌨',
|
|
||||||
query: '?',
|
|
||||||
screenshot: '📷',
|
|
||||||
url: '🔗',
|
|
||||||
html: '<>',
|
|
||||||
back: '←',
|
|
||||||
forward: '→',
|
|
||||||
reload: '↻',
|
|
||||||
wait: '⏳',
|
|
||||||
newpage: '+',
|
|
||||||
close: '✕',
|
|
||||||
eval: '⚡',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Colors for command types
|
|
||||||
const cmdColor: Record<string, (s: string) => string> = {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ts(): string {
|
|
||||||
return chalk.gray(`[${new Date().toISOString()}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncate(str: string, max: number): string {
|
|
||||||
return str.length > max ? `${str.slice(0, max)}...` : str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommandDetail(cmd: BrowserCommand): string | undefined {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logCommand(cmd: BrowserCommand): void {
|
|
||||||
const color = cmdColor[cmd.cmd] || chalk.white;
|
|
||||||
const icon = icons[cmd.cmd as keyof typeof icons] || '•';
|
|
||||||
const detail = getCommandDetail(cmd);
|
|
||||||
const suffix = detail ? ` ${detail}` : '';
|
|
||||||
console.log(`${ts()} ${chalk.bold(color(icon))} ${color(cmd.cmd.toUpperCase())}${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResultFormatter = (result: CommandResponse) => string | undefined;
|
|
||||||
|
|
||||||
const resultFormatters: Record<string, ResultFormatter> = {
|
|
||||||
goto: (r) => ('title' in r ? r.title : undefined),
|
|
||||||
click: (r) => ('url' in r ? `→ ${r.url}` : undefined),
|
|
||||||
query: (r) => ('count' in r ? `Found ${r.count} element(s)` : undefined),
|
|
||||||
screenshot: (r) => ('path' in r ? `Saved to ${r.path}` : undefined),
|
|
||||||
url: (r) => ('url' in r ? r.url : undefined),
|
|
||||||
html: (r) => ('html' in r ? `${r.html?.length || 0} chars` : undefined),
|
|
||||||
eval: (r) => ('result' in r ? truncate(JSON.stringify(r.result), 80) : undefined),
|
|
||||||
};
|
|
||||||
|
|
||||||
function getResultMessage(cmd: BrowserCommand, result: CommandResponse): string | undefined {
|
|
||||||
if (!result.ok) return undefined;
|
|
||||||
const formatter = resultFormatters[cmd.cmd];
|
|
||||||
return formatter ? formatter(result) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function logResult(cmd: BrowserCommand, result: CommandResponse): void {
|
|
||||||
if (!result.ok) {
|
|
||||||
console.log(`${ts()} ${logSymbols.error} ${chalk.red(result.error)}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const msg = getResultMessage(cmd, result);
|
|
||||||
const suffix = msg ? ` ${chalk.dim(msg)}` : '';
|
|
||||||
console.log(`${ts()} ${logSymbols.success}${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function printBanner(port: number): void {
|
function printBanner(port: number): void {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.cyan.bold(' 🌐 Claude Browse Server'));
|
console.log(chalk.cyan.bold(' 🌐 Claude Browse Server'));
|
||||||
@@ -139,7 +38,7 @@ export class BrowserServer {
|
|||||||
|
|
||||||
constructor(options: ServerOptions = {}) {
|
constructor(options: ServerOptions = {}) {
|
||||||
this.browser = new ClaudeBrowser(options);
|
this.browser = new ClaudeBrowser(options);
|
||||||
this.port = options.port ?? 3000;
|
this.port = options.port ?? 13373;
|
||||||
this.setupMiddleware();
|
this.setupMiddleware();
|
||||||
this.setupRoutes();
|
this.setupRoutes();
|
||||||
}
|
}
|
||||||
@@ -156,17 +55,17 @@ export class BrowserServer {
|
|||||||
private async handleCommand(req: Request, res: Response): Promise<void> {
|
private async handleCommand(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const cmd: BrowserCommand = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
const cmd: BrowserCommand = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
||||||
logCommand(cmd);
|
logger.command(cmd);
|
||||||
|
|
||||||
if (cmd.cmd === 'close') {
|
if (cmd.cmd === 'close') {
|
||||||
logResult(cmd, { ok: true });
|
logger.result(cmd, { ok: true });
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
await this.stop();
|
await this.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.browser.executeCommand(cmd);
|
const result = await this.browser.executeCommand(cmd);
|
||||||
logResult(cmd, result);
|
logger.result(cmd, result);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = (err as Error).message;
|
const error = (err as Error).message;
|
||||||
|
|||||||
Reference in New Issue
Block a user