Add HTTP server mode with native node:http

Reintroduce server mode (removed in 3014cf9) using node:http instead of
Express — zero new dependencies. Accepts JSON commands via POST to /,
returns JSON responses with CORS support. Screenshot command returns
base64 data when no path is specified.

- Add src/server.ts with BrowserServer class using node:http
- Add -s/--server <port> CLI flag (default 13373)
- Export BrowserServer and startServer from index.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 16:23:16 +02:00
parent a80224df36
commit 1426ec9173
14 changed files with 228 additions and 33 deletions
+50 -22
View File
@@ -1,11 +1,16 @@
import { createServer } from 'node:http';
import chalk from 'chalk';
import express from 'express';
import logSymbols from 'log-symbols';
import { ClaudeBrowser } from './browser.js';
import { logger, ts } from './logger.js';
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
function printBanner(port) {
console.log();
console.log(chalk.cyan.bold(' 🌐 Claude Browse Server'));
console.log(chalk.cyan.bold(' browse server'));
console.log(chalk.dim(' ─────────────────────────'));
console.log(` ${logSymbols.success} Listening on ${chalk.bold(`http://localhost:${port}`)}`);
console.log();
@@ -14,51 +19,77 @@ function printBanner(port) {
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(chalk.gray(` curl localhost:${port} -d '{"cmd":"goto","url":"https://example.com"}'`));
console.log();
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
req.on('error', reject);
});
}
function sendJson(res, status, data) {
const body = JSON.stringify(data);
res.writeHead(status, { ...CORS_HEADERS, 'Content-Type': 'application/json' });
res.end(body);
}
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) {
async handleRequest(req, res) {
if (req.method === 'OPTIONS') {
res.writeHead(204, CORS_HEADERS);
res.end();
return;
}
if (req.method !== 'POST' || req.url !== '/') {
sendJson(res, 404, { ok: false, error: 'POST / only' });
return;
}
try {
const cmd = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
const raw = await readBody(req);
const cmd = JSON.parse(raw);
logger.command(cmd);
if (cmd.cmd === 'close') {
logger.result(cmd, { ok: true });
res.json({ ok: true });
sendJson(res, 200, { ok: true });
await this.stop();
process.exit(0);
}
const result = await this.browser.executeCommand(cmd);
logger.result(cmd, result);
res.json(result);
// For screenshot without a path, include base64 data
if (cmd.cmd === 'screenshot' && !cmd.path && result.ok) {
const page = this.browser.getPage();
if (page) {
const buffer = await page.screenshot();
result.data = buffer.toString('base64');
}
}
sendJson(res, 200, result);
}
catch (err) {
const error = err.message;
console.log(`${ts()} ${logSymbols.error} ${chalk.red(error)}`);
res.status(500).json({ ok: false, error });
sendJson(res, 500, { ok: false, error });
}
}
async start() {
await this.browser.launch();
return new Promise((resolve) => {
this.server = this.app.listen(this.port, () => {
this.server = createServer((req, res) => {
this.handleRequest(req, res).catch((err) => {
sendJson(res, 500, { ok: false, error: err.message });
});
});
this.server.listen(this.port, () => {
printBanner(this.port);
resolve();
});
@@ -76,9 +107,6 @@ export class BrowserServer {
getPort() {
return this.port;
}
getApp() {
return this.app;
}
}
export async function startServer(options = {}) {
const server = new BrowserServer(options);