💬 Commit message: Update 2026-02-06 20:40:01, 3 files, 285 lines
📁 Files changed: 3 📝 Lines changed: 285 • package-lock.json • package.json • server.ts
This commit is contained in:
Generated
+54
@@ -9,7 +9,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"log-symbols": "^7.0.1",
|
||||||
"playwright": "^1.41.0"
|
"playwright": "^1.41.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -362,6 +364,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chalk": {
|
||||||
|
"version": "5.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||||
|
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
@@ -737,6 +751,34 @@
|
|||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-unicode-supported": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-unicode-supported": "^2.0.0",
|
||||||
|
"yoctocolors": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1162,6 +1204,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yoctocolors": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chalk": "^5.6.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"log-symbols": "^7.0.1",
|
||||||
"playwright": "^1.41.0"
|
"playwright": "^1.41.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+92
-149
@@ -1,4 +1,6 @@
|
|||||||
|
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 { ClaudeBrowser } from './browser.js';
|
import { ClaudeBrowser } from './browser.js';
|
||||||
import type { BrowserCommand, BrowserOptions, CommandResponse } from './types.js';
|
import type { BrowserCommand, BrowserOptions, CommandResponse } from './types.js';
|
||||||
|
|
||||||
@@ -6,184 +8,125 @@ export interface ServerOptions extends BrowserOptions {
|
|||||||
port?: number;
|
port?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ANSI colors
|
// Icons for commands
|
||||||
const c = {
|
const icons = {
|
||||||
reset: '\x1b[0m',
|
goto: '→',
|
||||||
bold: '\x1b[1m',
|
click: '◉',
|
||||||
dim: '\x1b[2m',
|
type: '⌨',
|
||||||
cyan: '\x1b[36m',
|
query: '?',
|
||||||
green: '\x1b[32m',
|
screenshot: '📷',
|
||||||
yellow: '\x1b[33m',
|
url: '🔗',
|
||||||
blue: '\x1b[34m',
|
html: '<>',
|
||||||
magenta: '\x1b[35m',
|
back: '←',
|
||||||
red: '\x1b[31m',
|
forward: '→',
|
||||||
gray: '\x1b[90m',
|
reload: '↻',
|
||||||
white: '\x1b[37m',
|
wait: '⏳',
|
||||||
|
newpage: '+',
|
||||||
|
close: '✕',
|
||||||
|
eval: '⚡',
|
||||||
};
|
};
|
||||||
|
|
||||||
function timestamp(): string {
|
// Colors for command types
|
||||||
return `${c.gray}[${new Date().toISOString()}]${c.reset}`;
|
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 logOk(ts: string, msg?: string): void {
|
function ts(): string {
|
||||||
const suffix = msg ? ` ${c.dim}${msg}${c.reset}` : '';
|
return chalk.gray(`[${new Date().toISOString()}]`);
|
||||||
console.log(`${ts} ${c.green}✓${c.reset}${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function logError(ts: string, error: string): void {
|
|
||||||
console.log(`${ts} ${c.red}✗ Error: ${error}${c.reset}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function logCmd(ts: string, icon: string, color: string, name: string, detail?: string): void {
|
|
||||||
const suffix = detail ? ` ${detail}` : '';
|
|
||||||
console.log(`${ts} ${color}${c.bold}${icon}${c.reset} ${color}${name}${c.reset}${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function logCommand(cmd: BrowserCommand): void {
|
|
||||||
const ts = timestamp();
|
|
||||||
switch (cmd.cmd) {
|
|
||||||
case 'goto':
|
|
||||||
logCmd(ts, '→', c.cyan, 'GOTO', `${c.white}${cmd.url}${c.reset}`);
|
|
||||||
break;
|
|
||||||
case 'click':
|
|
||||||
logCmd(ts, '◉', c.yellow, 'CLICK', `${c.white}${cmd.selector}${c.reset}`);
|
|
||||||
break;
|
|
||||||
case 'type':
|
|
||||||
logCmd(
|
|
||||||
ts,
|
|
||||||
'⌨',
|
|
||||||
c.magenta,
|
|
||||||
'TYPE',
|
|
||||||
`${c.white}${cmd.selector}${c.reset} ${c.dim}="${cmd.text}"${c.reset}`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'query':
|
|
||||||
logCmd(ts, '?', c.blue, 'QUERY', `${c.white}${cmd.selector}${c.reset}`);
|
|
||||||
break;
|
|
||||||
case 'screenshot':
|
|
||||||
logCmd(ts, '📷', c.green, 'SCREENSHOT', `${c.dim}${cmd.path || 'screenshot.png'}${c.reset}`);
|
|
||||||
break;
|
|
||||||
case 'url':
|
|
||||||
logCmd(ts, '🔗', c.cyan, 'URL');
|
|
||||||
break;
|
|
||||||
case 'html':
|
|
||||||
logCmd(ts, '<>', c.blue, 'HTML', cmd.full ? `${c.dim}(full)${c.reset}` : undefined);
|
|
||||||
break;
|
|
||||||
case 'back':
|
|
||||||
logCmd(ts, '←', c.yellow, 'BACK');
|
|
||||||
break;
|
|
||||||
case 'forward':
|
|
||||||
logCmd(ts, '→', c.yellow, 'FORWARD');
|
|
||||||
break;
|
|
||||||
case 'reload':
|
|
||||||
logCmd(ts, '↻', c.yellow, 'RELOAD');
|
|
||||||
break;
|
|
||||||
case 'wait':
|
|
||||||
logCmd(ts, '⏳', c.gray, 'WAIT', `${c.dim}${cmd.ms || 1000}ms${c.reset}`);
|
|
||||||
break;
|
|
||||||
case 'newpage':
|
|
||||||
logCmd(ts, '+', c.green, 'NEW PAGE');
|
|
||||||
break;
|
|
||||||
case 'close':
|
|
||||||
logCmd(ts, '✕', c.red, 'CLOSE');
|
|
||||||
break;
|
|
||||||
case 'eval': {
|
|
||||||
const preview = cmd.script.length > 50 ? `${cmd.script.slice(0, 50)}...` : cmd.script;
|
|
||||||
logCmd(ts, '⚡', c.magenta, 'EVAL', `${c.dim}${preview}${c.reset}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncate(str: string, max: number): string {
|
function truncate(str: string, max: number): string {
|
||||||
return str.length > max ? `${str.slice(0, max)}...` : str;
|
return str.length > max ? `${str.slice(0, max)}...` : str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function logResultGoto(ts: string, result: CommandResponse): void {
|
function getCommandDetail(cmd: BrowserCommand): string | undefined {
|
||||||
if (result.ok && 'title' in result && result.title) {
|
switch (cmd.cmd) {
|
||||||
logOk(ts, result.title);
|
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 logResultClick(ts: string, result: CommandResponse): void {
|
function logCommand(cmd: BrowserCommand): void {
|
||||||
if (result.ok && 'url' in result) {
|
const color = cmdColor[cmd.cmd] || chalk.white;
|
||||||
logOk(ts, `→ ${result.url}`);
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function logResultQuery(ts: string, result: CommandResponse): void {
|
type ResultFormatter = (result: CommandResponse) => string | undefined;
|
||||||
if (result.ok && 'count' in result) {
|
|
||||||
logOk(ts, `Found ${result.count} element(s)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logResultScreenshot(ts: string, result: CommandResponse): void {
|
const resultFormatters: Record<string, ResultFormatter> = {
|
||||||
if (result.ok && 'path' in result) {
|
goto: (r) => ('title' in r ? r.title : undefined),
|
||||||
logOk(ts, `Saved to ${result.path}`);
|
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),
|
||||||
function logResultUrl(ts: string, result: CommandResponse): void {
|
html: (r) => ('html' in r ? `${r.html?.length || 0} chars` : undefined),
|
||||||
if (result.ok && 'url' in result) {
|
eval: (r) => ('result' in r ? truncate(JSON.stringify(r.result), 80) : undefined),
|
||||||
logOk(ts, result.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logResultHtml(ts: string, result: CommandResponse): void {
|
|
||||||
if (result.ok && 'html' in result) {
|
|
||||||
logOk(ts, `${result.html?.length || 0} chars`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logResultEval(ts: string, result: CommandResponse): void {
|
|
||||||
if (result.ok && 'result' in result) {
|
|
||||||
const json = JSON.stringify(result.result);
|
|
||||||
logOk(ts, truncate(json, 80));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultLoggers: Record<string, (ts: string, result: CommandResponse) => void> = {
|
|
||||||
goto: logResultGoto,
|
|
||||||
click: logResultClick,
|
|
||||||
query: logResultQuery,
|
|
||||||
screenshot: logResultScreenshot,
|
|
||||||
url: logResultUrl,
|
|
||||||
html: logResultHtml,
|
|
||||||
eval: logResultEval,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
function logResult(cmd: BrowserCommand, result: CommandResponse): void {
|
||||||
const ts = timestamp();
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
logError(ts, result.error);
|
console.log(`${ts()} ${logSymbols.error} ${chalk.red(result.error)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const msg = getResultMessage(cmd, result);
|
||||||
const logger = resultLoggers[cmd.cmd];
|
const suffix = msg ? ` ${chalk.dim(msg)}` : '';
|
||||||
if (logger) {
|
console.log(`${ts()} ${logSymbols.success}${suffix}`);
|
||||||
logger(ts, result);
|
|
||||||
} else {
|
|
||||||
logOk(ts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function printBanner(port: number): void {
|
function printBanner(port: number): void {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(`${c.bold}${c.cyan} 🌐 Claude Browse Server${c.reset}`);
|
console.log(chalk.cyan.bold(' 🌐 Claude Browse Server'));
|
||||||
console.log(`${c.dim} ─────────────────────────${c.reset}`);
|
console.log(chalk.dim(' ─────────────────────────'));
|
||||||
console.log(` ${c.green}▶${c.reset} Listening on ${c.bold}http://localhost:${port}${c.reset}`);
|
console.log(` ${logSymbols.success} Listening on ${chalk.bold(`http://localhost:${port}`)}`);
|
||||||
console.log();
|
console.log();
|
||||||
console.log(`${c.dim} Commands:${c.reset}`);
|
console.log(chalk.dim(' Commands:'));
|
||||||
console.log(
|
console.log(
|
||||||
` ${c.cyan}goto${c.reset} ${c.yellow}click${c.reset} ${c.magenta}type${c.reset} ${c.blue}query${c.reset} ${c.green}screenshot${c.reset}`
|
` ${chalk.cyan('goto')} ${chalk.yellow('click')} ${chalk.magenta('type')} ${chalk.blue('query')} ${chalk.green('screenshot')}`
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
` ${c.cyan}url${c.reset} ${c.blue}html${c.reset} ${c.yellow}back${c.reset} ${c.yellow}forward${c.reset} ${c.yellow}reload${c.reset} ${c.gray}wait${c.reset} ${c.red}close${c.reset}`
|
` ${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();
|
||||||
console.log(`${c.dim} Example:${c.reset}`);
|
console.log(chalk.dim(' Example:'));
|
||||||
console.log(
|
console.log(
|
||||||
` ${c.gray}curl -X POST localhost:${port} -d '{"cmd":"goto","url":"https://example.com"}'${c.reset}`
|
chalk.gray(` curl -X POST localhost:${port} -d '{"cmd":"goto","url":"https://example.com"}'`)
|
||||||
);
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
@@ -226,7 +169,7 @@ export class BrowserServer {
|
|||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = (err as Error).message;
|
const error = (err as Error).message;
|
||||||
logError(timestamp(), error);
|
console.log(`${ts()} ${logSymbols.error} ${chalk.red(error)}`);
|
||||||
res.status(500).json({ ok: false, error });
|
res.status(500).json({ ok: false, error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,13 +186,13 @@ export class BrowserServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
console.log(`\n${c.dim} Shutting down...${c.reset}`);
|
console.log(chalk.dim('\n Shutting down...'));
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.close();
|
this.server.close();
|
||||||
this.server = null;
|
this.server = null;
|
||||||
}
|
}
|
||||||
await this.browser.close();
|
await this.browser.close();
|
||||||
console.log(` ${c.green}✓${c.reset} Browser closed\n`);
|
console.log(` ${logSymbols.success} Browser closed\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPort(): number {
|
getPort(): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user