💬 Commit message: Update 2026-02-06 20:32:18, 7 files, 781 lines
📁 Files changed: 7 📝 Lines changed: 781 • CLAUDE.md • biome.json • package-lock.json • package.json • browser.ts • cli.ts • server.ts
This commit is contained in:
@@ -5,13 +5,36 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
## Build & Development
|
## Build & Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install # Install dependencies
|
npm install # Install dependencies
|
||||||
npx playwright install webkit # Install WebKit browser
|
npx playwright install webkit # Install WebKit browser
|
||||||
npm run build # Compile TypeScript to dist/
|
npm run build # Compile TypeScript to dist/
|
||||||
npm run dev # Watch mode compilation
|
npm run dev # Watch mode compilation
|
||||||
npm start # Run the CLI (after build)
|
npm start # Run the CLI (after build)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Lint, Format & Typecheck
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check # Run all checks (lint + format)
|
||||||
|
npm run fix # Fix all auto-fixable issues
|
||||||
|
npm run lint # Lint only
|
||||||
|
npm run lint:fix # Lint with autofix
|
||||||
|
npm run format # Check formatting
|
||||||
|
npm run format:fix # Fix formatting
|
||||||
|
npm run typecheck # TypeScript type checking
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Short methods**: Keep methods under 20 lines. Extract logic into focused helper methods.
|
||||||
|
- **Single responsibility**: Each class handles one concern. Split large classes by domain.
|
||||||
|
- **Class structure**:
|
||||||
|
- `ClaudeBrowser` - browser lifecycle and page interactions
|
||||||
|
- `BrowserServer` - HTTP server and request handling
|
||||||
|
- `Logger` (planned) - colored console output
|
||||||
|
- **Naming**: Commands use `verbNoun` pattern (e.g., `getCookies`, `setStorage`).
|
||||||
|
- **Types**: All commands in discriminated union. Add new commands to `BrowserCommand` type.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
This is a TypeScript library providing headless browser automation via Playwright WebKit. It has three modes of operation:
|
This is a TypeScript library providing headless browser automation via Playwright WebKit. It has three modes of operation:
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"complexity": {
|
||||||
|
"noExcessiveCognitiveComplexity": {
|
||||||
|
"level": "warn",
|
||||||
|
"options": { "maxAllowedComplexity": 10 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"noNonNullAssertion": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 100
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"trailingCommas": "es5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"ignore": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+165
@@ -15,6 +15,7 @@
|
|||||||
"claude-browse": "dist/cli.js"
|
"claude-browse": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.0",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
},
|
},
|
||||||
@@ -22,6 +23,170 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@biomejs/biome": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"biome": "bin/biome"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/biome"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@biomejs/cli-darwin-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-darwin-x64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-arm64-musl": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-x64": "1.9.4",
|
||||||
|
"@biomejs/cli-linux-x64-musl": "1.9.4",
|
||||||
|
"@biomejs/cli-win32-arm64": "1.9.4",
|
||||||
|
"@biomejs/cli-win32-x64": "1.9.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.32",
|
"version": "20.19.32",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,13 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"start": "node dist/cli.js",
|
"start": "node dist/cli.js",
|
||||||
|
"lint": "biome lint src/",
|
||||||
|
"lint:fix": "biome lint --write src/",
|
||||||
|
"format": "biome format src/",
|
||||||
|
"format:fix": "biome format --write src/",
|
||||||
|
"check": "biome check src/",
|
||||||
|
"fix": "biome check --write src/",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -35,6 +42,7 @@
|
|||||||
"playwright": "^1.41.0"
|
"playwright": "^1.41.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.0",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+7
-13
@@ -1,11 +1,6 @@
|
|||||||
import { webkit, Browser, BrowserContext, Page } from 'playwright';
|
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import type {
|
import { type Browser, type BrowserContext, type Page, webkit } from 'playwright';
|
||||||
BrowserOptions,
|
import type { BrowserCommand, BrowserOptions, CommandResponse, ElementInfo } from './types.js';
|
||||||
BrowserCommand,
|
|
||||||
CommandResponse,
|
|
||||||
ElementInfo,
|
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
export class ClaudeBrowser {
|
export class ClaudeBrowser {
|
||||||
private browser: Browser | null = null;
|
private browser: Browser | null = null;
|
||||||
@@ -83,10 +78,7 @@ export class ClaudeBrowser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(
|
async screenshot(path?: string, fullPage = false): Promise<{ path: string; buffer?: Buffer }> {
|
||||||
path?: string,
|
|
||||||
fullPage = false
|
|
||||||
): Promise<{ path: string; buffer?: Buffer }> {
|
|
||||||
const page = this.ensurePage();
|
const page = this.ensurePage();
|
||||||
const resolvedPath = resolve(path || 'screenshot.png');
|
const resolvedPath = resolve(path || 'screenshot.png');
|
||||||
const buffer = await page.screenshot({ path: resolvedPath, fullPage });
|
const buffer = await page.screenshot({ path: resolvedPath, fullPage });
|
||||||
@@ -198,8 +190,10 @@ export class ClaudeBrowser {
|
|||||||
const result = await this.eval(cmd.script);
|
const result = await this.eval(cmd.script);
|
||||||
return { ok: true, result };
|
return { ok: true, result };
|
||||||
}
|
}
|
||||||
default:
|
default: {
|
||||||
return { ok: false, error: `Unknown command: ${(cmd as any).cmd}` };
|
const _exhaustive: never = cmd;
|
||||||
|
return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, error: (err as Error).message };
|
return { ok: false, error: (err as Error).message };
|
||||||
|
|||||||
+126
-109
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { parseArgs } from 'node:util';
|
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
import { parseArgs } from 'node:util';
|
||||||
import { ClaudeBrowser } from './browser.js';
|
import { ClaudeBrowser } from './browser.js';
|
||||||
import { startServer } from './server.js';
|
import { startServer } from './server.js';
|
||||||
|
import type { ElementInfo } from './types.js';
|
||||||
|
|
||||||
const { values, positionals } = parseArgs({
|
const { values, positionals } = parseArgs({
|
||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
@@ -68,30 +69,136 @@ Server mode:
|
|||||||
curl -X POST http://localhost:3000 -d '{"cmd":"close"}'
|
curl -X POST http://localhost:3000 -d '{"cmd":"close"}'
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function getViewportConfig() {
|
||||||
|
return {
|
||||||
|
headless: !values.headed,
|
||||||
|
width: Number.parseInt(values.width as string),
|
||||||
|
height: Number.parseInt(values.height as string),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runServerMode(): Promise<void> {
|
||||||
|
const port = Number.parseInt(values.serve as string) || 3000;
|
||||||
|
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: ClaudeBrowser): Promise<void> {
|
||||||
|
const typeActions = values.type as string[] | undefined;
|
||||||
|
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: ClaudeBrowser): Promise<void> {
|
||||||
|
const clickActions = values.click as string[] | undefined;
|
||||||
|
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: ElementInfo, index: number): void {
|
||||||
|
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: ClaudeBrowser): Promise<void> {
|
||||||
|
const elements = await browser.query(values.query as string);
|
||||||
|
|
||||||
|
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: ClaudeBrowser): Promise<void> {
|
||||||
|
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 runScreenshotMode(browser: ClaudeBrowser): Promise<void> {
|
||||||
|
const outputPath = resolve(values.output as string);
|
||||||
|
console.log(`Saving screenshot to: ${outputPath}`);
|
||||||
|
await browser.screenshot(outputPath, values.fullpage);
|
||||||
|
await browser.close();
|
||||||
|
console.log('Done!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBrowserMode(): Promise<void> {
|
||||||
|
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 as string));
|
||||||
|
|
||||||
|
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(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
if (values.version) {
|
if (values.version) {
|
||||||
console.log('claude-browse 0.1.0');
|
console.log('claude-browse 0.1.0');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server mode
|
|
||||||
if (values.serve) {
|
if (values.serve) {
|
||||||
const port = parseInt(values.serve) || 3000;
|
await runServerMode();
|
||||||
const server = await startServer({
|
|
||||||
port,
|
|
||||||
headless: !values.headed,
|
|
||||||
width: parseInt(values.width as string),
|
|
||||||
height: parseInt(values.height as string),
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
console.log('\nShutting down...');
|
|
||||||
await server.stop();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep alive
|
|
||||||
await new Promise(() => {});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,97 +207,7 @@ async function main(): Promise<void> {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = positionals[0];
|
await runBrowserMode();
|
||||||
const outputPath = resolve(values.output as string);
|
|
||||||
|
|
||||||
const browser = new ClaudeBrowser({
|
|
||||||
headless: !values.headed,
|
|
||||||
width: parseInt(values.width as string),
|
|
||||||
height: parseInt(values.height as string),
|
|
||||||
});
|
|
||||||
|
|
||||||
await browser.launch();
|
|
||||||
|
|
||||||
console.log(`Navigating to: ${url}`);
|
|
||||||
await browser.goto(url);
|
|
||||||
await browser.wait(parseInt(values.wait as string));
|
|
||||||
|
|
||||||
// Process type actions (before clicks, typically for form filling)
|
|
||||||
const typeActions = values.type as string[] | undefined;
|
|
||||||
if (typeActions?.length) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process click actions
|
|
||||||
const clickActions = values.click as string[] | undefined;
|
|
||||||
if (clickActions?.length) {
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query elements if -q/--query is specified
|
|
||||||
if (values.query) {
|
|
||||||
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((el, i) => {
|
|
||||||
console.log(`[${i + 1}] <${el.tag}>`);
|
|
||||||
for (const [name, value] of Object.entries(el.attributes)) {
|
|
||||||
console.log(` ${name}="${value}"`);
|
|
||||||
}
|
|
||||||
if (el.text) {
|
|
||||||
console.log(
|
|
||||||
` text: "${el.text.slice(0, 100)}${el.text.length > 100 ? '...' : ''}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.interactive) {
|
|
||||||
console.log(`Saving screenshot to: ${outputPath}`);
|
|
||||||
await browser.screenshot(outputPath, values.fullpage);
|
|
||||||
await browser.close();
|
|
||||||
console.log('Done!');
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep alive
|
|
||||||
await new Promise(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
|
|||||||
+176
-125
@@ -1,4 +1,4 @@
|
|||||||
import { createServer, Server, IncomingMessage, ServerResponse } from 'node:http';
|
import { type IncomingMessage, type Server, type ServerResponse, createServer } from 'node:http';
|
||||||
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';
|
||||||
|
|
||||||
@@ -19,115 +19,175 @@ const c = {
|
|||||||
red: '\x1b[31m',
|
red: '\x1b[31m',
|
||||||
gray: '\x1b[90m',
|
gray: '\x1b[90m',
|
||||||
white: '\x1b[37m',
|
white: '\x1b[37m',
|
||||||
bgBlue: '\x1b[44m',
|
|
||||||
bgGreen: '\x1b[42m',
|
|
||||||
bgYellow: '\x1b[43m',
|
|
||||||
bgMagenta: '\x1b[45m',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function timestamp(): string {
|
function timestamp(): string {
|
||||||
return c.gray + '[' + new Date().toISOString() + ']' + c.reset;
|
return `${c.gray}[${new Date().toISOString()}]${c.reset}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logOk(ts: string, msg?: string): void {
|
||||||
|
const suffix = msg ? ` ${c.dim}${msg}${c.reset}` : '';
|
||||||
|
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 {
|
function logCommand(cmd: BrowserCommand): void {
|
||||||
const ts = timestamp();
|
const ts = timestamp();
|
||||||
switch (cmd.cmd) {
|
switch (cmd.cmd) {
|
||||||
case 'goto':
|
case 'goto':
|
||||||
console.log(`${ts} ${c.cyan}${c.bold}→${c.reset} ${c.cyan}GOTO${c.reset} ${c.white}${cmd.url}${c.reset}`);
|
logCmd(ts, '→', c.cyan, 'GOTO', `${c.white}${cmd.url}${c.reset}`);
|
||||||
break;
|
break;
|
||||||
case 'click':
|
case 'click':
|
||||||
console.log(`${ts} ${c.yellow}${c.bold}◉${c.reset} ${c.yellow}CLICK${c.reset} ${c.white}${cmd.selector}${c.reset}`);
|
logCmd(ts, '◉', c.yellow, 'CLICK', `${c.white}${cmd.selector}${c.reset}`);
|
||||||
break;
|
break;
|
||||||
case 'type':
|
case 'type':
|
||||||
console.log(`${ts} ${c.magenta}${c.bold}⌨${c.reset} ${c.magenta}TYPE${c.reset} ${c.white}${cmd.selector}${c.reset} ${c.dim}="${cmd.text}"${c.reset}`);
|
logCmd(
|
||||||
|
ts,
|
||||||
|
'⌨',
|
||||||
|
c.magenta,
|
||||||
|
'TYPE',
|
||||||
|
`${c.white}${cmd.selector}${c.reset} ${c.dim}="${cmd.text}"${c.reset}`
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'query':
|
case 'query':
|
||||||
console.log(`${ts} ${c.blue}${c.bold}?${c.reset} ${c.blue}QUERY${c.reset} ${c.white}${cmd.selector}${c.reset}`);
|
logCmd(ts, '?', c.blue, 'QUERY', `${c.white}${cmd.selector}${c.reset}`);
|
||||||
break;
|
break;
|
||||||
case 'screenshot':
|
case 'screenshot':
|
||||||
console.log(`${ts} ${c.green}${c.bold}📷${c.reset} ${c.green}SCREENSHOT${c.reset} ${c.dim}${cmd.path || 'screenshot.png'}${c.reset}`);
|
logCmd(ts, '📷', c.green, 'SCREENSHOT', `${c.dim}${cmd.path || 'screenshot.png'}${c.reset}`);
|
||||||
break;
|
break;
|
||||||
case 'url':
|
case 'url':
|
||||||
console.log(`${ts} ${c.cyan}${c.bold}🔗${c.reset} ${c.cyan}URL${c.reset}`);
|
logCmd(ts, '🔗', c.cyan, 'URL');
|
||||||
break;
|
break;
|
||||||
case 'html':
|
case 'html':
|
||||||
console.log(`${ts} ${c.blue}${c.bold}<>${c.reset} ${c.blue}HTML${c.reset} ${cmd.full ? c.dim + '(full)' + c.reset : ''}`);
|
logCmd(ts, '<>', c.blue, 'HTML', cmd.full ? `${c.dim}(full)${c.reset}` : undefined);
|
||||||
break;
|
break;
|
||||||
case 'back':
|
case 'back':
|
||||||
console.log(`${ts} ${c.yellow}${c.bold}←${c.reset} ${c.yellow}BACK${c.reset}`);
|
logCmd(ts, '←', c.yellow, 'BACK');
|
||||||
break;
|
break;
|
||||||
case 'forward':
|
case 'forward':
|
||||||
console.log(`${ts} ${c.yellow}${c.bold}→${c.reset} ${c.yellow}FORWARD${c.reset}`);
|
logCmd(ts, '→', c.yellow, 'FORWARD');
|
||||||
break;
|
break;
|
||||||
case 'reload':
|
case 'reload':
|
||||||
console.log(`${ts} ${c.yellow}${c.bold}↻${c.reset} ${c.yellow}RELOAD${c.reset}`);
|
logCmd(ts, '↻', c.yellow, 'RELOAD');
|
||||||
break;
|
break;
|
||||||
case 'wait':
|
case 'wait':
|
||||||
console.log(`${ts} ${c.gray}${c.bold}⏳${c.reset} ${c.gray}WAIT${c.reset} ${c.dim}${cmd.ms || 1000}ms${c.reset}`);
|
logCmd(ts, '⏳', c.gray, 'WAIT', `${c.dim}${cmd.ms || 1000}ms${c.reset}`);
|
||||||
break;
|
break;
|
||||||
case 'newpage':
|
case 'newpage':
|
||||||
console.log(`${ts} ${c.green}${c.bold}+${c.reset} ${c.green}NEW PAGE${c.reset}`);
|
logCmd(ts, '+', c.green, 'NEW PAGE');
|
||||||
break;
|
break;
|
||||||
case 'close':
|
case 'close':
|
||||||
console.log(`${ts} ${c.red}${c.bold}✕${c.reset} ${c.red}CLOSE${c.reset}`);
|
logCmd(ts, '✕', c.red, 'CLOSE');
|
||||||
break;
|
break;
|
||||||
case 'eval':
|
case 'eval': {
|
||||||
const preview = cmd.script.length > 50 ? cmd.script.slice(0, 50) + '...' : cmd.script;
|
const preview = cmd.script.length > 50 ? `${cmd.script.slice(0, 50)}...` : cmd.script;
|
||||||
console.log(`${ts} ${c.magenta}${c.bold}⚡${c.reset} ${c.magenta}EVAL${c.reset} ${c.dim}${preview}${c.reset}`);
|
logCmd(ts, '⚡', c.magenta, 'EVAL', `${c.dim}${preview}${c.reset}`);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function truncate(str: string, max: number): string {
|
||||||
|
return str.length > max ? `${str.slice(0, max)}...` : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logResultGoto(ts: string, result: CommandResponse): void {
|
||||||
|
if (result.ok && 'title' in result && result.title) {
|
||||||
|
logOk(ts, result.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logResultClick(ts: string, result: CommandResponse): void {
|
||||||
|
if (result.ok && 'url' in result) {
|
||||||
|
logOk(ts, `→ ${result.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logResultQuery(ts: string, result: CommandResponse): void {
|
||||||
|
if (result.ok && 'count' in result) {
|
||||||
|
logOk(ts, `Found ${result.count} element(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logResultScreenshot(ts: string, result: CommandResponse): void {
|
||||||
|
if (result.ok && 'path' in result) {
|
||||||
|
logOk(ts, `Saved to ${result.path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logResultUrl(ts: string, result: CommandResponse): void {
|
||||||
|
if (result.ok && 'url' in result) {
|
||||||
|
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 logResult(cmd: BrowserCommand, result: CommandResponse): void {
|
function logResult(cmd: BrowserCommand, result: CommandResponse): void {
|
||||||
const ts = timestamp();
|
const ts = timestamp();
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
console.log(`${ts} ${c.red}✗ Error: ${result.error}${c.reset}`);
|
logError(ts, result.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (cmd.cmd) {
|
const logger = resultLoggers[cmd.cmd];
|
||||||
case 'goto':
|
if (logger) {
|
||||||
if ('title' in result && result.title) {
|
logger(ts, result);
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${result.title}${c.reset}`);
|
} else {
|
||||||
}
|
logOk(ts);
|
||||||
break;
|
|
||||||
case 'click':
|
|
||||||
if ('url' in result) {
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}→ ${result.url}${c.reset}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'query':
|
|
||||||
if ('count' in result) {
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}Found ${result.count} element(s)${c.reset}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'screenshot':
|
|
||||||
if ('path' in result) {
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}Saved to ${result.path}${c.reset}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'url':
|
|
||||||
if ('url' in result) {
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${result.url}${c.reset}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'html':
|
|
||||||
if ('html' in result) {
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${result.html?.length || 0} chars${c.reset}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'eval':
|
|
||||||
if ('result' in result) {
|
|
||||||
const json = JSON.stringify(result.result);
|
|
||||||
const preview = json && json.length > 80 ? json.slice(0, 80) + '...' : json;
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset} ${c.dim}${preview}${c.reset}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(`${ts} ${c.green}✓${c.reset}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printBanner(port: number): void {
|
||||||
|
console.log();
|
||||||
|
console.log(`${c.bold}${c.cyan} 🌐 Claude Browse Server${c.reset}`);
|
||||||
|
console.log(`${c.dim} ─────────────────────────${c.reset}`);
|
||||||
|
console.log(` ${c.green}▶${c.reset} Listening on ${c.bold}http://localhost:${port}${c.reset}`);
|
||||||
|
console.log();
|
||||||
|
console.log(`${c.dim} Commands:${c.reset}`);
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
console.log(`${c.dim} Example:${c.reset}`);
|
||||||
|
console.log(
|
||||||
|
` ${c.gray}curl -X POST localhost:${port} -d '{"cmd":"goto","url":"https://example.com"}'${c.reset}`
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
export class BrowserServer {
|
export class BrowserServer {
|
||||||
private browser: ClaudeBrowser;
|
private browser: ClaudeBrowser;
|
||||||
private server: Server | null = null;
|
private server: Server | null = null;
|
||||||
@@ -138,74 +198,65 @@ export class BrowserServer {
|
|||||||
this.port = options.port ?? 3000;
|
this.port = options.port ?? 3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: false, error: 'POST only' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = '';
|
||||||
|
for await (const chunk of req) {
|
||||||
|
body += chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.executeRequest(body, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeRequest(body: string, res: ServerResponse): Promise<void> {
|
||||||
|
try {
|
||||||
|
const cmd: BrowserCommand = JSON.parse(body);
|
||||||
|
logCommand(cmd);
|
||||||
|
|
||||||
|
if (cmd.cmd === 'close') {
|
||||||
|
const result = { ok: true as const };
|
||||||
|
logResult(cmd, result);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
await this.stop();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.browser.executeCommand(cmd);
|
||||||
|
logResult(cmd, result);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} catch (err) {
|
||||||
|
const errorResult = { ok: false as const, error: (err as Error).message };
|
||||||
|
logError(timestamp(), errorResult.error);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(errorResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
await this.browser.launch();
|
await this.browser.launch();
|
||||||
|
|
||||||
this.server = createServer(
|
this.server = createServer((req, res) => this.handleRequest(req, res));
|
||||||
async (req: IncomingMessage, res: ServerResponse) => {
|
|
||||||
// CORS headers
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
res.writeHead(204);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method !== 'POST') {
|
|
||||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ ok: false, error: 'POST only' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = '';
|
|
||||||
for await (const chunk of req) {
|
|
||||||
body += chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cmd: BrowserCommand = JSON.parse(body);
|
|
||||||
logCommand(cmd);
|
|
||||||
|
|
||||||
// Handle close specially to shut down server
|
|
||||||
if (cmd.cmd === 'close') {
|
|
||||||
const result = { ok: true as const };
|
|
||||||
logResult(cmd, result);
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(result));
|
|
||||||
await this.stop();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.browser.executeCommand(cmd);
|
|
||||||
logResult(cmd, result);
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(result));
|
|
||||||
} catch (err) {
|
|
||||||
const errorResult = { ok: false as const, error: (err as Error).message };
|
|
||||||
console.log(`${timestamp()} ${c.red}✗ ${errorResult.error}${c.reset}`);
|
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify(errorResult));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.server!.listen(this.port, () => {
|
this.server!.listen(this.port, () => {
|
||||||
console.log();
|
printBanner(this.port);
|
||||||
console.log(`${c.bold}${c.cyan} 🌐 Claude Browse Server${c.reset}`);
|
|
||||||
console.log(`${c.dim} ─────────────────────────${c.reset}`);
|
|
||||||
console.log(` ${c.green}▶${c.reset} Listening on ${c.bold}http://localhost:${this.port}${c.reset}`);
|
|
||||||
console.log();
|
|
||||||
console.log(`${c.dim} Commands:${c.reset}`);
|
|
||||||
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}`);
|
|
||||||
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}`);
|
|
||||||
console.log();
|
|
||||||
console.log(`${c.dim} Example:${c.reset}`);
|
|
||||||
console.log(` ${c.gray}curl -X POST localhost:${this.port} -d '{"cmd":"goto","url":"https://example.com"}'${c.reset}`);
|
|
||||||
console.log();
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user