mirror of
https://github.com/SawyerHood/dev-browser.git
synced 2026-09-20 13:44:44 +08:00
chore: dead code cleanup and update CLAUDE.md (#75)
* chore: dead code cleanup and update CLAUDE.md * fix: restore BrowserEntry export
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
|
||||
_scratch/
|
||||
!bin/dev-browser.js
|
||||
!daemon/dist/daemon.bundle.mjs
|
||||
!daemon/dist/sandbox-client.js
|
||||
.dev-browser/
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This repository ships `dev-browser`: a Rust CLI plus a Node.js daemon for browser automation with a QuickJS sandbox. Use this file as the repo-specific guide when making code changes.
|
||||
|
||||
## Tooling
|
||||
|
||||
- Use Node.js tooling for `daemon/` and Cargo for `cli/`. Do not use Bun.
|
||||
- The daemon package uses `pnpm`.
|
||||
- The repo root contains packaging glue (`bin/`, `scripts/`, `README.md`), but most runtime behavior lives in `cli/` and `daemon/`.
|
||||
|
||||
## Validation
|
||||
|
||||
Run these before finishing changes that touch runtime code:
|
||||
|
||||
```bash
|
||||
cd daemon && npx tsc --noEmit
|
||||
cd daemon && pnpm vitest run
|
||||
cd cli && cargo build
|
||||
```
|
||||
|
||||
If you change daemon runtime code that is embedded into the Rust binary, rebuild the bundles first:
|
||||
|
||||
```bash
|
||||
cd daemon && pnpm bundle
|
||||
cd daemon && pnpm bundle:sandbox-client
|
||||
```
|
||||
|
||||
`cli/src/daemon.rs` embeds `daemon/dist/daemon.bundle.mjs` and `daemon/dist/sandbox-client.js` via `include_str!`, so `cargo build` only sees the latest daemon changes after those bundles are regenerated.
|
||||
@@ -1,102 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build and Development Commands
|
||||
|
||||
Always use Node.js/npm instead of Bun.
|
||||
|
||||
```bash
|
||||
# Install dependencies (from skills/dev-browser/ directory)
|
||||
cd skills/dev-browser && npm install
|
||||
|
||||
# Start the dev-browser server
|
||||
cd skills/dev-browser && npm run start-server
|
||||
|
||||
# Run dev mode with watch
|
||||
cd skills/dev-browser && npm run dev
|
||||
|
||||
# Run tests (uses vitest)
|
||||
cd skills/dev-browser && npm test
|
||||
|
||||
# Run TypeScript check
|
||||
cd skills/dev-browser && npx tsc --noEmit
|
||||
```
|
||||
|
||||
## Important: Before Completing Code Changes
|
||||
|
||||
**Always run these checks before considering a task complete:**
|
||||
|
||||
1. **TypeScript check**: `npx tsc --noEmit` - Ensure no type errors
|
||||
2. **Tests**: `npm test` - Ensure all tests pass
|
||||
|
||||
Common TypeScript issues in this codebase:
|
||||
|
||||
- Use `import type { ... }` for type-only imports (required by `verbatimModuleSyntax`)
|
||||
- Browser globals (`document`, `window`) in `page.evaluate()` callbacks need `declare const document: any;` since DOM lib is not included
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Overview
|
||||
|
||||
This is a browser automation tool designed for developers and AI agents. It solves the problem of maintaining browser state across multiple script executions - unlike Playwright scripts that start fresh each time, dev-browser keeps pages alive and reusable.
|
||||
|
||||
### Structure
|
||||
|
||||
All source code lives in `skills/dev-browser/`:
|
||||
|
||||
- `src/index.ts` - Server: launches persistent Chromium context, exposes HTTP API for page management
|
||||
- `src/client.ts` - Client: connects to server, retrieves pages by name via CDP
|
||||
- `src/types.ts` - Shared TypeScript types for API requests/responses
|
||||
- `src/dom/` - DOM tree extraction utilities for LLM-friendly page inspection
|
||||
- `scripts/start-server.ts` - Entry point to start the server
|
||||
- `tmp/` - Directory for temporary automation scripts
|
||||
|
||||
### Path Aliases
|
||||
|
||||
The project uses `@/` as a path alias to `./src/`. This is configured in both `package.json` (via `imports`) and `tsconfig.json` (via `paths`).
|
||||
|
||||
```typescript
|
||||
// Import from src/client.ts
|
||||
import { connect } from "@/client.js";
|
||||
|
||||
// Import from src/index.ts
|
||||
import { serve } from "@/index.js";
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Server** (`serve()` in `src/index.ts`):
|
||||
- Launches Chromium with `launchPersistentContext` (preserves cookies, localStorage)
|
||||
- Exposes HTTP API on port 9222 for page management
|
||||
- Exposes CDP WebSocket endpoint on port 9223
|
||||
- Pages are registered by name and persist until explicitly closed
|
||||
|
||||
2. **Client** (`connect()` in `src/client.ts`):
|
||||
- Connects to server's HTTP API
|
||||
- Uses CDP `targetId` to reliably find pages across reconnections
|
||||
- Returns standard Playwright `Page` objects for automation
|
||||
|
||||
3. **Key API Endpoints**:
|
||||
- `GET /` - Returns CDP WebSocket endpoint
|
||||
- `GET /pages` - Lists all named pages
|
||||
- `POST /pages` - Gets or creates a page by name (body: `{ name: string }`)
|
||||
- `DELETE /pages/:name` - Closes a page
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
```typescript
|
||||
import { connect } from "@/client.js";
|
||||
|
||||
const client = await connect("http://localhost:9222");
|
||||
const page = await client.page("my-page"); // Gets existing or creates new
|
||||
await page.goto("https://example.com");
|
||||
// Page persists for future scripts
|
||||
await client.disconnect(); // Disconnects CDP but page stays alive on server
|
||||
```
|
||||
|
||||
## Node.js Guidelines
|
||||
|
||||
- Use `npx tsx` for running TypeScript files
|
||||
- Use `dotenv` or similar if you need to load `.env` files
|
||||
- Use `node:fs` for file system operations
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { Page } from "playwright";
|
||||
import type { BrowserManager, BrowserPageSummary } from "./browser-manager.js";
|
||||
|
||||
export interface CleanupTracker {
|
||||
anonymousPages: Page[];
|
||||
}
|
||||
|
||||
export interface ScriptBrowserAPI {
|
||||
getPage(name: string): Promise<Page>;
|
||||
newPage(): Promise<Page>;
|
||||
listPages(): Promise<BrowserPageSummary[]>;
|
||||
closePage(name: string): Promise<void>;
|
||||
}
|
||||
|
||||
export function createBrowserAPI(
|
||||
manager: BrowserManager,
|
||||
browserName: string,
|
||||
cleanupTracker: CleanupTracker
|
||||
): ScriptBrowserAPI {
|
||||
return {
|
||||
getPage(name: string): Promise<Page> {
|
||||
return manager.getPage(browserName, name);
|
||||
},
|
||||
|
||||
async newPage(): Promise<Page> {
|
||||
const page = await manager.newPage(browserName);
|
||||
cleanupTracker.anonymousPages.push(page);
|
||||
return page;
|
||||
},
|
||||
|
||||
async listPages(): Promise<BrowserPageSummary[]> {
|
||||
return manager.listPages(browserName);
|
||||
},
|
||||
|
||||
closePage(name: string): Promise<void> {
|
||||
return manager.closePage(browserName, name);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -14,14 +14,14 @@ export interface BrowserEntry {
|
||||
headless: boolean;
|
||||
}
|
||||
|
||||
export interface BrowserSummary {
|
||||
interface BrowserSummary {
|
||||
name: string;
|
||||
type: BrowserEntry["type"];
|
||||
status: "running" | "connected" | "disconnected";
|
||||
pages: string[];
|
||||
}
|
||||
|
||||
export interface BrowserPageSummary {
|
||||
interface BrowserPageSummary {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
|
||||
+14
-14
@@ -4,7 +4,7 @@ const RequestBaseSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ExecuteRequestSchema = RequestBaseSchema.extend({
|
||||
const ExecuteRequestSchema = RequestBaseSchema.extend({
|
||||
type: z.literal("execute"),
|
||||
browser: z.string().min(1).default("default"),
|
||||
script: z.string(),
|
||||
@@ -13,28 +13,28 @@ export const ExecuteRequestSchema = RequestBaseSchema.extend({
|
||||
timeoutMs: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
export const BrowsersRequestSchema = RequestBaseSchema.extend({
|
||||
const BrowsersRequestSchema = RequestBaseSchema.extend({
|
||||
type: z.literal("browsers"),
|
||||
});
|
||||
|
||||
export const BrowserStopRequestSchema = RequestBaseSchema.extend({
|
||||
const BrowserStopRequestSchema = RequestBaseSchema.extend({
|
||||
type: z.literal("browser-stop"),
|
||||
browser: z.string().min(1),
|
||||
});
|
||||
|
||||
export const StatusRequestSchema = RequestBaseSchema.extend({
|
||||
const StatusRequestSchema = RequestBaseSchema.extend({
|
||||
type: z.literal("status"),
|
||||
});
|
||||
|
||||
export const InstallRequestSchema = RequestBaseSchema.extend({
|
||||
const InstallRequestSchema = RequestBaseSchema.extend({
|
||||
type: z.literal("install"),
|
||||
});
|
||||
|
||||
export const StopRequestSchema = RequestBaseSchema.extend({
|
||||
const StopRequestSchema = RequestBaseSchema.extend({
|
||||
type: z.literal("stop"),
|
||||
});
|
||||
|
||||
export const RequestSchema = z.discriminatedUnion("type", [
|
||||
const RequestSchema = z.discriminatedUnion("type", [
|
||||
ExecuteRequestSchema,
|
||||
BrowsersRequestSchema,
|
||||
BrowserStopRequestSchema,
|
||||
@@ -47,32 +47,32 @@ const ResponseBaseSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export const StdoutMessageSchema = ResponseBaseSchema.extend({
|
||||
const StdoutMessageSchema = ResponseBaseSchema.extend({
|
||||
type: z.literal("stdout"),
|
||||
data: z.string(),
|
||||
});
|
||||
|
||||
export const StderrMessageSchema = ResponseBaseSchema.extend({
|
||||
const StderrMessageSchema = ResponseBaseSchema.extend({
|
||||
type: z.literal("stderr"),
|
||||
data: z.string(),
|
||||
});
|
||||
|
||||
export const CompleteMessageSchema = ResponseBaseSchema.extend({
|
||||
const CompleteMessageSchema = ResponseBaseSchema.extend({
|
||||
type: z.literal("complete"),
|
||||
success: z.literal(true),
|
||||
});
|
||||
|
||||
export const ErrorMessageSchema = ResponseBaseSchema.extend({
|
||||
const ErrorMessageSchema = ResponseBaseSchema.extend({
|
||||
type: z.literal("error"),
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const ResultMessageSchema = ResponseBaseSchema.extend({
|
||||
const ResultMessageSchema = ResponseBaseSchema.extend({
|
||||
type: z.literal("result"),
|
||||
data: z.unknown(),
|
||||
});
|
||||
|
||||
export const ResponseSchema = z.discriminatedUnion("type", [
|
||||
const ResponseSchema = z.discriminatedUnion("type", [
|
||||
StdoutMessageSchema,
|
||||
StderrMessageSchema,
|
||||
CompleteMessageSchema,
|
||||
@@ -80,7 +80,7 @@ export const ResponseSchema = z.discriminatedUnion("type", [
|
||||
ResultMessageSchema,
|
||||
]);
|
||||
|
||||
export type Request = z.infer<typeof RequestSchema>;
|
||||
type Request = z.infer<typeof RequestSchema>;
|
||||
export type ExecuteRequest = z.infer<typeof ExecuteRequestSchema>;
|
||||
export type Response = z.infer<typeof ResponseSchema>;
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ function decodeSandboxFilePayload(value: unknown, label: string): string | Uint8
|
||||
return Buffer.from(data, "base64");
|
||||
}
|
||||
|
||||
export interface QuickJSSandboxOptions {
|
||||
interface QuickJSSandboxOptions {
|
||||
manager: BrowserManager;
|
||||
browserName: string;
|
||||
onStdout: (data: string) => void;
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import util from "node:util";
|
||||
import vm from "node:vm";
|
||||
import type { ScriptBrowserAPI } from "./browser-api.js";
|
||||
|
||||
interface ScriptOutput {
|
||||
onStdout: (data: string) => void;
|
||||
onStderr: (data: string) => void;
|
||||
}
|
||||
|
||||
function withWallClockTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Script timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
promise.then(
|
||||
(value) => {
|
||||
clearTimeout(timer);
|
||||
resolve(value);
|
||||
},
|
||||
(error: unknown) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function formatArgs(args: unknown[]): string {
|
||||
return args
|
||||
.map((arg) =>
|
||||
typeof arg === "string"
|
||||
? arg
|
||||
: util.inspect(arg, {
|
||||
colors: false,
|
||||
depth: 6,
|
||||
compact: 3,
|
||||
breakLength: Infinity,
|
||||
})
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export async function runScript(
|
||||
script: string,
|
||||
browser: ScriptBrowserAPI,
|
||||
output: ScriptOutput,
|
||||
options: { timeout?: number } = {}
|
||||
): Promise<void> {
|
||||
const { timeout = 30_000 } = options;
|
||||
|
||||
const routedConsole = {
|
||||
log: (...args: unknown[]) => output.onStdout(`${formatArgs(args)}\n`),
|
||||
info: (...args: unknown[]) => output.onStdout(`${formatArgs(args)}\n`),
|
||||
debug: (...args: unknown[]) => output.onStdout(`${formatArgs(args)}\n`),
|
||||
warn: (...args: unknown[]) => output.onStderr(`${formatArgs(args)}\n`),
|
||||
error: (...args: unknown[]) => output.onStderr(`${formatArgs(args)}\n`),
|
||||
};
|
||||
|
||||
const context = vm.createContext({
|
||||
browser,
|
||||
console: routedConsole,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
});
|
||||
|
||||
const wrappedScript = `(async () => {\n${script}\n})()`;
|
||||
const compiledScript = new vm.Script(wrappedScript, {
|
||||
filename: "user-script.js",
|
||||
});
|
||||
const result = compiledScript.runInContext(context, {
|
||||
timeout,
|
||||
displayErrors: true,
|
||||
});
|
||||
|
||||
await withWallClockTimeout(Promise.resolve(result), timeout);
|
||||
}
|
||||
Reference in New Issue
Block a user