Quickstart (Typescript)
How to use Claude Computer Use with Steel
This guide shows you how to create AI agents with Claude's computer use capabilities and Steel browsers for autonomous web task execution.
Prerequisites
-
Node.js 20+
-
A Steel API key (sign up here)
-
An Anthropic API key with access to Claude models
Step 1: Setup and Dependencies
First, create a project directory and install the required packages:
# Create a project directorymkdir steel-claude-computer-usecd steel-claude-computer-use# Initialize package.jsonnpm init -y# Install required packagesnpm install steel-sdk @anthropic-ai/sdk playwright dotenvnpm install -D @types/node typescript ts-node
Create a .env
file with your API keys:
1STEEL_API_KEY=your_steel_api_key_here2ANTHROPIC_API_KEY=your_anthropic_api_key_here3TASK=Go to Wikipedia and search for machine learning
Step 2: Create Helper Functions
1import { chromium } from "playwright";2import type { Browser, Page } from "playwright";3import { Steel } from "steel-sdk";4import * as dotenv from "dotenv";5import Anthropic from "@anthropic-ai/sdk";6import type {7MessageParam,8ToolResultBlockParam,9Message,10} from "@anthropic-ai/sdk/resources/messages";1112dotenv.config();1314// Replace with your own API keys15export const STEEL_API_KEY =16process.env.STEEL_API_KEY || "your-steel-api-key-here";17export const ANTHROPIC_API_KEY =18process.env.ANTHROPIC_API_KEY || "your-anthropic-api-key-here";1920// Replace with your own task21export const TASK =22process.env.TASK || "Go to Wikipedia and search for machine learning";2324export const SYSTEM_PROMPT = `You are an expert browser automation assistant operating in an iterative execution loop. Your goal is to efficiently complete tasks using a Chrome browser with full internet access.2526<CAPABILITIES>27* You control a Chrome browser tab and can navigate to any website28* You can click, type, scroll, take screenshots, and interact with web elements29* You have full internet access and can visit any public website30* You can read content, fill forms, search for information, and perform complex multi-step tasks31* After each action, you receive a screenshot showing the current state3233<COORDINATE_SYSTEM>34* The browser viewport has specific dimensions that you must respect35* All coordinates (x, y) must be within the viewport bounds36* X coordinates must be between 0 and the display width (inclusive)37* Y coordinates must be between 0 and the display height (inclusive)38* Always ensure your click, move, scroll, and drag coordinates are within these bounds39* If you're unsure about element locations, take a screenshot first to see the current state4041<AUTONOMOUS_EXECUTION>42* Work completely independently - make decisions and act immediately without asking questions43* Never request clarification, present options, or ask for permission44* Make intelligent assumptions based on task context45* If something is ambiguous, choose the most logical interpretation and proceed46* Take immediate action rather than explaining what you might do47* When the task objective is achieved, immediately declare "TASK_COMPLETED:" - do not provide commentary or ask questions4849<REASONING_STRUCTURE>50For each step, you must reason systematically:51* Analyze your previous action's success/failure and current state52* Identify what specific progress has been made toward the goal53* Determine the next immediate objective and how to achieve it54* Choose the most efficient action sequence to make progress5556<EFFICIENCY_PRINCIPLES>57* Combine related actions when possible rather than single-step execution58* Navigate directly to relevant websites without unnecessary exploration59* Use screenshots strategically to understand page state before acting60* Be persistent with alternative approaches if initial attempts fail61* Focus on the specific information or outcome requested6263<COMPLETION_CRITERIA>64* MANDATORY: When you complete the task, your final message MUST start with "TASK_COMPLETED: [brief summary]"65* MANDATORY: If technical issues prevent completion, your final message MUST start with "TASK_FAILED: [reason]"66* MANDATORY: If you abandon the task, your final message MUST start with "TASK_ABANDONED: [explanation]"67* Do not write anything after completing the task except the required completion message68* Do not ask questions, provide commentary, or offer additional help after task completion69* The completion message is the end of the interaction - nothing else should follow7071<CRITICAL_REQUIREMENTS>72* This is fully automated execution - work completely independently73* Start by taking a screenshot to understand the current state74* Never click on browser UI elements75* Always respect coordinate boundaries - invalid coordinates will fail76* Recognize when the stated objective has been achieved and declare completion immediately77* Focus on the explicit task given, not implied or potential follow-up tasks7879Remember: Be thorough but focused. Complete the specific task requested efficiently and provide clear results.`;8081export const BLOCKED_DOMAINS = [82"maliciousbook.com",83"evilvideos.com",84"darkwebforum.com",85"shadytok.com",86"suspiciouspins.com",87"ilanbigio.com",88];8990export const MODEL_CONFIGS = {91"claude-3-5-sonnet-20241022": {92toolType: "computer_20241022",93betaFlag: "computer-use-2024-10-22",94description: "Stable Claude 3.5 Sonnet (recommended)",95},96"claude-3-7-sonnet-20250219": {97toolType: "computer_20250124",98betaFlag: "computer-use-2025-01-24",99description: "Claude 3.7 Sonnet (newer)",100},101"claude-sonnet-4-20250514": {102toolType: "computer_20250124",103betaFlag: "computer-use-2025-01-24",104description: "Claude 4 Sonnet (newest)",105},106"claude-opus-4-20250514": {107toolType: "computer_20250124",108betaFlag: "computer-use-2025-01-24",109description: "Claude 4 Opus (newest)",110},111};112113export const CUA_KEY_TO_PLAYWRIGHT_KEY: Record<string, string> = {114"/": "Divide",115"\\": "Backslash",116alt: "Alt",117arrowdown: "ArrowDown",118arrowleft: "ArrowLeft",119arrowright: "ArrowRight",120arrowup: "ArrowUp",121backspace: "Backspace",122capslock: "CapsLock",123cmd: "Meta",124ctrl: "Control",125delete: "Delete",126end: "End",127enter: "Enter",128esc: "Escape",129home: "Home",130insert: "Insert",131option: "Alt",132pagedown: "PageDown",133pageup: "PageUp",134shift: "Shift",135space: " ",136super: "Meta",137tab: "Tab",138win: "Meta",139Return: "Enter",140KP_Enter: "Enter",141Escape: "Escape",142BackSpace: "Backspace",143Delete: "Delete",144Tab: "Tab",145ISO_Left_Tab: "Shift+Tab",146Up: "ArrowUp",147Down: "ArrowDown",148Left: "ArrowLeft",149Right: "ArrowRight",150Page_Up: "PageUp",151Page_Down: "PageDown",152Home: "Home",153End: "End",154Insert: "Insert",155F1: "F1",156F2: "F2",157F3: "F3",158F4: "F4",159F5: "F5",160F6: "F6",161F7: "F7",162F8: "F8",163F9: "F9",164F10: "F10",165F11: "F11",166F12: "F12",167Shift_L: "Shift",168Shift_R: "Shift",169Control_L: "Control",170Control_R: "Control",171Alt_L: "Alt",172Alt_R: "Alt",173Meta_L: "Meta",174Meta_R: "Meta",175Super_L: "Meta",176Super_R: "Meta",177minus: "-",178equal: "=",179bracketleft: "[",180bracketright: "]",181semicolon: ";",182apostrophe: "'",183grave: "`",184comma: ",",185period: ".",186slash: "/",187};188189type ModelName = keyof typeof MODEL_CONFIGS;190191interface ModelConfig {192toolType: string;193betaFlag: string;194description: string;195}196197export function chunks(s: string, chunkSize: number): string[] {198const result: string[] = [];199for (let i = 0; i < s.length; i += chunkSize) {200result.push(s.slice(i, i + chunkSize));201}202return result;203}204205export function pp(obj: any): void {206console.log(JSON.stringify(obj, null, 2));207}208209export function checkBlocklistedUrl(url: string): void {210try {211const hostname = new URL(url).hostname || "";212const isBlocked = BLOCKED_DOMAINS.some(213(blocked) => hostname === blocked || hostname.endsWith(`.${blocked}`)214);215if (isBlocked) {216throw new Error(`Blocked URL: ${url}`);217}218} catch (error) {219if (error instanceof Error && error.message.startsWith("Blocked URL:")) {220throw error;221}222}223}
Step 3: Create Steel Browser Integration
1const TYPING_DELAY_MS = 12;2const TYPING_GROUP_SIZE = 50;34export class SteelBrowser {5private client: Steel;6private session: any;7private browser: Browser | null = null;8private page: Page | null = null;9private dimensions: [number, number];10private proxy: boolean;11private solveCaptcha: boolean;12private virtualMouse: boolean;13private sessionTimeout: number;14private adBlocker: boolean;15private startUrl: string;16private lastMousePosition: [number, number] | null = null;1718constructor(19width: number = 1024,20height: number = 768,21proxy: boolean = false,22solveCaptcha: boolean = false,23virtualMouse: boolean = true,24sessionTimeout: number = 900000,25adBlocker: boolean = true,26startUrl: string = "https://www.google.com"27) {28this.client = new Steel({29steelAPIKey: process.env.STEEL_API_KEY!,30});31this.dimensions = [width, height];32this.proxy = proxy;33this.solveCaptcha = solveCaptcha;34this.virtualMouse = virtualMouse;35this.sessionTimeout = sessionTimeout;36this.adBlocker = adBlocker;37this.startUrl = startUrl;38}3940getDimensions(): [number, number] {41return this.dimensions;42}4344getCurrentUrl(): string {45return this.page?.url() || "";46}4748async initialize(): Promise<void> {49const [width, height] = this.dimensions;50const sessionParams = {51useProxy: this.proxy,52solveCaptcha: this.solveCaptcha,53apiTimeout: this.sessionTimeout,54blockAds: this.adBlocker,55dimensions: { width, height },56};5758this.session = await this.client.sessions.create(sessionParams);59console.log("Steel Session created successfully!");60console.log(`View live session at: ${this.session.sessionViewerUrl}`);6162const cdpUrl = `${this.session.websocketUrl}&apiKey=${process.env.STEEL_API_KEY}`;6364this.browser = await chromium.connectOverCDP(cdpUrl, {65timeout: 60000,66});6768const context = this.browser.contexts()69[0];7071await context.route("**/*", async (route, request) => {72const url = request.url();73try {74checkBlocklistedUrl(url);75await route.continue();76} catch (error) {77console.log(`Blocking URL: ${url}`);78await route.abort();79}80});8182if (this.virtualMouse) {83await context.addInitScript(`84if (window.self === window.top) {85function initCursor() {86const CURSOR_ID = '__cursor__';87if (document.getElementById(CURSOR_ID)) return;8889const cursor = document.createElement('div');90cursor.id = CURSOR_ID;91Object.assign(cursor.style, {92position: 'fixed',93top: '0px',94left: '0px',95width: '20px',96height: '20px',97backgroundImage: 'url("data:image/svg+xml;utf8,<svg width=\\'16\\' height=\\'16\\' viewBox=\\'0 0 20 20\\' fill=\\'black\\' outline=\\'white\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M15.8089 7.22221C15.9333 7.00888 15.9911 6.78221 15.9822 6.54221C15.9733 6.29333 15.8978 6.06667 15.7555 5.86221C15.6133 5.66667 15.4311 5.52445 15.2089 5.43555L1.70222 0.0888888C1.47111 0 1.23555 -0.0222222 0.995555 0.0222222C0.746667 0.0755555 0.537779 0.186667 0.368888 0.355555C0.191111 0.533333 0.0755555 0.746667 0.0222222 0.995555C-0.0222222 1.23555 0 1.47111 0.0888888 1.70222L5.43555 15.2222C5.52445 15.4445 5.66667 15.6267 5.86221 15.7689C6.06667 15.9111 6.28888 15.9867 6.52888 15.9955H6.58221C6.82221 15.9955 7.04445 15.9333 7.24888 15.8089C7.44445 15.6845 7.59555 15.52 7.70221 15.3155L10.2089 10.2222L15.3022 7.70221C15.5155 7.59555 15.6845 7.43555 15.8089 7.22221Z\\' ></path></svg>")',98backgroundSize: 'cover',99pointerEvents: 'none',100zIndex: '99999',101transform: 'translate(-2px, -2px)',102});103104document.body.appendChild(cursor);105106document.addEventListener("mousemove", (e) => {107cursor.style.top = e.clientY + "px";108cursor.style.left = e.clientX + "px";109});110}111112function checkBody() {113if (document.body) {114initCursor();115} else {116requestAnimationFrame(checkBody);117}118}119requestAnimationFrame(checkBody);120}121`);122}123124this.page = context.pages()125[0];126127const [viewportWidth, viewportHeight] = this.dimensions;128await this.page.setViewportSize({129width: viewportWidth,130height: viewportHeight,131});132133await this.page.goto(this.startUrl);134}135136async cleanup(): Promise<void> {137if (this.page) {138await this.page.close();139}140if (this.browser) {141await this.browser.close();142}143if (this.session) {144console.log("Releasing Steel session...");145await this.client.sessions.release(this.session.id);146console.log(147`Session completed. View replay at ${this.session.sessionViewerUrl}`148);149}150}151152async screenshot(): Promise<string> {153if (!this.page) throw new Error("Page not initialized");154155try {156const [width, height] = this.dimensions;157const buffer = await this.page.screenshot({158fullPage: false,159clip: { x: 0, y: 0, width, height },160});161return buffer.toString("base64");162} catch (error) {163console.log(`Screenshot failed, trying CDP fallback: ${error}`);164try {165const cdpSession = await this.page.context().newCDPSession(this.page);166const result = await cdpSession.send("Page.captureScreenshot", {167format: "png",168fromSurface: false,169});170await cdpSession.detach();171return result.data;172} catch (cdpError) {173console.log(`CDP screenshot also failed: ${cdpError}`);174throw error;175}176}177}178179private validateAndGetCoordinates(180coordinate: [number, number] | number[]181): [number, number] {182if (!Array.isArray(coordinate) || coordinate.length !== 2) {183throw new Error(`${coordinate} must be a tuple or list of length 2`);184}185if (!coordinate.every((i) => typeof i === "number" && i >= 0)) {186throw new Error(187`${coordinate} must be a tuple/list of non-negative numbers`188);189}190191const [x, y] = this.clampCoordinates(coordinate[0], coordinate[1]);192return [x, y];193}194195private clampCoordinates(x: number, y: number): [number, number] {196const [width, height] = this.dimensions;197const clampedX = Math.max(0, Math.min(x, width - 1));198const clampedY = Math.max(0, Math.min(y, height - 1));199200if (x !== clampedX || y !== clampedY) {201console.log(202`⚠️ Coordinate clamped: (${x}, ${y}) → (${clampedX}, ${clampedY})`203);204}205206return [clampedX, clampedY];207}208209async executeComputerAction(210action: string,211text?: string,212coordinate?: [number, number] | number[],213scrollDirection?: "up" | "down" | "left" | "right",214scrollAmount?: number,215duration?: number,216key?: string217): Promise<string> {218if (!this.page) throw new Error("Page not initialized");219220if (action === "left_mouse_down" || action === "left_mouse_up") {221if (coordinate !== undefined) {222throw new Error(`coordinate is not accepted for ${action}`);223}224225if (action === "left_mouse_down") {226await this.page.mouse.down();227} else {228await this.page.mouse.up();229}230231return this.screenshot();232}233234if (action === "scroll") {235if (236!scrollDirection ||237!["up", "down", "left", "right"].includes(scrollDirection)238) {239throw new Error(240"scroll_direction must be 'up', 'down', 'left', or 'right'"241);242}243if (scrollAmount === undefined || scrollAmount < 0) {244throw new Error("scroll_amount must be a non-negative number");245}246247if (coordinate !== undefined) {248const [x, y] = this.validateAndGetCoordinates(coordinate);249await this.page.mouse.move(x, y);250this.lastMousePosition = [x, y];251}252253if (text) {254let modifierKey = text;255if (modifierKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {256modifierKey = CUA_KEY_TO_PLAYWRIGHT_KEY[modifierKey];257}258await this.page.keyboard.down(modifierKey);259}260261const scrollMapping = {262down: [0, 100 * scrollAmount],263up: [0, -100 * scrollAmount],264right: [100 * scrollAmount, 0],265left: [-100 * scrollAmount, 0],266};267const [deltaX, deltaY] = scrollMapping[scrollDirection];268await this.page.mouse.wheel(deltaX, deltaY);269270if (text) {271let modifierKey = text;272if (modifierKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {273modifierKey = CUA_KEY_TO_PLAYWRIGHT_KEY[modifierKey];274}275await this.page.keyboard.up(modifierKey);276}277278return this.screenshot();279}280281if (action === "hold_key" || action === "wait") {282if (duration === undefined || duration < 0) {283throw new Error("duration must be a non-negative number");284}285if (duration > 100) {286throw new Error("duration is too long");287}288289if (action === "hold_key") {290if (text === undefined) {291throw new Error("text is required for hold_key");292}293294let holdKey = text;295if (holdKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {296holdKey = CUA_KEY_TO_PLAYWRIGHT_KEY[holdKey];297}298299await this.page.keyboard.down(holdKey);300await new Promise((resolve) => setTimeout(resolve, duration * 1000));301await this.page.keyboard.up(holdKey);302} else if (action === "wait") {303await new Promise((resolve) => setTimeout(resolve, duration * 1000));304}305306return this.screenshot();307}308309if (310[311"left_click",312"right_click",313"double_click",314"triple_click",315"middle_click",316].includes(action)317) {318if (text !== undefined) {319throw new Error(`text is not accepted for ${action}`);320}321322let clickX: number, clickY: number;323if (coordinate !== undefined) {324const [x, y] = this.validateAndGetCoordinates(coordinate);325await this.page.mouse.move(x, y);326this.lastMousePosition = [x, y];327clickX = x;328clickY = y;329} else if (this.lastMousePosition) {330[clickX, clickY] = this.lastMousePosition;331} else {332const [width, height] = this.dimensions;333clickX = Math.floor(width / 2);334clickY = Math.floor(height / 2);335}336337if (key) {338let modifierKey = key;339if (modifierKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {340modifierKey = CUA_KEY_TO_PLAYWRIGHT_KEY[modifierKey];341}342await this.page.keyboard.down(modifierKey);343}344345if (action === "left_click") {346await this.page.mouse.click(clickX, clickY);347} else if (action === "right_click") {348await this.page.mouse.click(clickX, clickY, { button: "right" });349} else if (action === "double_click") {350await this.page.mouse.dblclick(clickX, clickY);351} else if (action === "triple_click") {352for (let i = 0; i < 3; i++) {353await this.page.mouse.click(clickX, clickY);354}355} else if (action === "middle_click") {356await this.page.mouse.click(clickX, clickY, { button: "middle" });357}358359if (key) {360let modifierKey = key;361if (modifierKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {362modifierKey = CUA_KEY_TO_PLAYWRIGHT_KEY[modifierKey];363}364await this.page.keyboard.up(modifierKey);365}366367return this.screenshot();368}369370if (action === "mouse_move" || action === "left_click_drag") {371if (coordinate === undefined) {372throw new Error(`coordinate is required for ${action}`);373}374if (text !== undefined) {375throw new Error(`text is not accepted for ${action}`);376}377378const [x, y] = this.validateAndGetCoordinates(coordinate);379380if (action === "mouse_move") {381await this.page.mouse.move(x, y);382this.lastMousePosition = [x, y];383} else if (action === "left_click_drag") {384await this.page.mouse.down();385await this.page.mouse.move(x, y);386await this.page.mouse.up();387this.lastMousePosition = [x, y];388}389390return this.screenshot();391}392393if (action === "key" || action === "type") {394if (text === undefined) {395throw new Error(`text is required for ${action}`);396}397if (coordinate !== undefined) {398throw new Error(`coordinate is not accepted for ${action}`);399}400401if (action === "key") {402let pressKey = text;403404if (pressKey.includes("+")) {405const keyParts = pressKey.split("+");406const modifierKeys = keyParts.slice(0, -1);407const mainKey = keyParts[keyParts.length - 1];408409const playwrightModifiers: string[] = [];410for (const mod of modifierKeys) {411if (["ctrl", "control"].includes(mod.toLowerCase())) {412playwrightModifiers.push("Control");413} else if (mod.toLowerCase() === "shift") {414playwrightModifiers.push("Shift");415} else if (["alt", "option"].includes(mod.toLowerCase())) {416playwrightModifiers.push("Alt");417} else if (["cmd", "meta", "super"].includes(mod.toLowerCase())) {418playwrightModifiers.push("Meta");419} else {420playwrightModifiers.push(mod);421}422}423424let finalMainKey = mainKey;425if (mainKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {426finalMainKey = CUA_KEY_TO_PLAYWRIGHT_KEY[mainKey];427}428429pressKey = [...playwrightModifiers, finalMainKey].join("+");430} else {431if (pressKey in CUA_KEY_TO_PLAYWRIGHT_KEY) {432pressKey = CUA_KEY_TO_PLAYWRIGHT_KEY[pressKey];433}434}435436await this.page.keyboard.press(pressKey);437} else if (action === "type") {438for (const chunk of chunks(text, TYPING_GROUP_SIZE)) {439await this.page.keyboard.type(chunk, { delay: TYPING_DELAY_MS });440await new Promise((resolve) => setTimeout(resolve, 10));441}442}443444return this.screenshot();445}446447if (action === "screenshot" || action === "cursor_position") {448if (text !== undefined) {449throw new Error(`text is not accepted for ${action}`);450}451if (coordinate !== undefined) {452throw new Error(`coordinate is not accepted for ${action}`);453}454455return this.screenshot();456}457458throw new Error(`Invalid action: ${action}`);459}460}
Step 4: Create the Agent Class
1type ModelName = keyof typeof MODEL_CONFIGS;23interface ModelConfig {4toolType: string;5betaFlag: string;6description: string;7}89export class ClaudeAgent {10private client: Anthropic;11private computer: SteelBrowser;12private messages: MessageParam[];13private model: ModelName;14private modelConfig: ModelConfig;15private tools: any[];16private systemPrompt: string;17private viewportWidth: number;18private viewportHeight: number;1920constructor(21computer: SteelBrowser,22model: ModelName = "claude-3-5-sonnet-20241022"23) {24this.client = new Anthropic({25apiKey: process.env.ANTHROPIC_API_KEY!,26});27this.computer = computer;28this.model = model;29this.messages = [];3031if (!(model in MODEL_CONFIGS)) {32throw new Error(33`Unsupported model: ${model}. Available models: ${Object.keys(34MODEL_CONFIGS35)}`36);37}3839this.modelConfig = MODEL_CONFIGS[model];4041const [width, height] = computer.getDimensions();42this.viewportWidth = width;43this.viewportHeight = height;4445this.systemPrompt = SYSTEM_PROMPT.replace(46"<COORDINATE_SYSTEM>",47`<COORDINATE_SYSTEM>48* The browser viewport dimensions are ${width}x${height} pixels49* The browser viewport has specific dimensions that you must respect`50);5152this.tools = [53{54type: this.modelConfig.toolType,55name: "computer",56display_width_px: width,57display_height_px: height,58display_number: 1,59},60];61}6263getViewportInfo(): any {64return {65innerWidth: this.viewportWidth,66innerHeight: this.viewportHeight,67devicePixelRatio: 1.0,68screenWidth: this.viewportWidth,69screenHeight: this.viewportHeight,70scrollX: 0,71scrollY: 0,72};73}7475validateScreenshotDimensions(screenshotBase64: string): any {76try {77const imageBuffer = Buffer.from(screenshotBase64, "base64");7879if (imageBuffer.length === 0) {80console.log("⚠️ Empty screenshot data");81return {};82}8384const viewportInfo = this.getViewportInfo();8586const scalingInfo = {87screenshot_size: ["unknown", "unknown"],88viewport_size: [this.viewportWidth, this.viewportHeight],89actual_viewport: [viewportInfo.innerWidth, viewportInfo.innerHeight],90device_pixel_ratio: viewportInfo.devicePixelRatio,91width_scale: 1.0,92height_scale: 1.0,93};9495return scalingInfo;96} catch (e) {97console.log(`⚠️ Error validating screenshot dimensions: ${e}`);98return {};99}100}101102async processResponse(message: Message): Promise<string> {103let responseText = "";104105for (const block of message.content) {106if (block.type === "text") {107responseText += block.text;108console.log(block.text);109} else if (block.type === "tool_use") {110const toolName = block.name;111const toolInput = block.input as any;112113console.log(`🔧 ${toolName}(${JSON.stringify(toolInput)})`);114115if (toolName === "computer") {116const action = toolInput.action;117const params = {118text: toolInput.text,119coordinate: toolInput.coordinate,120scrollDirection: toolInput.scroll_direction,121scrollAmount: toolInput.scroll_amount,122duration: toolInput.duration,123key: toolInput.key,124};125126try {127const screenshotBase64 = await this.computer.executeComputerAction(128action,129params.text,130params.coordinate,131params.scrollDirection,132params.scrollAmount,133params.duration,134params.key135);136137if (action === "screenshot") {138this.validateScreenshotDimensions(screenshotBase64);139}140141const toolResult: ToolResultBlockParam = {142type: "tool_result",143tool_use_id: block.id,144content: [145{146type: "image",147source: {148type: "base64",149media_type: "image/png",150data: screenshotBase64,151},152},153],154};155156this.messages.push({157role: "assistant",158content: [block],159});160this.messages.push({161role: "user",162content: [toolResult],163});164165return this.getClaudeResponse();166} catch (error) {167console.log(`❌ Error executing ${action}: ${error}`);168const toolResult: ToolResultBlockParam = {169type: "tool_result",170tool_use_id: block.id,171content: `Error executing ${action}: ${String(error)}`,172is_error: true,173};174175this.messages.push({176role: "assistant",177content: [block],178});179this.messages.push({180role: "user",181content: [toolResult],182});183184return this.getClaudeResponse();185}186}187}188}189190if (191responseText &&192!message.content.some((block) => block.type === "tool_use")193) {194this.messages.push({195role: "assistant",196content: responseText,197});198}199200return responseText;201}202203async getClaudeResponse(): Promise<string> {204try {205const response = await this.client.beta.messages.create(206{207model: this.model,208max_tokens: 4096,209messages: this.messages,210tools: this.tools,211},212{213headers: {214"anthropic-beta": this.modelConfig.betaFlag,215},216}217);218219return this.processResponse(response);220} catch (error) {221const errorMsg = `Error communicating with Claude: ${error}`;222console.log(`❌ ${errorMsg}`);223return errorMsg;224}225}226227async executeTask(228task: string,229printSteps: boolean = true,230debug: boolean = false,231maxIterations: number = 50232): Promise<string> {233this.messages = [234{235role: "user",236content: this.systemPrompt,237},238{239role: "user",240content: task,241},242];243244let iterations = 0;245let consecutiveNoActions = 0;246let lastAssistantMessages: string[] = [];247248console.log(`🎯 Executing task: ${task}`);249console.log("=".repeat(60));250251const isTaskComplete = (252content: string253): { completed: boolean; reason?: string } => {254if (content.includes("TASK_COMPLETED:")) {255return { completed: true, reason: "explicit_completion" };256}257if (258content.includes("TASK_FAILED:") ||259content.includes("TASK_ABANDONED:")260) {261return { completed: true, reason: "explicit_failure" };262}263264const completionPatterns = [265/task\s+(completed|finished|done|accomplished)/i,266/successfully\s+(completed|finished|found|gathered)/i,267/here\s+(is|are)\s+the\s+(results?|information|summary)/i,268/to\s+summarize/i,269/in\s+conclusion/i,270/final\s+(answer|result|summary)/i,271];272273const failurePatterns = [274/cannot\s+(complete|proceed|access|continue)/i,275/unable\s+to\s+(complete|access|find|proceed)/i,276/blocked\s+by\s+(captcha|security|authentication)/i,277/giving\s+up/i,278/no\s+longer\s+able/i,279/have\s+tried\s+multiple\s+approaches/i,280];281282if (completionPatterns.some((pattern) => pattern.test(content))) {283return { completed: true, reason: "natural_completion" };284}285286if (failurePatterns.some((pattern) => pattern.test(content))) {287return { completed: true, reason: "natural_failure" };288}289290return { completed: false };291};292293const detectRepetition = (newMessage: string): boolean => {294if (lastAssistantMessages.length < 2) return false;295296const similarity = (str1: string, str2: string): number => {297const words1 = str1.toLowerCase().split(/\s+/);298const words2 = str2.toLowerCase().split(/\s+/);299const commonWords = words1.filter((word) => words2.includes(word));300return commonWords.length / Math.max(words1.length, words2.length);301};302303return lastAssistantMessages.some(304(prevMessage) => similarity(newMessage, prevMessage) > 0.8305);306};307308while (iterations < maxIterations) {309iterations++;310let hasActions = false;311312if (this.messages.length > 0) {313const lastMessage = this.messages[this.messages.length - 1];314if (315lastMessage?.role === "assistant" &&316typeof lastMessage.content === "string"317) {318const content = lastMessage.content;319320const completion = isTaskComplete(content);321if (completion.completed) {322console.log(`✅ Task completed (${completion.reason})`);323break;324}325326if (detectRepetition(content)) {327console.log("🔄 Repetition detected - stopping execution");328lastAssistantMessages.push(content);329break;330}331332lastAssistantMessages.push(content);333if (lastAssistantMessages.length > 3) {334lastAssistantMessages.shift();335}336}337}338339if (debug) {340pp(this.messages);341}342343try {344const response = await this.client.beta.messages.create(345{346model: this.model,347max_tokens: 4096,348messages: this.messages,349tools: this.tools,350},351{352headers: {353"anthropic-beta": this.modelConfig.betaFlag,354},355}356);357358if (debug) {359pp(response);360}361362for (const block of response.content) {363if (block.type === "tool_use") {364hasActions = true;365}366}367368await this.processResponse(response);369370if (!hasActions) {371consecutiveNoActions++;372if (consecutiveNoActions >= 3) {373console.log(374"⚠️ No actions for 3 consecutive iterations - stopping"375);376break;377}378} else {379consecutiveNoActions = 0;380}381} catch (error) {382console.error(`❌ Error during task execution: ${error}`);383throw error;384}385}386387if (iterations >= maxIterations) {388console.warn(389`⚠️ Task execution stopped after ${maxIterations} iterations`390);391}392393const assistantMessages = this.messages.filter(394(item) => item.role === "assistant"395);396const finalMessage = assistantMessages[assistantMessages.length - 1];397398if (finalMessage && typeof finalMessage.content === "string") {399return finalMessage.content;400}401402return "Task execution completed (no final message)";403}404}
Step 5: Create the Main Script
1async function main(): Promise<void> {2console.log("🚀 Steel + Claude Computer Use Assistant");3console.log("=".repeat(60));45if (STEEL_API_KEY === "your-steel-api-key-here") {6console.warn(7"⚠️ WARNING: Please replace 'your-steel-api-key-here' with your actual Steel API key"8);9console.warn(10" Get your API key at: https://app.steel.dev/settings/api-keys"11);12return;13}1415if (ANTHROPIC_API_KEY === "your-anthropic-api-key-here") {16console.warn(17"⚠️ WARNING: Please replace 'your-anthropic-api-key-here' with your actual Anthropic API key"18);19console.warn(" Get your API key at: https://console.anthropic.com/");20return;21}2223console.log("\nStarting Steel browser session...");2425const computer = new SteelBrowser();2627try {28await computer.initialize();29console.log("✅ Steel browser session started!");3031const agent = new ClaudeAgent(computer, "claude-3-5-sonnet-20241022");3233const startTime = Date.now();3435try {36const result = await agent.executeTask(TASK, true, false, 50);3738const duration = ((Date.now() - startTime) / 1000).toFixed(1);3940console.log("\n" + "=".repeat(60));41console.log("🎉 TASK EXECUTION COMPLETED");42console.log("=".repeat(60));43console.log(`⏱️ Duration: ${duration} seconds`);44console.log(`🎯 Task: ${TASK}`);45console.log(`📋 Result:\n${result}`);46console.log("=".repeat(60));47} catch (error) {48console.error(`❌ Task execution failed: ${error}`);49process.exit(1);50}51} catch (error) {52console.log(`❌ Failed to start Steel browser: ${error}`);53console.log("Please check your STEEL_API_KEY and internet connection.");54process.exit(1);55} finally {56await computer.cleanup();57}58}5960main().catch(console.error);
Running Your Agent
Execute your script:
You'll see the session URL printed in the console. Open this URL to view the live browser session.
The agent will execute the task defined in the TASK
environment variable or the default task.
You can modify the task by setting the environment variable:
export TASK="Research the latest developments in artificial intelligence"npx ts-node main.ts
Customizing your agent's task
Try modifying the task to make your agent perform different actions:
1// Research specific topics2TASK = "Go to https://arxiv.org, search for 'machine learning', and summarize the latest papers.";34// E-commerce tasks5TASK = "Go to https://www.amazon.com, search for 'wireless headphones', and compare the top 3 results.";67// Information gathering8TASK = "Go to https://docs.anthropic.com, find information about Claude's capabilities, and provide a summary.";
Supported Models: This example uses Claude 3.5 Sonnet, but you can use any of the supported Claude models including Claude 3.7 Sonnet, Claude 4 Sonnet, or Claude 4 Opus. Update the model parameter in the ClaudeAgent constructor to switch models.
Next Steps
-
Explore the Steel API documentation for more advanced features
-
Check out the Anthropic documentation for more information about Claude's computer use capabilities
-
Add additional features like session recording or multi-session management