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:
Sawyer Hood
2026-03-26 14:38:15 -07:00
committed by GitHub
parent ea752ba03f
commit 06a5e7f3e2
8 changed files with 46 additions and 239 deletions
-2
View File
@@ -1,8 +1,6 @@
_scratch/
!bin/dev-browser.js
!daemon/dist/daemon.bundle.mjs
!daemon/dist/sandbox-client.js
.dev-browser/
.DS_Store
.env
+28
View File
@@ -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.
-102
View File
@@ -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
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
-39
View File
@@ -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);
},
};
}
+2 -2
View File
@@ -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
View File
@@ -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>;
+1 -1
View File
@@ -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;
-79
View File
@@ -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);
}