diff --git a/index.ts b/index.ts index 1eaed85..5e131e4 100644 --- a/index.ts +++ b/index.ts @@ -96,7 +96,7 @@ const INVOKE_SAMPLES: Record< [TEMPLATE_COMPUTER_USE]: 'kernel invoke ts-cu cu-task --payload \'{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}\'', [TEMPLATE_CUA]: - 'kernel invoke ts-cua cua-task --payload \'{"query": "Go to https://news.ycombinator.com and get the top 5 articles"}\'', + 'kernel invoke ts-cua cua-task --payload \'{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}\'', }, [LANGUAGE_PYTHON]: { [TEMPLATE_SAMPLE_APP]: @@ -108,7 +108,7 @@ const INVOKE_SAMPLES: Record< [TEMPLATE_COMPUTER_USE]: 'kernel invoke python-cu cu-task --payload \'{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}\'', [TEMPLATE_CUA]: - 'kernel invoke python-cua cua-task --payload \'{"query": "Go to https://news.ycombinator.com and get the top 5 articles"}\'', + 'kernel invoke python-cua cua-task --payload \'{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}\'', }, }; diff --git a/templates/python/cua/pyproject.toml b/templates/python/cua/pyproject.toml index 7115077..3d558e8 100644 --- a/templates/python/cua/pyproject.toml +++ b/templates/python/cua/pyproject.toml @@ -6,24 +6,25 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "annotated-types==0.7.0", - "anyio==4.8.0", - "certifi==2025.1.31", - "charset-normalizer==3.4.1", + "anyio==4.9.0", + "certifi==2025.6.15", + "charset-normalizer==3.4.2", "distro==1.9.0", - "greenlet==3.1.1", - "h11==0.14.0", - "httpcore==1.0.7", + "greenlet==3.2.3", + "h11==0.16.0", + "httpcore==1.0.9", "httpx==0.28.1", "idna==3.10", - "jiter==0.8.2", - "pillow==11.1.0", - "playwright==1.50.0", - "pydantic==2.10.6", - "pydantic_core==2.27.2", - "pyee==12.1.1", - "python-dotenv==1.0.1", - "requests==2.32.3", + "jiter==0.10.0", + "pillow==11.2.1", + "kernel>=0.6.0", + "playwright==1.52.0", + "pydantic==2.11.7", + "pydantic_core==2.35.1", + "pyee==13.0.0", + "python-dotenv==1.1.0", + "requests==2.32.4", "sniffio==1.3.1", - "typing_extensions==4.12.2", - "urllib3==2.3.0", + "typing_extensions==4.14.0", + "urllib3==2.5.0", ] diff --git a/templates/typescript/cua/.env.example b/templates/typescript/cua/.env.example new file mode 100644 index 0000000..4270afa --- /dev/null +++ b/templates/typescript/cua/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY=YOUR_OPENAI_API_KEY +# KERNEL_API_KEY=YOUR_KERNEL_KEY \ No newline at end of file diff --git a/templates/typescript/cua/.prettierrc b/templates/typescript/cua/.prettierrc index 3ee282f..ca8527e 100644 --- a/templates/typescript/cua/.prettierrc +++ b/templates/typescript/cua/.prettierrc @@ -4,4 +4,4 @@ "singleQuote": true, "printWidth": 100, "tabWidth": 2 -} \ No newline at end of file +} diff --git a/templates/typescript/cua/README.md b/templates/typescript/cua/README.md index 0cb2dfe..c196bcd 100644 --- a/templates/typescript/cua/README.md +++ b/templates/typescript/cua/README.md @@ -5,4 +5,4 @@ This is a Kernel application that demonstrates using the Computer Using Agent (C It generally follows the [OpenAI CUA Sample App Reference](https://github.com/openai/openai-cua-sample-app) and uses Playwright via Kernel for browser automation. Also makes use of the latest OpenAI SDK format, and has local equivalent to Kernel methods for local testing before deploying on Kernel. -See the [docs](https://docs.onkernel.com/quickstart) for information. \ No newline at end of file +See the [docs](https://docs.onkernel.com/quickstart) for information. diff --git a/templates/typescript/cua/index.ts b/templates/typescript/cua/index.ts index e32a090..4b01c69 100644 --- a/templates/typescript/cua/index.ts +++ b/templates/typescript/cua/index.ts @@ -1,14 +1,24 @@ -import "dotenv/config"; -import { Kernel, type KernelContext } from "@onkernel/sdk"; -import { Agent } from "./lib/agent"; -import computers from "./lib/computers"; +import 'dotenv/config'; +import { Kernel, type KernelContext } from '@onkernel/sdk'; +import { Agent } from './lib/agent'; +import computers from './lib/computers'; +import type { ResponseOutputMessage, ResponseItem } from 'openai/resources/responses/responses'; + +interface CuaInput { + task: string; +} +interface CuaOutput { + elapsed: number; + answer: string | null; + logs?: ResponseItem[]; +} const kernel = new Kernel(); -const app = kernel.app("ts-cua"); +const app = kernel.app('ts-cua'); -// LLM API Keys are set in the environment during `kernel deploy -e ANTHROPIC_API_KEY=XXX` -// See https://docs.onkernel.com/launch/deploy#environment-variables -if (!process.env.OPENAI_API_KEY) throw new Error('OPENAI_API_KEY is not set'); +if (!process.env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY is not set'); +} /** * Example app that run an agent using openai CUA @@ -24,88 +34,74 @@ if (!process.env.OPENAI_API_KEY) throw new Error('OPENAI_API_KEY is not set'); * kernel logs ts-cua -f # Open in separate tab */ -interface CuaInput { - task: string; -} - -interface CuaOutput { - elapsed: number; - response?: Array; - answer: object; -} - app.action( - "cua-task", - async (ctx: KernelContext, payload?: CuaInput): Promise => { - const startTime = Date.now(); - const kernelBrowser = await kernel.browsers.create({ - invocation_id: ctx.invocation_id, - }); - console.log( - "> Kernel browser live view url: ", - kernelBrowser.browser_live_view_url, - ); - - if (!payload?.task){ - throw new Error('task is required'); - } - - try { + 'cua-task', + async (ctx: KernelContext, payload?: CuaInput): Promise => { + const start = Date.now(); + if (!payload?.task) throw new Error('task is required'); - // kernel browser - const { computer } = await computers.create({ - type: "kernel", // for local testing before deploying to Kernel, you can use type: "local" - cdp_ws_url: kernelBrowser.cdp_ws_url, - }); + try { + const kb = await kernel.browsers.create({ invocation_id: ctx.invocation_id }); + console.log('> Kernel browser live view url:', kb.browser_live_view_url); - // setup agent - const agent = new Agent({ - model: "computer-use-preview", - computer, - tools: [], // additional function_call tools to provide to the llm - acknowledge_safety_check_callback: (message: string) => { - console.log(`> safety check: ${message}`); - return true; // Auto-acknowledge all safety checks for testing - }, - }); + const { computer } = await computers.create({ type: 'kernel', cdp_ws_url: kb.cdp_ws_url }); + const agent = new Agent({ + model: 'computer-use-preview', + computer, + tools: [], + acknowledge_safety_check_callback: (m: string): boolean => { + console.log(`> safety check: ${m}`); + return true; + }, + }); - // start agent run - const response = await agent.runFullTurn({ - messages: [ - { - role: "system", - content: `- Current date and time: ${new Date().toISOString()} (${new Date().toLocaleDateString("en-US", { weekday: "long" })})`, - }, - { - type: "message", - role: "user", - content: [ - { - type: "input_text", - text: payload.task, - // text: "go to https://news.ycombinator.com , open top article , describe the target website design (in yaml format)" - }, - ], - }, - ], - print_steps: true, // log function_call and computer_call actions - debug: true, // show agent debug logs (llm messages and responses) - show_images: false, // if set to true, response messages stack will return base64 images (webp format) of screenshots, if false, replaced with "[omitted]"" - }); + // run agent and get response + const logs = await agent.runFullTurn({ + messages: [ + { + role: 'system', + content: `- Current date and time: ${new Date().toISOString()} (${new Date().toLocaleDateString( + 'en-US', + { weekday: 'long' }, + )})`, + }, + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: payload.task }], + }, + ], + print_steps: true, + debug: true, + show_images: false, + }); - console.log("> agent run done"); + const elapsed = parseFloat(((Date.now() - start) / 1000).toFixed(2)); - const endTime = Date.now(); - const timeElapsed = (endTime - startTime) / 1000; // Convert to seconds + // filter only LLM messages + const messages = logs.filter( + (item): item is ResponseOutputMessage => + item.type === 'message' && + typeof (item as ResponseOutputMessage).role === 'string' && + Array.isArray((item as ResponseOutputMessage).content), + ); + const assistant = messages.find((m) => m.role === 'assistant'); + const lastContentIndex = assistant?.content?.length ? assistant.content.length - 1 : -1; + const lastContent = lastContentIndex >= 0 ? assistant?.content?.[lastContentIndex] : null; + const answer = lastContent && 'text' in lastContent ? lastContent.text : null; - return { - // response, // full messages stack trace - elapsed: parseFloat(timeElapsed.toFixed(2)), - answer: response?.slice(-1)?.[0]?.content?.[0]?.text ?? null, - }; - } finally { - // Note: KernelPlaywrightComputer handles browser cleanup internally - // No need to manually close browser here - } - }, + return { + // logs, // optionally, get the full agent run messages logs + elapsed, + answer, + }; + } catch (error) { + const elapsed = parseFloat(((Date.now() - start) / 1000).toFixed(2)); + console.error('Error in cua-task:', error); + return { + elapsed, + answer: null, + }; + } + }, ); diff --git a/templates/typescript/cua/lib/agent.ts b/templates/typescript/cua/lib/agent.ts index f28b298..9744165 100644 --- a/templates/typescript/cua/lib/agent.ts +++ b/templates/typescript/cua/lib/agent.ts @@ -1,220 +1,208 @@ -import utils from "./utils"; -import computers from "./computers"; -import toolset from "./toolset"; +import { + type ResponseItem, + type ResponseInputItem, + type ResponseOutputMessage, + type ResponseFunctionToolCallItem, + type ResponseFunctionToolCallOutputItem, + type ResponseComputerToolCall, + type ResponseComputerToolCallOutputItem, + type ComputerTool, +} from 'openai/resources/responses/responses'; + +import * as utils from './utils'; +import toolset from './toolset'; +import type { BasePlaywrightComputer } from './playwright/base'; +import type { LocalPlaywrightComputer } from './playwright/local'; +import type { KernelPlaywrightComputer } from './playwright/kernel'; -import type { BasePlaywrightComputer } from "./playwright/base"; - - -interface Item { - [key: string]: any; -} - -interface Tool { - type: string; - display_width?: number; - display_height?: number; - environment?: string; - [key: string]: any; -} - -interface SafetyCheck { - message: string; - [key: string]: any; -} - -interface ComputerCallOutput { - type: string; - call_id: string; - acknowledged_safety_checks: SafetyCheck[]; - output: { - type: string; - image_url: string; - current_url?: string; - }; -} - -type AcknowledgeSafetyCheckCallback = (message: string) => boolean; - -/** - * A sample agent class that can be used to interact with a computer. - */ export class Agent { - private model: string; - private computer: BasePlaywrightComputer | null; - private tools: Tool[]; - private print_steps: boolean; - private debug: boolean; - private show_images: boolean; - private acknowledge_safety_check_callback: AcknowledgeSafetyCheckCallback; - - constructor({ - model = "computer-use-preview", - computer = null, - tools = [], - acknowledge_safety_check_callback = () => true, - }: { - model?: string; - computer?: BasePlaywrightComputer | null; - tools?: Tool[]; - acknowledge_safety_check_callback?: AcknowledgeSafetyCheckCallback; - }) { - this.model = model; - this.computer = computer; - this.tools = [...toolset.shared, ...tools]; - this.print_steps = true; - this.debug = false; - this.show_images = false; - this.acknowledge_safety_check_callback = acknowledge_safety_check_callback; - - if (computer) { - const dimensions = computer.getDimensions(); - this.tools.push({ - type: "computer-preview", - display_width: dimensions[0], - display_height: dimensions[1], - environment: computer.getEnvironment(), - }); - } - } - - private debugPrint(...args: any[]): void { - if (this.debug) { - console.warn("--- debug:agent:debugPrint"); - console.dir(...args, { depth: null }); - } - } - - private async handleItem(item: Item): Promise { - /**Handle each item; may cause a computer action + screenshot.*/ - if (item.type === "message") { - if (this.print_steps && item.content?.[0]?.text) { - console.log(item.content[0].text); - } - } - - if (item.type === "function_call") { - const name = item.name!; - const args = JSON.parse(item.arguments!); - if (this.print_steps) { - console.log(`${name}(${JSON.stringify(args)})`); - } - - if (this.computer && (this.computer as any)[name]) { - const method = (this.computer as any)[name]; - await method.call(this.computer, ...Object.values(args)); - } - return [ - { - type: "function_call_output", - call_id: item.call_id!, - output: "success", // hard-coded output for demo - }, - ]; - } - - if (item.type === "computer_call") { - const action = item.action!; - const action_type = action.type; - const action_args = Object.fromEntries( - Object.entries(action).filter(([k]) => k !== "type"), - ); - if (this.print_steps) { - console.log(`${action_type}(${JSON.stringify(action_args)})`); - } - - if (this.computer) { - const method = (this.computer as any)[action_type]; - await method.call(this.computer, ...Object.values(action_args)); - - const screenshot_base64 = await this.computer.screenshot(); - // console.dir({ debug: { screenshot_base64 }}) - - // if user doesn't ack all safety checks exit with error - const pending_checks = item.pending_safety_checks || []; - for (const check of pending_checks) { - const message = check.message; - if (!this.acknowledge_safety_check_callback(message)) { - throw new Error( - `Safety check failed: ${message}. Cannot continue with unacknowledged safety checks.`, - ); - } - } - - const call_output: ComputerCallOutput = { - type: "computer_call_output", - call_id: item.call_id!, - acknowledged_safety_checks: pending_checks, - output: { - type: "input_image", - image_url: `data:image/webp;base64,${screenshot_base64}`, - }, - }; - - // additional URL safety checks for browser environments - if (this.computer.getEnvironment() === "browser") { - const current_url = this.computer.getCurrentUrl(); - utils.checkBlocklistedUrl(current_url); - call_output.output.current_url = current_url; - } - - return [call_output]; - } - } - return []; - } - - async runFullTurn({ - messages, - print_steps = true, - debug = false, - show_images = false, - }: { - messages: Item[]; - print_steps?: boolean; - debug?: boolean; - show_images?: boolean; - }): Promise { - this.print_steps = print_steps; - this.debug = debug; - this.show_images = show_images; - const new_items: Item[] = []; - - // keep looping until we get a final response - while ( - new_items.length === 0 || - (new_items[new_items.length - 1]?.role !== "assistant") - ) { - this.debugPrint( - messages.concat(new_items).map((msg) => utils.sanitizeMessage(msg)), - ); - - const response = await utils.createResponse({ - model: this.model, - input: messages.concat(new_items), - tools: this.tools, - truncation: "auto", - }); - this.debugPrint(response); - - if (!response.output && this.debug) { - console.log(response); - throw new Error("No output from model"); - } else if (response.output) { - new_items.push(...response.output); - for (const item of response.output) { - const handled_items = await this.handleItem(item); - new_items.push(...handled_items); - } - } - } - - // Return sanitized messages if show_images is false - if (!show_images) { - return new_items.map((msg) => utils.sanitizeMessage(msg)); - } - - return new_items; - } + private model: string; + private computer: + | BasePlaywrightComputer + | LocalPlaywrightComputer + | KernelPlaywrightComputer + | undefined; + private tools: ComputerTool[]; + private print_steps = true; + private debug = false; + private show_images = false; + private ackCb: (msg: string) => boolean; + + constructor(opts: { + model?: string; + computer?: + | BasePlaywrightComputer + | LocalPlaywrightComputer + | KernelPlaywrightComputer + | undefined; + tools?: ComputerTool[]; + acknowledge_safety_check_callback?: (msg: string) => boolean; + }) { + this.model = opts.model ?? 'computer-use-preview'; + this.computer = opts.computer; + this.tools = [...toolset.shared, ...(opts.tools ?? [])] as ComputerTool[]; + this.ackCb = opts.acknowledge_safety_check_callback ?? ((): boolean => true); + + if (this.computer) { + const [w, h] = this.computer.getDimensions(); + this.tools.push({ + type: 'computer_use_preview', + display_width: w, + display_height: h, + environment: this.computer.getEnvironment(), + }); + } + } + + private debugPrint(...args: unknown[]): void { + if (this.debug) { + console.warn('--- debug:agent:debugPrint'); + try { + console.dir( + args.map((msg) => utils.sanitizeMessage(msg as ResponseItem)), + { depth: null }, + ); + } catch { + console.dir(args, { depth: null }); + } + } + } + + private async handleItem(item: ResponseItem): Promise { + if (item.type === 'message' && this.print_steps) { + const msg = item as ResponseOutputMessage; + const c = msg.content; + if (Array.isArray(c) && c[0] && 'text' in c[0] && typeof c[0].text === 'string') + console.log(c[0].text); + } + + if (item.type === 'function_call') { + const fc = item as ResponseFunctionToolCallItem; + const argsObj = JSON.parse(fc.arguments) as Record; + if (this.print_steps) console.log(`${fc.name}(${JSON.stringify(argsObj)})`); + if (this.computer) { + const fn = (this.computer as unknown as Record)[fc.name]; + if (typeof fn === 'function') + await (fn as (...a: unknown[]) => unknown)(...Object.values(argsObj)); + } + return [ + { + type: 'function_call_output', + call_id: fc.call_id, + output: 'success', + } as unknown as ResponseFunctionToolCallOutputItem, + ]; + } + + if (item.type === 'computer_call') { + const cc = item as ResponseComputerToolCall; + const { type: actionType, ...actionArgs } = cc.action; + if (this.print_steps) console.log(`${actionType}(${JSON.stringify(actionArgs)})`); + if (this.computer) { + const fn = (this.computer as unknown as Record)[actionType as string]; + if (typeof fn === 'function') { + await (fn as (...a: unknown[]) => unknown)(...Object.values(actionArgs)); + const screenshot = await this.computer.screenshot(); + const pending = cc.pending_safety_checks ?? []; + for (const { message } of pending) + if (!this.ackCb(message)) throw new Error(`Safety check failed: ${message}`); + const out: Omit = { + type: 'computer_call_output', + call_id: cc.call_id, + // id: "?", // <---- omitting to work - need to determine id source, != call_id + acknowledged_safety_checks: pending, + output: { + type: 'computer_screenshot', + image_url: `data:image/webp;base64,${screenshot}`, + }, + }; + if (this.computer.getEnvironment() === 'browser') + utils.checkBlocklistedUrl(this.computer.getCurrentUrl()); + return [out as ResponseItem]; + } + } + } + + return []; + } + + async runFullTurn(opts: { + messages: ResponseInputItem[]; + print_steps?: boolean; + debug?: boolean; + show_images?: boolean; + }): Promise { + this.print_steps = opts.print_steps ?? true; + this.debug = opts.debug ?? false; + this.show_images = opts.show_images ?? false; + const newItems: ResponseItem[] = []; + + while ( + newItems.length === 0 || + (newItems[newItems.length - 1] as ResponseItem & { role?: string }).role !== 'assistant' + ) { + // Add current URL to system message if in browser environment + const inputMessages = [...opts.messages]; + + if (this.computer?.getEnvironment() === 'browser') { + const current_url = this.computer.getCurrentUrl(); + // Find system message by checking if it has a role property with value 'system' + const sysIndex = inputMessages.findIndex((msg) => 'role' in msg && msg.role === 'system'); + + if (sysIndex >= 0) { + const msg = inputMessages[sysIndex]; + const urlInfo = `\n- Current URL: ${current_url}`; + + // Create a properly typed message based on the original + if (msg && 'content' in msg) { + if (typeof msg.content === 'string') { + // Create a new message with the updated content + const updatedMsg = { + ...msg, + content: msg.content + urlInfo, + }; + // Type assertion to ensure compatibility + inputMessages[sysIndex] = updatedMsg as typeof msg; + } else if (Array.isArray(msg.content) && msg.content.length > 0) { + // Handle array content case + const updatedContent = [...msg.content]; + + // Check if first item has text property + if (updatedContent[0] && 'text' in updatedContent[0]) { + updatedContent[0] = { + ...updatedContent[0], + text: updatedContent[0].text + urlInfo, + }; + } + + // Create updated message with new content + const updatedMsg = { + ...msg, + content: updatedContent, + }; + // Type assertion to ensure compatibility + inputMessages[sysIndex] = updatedMsg as typeof msg; + } + } + } + } + + this.debugPrint(...inputMessages, ...newItems); + const response = await utils.createResponse({ + model: this.model, + input: [...inputMessages, ...newItems], + tools: this.tools, + truncation: 'auto', + }); + if (!response.output) throw new Error('No output from model'); + for (const msg of response.output as ResponseItem[]) { + newItems.push(msg, ...(await this.handleItem(msg))); + } + } + + // Return sanitized messages if show_images is false + return !this.show_images + ? newItems.map((msg) => utils.sanitizeMessage(msg) as ResponseItem) + : newItems; + } } - -export default { Agent }; diff --git a/templates/typescript/cua/lib/computers.ts b/templates/typescript/cua/lib/computers.ts index 3c8aa47..5828fc8 100644 --- a/templates/typescript/cua/lib/computers.ts +++ b/templates/typescript/cua/lib/computers.ts @@ -1,25 +1,28 @@ -import { KernelPlaywrightComputer } from "./playwright/kernel.ts"; -import { LocalPlaywrightComputer } from "./playwright/local.ts"; +import { KernelPlaywrightComputer } from './playwright/kernel'; +import { LocalPlaywrightComputer } from './playwright/local'; -interface ComputerConfig { - type: "local" | "kernel"; - [key: string]: any; +interface KernelConfig { + type: 'kernel'; + cdp_ws_url: string; } +interface LocalConfig { + type: 'local'; + headless?: boolean; +} +type ComputerConfig = KernelConfig | LocalConfig; -const computers = { - async create({ type, ...args }: ComputerConfig) { - if (type === "kernel") { - const computer = new KernelPlaywrightComputer(args.cdp_ws_url); - await computer.enter(); - return { computer }; - } else if (type === "local") { - const computer = new LocalPlaywrightComputer(args.headless); - await computer.enter(); - return { computer }; - } else { - throw new Error(`Unknown computer type: ${type}`); - } - }, +export default { + async create( + cfg: ComputerConfig, + ): Promise<{ computer: KernelPlaywrightComputer | LocalPlaywrightComputer }> { + if (cfg.type === 'kernel') { + const computer = new KernelPlaywrightComputer(cfg.cdp_ws_url); + await computer.enter(); + return { computer }; + } else { + const computer = new LocalPlaywrightComputer(cfg.headless ?? false); + await computer.enter(); + return { computer }; + } + }, }; - -export default computers; diff --git a/templates/typescript/cua/lib/playwright/base.ts b/templates/typescript/cua/lib/playwright/base.ts index 5176869..7a06151 100644 --- a/templates/typescript/cua/lib/playwright/base.ts +++ b/templates/typescript/cua/lib/playwright/base.ts @@ -1,220 +1,242 @@ -import utils from "../utils.ts"; -import sharp from "sharp"; -import type { Browser, Page, Route, Request } from "playwright"; - -// Optional: key mapping if your model uses "CUA" style keys -const CUA_KEY_TO_PLAYWRIGHT_KEY: Record = { - "/": "/", - "\\": "\\", - alt: "Alt", - arrowdown: "ArrowDown", - arrowleft: "ArrowLeft", - arrowright: "ArrowRight", - arrowup: "ArrowUp", - backspace: "Backspace", - capslock: "CapsLock", - cmd: "Meta", - ctrl: "Control", - delete: "Delete", - end: "End", - enter: "Enter", - esc: "Escape", - home: "Home", - insert: "Insert", - option: "Alt", - pagedown: "PageDown", - pageup: "PageUp", - shift: "Shift", - space: " ", - super: "Meta", - tab: "Tab", - win: "Meta", +import utils from '../utils'; +import sharp from 'sharp'; +import type { Browser, Page, Route, Request, Response } from 'playwright'; + +// CUA key -> Playwright key mapping +const KEY_MAP: Record = { + '/': '/', + '\\': '\\', + alt: 'Alt', + arrowdown: 'ArrowDown', + arrowleft: 'ArrowLeft', + arrowright: 'ArrowRight', + arrowup: 'ArrowUp', + backspace: 'Backspace', + capslock: 'CapsLock', + cmd: 'Meta', + ctrl: 'Control', + delete: 'Delete', + end: 'End', + enter: 'Enter', + esc: 'Escape', + home: 'Home', + insert: 'Insert', + option: 'Alt', + pagedown: 'PageDown', + pageup: 'PageUp', + shift: 'Shift', + space: ' ', + super: 'Meta', + tab: 'Tab', + win: 'Meta', }; interface Point { - x: number; - y: number; + x: number; + y: number; } -/** - * Abstract base for Playwright-based computers: - * - * - Subclasses override `_getBrowserAndPage()` to do local or remote connection, - * returning [Browser, Page]. - * - This base class handles context creation (`enter()`/`exit()`), - * plus standard "Computer" actions like click, scroll, etc. - * - We also have extra browser actions: `goto(url)` and `back()`. - */ - export class BasePlaywrightComputer { - protected _browser: Browser | null = null; - protected _page: Page | null = null; - - constructor() { - this._browser = null; - this._page = null; - } - - /** - * Type guard to assert that this._page is present and is a Playwright Page. - * Throws an error if not present. - */ - protected _assertPage(): asserts this is { _page: Page } { - if (!this._page) { - throw new Error("Playwright Page is not initialized. Did you forget to call enter()?"); - } - } - - getEnvironment(): string { - return "browser"; - } - - getDimensions(): [number, number] { - return [1024, 768]; - } - - async enter(): Promise { - // Call the subclass hook for getting browser/page - [this._browser, this._page] = await this._getBrowserAndPage(); - - // Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS - const handleRoute = (route: Route, request: Request): void => { - const url = request.url(); - if (utils.checkBlocklistedUrl(url)) { - console.log(`Flagging blocked domain: ${url}`); - route.abort(); - } else { - route.continue(); - } - }; - - this._assertPage(); - await this._page.route("**/*", handleRoute); - return this; - } - - async exit(): Promise { - if (this._browser) { - await this._browser.close(); - } - } - - getCurrentUrl(): string { - this._assertPage(); - return this._page.url(); - } - - // Common "Computer" actions - async screenshot(): Promise { - this._assertPage(); - // Capture only the viewport (not full_page) - const screenshotBuffer = await this._page.screenshot({ fullPage: false }); - const webpBuffer = await sharp(screenshotBuffer).webp().toBuffer(); - return webpBuffer.toString("base64"); - } - - async click(button: string = "left", x: number, y: number): Promise { - this._assertPage(); - // console.dir({ debug:{base:{click:{x,y,button}}} },{depth:null}) - switch (button) { - case "back": - await this.back(); - break; - case "forward": - await this.forward(); - break; - case "wheel": - await this._page.mouse.wheel(x, y); - break; - default: - const buttonMapping: Record = { - left: "left", - right: "right", - }; - const buttonType = - buttonMapping[button as keyof typeof buttonMapping] || "left"; - await this._page.mouse.click(x, y, { button: buttonType }); - } - } - - async doubleClick(x: number, y: number): Promise { - this._assertPage(); - await this._page.mouse.dblclick(x, y); - } - - async scroll( - x: number, - y: number, - scrollX: number, - scrollY: number, - ): Promise { - this._assertPage(); - await this._page.mouse.move(x, y); - await this._page.evaluate(`window.scrollBy(${scrollX}, ${scrollY})`); - } - - async type(text: string): Promise { - this._assertPage(); - await this._page.keyboard.type(text); - } - - async keypress(keys: string[]): Promise { - this._assertPage(); - const mappedKeys = keys.map( - (key) => CUA_KEY_TO_PLAYWRIGHT_KEY[key.toLowerCase()] || key, - ); - for (const key of mappedKeys) { - await this._page.keyboard.down(key); - } - for (const key of mappedKeys.reverse()) { - await this._page.keyboard.up(key); - } - } - - async wait(ms: number = 1000): Promise { - await new Promise((resolve) => setTimeout(resolve, ms)); - } - - async move(x: number, y: number): Promise { - this._assertPage(); - await this._page.mouse.move(x, y); - } - - async drag(path: Point[]): Promise { - this._assertPage(); - const first = path[0]; - if (!first) return; - await this._page.mouse.move(first.x, first.y); - await this._page.mouse.down(); - for (const point of path.slice(1)) { - await this._page.mouse.move(point.x, point.y); - } - await this._page.mouse.up(); - } - - // Extra browser-oriented actions - async goto(url: string): Promise { - this._assertPage(); - try { - return await this._page.goto(url); - } catch (e) { - console.log(`Error navigating to ${url}: ${e}`); - } - } - - async back(): Promise { - this._assertPage(); - return await this._page.goBack(); - } - - async forward(): Promise { - this._assertPage(); - return await this._page.goForward(); - } - - // Subclass hook - async _getBrowserAndPage(): Promise<[Browser, Page]> { - // Subclasses must implement, returning [Browser, Page] - throw new Error("Subclasses must implement _getBrowserAndPage()"); - } + protected _browser: Browser | null = null; + protected _page: Page | null = null; + + constructor() { + this._browser = null; + this._page = null; + } + + /** + * Type guard to assert that this._page is present and is a Playwright Page. + * Throws an error if not present. + */ + protected _assertPage(): asserts this is { _page: Page } { + if (!this._page) { + throw new Error('Playwright Page is not initialized. Did you forget to call enter()?'); + } + } + + protected _handleNewPage = (page: Page): void => { + /** Handle the creation of a new page. */ + console.log('New page created'); + this._page = page; + page.on('close', this._handlePageClose.bind(this)); + }; + + protected _handlePageClose = (page: Page): void => { + /** Handle the closure of a page. */ + console.log('Page closed'); + try { + this._assertPage(); + } catch { + return; + } + if (this._page !== page) return; + + const browser = this._browser; + if (!browser || typeof browser.contexts !== 'function') { + console.log('Warning: Browser or context not available.'); + this._page = undefined as unknown as Page; + return; + } + + const contexts = browser.contexts(); + if (!contexts.length) { + console.log('Warning: No browser contexts available.'); + this._page = undefined as unknown as Page; + return; + } + + const context = contexts[0]; + if (!context || typeof context.pages !== 'function') { + console.log('Warning: Context pages not available.'); + this._page = undefined as unknown as Page; + return; + } + + const pages = context.pages(); + if (pages.length) { + this._page = pages[pages.length - 1] as Page; + } else { + console.log('Warning: All pages have been closed.'); + this._page = undefined as unknown as Page; + } + }; + + // Subclass hook + protected _getBrowserAndPage = async (): Promise<[Browser, Page]> => { + // Subclasses must implement, returning [Browser, Page] + throw new Error('Subclasses must implement _getBrowserAndPage()'); + }; + + getEnvironment = (): 'windows' | 'mac' | 'linux' | 'ubuntu' | 'browser' => { + return 'browser'; + }; + + getDimensions = (): [number, number] => { + return [1024, 768]; + }; + + enter = async (): Promise => { + // Call the subclass hook for getting browser/page + [this._browser, this._page] = await this._getBrowserAndPage(); + + // Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS + const handleRoute = (route: Route, request: Request): void => { + const url = request.url(); + if (utils.checkBlocklistedUrl(url)) { + console.log(`Flagging blocked domain: ${url}`); + route.abort(); + } else { + route.continue(); + } + }; + + this._assertPage(); + await this._page.route('**/*', handleRoute); + return this; + }; + + exit = async (): Promise => { + if (this._browser) await this._browser.close(); + }; + + getCurrentUrl = (): string => { + this._assertPage(); + return this._page.url(); + }; + + screenshot = async (): Promise => { + this._assertPage(); + const buf = await this._page.screenshot({ fullPage: false }); + const webp = await sharp(buf).webp().toBuffer(); + return webp.toString('base64'); + }; + + click = async ( + button: 'left' | 'right' | 'back' | 'forward' | 'wheel', + x: number, + y: number, + ): Promise => { + this._assertPage(); + switch (button) { + case 'back': + await this.back(); + return; + case 'forward': + await this.forward(); + return; + case 'wheel': + await this._page.mouse.wheel(x, y); + return; + default: { + const btn = button === 'right' ? 'right' : 'left'; + await this._page.mouse.click(x, y, { button: btn }); + return; + } + } + }; + + doubleClick = async (x: number, y: number): Promise => { + this._assertPage(); + await this._page.mouse.dblclick(x, y); + }; + + scroll = async (x: number, y: number, scrollX: number, scrollY: number): Promise => { + this._assertPage(); + await this._page.mouse.move(x, y); + await this._page.evaluate( + (params: { dx: number; dy: number }) => window.scrollBy(params.dx, params.dy), + { dx: scrollX, dy: scrollY }, + ); + }; + + type = async (text: string): Promise => { + this._assertPage(); + await this._page.keyboard.type(text); + }; + + keypress = async (keys: string[]): Promise => { + this._assertPage(); + const mapped = keys.map((k) => KEY_MAP[k.toLowerCase()] ?? k); + for (const k of mapped) await this._page.keyboard.down(k); + for (const k of [...mapped].reverse()) await this._page.keyboard.up(k); + }; + + wait = async (ms = 1000): Promise => { + await new Promise((resolve) => setTimeout(resolve, ms)); + }; + + move = async (x: number, y: number): Promise => { + this._assertPage(); + await this._page.mouse.move(x, y); + }; + + drag = async (path: Point[]): Promise => { + this._assertPage(); + const first = path[0]; + if (!first) return; + await this._page.mouse.move(first.x, first.y); + await this._page.mouse.down(); + for (const pt of path.slice(1)) await this._page.mouse.move(pt.x, pt.y); + await this._page.mouse.up(); + }; + + goto = async (url: string): Promise => { + this._assertPage(); + try { + return await this._page.goto(url); + } catch { + return null; + } + }; + + back = async (): Promise => { + this._assertPage(); + return (await this._page.goBack()) || null; + }; + + forward = async (): Promise => { + this._assertPage(); + return (await this._page.goForward()) || null; + }; } diff --git a/templates/typescript/cua/lib/playwright/kernel.ts b/templates/typescript/cua/lib/playwright/kernel.ts index 8f1cc6d..45ceb97 100644 --- a/templates/typescript/cua/lib/playwright/kernel.ts +++ b/templates/typescript/cua/lib/playwright/kernel.ts @@ -1,90 +1,43 @@ -import { chromium, type Browser, type Page } from "playwright"; -import { BasePlaywrightComputer } from "./base"; +import { chromium, type Browser, type Page } from 'playwright'; +import { BasePlaywrightComputer } from './base'; /** * KernelPlaywrightComputer connects to a remote browser instance via CDP WebSocket URL. * Similar to LocalPlaywrightComputer but uses an existing browser instance instead of launching one. */ export class KernelPlaywrightComputer extends BasePlaywrightComputer { - private cdp_ws_url: string; + private cdp_ws_url: string; - constructor(cdp_ws_url: string) { - super(); - this.cdp_ws_url = cdp_ws_url; - } + constructor(cdp_ws_url: string) { + super(); + this.cdp_ws_url = cdp_ws_url; + } - async _getBrowserAndPage(): Promise<[Browser, Page]> { - const [width, height] = this.getDimensions(); + _getBrowserAndPage = async (): Promise<[Browser, Page]> => { + const [width, height] = this.getDimensions(); - // Connect to existing browser instance via CDP - const browser = await chromium.connectOverCDP(this.cdp_ws_url); + // Connect to existing browser instance via CDP + const browser = await chromium.connectOverCDP(this.cdp_ws_url); - // Get existing context or create new one - let context = browser.contexts()[0]; - if (!context) { - context = await browser.newContext(); - } + // Get existing context or create new one + let context = browser.contexts()[0]; + if (!context) { + context = await browser.newContext(); + } - // Add event listeners for page creation and closure - context.on("page", this._handleNewPage.bind(this)); + // Add event listeners for page creation and closure + context.on('page', this._handleNewPage.bind(this)); - // Get existing page or create new one - let page = context.pages()[0]; - if (!page) { - page = await context.newPage(); - } + // Get existing page or create new one + let page = context.pages()[0]; + if (!page) { + page = await context.newPage(); + } - // Set viewport size - await page.setViewportSize({ width, height }); - page.on("close", this._handlePageClose.bind(this)); + // Set viewport size + await page.setViewportSize({ width, height }); + page.on('close', this._handlePageClose.bind(this)); - return [browser, page]; - } - - private _handleNewPage(page: Page): void { - /** Handle the creation of a new page. */ - console.log("New page created"); - this._page = page; - page.on("close", this._handlePageClose.bind(this)); - } - - private _handlePageClose(page: Page): void { - /** Handle the closure of a page. */ - console.log("Page closed"); - try { - this._assertPage(); - } catch { - return; - } - if (this._page !== page) return; - - const browser = this._browser; - if (!browser || typeof browser.contexts !== "function") { - console.log("Warning: Browser or context not available."); - this._page = undefined as any; - return; - } - - const contexts = browser.contexts(); - if (!contexts.length) { - console.log("Warning: No browser contexts available."); - this._page = undefined as any; - return; - } - - const context = contexts[0]; - if (!context || typeof context.pages !== "function") { - console.log("Warning: Context pages not available."); - this._page = undefined as any; - return; - } - - const pages = context.pages(); - if (pages.length) { - this._page = pages[pages.length - 1]!; - } else { - console.log("Warning: All pages have been closed."); - this._page = undefined as any; - } - } + return [browser, page]; + }; } diff --git a/templates/typescript/cua/lib/playwright/local.ts b/templates/typescript/cua/lib/playwright/local.ts index d5cb284..d63f123 100644 --- a/templates/typescript/cua/lib/playwright/local.ts +++ b/templates/typescript/cua/lib/playwright/local.ts @@ -1,89 +1,43 @@ -import { chromium, type Browser, type Page } from "playwright"; -import { BasePlaywrightComputer } from "./base"; +import { chromium, type Browser, type Page } from 'playwright'; +import { BasePlaywrightComputer } from './base'; /** * Launches a local Chromium instance using Playwright. */ export class LocalPlaywrightComputer extends BasePlaywrightComputer { - private headless: boolean; + private headless: boolean; - constructor(headless: boolean = false) { - super(); - this.headless = headless; - } + constructor(headless = false) { + super(); + this.headless = headless; + } - async _getBrowserAndPage(): Promise<[Browser, Page]> { - const [width, height] = this.getDimensions(); - const launchArgs = [ - `--window-size=${width},${height}`, - "--disable-extensions", - "--disable-file-system", - ]; + _getBrowserAndPage = async (): Promise<[Browser, Page]> => { + const [width, height] = this.getDimensions(); + const launchArgs = [ + `--window-size=${width},${height}`, + '--disable-extensions', + '--disable-file-system', + ]; - const browser = await chromium.launch({ - headless: this.headless, - args: launchArgs, - env: { DISPLAY: ":0" }, - }); + const browser = await chromium.launch({ + headless: this.headless, + args: launchArgs, + env: { DISPLAY: ':0' }, + }); - const context = await browser.newContext(); + const context = await browser.newContext(); - // Add event listeners for page creation and closure - context.on("page", this._handleNewPage.bind(this)); + // Add event listeners for page creation and closure + context.on('page', this._handleNewPage.bind(this)); - const page = await context.newPage(); - await page.setViewportSize({ width, height }); - page.on("close", this._handlePageClose.bind(this)); + const page = await context.newPage(); + await page.setViewportSize({ width, height }); + page.on('close', this._handlePageClose.bind(this)); - await page.goto("https://bing.com"); + await page.goto('https://duckduckgo.com'); - return [browser, page]; - } - - private _handleNewPage(page: Page): void { - /** Handle the creation of a new page. */ - console.log("New page created"); - this._page = page; - page.on("close", this._handlePageClose.bind(this)); - } - - private _handlePageClose(page: Page): void { - /** Handle the closure of a page. */ - console.log("Page closed"); - try { - this._assertPage(); - } catch { - return; - } - if (this._page !== page) return; - - const browser = this._browser; - if (!browser || typeof browser.contexts !== "function") { - console.log("Warning: Browser or context not available."); - this._page = undefined as any; - return; - } - - const contexts = browser.contexts(); - if (!contexts.length) { - console.log("Warning: No browser contexts available."); - this._page = undefined as any; - return; - } - - const context = contexts[0]; - if (!context || typeof context.pages !== "function") { - console.log("Warning: Context pages not available."); - this._page = undefined as any; - return; - } - - const pages = context.pages(); - if (pages.length) { - this._page = pages[pages.length - 1]!; - } else { - console.log("Warning: All pages have been closed."); - this._page = undefined as any; - } - } + // console.dir({debug_getBrowserAndPage: [browser, page]}); + return [browser, page]; + }; } diff --git a/templates/typescript/cua/lib/toolset.ts b/templates/typescript/cua/lib/toolset.ts index d15fdc0..2999d0b 100644 --- a/templates/typescript/cua/lib/toolset.ts +++ b/templates/typescript/cua/lib/toolset.ts @@ -1,40 +1,40 @@ const shared = [ - { - type: "function", - name: "goto", - description: "Go to a specific URL.", - parameters: { - type: "object", - properties: { - url: { - type: "string", - description: "Fully qualified URL to navigate to.", - }, - }, - additionalProperties: false, - required: ["url"], - }, - }, - { - type: "function", - name: "back", - description: "Navigate back in the browser history.", - parameters: { - type: "object", - properties: {}, - additionalProperties: false, - }, - }, - { - type: "function", - name: "forward", - description: "Navigate forward in the browser history.", - parameters: { - type: "object", - properties: {}, - additionalProperties: false, - }, - }, + { + type: 'function', + name: 'goto', + description: 'Go to a specific URL.', + parameters: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Fully qualified URL to navigate to.', + }, + }, + additionalProperties: false, + required: ['url'], + }, + }, + { + type: 'function', + name: 'back', + description: 'Navigate back in the browser history.', + parameters: { + type: 'object', + properties: {}, + additionalProperties: false, + }, + }, + { + type: 'function', + name: 'forward', + description: 'Navigate forward in the browser history.', + parameters: { + type: 'object', + properties: {}, + additionalProperties: false, + }, + }, ]; export default { shared }; diff --git a/templates/typescript/cua/lib/utils.ts b/templates/typescript/cua/lib/utils.ts index e45d733..f2dc0fd 100644 --- a/templates/typescript/cua/lib/utils.ts +++ b/templates/typescript/cua/lib/utils.ts @@ -1,77 +1,61 @@ -import "dotenv/config"; -import sharp from "sharp"; -import OpenAI from "openai"; +import 'dotenv/config'; +import sharp from 'sharp'; +import OpenAI from 'openai'; +import { type ResponseItem } from 'openai/resources/responses/responses'; +const openai = new OpenAI(); const BLOCKED_DOMAINS: readonly string[] = [ - "maliciousbook.com", - "evilvideos.com", - "darkwebforum.com", - "shadytok.com", - "suspiciouspins.com", - "ilanbigio.com", + 'maliciousbook.com', + 'evilvideos.com', + 'darkwebforum.com', + 'shadytok.com', + 'suspiciouspins.com', + 'ilanbigio.com', ] as const; -interface ImageDimensions { - width: number; - height: number; +export async function calculateImageDimensions( + base64Image: string, +): Promise<{ width: number; height: number }> { + const buf = Buffer.from(base64Image, 'base64'); + const meta = await sharp(buf).metadata(); + return { width: meta.width ?? 0, height: meta.height ?? 0 }; } - -interface ComputerCallOutput { - type: "computer_call_output"; - output?: { - image_url?: string; - [key: string]: any; - }; - [key: string]: any; -} - -interface Message { - [key: string]: any; -} - -async function calculateImageDimensions( - base64Image: string, -): Promise { - const imageBuffer = Buffer.from(base64Image, "base64"); - const metadata = await sharp(imageBuffer).metadata(); - return { width: metadata.width!, height: metadata.height! }; -} - -function sanitizeMessage(msg: Message): Message { - /** Return a copy of the message with image_url omitted for computer_call_output messages. */ - if (msg.type === "computer_call_output") { - const output = msg.output || {}; - if (typeof output === "object") { - const sanitized = { ...msg }; - sanitized.output = { ...output, image_url: "[omitted]" }; - return sanitized; - } - } - return msg; +export function sanitizeMessage(msg: ResponseItem): ResponseItem { + const sanitizedMsg = { ...msg } as ResponseItem; + if ( + sanitizedMsg.type === 'computer_call_output' && + typeof sanitizedMsg.output === 'object' && + sanitizedMsg.output !== null + ) { + sanitizedMsg.output = { ...sanitizedMsg.output }; + const output = sanitizedMsg.output as { image_url?: string }; + if (output.image_url) { + output.image_url = '[omitted]'; + } + } + return sanitizedMsg; } -async function createResponse(kwargs: any): Promise { - const openai = new OpenAI(); - try { - const response = await openai.responses.create(kwargs); - return response; - } catch (error: any) { - console.error(`Error: ${error.status} ${error.message}`); - throw error; - } +export async function createResponse( + params: OpenAI.Responses.ResponseCreateParams, +): Promise<{ output?: OpenAI.Responses.ResponseOutputItem[] }> { + try { + const response = await openai.responses.create(params); + return 'output' in response ? response : { output: undefined }; + } catch (err: unknown) { + console.error((err as Error).message); + throw err; + } } -function checkBlocklistedUrl(url: string): boolean { - /** Return true if the given URL (including subdomains) is in the blocklist. */ - const hostname = new URL(url).hostname || ""; - return BLOCKED_DOMAINS.some( - (blocked) => hostname === blocked || hostname.endsWith(`.${blocked}`), - ); +export function checkBlocklistedUrl(url: string): boolean { + const host = new URL(url).hostname; + return BLOCKED_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`)); } export default { - calculateImageDimensions, - sanitizeMessage, - createResponse, - checkBlocklistedUrl, + calculateImageDimensions, + sanitizeMessage, + createResponse, + checkBlocklistedUrl, }; diff --git a/templates/typescript/cua/package.json b/templates/typescript/cua/package.json index 70b296c..dfbb715 100644 --- a/templates/typescript/cua/package.json +++ b/templates/typescript/cua/package.json @@ -1,15 +1,18 @@ { - "type": "module", - "private": true, - "dependencies": { - "@onkernel/sdk": "^0.6.0", - "@types/node": "^24.0.3", - "dotenv": "^16.5.0", - "openai": "^5.5.1", - "playwright": "^1.53.0", - "sharp": "^0.34.2" - }, - "peerDependencies": { - "typescript": "^5.8.3" - } + "type": "module", + "private": true, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@onkernel/sdk": "^0.6.0", + "dotenv": "^16.5.0", + "openai": "^5.7.0", + "playwright": "^1.53.0", + "sharp": "^0.34.2" + }, + "devDependencies": { + "@types/node": "^24.0.3", + "typescript": "^5.8.3" + } } diff --git a/templates/typescript/cua/test.local.ts b/templates/typescript/cua/test.local.ts new file mode 100644 index 0000000..23f9a5c --- /dev/null +++ b/templates/typescript/cua/test.local.ts @@ -0,0 +1,49 @@ +import 'dotenv/config'; +import { Agent } from './lib/agent'; +import computers from './lib/computers'; + +/* + to run a local browser test before deploying to kernel +*/ + +async function test(): Promise { + const { computer } = await computers.create({ type: 'local' }); + const agent = new Agent({ + model: 'computer-use-preview', + computer, + tools: [], + acknowledge_safety_check_callback: (m: string): boolean => { + console.log(`> safety check: ${m}`); + return true; + }, + }); + + // run agent and get response + const logs = await agent.runFullTurn({ + messages: [ + { + role: 'system', + content: `- Current date and time: ${new Date().toISOString()} (${new Date().toLocaleDateString( + 'en-US', + { weekday: 'long' }, + )})`, + }, + { + type: 'message', + role: 'user', + content: [ + { + type: 'input_text', + text: 'go to ebay.com and look up oberheim ob-x prices and give me a report', + }, + ], + }, + ], + print_steps: true, + debug: true, + show_images: false, + }); + console.dir(logs, { depth: null }); +} + +test(); diff --git a/templates/typescript/cua/tsconfig.json b/templates/typescript/cua/tsconfig.json index f5c1fe2..05b1408 100644 --- a/templates/typescript/cua/tsconfig.json +++ b/templates/typescript/cua/tsconfig.json @@ -1,30 +1,16 @@ { - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - }, - "include": ["./**/*.ts", "./**/*.tsx"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowJs": false, + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules", "dist"] }