diff --git a/.github/workflows/stagehand-codemode.yml b/.github/workflows/stagehand-codemode.yml new file mode 100644 index 0000000..826e92e --- /dev/null +++ b/.github/workflows/stagehand-codemode.yml @@ -0,0 +1,32 @@ +name: Stagehand code-mode tool + +on: + pull_request: + paths: + - '.github/workflows/stagehand-codemode.yml' + - 'packages/stagehand-codemode/**' + push: + branches: [main] + paths: + - '.github/workflows/stagehand-codemode.yml' + - 'packages/stagehand-codemode/**' + +permissions: + contents: read + +jobs: + core: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + - uses: actions/setup-node@v4 + with: + node-version: 24.x + + - run: pnpm install --no-frozen-lockfile + - run: pnpm --filter @browserbasehq/stagehand-codemode run typecheck + - run: pnpm --filter @browserbasehq/stagehand-codemode run build diff --git a/packages/stagehand-codemode/README.md b/packages/stagehand-codemode/README.md new file mode 100644 index 0000000..09fa708 --- /dev/null +++ b/packages/stagehand-codemode/README.md @@ -0,0 +1,156 @@ +# Stagehand Code Mode + +Stagehand Code Mode gives an agent one `code_execute` tool for operating a Browserbase browser with +Stagehand V4 JavaScript. It is framework-neutral: agent frameworks can launch the included local +MCP server over stdio or wrap `StagehandCodeExecutor` in a native tool binding. + +The executor creates a Browserbase browser on the first valid call, serializes calls, and reuses the +same browser until its owner closes it. This lets an agent complete multi-step tasks across tool calls +without managing a browser or session identifier. + +## Tool contract + +`code_execute` accepts the body of an async JavaScript function: + +```ts +type CodeExecuteInput = { + code: string; +}; +``` + +The generated function receives these objects: + +- `page`: the active Stagehand V4 `Page`; +- `context`: the shared Stagehand V4 `BrowserContext`; +- `stagehand`: the Stagehand V4 `act`, `observe`, and `extract` methods; +- `z`: Zod V4 for structured extraction schemas; and +- `console`: captured `log`, `warn`, and `error` methods. + +For example: + +```js +await page.goto('https://example.com', { waitUntil: 'load' }); +return { + title: await page.title(), + url: await page.url(), +}; +``` + +Calls return a JSON-safe result containing the active page state, the generated function's return +value, and any captured logs: + +```ts +type CodeExecuteResult = + | { + ok: true; + page: { url: string; title: string }; + value?: unknown; + logs?: Array<{ level: 'log' | 'warn' | 'error'; text: string }>; + } + | { + ok: false; + page?: { url: string; title: string }; + logs?: Array<{ level: 'log' | 'warn' | 'error'; text: string }>; + error: { + kind: 'validation' | 'runtime' | 'aborted' | 'closed'; + name: string; + message: string; + }; + }; +``` + +## Local MCP integration + +Frameworks with local-process MCP support should launch the built stdio server and keep that process +alive for the complete agent run: + +```text +node packages/stagehand-codemode/dist/stdio-server.js +``` + +The stdio server is an internal process entrypoint, not a user-facing CLI. The package does not +publish a `bin` command or accept command-line arguments. + +The framework owns the process lifecycle: + +1. Launch one stdio server for the agent run. +2. Reuse it for every `code_execute` call that should share browser state. +3. Terminate and relaunch it if a call stops responding. +4. Close it when the agent run finishes. + +Restarting the process creates a new browser, so browser state from the previous process is not +preserved. + +## Native tool integration + +Frameworks that do not launch local MCP servers can wrap the executor directly: + +```ts +import { + StagehandCodeExecutor, + stagehandCodeConfigFromEnv, +} from '@browserbasehq/stagehand-codemode'; + +const executor = new StagehandCodeExecutor(stagehandCodeConfigFromEnv()); + +try { + const result = await executor.execute({ + code: ` + await page.goto("https://example.com", { waitUntil: "load" }); + return { title: await page.title() }; + `, + }); + console.log(result); +} finally { + await executor.close(); +} +``` + +Create one executor per agent run and close it in `finally` so the Browserbase browser is released +when the run succeeds, fails, or is cancelled. + +## Configuration + +Stagehand Code Mode reads configuration from the owning framework's environment: + +| Variable | Required | Purpose | +| --------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------- | +| `BROWSERBASE_API_KEY` | Yes, before the first `code_execute` call | Creates the Browserbase browser | +| `STAGEHAND_MODEL_NAME` | Only for Stagehand AI methods | Provider and model name used by `act`, `observe`, and `extract` | +| `STAGEHAND_MODEL_API_KEY` | Provider-dependent | Explicit model-provider API key | +| `STAGEHAND_MODEL_BASE_URL` | No | Custom model-provider base URL | +| `GEMINI_API_KEY`, `GOOGLE_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY` | No | Selects `google/gemini-2.5-flash-lite` when no explicit model is configured | + +The consuming application must also make a compatible `@browserbasehq/stagehand` V4 package +available to the executor. + +## Model syntax guide + +[`SKILL.md`](./SKILL.md) is the canonical Stagehand V4 syntax guide. The MCP server includes the +complete guide in the `code_execute` tool description. Native integrations should also add the +exported `STAGEHAND_CODEMODE_SKILL` string to the agent's system instructions or equivalent +high-priority context. + +The guide covers deterministic page and locator methods, `act`, `observe`, `extract`, Zod schemas, +multiple pages, cross-call state, and return-value discipline. + +## Lifecycle and limits + +- Browser creation is lazy; MCP discovery does not create a Browserbase browser. +- Calls are serialized because they operate on one shared browser context. +- Pages, cookies, and navigation state persist across successful calls in the same process. +- JavaScript variables declared inside generated code do not persist between calls. +- Input code is limited to 100,000 UTF-8 bytes. +- Captured logs are limited to 64 KiB. +- Returned values are limited to 256 KiB and are truncated with metadata when necessary. +- BigInt and byte-array values are converted into JSON-safe representations. + +## Security + +Generated JavaScript runs directly in the MCP or native-tool process. Stagehand Code Mode is not a +security sandbox: generated code can access the filesystem, network, environment variables, Node +globals, and in-process SDK state available to that process. + +Use Stagehand Code Mode only with trusted agents in a trusted execution environment. Applications +that execute untrusted code must provide a real isolation boundary, such as a restricted container, +virtual machine, or purpose-built code sandbox. diff --git a/packages/stagehand-codemode/SKILL.md b/packages/stagehand-codemode/SKILL.md new file mode 100644 index 0000000..248c894 --- /dev/null +++ b/packages/stagehand-codemode/SKILL.md @@ -0,0 +1,88 @@ +# Stagehand V4 code-mode syntax skill + +You have one `code_execute` tool. Its `code` argument is the body of an async JavaScript function, +not a complete program. Write direct `await` statements and finish with a JSON-serializable return +value. + +The following objects are already in scope: + +- `page`: the active Stagehand `Page`. +- `context`: the Stagehand `BrowserContext` shared across calls. +- `stagehand`: the Stagehand AI methods `act`, `observe`, and `extract`. +- `z`: Zod V4, for `stagehand.extract` schemas. +- `console`: captured `log`, `warn`, and `error` methods. + +Do not import packages, read environment variables, construct Stagehand, call `stagehand.init()`, or +close the page/browser. The tool process owns initialization and cleanup. + +## Direct browser syntax + +Use deterministic page and locator methods when you know the target: + +```js +await page.goto('https://example.com', { waitUntil: 'load' }); +const heading = await page.locator('h1').innerText(); +const visible = await page.locator('a').first().isVisible(); +return { heading, visible, url: await page.url(), title: await page.title() }; +``` + +Common page methods include `goto`, `reload`, `goBack`, `goForward`, `click`, `hover`, `scroll`, +`dragAndDrop`, `type`, `keyPress`, `evaluate`, `waitForLoadState`, `waitForTimeout`, +`waitForSelector`, `screenshot`, `snapshot`, `url`, `title`, and `locator`. + +Common locator methods include `click`, `hover`, `fill`, `count`, `isChecked`, `inputValue`, +`isVisible`, `innerText`, `innerHtml`, `textContent`, `scrollTo`, `type`, `selectOption`, `first`, +and `nth`. + +## Stagehand AI syntax + +Use `act` for an interaction described in natural language: + +```js +const result = await stagehand.act('Click the sign-in button'); +if (!result.success) throw new Error(result.message); +return result; +``` + +Use `observe` to find candidate actions without performing them: + +```js +const actions = await stagehand.observe('Find the checkout button'); +return { actions }; +``` + +Use `extract` with a Zod schema for structured page data: + +```js +const product = await stagehand.extract( + 'Extract the product name and price', + z.object({ name: z.string(), price: z.string() }) +); +return product; +``` + +Pass `{ page: anotherPage }` as the final options object to `act`, `observe`, or `extract` when the +active page is not the intended target. + +## Pages and state across calls + +```js +const pages = await context.pages(); +const secondPage = pages[1] ?? (await context.newPage()); +await context.setActivePage(secondPage); +return { + pageCount: (await context.pages()).length, + activeUrl: await secondPage.url(), +}; +``` + +The same browser, pages, cookies, and navigation state persist across successful tool calls. Local +JavaScript variables do not persist, so rediscover pages and elements each call. If a call stops +responding, the owning agent framework should terminate and restart the local MCP process. That +restart begins a fresh browser and loses the previous browser state. + +## Return discipline + +Return only the compact evidence needed by the agent. Prefer strings, numbers, booleans, arrays, +and plain objects. Do not return page, locator, context, Stagehand, or Zod objects. Await asynchronous +methods before returning. Logs and oversized return values are bounded by the executor. diff --git a/packages/stagehand-codemode/package.json b/packages/stagehand-codemode/package.json new file mode 100644 index 0000000..6ec966c --- /dev/null +++ b/packages/stagehand-codemode/package.json @@ -0,0 +1,37 @@ +{ + "name": "@browserbasehq/stagehand-codemode", + "version": "0.0.0", + "private": true, + "description": "Local stdio MCP tool for Stagehand code mode", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsdown", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@browserbasehq/stagehand": "*" + }, + "peerDependenciesMeta": { + "@browserbasehq/stagehand": { + "optional": true + } + }, + "devDependencies": { + "@types/node": "^25.0.9", + "tsdown": "^0.15.4", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.18.0" + } +} diff --git a/packages/stagehand-codemode/src/config.ts b/packages/stagehand-codemode/src/config.ts new file mode 100644 index 0000000..4fea077 --- /dev/null +++ b/packages/stagehand-codemode/src/config.ts @@ -0,0 +1,35 @@ +import type { StagehandCodeConfig } from './types.js'; + +export function stagehandCodeConfigFromEnv( + env: NodeJS.ProcessEnv = process.env +): StagehandCodeConfig { + const explicitModelName = nonEmpty(env.STAGEHAND_MODEL_NAME); + const explicitModelApiKey = nonEmpty(env.STAGEHAND_MODEL_API_KEY); + const inferredGoogleKey = + nonEmpty(env.GEMINI_API_KEY) ?? + nonEmpty(env.GOOGLE_API_KEY) ?? + nonEmpty(env.GOOGLE_GENERATIVE_AI_API_KEY); + const modelName = + explicitModelName ?? + (inferredGoogleKey ? 'google/gemini-2.5-flash-lite' : undefined); + const modelApiKey = explicitModelApiKey ?? inferredGoogleKey; + return { + browserbaseApiKey: nonEmpty(env.BROWSERBASE_API_KEY), + ...(modelName + ? { + model: { + modelName, + ...(modelApiKey ? { apiKey: modelApiKey } : {}), + ...(nonEmpty(env.STAGEHAND_MODEL_BASE_URL) + ? { baseURL: nonEmpty(env.STAGEHAND_MODEL_BASE_URL) } + : {}), + }, + } + : {}), + }; +} + +function nonEmpty(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} diff --git a/packages/stagehand-codemode/src/executor.ts b/packages/stagehand-codemode/src/executor.ts new file mode 100644 index 0000000..9610eef --- /dev/null +++ b/packages/stagehand-codemode/src/executor.ts @@ -0,0 +1,262 @@ +import { z } from 'zod/v4'; +import type { + CodeExecuteFailure, + CodeExecuteInput, + CodeExecuteResult, + CodeLogEntry, + CodePageState, + StagehandCodeConfig, +} from './types.js'; + +type PageLike = { + url(): Promise; + title(): Promise; +}; + +type ContextLike = { + activePage(): Promise; + pages(): Promise; + newPage(): Promise; +}; + +type StagehandLike = Record & { + context: ContextLike; + init(): Promise; + close(): Promise; +}; + +type StagehandConstructor = new ( + options: Record +) => StagehandLike; + +export type StagehandCodeExecutorOptions = StagehandCodeConfig; + +const AsyncFunction = Object.getPrototypeOf(async function () {}) + .constructor as new ( + ...args: string[] +) => (...values: unknown[]) => Promise; +const MAX_CODE_BYTES = 100_000; +const MAX_LOG_BYTES = 64 * 1024; +const MAX_RESULT_BYTES = 256 * 1024; + +export class StagehandCodeExecutor { + private stagehand?: StagehandLike; + private queue = Promise.resolve(); + private closed = false; + private closePromise?: Promise; + + constructor(private readonly options: StagehandCodeExecutorOptions) {} + + execute( + input: CodeExecuteInput, + signal?: AbortSignal + ): Promise { + const validation = validate(input); + if (validation) return Promise.resolve(validation); + + const operation = this.queue.then(() => this.executeQueued(input, signal)); + this.queue = operation.then( + () => undefined, + () => undefined + ); + return operation; + } + + close(): Promise { + this.closed = true; + this.closePromise ??= this.queue.then(async () => { + const current = this.stagehand; + this.stagehand = undefined; + await current?.close(); + }); + return this.closePromise; + } + + private async executeQueued( + input: CodeExecuteInput, + signal?: AbortSignal + ): Promise { + if (this.closed) { + return failure('closed', 'Code executor is closed.'); + } + if (signal?.aborted) { + return failure('aborted', 'Code execution was aborted before it began.'); + } + + const logs: CodeLogEntry[] = []; + let page: PageLike | undefined; + try { + const stagehand = await this.ensureStagehand(); + const context = stagehand.context; + page = + (await context.activePage()) ?? + (await context.pages())[0] ?? + (await context.newPage()); + + const fn = new AsyncFunction( + 'page', + 'context', + 'stagehand', + 'z', + 'console', + input.code + ); + const value = await fn( + page, + context, + stagehand, + z, + createCodeConsole(logs) + ); + + return { + ok: true, + page: await readPageState(page), + ...(value === undefined ? {} : { value: jsonSafe(value) }), + ...(logs.length === 0 ? {} : { logs }), + }; + } catch (error) { + const normalized = + error instanceof Error ? error : new Error(String(error)); + const currentPage = + page ?? (await this.activePage().catch(() => undefined)); + return failure('runtime', normalized.message, normalized.name, { + ...(currentPage + ? { page: await readPageState(currentPage).catch(() => undefined) } + : {}), + ...(logs.length === 0 ? {} : { logs }), + }); + } + } + + private async ensureStagehand(): Promise { + if (this.stagehand) return this.stagehand; + if (!this.options.browserbaseApiKey) { + throw new Error( + 'BROWSERBASE_API_KEY is required before the first code_execute call.' + ); + } + + const packageName = '@browserbasehq/stagehand'; + const imported = (await import(packageName)) as { + Stagehand?: StagehandConstructor; + StagehandClientInitParamsSchema?: unknown; + }; + if (!imported.Stagehand || !imported.StagehandClientInitParamsSchema) { + throw new Error( + 'Stagehand code mode requires a local Stagehand V4 build resolvable as @browserbasehq/stagehand.' + ); + } + + const next = new imported.Stagehand({ + apiKey: this.options.browserbaseApiKey, + browser: { type: 'browserbase' }, + logging: { level: 'off' }, + ...(this.options.model ? { model: this.options.model } : {}), + }); + try { + await next.init(); + } catch (error) { + await next.close().catch(() => undefined); + throw error; + } + this.stagehand = next; + return next; + } + + private async activePage(): Promise { + if (!this.stagehand) return undefined; + return ( + (await this.stagehand.context.activePage()) ?? + (await this.stagehand.context.pages())[0] + ); + } +} + +function validate(input: CodeExecuteInput): CodeExecuteFailure | undefined { + if ( + !input || + typeof input.code !== 'string' || + input.code.trim().length === 0 + ) { + return failure( + 'validation', + 'code must be a non-empty JavaScript function body.' + ); + } + if (Buffer.byteLength(input.code) > MAX_CODE_BYTES) { + return failure( + 'validation', + `code must be at most ${MAX_CODE_BYTES} UTF-8 bytes.` + ); + } + return undefined; +} + +function createCodeConsole(logs: CodeLogEntry[]) { + let logBytes = 0; + const append = (level: CodeLogEntry['level'], values: unknown[]) => { + if (logBytes >= MAX_LOG_BYTES) return; + const text = formatLog(values); + const remaining = MAX_LOG_BYTES - logBytes; + const bounded = Buffer.from(text).subarray(0, remaining).toString(); + logBytes += Buffer.byteLength(bounded); + logs.push({ level, text: bounded }); + }; + return Object.freeze({ + log: (...values: unknown[]) => append('log', values), + warn: (...values: unknown[]) => append('warn', values), + error: (...values: unknown[]) => append('error', values), + }); +} + +async function readPageState(page: PageLike): Promise { + const [url, title] = await Promise.all([page.url(), page.title()]); + return { url, title }; +} + +function jsonSafe(value: unknown): unknown { + if (value === undefined) return undefined; + const serialized = JSON.stringify(value, (_key, nested) => { + if (typeof nested === 'bigint') return nested.toString(); + if (nested instanceof Uint8Array) { + return { + type: 'bytes', + encoding: 'base64', + data: Buffer.from(nested).toString('base64'), + }; + } + return nested; + }); + if (serialized === undefined) return undefined; + const bytes = Buffer.byteLength(serialized); + if (bytes <= MAX_RESULT_BYTES) return JSON.parse(serialized); + return { + truncated: true, + original_bytes: bytes, + preview: Buffer.from(serialized).subarray(0, MAX_RESULT_BYTES).toString(), + }; +} + +function formatLog(values: unknown[]): string { + return values + .map(value => { + if (typeof value === 'string') return value; + const safe = jsonSafe(value); + return safe === undefined ? String(value) : JSON.stringify(safe); + }) + .join(' '); +} + +function failure( + kind: CodeExecuteFailure['error']['kind'], + message: string, + name = 'CodeModeError', + evidence: Pick = {} +): CodeExecuteFailure { + return { + ok: false, + ...evidence, + error: { kind, name, message }, + }; +} diff --git a/packages/stagehand-codemode/src/index.ts b/packages/stagehand-codemode/src/index.ts new file mode 100644 index 0000000..160b5c0 --- /dev/null +++ b/packages/stagehand-codemode/src/index.ts @@ -0,0 +1,17 @@ +export { stagehandCodeConfigFromEnv } from './config.js'; +export { + StagehandCodeExecutor, + type StagehandCodeExecutorOptions, +} from './executor.js'; +export { + connectCodeModeStdio, + createCodeModeMcp, + createCodeModeMcpServer, +} from './mcp-server.js'; +export { STAGEHAND_CODEMODE_SKILL } from './skill.js'; +export { + CODE_EXECUTE_DESCRIPTION, + codeExecuteResultText, + codeExecuteSchema, +} from './tool-contract.js'; +export * from './types.js'; diff --git a/packages/stagehand-codemode/src/mcp-server.ts b/packages/stagehand-codemode/src/mcp-server.ts new file mode 100644 index 0000000..ec3305d --- /dev/null +++ b/packages/stagehand-codemode/src/mcp-server.ts @@ -0,0 +1,67 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod/v4'; +import { + StagehandCodeExecutor, + type StagehandCodeExecutorOptions, +} from './executor.js'; +import { + CODE_EXECUTE_DESCRIPTION, + codeExecuteResultText, + codeExecuteSchema, +} from './tool-contract.js'; +import type { CodeExecuteInput, CodeExecuteResult } from './types.js'; + +export function createCodeModeMcpServer( + executor: StagehandCodeExecutor +): McpServer { + const server = new McpServer({ + name: 'stagehand-codemode', + version: '0.0.0', + }); + server.registerTool( + 'code_execute', + { + title: 'Execute Stagehand V4 code', + description: CODE_EXECUTE_DESCRIPTION, + inputSchema: codeExecuteSchema.shape, + outputSchema: z + .object({ + ok: z.boolean(), + }) + .loose(), + }, + async (input, extra) => { + const result = await executor.execute( + input as CodeExecuteInput, + extra.signal + ); + return mcpResult(result); + } + ); + return server; +} + +export async function connectCodeModeStdio( + executor: StagehandCodeExecutor +): Promise { + const server = createCodeModeMcpServer(executor); + await server.connect(new StdioServerTransport()); + return server; +} + +export function createCodeModeMcp(options: StagehandCodeExecutorOptions): { + executor: StagehandCodeExecutor; + server: McpServer; +} { + const executor = new StagehandCodeExecutor(options); + return { executor, server: createCodeModeMcpServer(executor) }; +} + +function mcpResult(result: CodeExecuteResult) { + return { + content: [{ type: 'text' as const, text: codeExecuteResultText(result) }], + structuredContent: result as unknown as Record, + isError: !result.ok, + }; +} diff --git a/packages/stagehand-codemode/src/skill.ts b/packages/stagehand-codemode/src/skill.ts new file mode 100644 index 0000000..921bb7f --- /dev/null +++ b/packages/stagehand-codemode/src/skill.ts @@ -0,0 +1,6 @@ +import { readFileSync } from 'node:fs'; + +export const STAGEHAND_CODEMODE_SKILL = readFileSync( + new URL('../SKILL.md', import.meta.url), + 'utf8' +).trim(); diff --git a/packages/stagehand-codemode/src/stdio-server.ts b/packages/stagehand-codemode/src/stdio-server.ts new file mode 100644 index 0000000..75400ba --- /dev/null +++ b/packages/stagehand-codemode/src/stdio-server.ts @@ -0,0 +1,25 @@ +import { stagehandCodeConfigFromEnv } from './config.js'; +import { StagehandCodeExecutor } from './executor.js'; +import { connectCodeModeStdio } from './mcp-server.js'; + +const executor = new StagehandCodeExecutor(stagehandCodeConfigFromEnv()); +const server = await connectCodeModeStdio(executor); +let closing = false; + +async function shutdown(code: number): Promise { + if (closing) return; + closing = true; + await server.close().catch(() => undefined); + await executor.close().catch(error => { + process.stderr.write( + `Failed to close Stagehand code mode: ${String(error)}\n` + ); + }); + process.exit(code); +} + +process.once('SIGINT', () => void shutdown(0)); +process.once('SIGTERM', () => void shutdown(0)); +process.stdin.once('end', () => void shutdown(0)); +process.stdin.once('close', () => void shutdown(0)); +process.stderr.write('Stagehand code-mode MCP listening on stdio\n'); diff --git a/packages/stagehand-codemode/src/tool-contract.ts b/packages/stagehand-codemode/src/tool-contract.ts new file mode 100644 index 0000000..e71613f --- /dev/null +++ b/packages/stagehand-codemode/src/tool-contract.ts @@ -0,0 +1,26 @@ +import { z } from 'zod/v4'; +import { STAGEHAND_CODEMODE_SKILL } from './skill.js'; +import type { CodeExecuteResult } from './types.js'; + +export const CODE_EXECUTE_DESCRIPTION = [ + 'Execute an async JavaScript function body against one long-lived Stagehand V4 browser on Browserbase.', + 'The local executor lazily creates the browser on the first call and reuses it for later calls.', + 'Code runs directly in the local MCP process. The owning agent framework should terminate and restart that process if it stops responding.', + 'This is trusted local code with filesystem, network, and in-process SDK access; it is not a security sandbox.', + '', + STAGEHAND_CODEMODE_SKILL, +].join('\n'); + +export const codeExecuteSchema = z.object({ + code: z + .string() + .min(1) + .max(100_000) + .describe( + 'Async JavaScript function body. page, context, stagehand, z, and console are in scope.' + ), +}); + +export function codeExecuteResultText(result: CodeExecuteResult): string { + return JSON.stringify(result, null, 2); +} diff --git a/packages/stagehand-codemode/src/types.ts b/packages/stagehand-codemode/src/types.ts new file mode 100644 index 0000000..69693eb --- /dev/null +++ b/packages/stagehand-codemode/src/types.ts @@ -0,0 +1,48 @@ +export type CodeExecuteInput = { + code: string; +}; + +export type CodePageState = { + url: string; + title: string; +}; + +export type CodeLogEntry = { + level: 'log' | 'warn' | 'error'; + text: string; +}; + +export type CodeExecuteErrorKind = + | 'validation' + | 'runtime' + | 'aborted' + | 'closed'; + +export type CodeExecuteSuccess = { + ok: true; + page: CodePageState; + value?: unknown; + logs?: CodeLogEntry[]; +}; + +export type CodeExecuteFailure = { + ok: false; + page?: CodePageState; + logs?: CodeLogEntry[]; + error: { + kind: CodeExecuteErrorKind; + name: string; + message: string; + }; +}; + +export type CodeExecuteResult = CodeExecuteSuccess | CodeExecuteFailure; + +export type StagehandCodeConfig = { + browserbaseApiKey?: string; + model?: { + modelName: string; + apiKey?: string; + baseURL?: string; + }; +}; diff --git a/packages/stagehand-codemode/tsconfig.json b/packages/stagehand-codemode/tsconfig.json new file mode 100644 index 0000000..fb71fd3 --- /dev/null +++ b/packages/stagehand-codemode/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "types": ["node"], + "rootDir": ".", + "noEmit": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/stagehand-codemode/tsdown.config.ts b/packages/stagehand-codemode/tsdown.config.ts new file mode 100644 index 0000000..191135e --- /dev/null +++ b/packages/stagehand-codemode/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts', 'src/stdio-server.ts'], + format: ['esm'], + platform: 'node', + target: 'node22', + dts: { sourcemap: true }, + sourcemap: true, + outDir: 'dist', +});