From 594e439f4547ec750415fda46e8ca03f3cab2fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Wed, 28 Jan 2026 17:17:36 -0300 Subject: [PATCH 1/7] feat(templates): add regulatory-filling-monitor typescript template scaffolding --- .../regulatory-filling-monitor/.env.example | 2 + .../regulatory-filling-monitor/_gitignore | 39 ++++++ .../package-lock.json | 121 ++++++++++++++++++ .../regulatory-filling-monitor/package.json | 16 +++ .../regulatory-filling-monitor/tsconfig.json | 23 ++++ 5 files changed, 201 insertions(+) create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/.env.example create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/_gitignore create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/package-lock.json create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/package.json create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/tsconfig.json diff --git a/pkg/templates/typescript/regulatory-filling-monitor/.env.example b/pkg/templates/typescript/regulatory-filling-monitor/.env.example new file mode 100644 index 0000000..877363b --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/.env.example @@ -0,0 +1,2 @@ +# Copy this file to .env and fill in your API key +ANTHROPIC_API_KEY=your_anthropic_api_key_here diff --git a/pkg/templates/typescript/regulatory-filling-monitor/_gitignore b/pkg/templates/typescript/regulatory-filling-monitor/_gitignore new file mode 100644 index 0000000..095f573 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/_gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ +package-lock.json + +# TypeScript +*.tsbuildinfo +dist/ +build/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Misc +.cache/ +.temp/ +.tmp/ diff --git a/pkg/templates/typescript/regulatory-filling-monitor/package-lock.json b/pkg/templates/typescript/regulatory-filling-monitor/package-lock.json new file mode 100644 index 0000000..8387298 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/package-lock.json @@ -0,0 +1,121 @@ +{ + "name": "ts-regulatory-filling-monitor", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts-regulatory-filling-monitor", + "dependencies": { + "@anthropic-ai/sdk": "^0.71.2", + "@onkernel/sdk": "^0.24.0", + "luxon": "^3.7.2" + }, + "devDependencies": { + "@types/luxon": "^3.4.2", + "@types/node": "^22.15.17", + "typescript": "^5.9.3" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.71.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz", + "integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@onkernel/sdk": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@onkernel/sdk/-/sdk-0.24.0.tgz", + "integrity": "sha512-f0xZGSaC9Nlg7CwLw6agyw682sc9Q8rPRG6Zyk82JmCKETFBdMqfyXuxK5uESidk0pQp/GYGG8rHy+vGa5jgCQ==", + "license": "Apache-2.0" + }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/package.json b/pkg/templates/typescript/regulatory-filling-monitor/package.json new file mode 100644 index 0000000..96570f6 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/package.json @@ -0,0 +1,16 @@ +{ + "name": "ts-regulatory-filling-monitor", + "module": "index.ts", + "type": "module", + "private": true, + "dependencies": { + "@anthropic-ai/sdk": "^0.71.2", + "@onkernel/sdk": "^0.24.0", + "luxon": "^3.7.2" + }, + "devDependencies": { + "@types/luxon": "^3.4.2", + "@types/node": "^22.15.17", + "typescript": "^5.9.3" + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/tsconfig.json b/pkg/templates/typescript/regulatory-filling-monitor/tsconfig.json new file mode 100644 index 0000000..43284c6 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["./**/*.ts", "./**/*.tsx"], + "exclude": ["node_modules", "dist"] +} From b559f51097c4c8a21be815df36b66aff6a4b2cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Wed, 28 Jan 2026 17:18:24 -0300 Subject: [PATCH 2/7] feat(templates): add computer use tools for regulatory-filling-monitor --- .../tools/collection.ts | 61 +++ .../tools/computer.ts | 393 ++++++++++++++++++ .../tools/types/computer.ts | 64 +++ .../tools/utils/keyboard.ts | 88 ++++ .../tools/utils/validator.ts | 67 +++ 5 files changed, 673 insertions(+) create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/tools/collection.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/tools/computer.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/tools/types/computer.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/tools/utils/keyboard.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/tools/utils/validator.ts diff --git a/pkg/templates/typescript/regulatory-filling-monitor/tools/collection.ts b/pkg/templates/typescript/regulatory-filling-monitor/tools/collection.ts new file mode 100644 index 0000000..7e08904 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/tools/collection.ts @@ -0,0 +1,61 @@ +import { ComputerTool20241022, ComputerTool20250124 } from './computer.ts'; +import { Action } from './types/computer.ts'; +import type { ActionParams, ToolResult } from './types/computer.ts'; + +export type ToolVersion = 'computer_use_20250124' | 'computer_use_20241022' | 'computer_use_20250429'; + +export const DEFAULT_TOOL_VERSION: ToolVersion = 'computer_use_20250429'; + +interface ToolGroup { + readonly version: ToolVersion; + readonly tools: (typeof ComputerTool20241022 | typeof ComputerTool20250124)[]; + readonly beta_flag: string; +} + +export const TOOL_GROUPS: ToolGroup[] = [ + { + version: 'computer_use_20241022', + tools: [ComputerTool20241022], + beta_flag: 'computer-use-2024-10-22', + }, + { + version: 'computer_use_20250124', + tools: [ComputerTool20250124], + beta_flag: 'computer-use-2025-01-24', + }, + // 20250429 version inherits from 20250124 + { + version: 'computer_use_20250429', + tools: [ComputerTool20250124], + beta_flag: 'computer-use-2025-01-24', + }, +]; + +export const TOOL_GROUPS_BY_VERSION: Record = Object.fromEntries( + TOOL_GROUPS.map(group => [group.version, group]) +) as Record; + +export class ToolCollection { + private tools: Map; + + constructor(...tools: (ComputerTool20241022 | ComputerTool20250124)[]) { + this.tools = new Map(tools.map(tool => [tool.name, tool])); + } + + toParams(): ActionParams[] { + return Array.from(this.tools.values()).map(tool => tool.toParams()); + } + + async run(name: string, toolInput: ActionParams): Promise { + const tool = this.tools.get(name); + if (!tool) { + throw new Error(`Tool ${name} not found`); + } + + if (!Object.values(Action).includes(toolInput.action)) { + throw new Error(`Invalid action ${toolInput.action} for tool ${name}`); + } + + return await tool.call(toolInput); + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/tools/computer.ts b/pkg/templates/typescript/regulatory-filling-monitor/tools/computer.ts new file mode 100644 index 0000000..e0b76c6 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/tools/computer.ts @@ -0,0 +1,393 @@ +import { Buffer } from 'buffer'; +import type { Kernel } from '@onkernel/sdk'; +import type { ActionParams, BaseAnthropicTool, ToolResult } from './types/computer.ts'; +import { Action, ToolError } from './types/computer.ts'; +import { ActionValidator } from './utils/validator.ts'; + +const TYPING_DELAY_MS = 12; + +export class ComputerTool implements BaseAnthropicTool { + name: 'computer' = 'computer'; + protected kernel: Kernel; + protected sessionId: string; + protected _screenshotDelay = 2.0; + protected version: '20241022' | '20250124'; + + private lastMousePosition: [number, number] = [0, 0]; + + private readonly mouseActions = new Set([ + Action.LEFT_CLICK, + Action.RIGHT_CLICK, + Action.MIDDLE_CLICK, + Action.DOUBLE_CLICK, + Action.TRIPLE_CLICK, + Action.MOUSE_MOVE, + Action.LEFT_MOUSE_DOWN, + Action.LEFT_MOUSE_UP, + ]); + + private readonly keyboardActions = new Set([ + Action.KEY, + Action.TYPE, + Action.HOLD_KEY, + ]); + + private readonly systemActions = new Set([ + Action.SCREENSHOT, + Action.CURSOR_POSITION, + Action.SCROLL, + Action.WAIT, + ]); + + constructor(kernel: Kernel, sessionId: string, version: '20241022' | '20250124' = '20250124') { + this.kernel = kernel; + this.sessionId = sessionId; + this.version = version; + } + + get apiType(): 'computer_20241022' | 'computer_20250124' { + return this.version === '20241022' ? 'computer_20241022' : 'computer_20250124'; + } + + toParams(): ActionParams { + const params = { + name: this.name, + type: this.apiType, + display_width_px: 1024, + display_height_px: 768, + display_number: null, + }; + return params; + } + + private getMouseButton(action: Action): 'left' | 'right' | 'middle' { + switch (action) { + case Action.LEFT_CLICK: + case Action.DOUBLE_CLICK: + case Action.TRIPLE_CLICK: + case Action.LEFT_CLICK_DRAG: + case Action.LEFT_MOUSE_DOWN: + case Action.LEFT_MOUSE_UP: + return 'left'; + case Action.RIGHT_CLICK: + return 'right'; + case Action.MIDDLE_CLICK: + return 'middle'; + default: + throw new ToolError(`Invalid mouse action: ${action}`); + } + } + + private async handleMouseAction(action: Action, coordinate: [number, number]): Promise { + const [x, y] = ActionValidator.validateAndGetCoordinates(coordinate); + + if (action === Action.MOUSE_MOVE) { + await this.kernel.browsers.computer.moveMouse(this.sessionId, { + x, + y, + }); + this.lastMousePosition = [x, y]; + } else if (action === Action.LEFT_MOUSE_DOWN) { + await this.kernel.browsers.computer.clickMouse(this.sessionId, { + x, + y, + button: 'left', + click_type: 'down', + }); + this.lastMousePosition = [x, y]; + } else if (action === Action.LEFT_MOUSE_UP) { + await this.kernel.browsers.computer.clickMouse(this.sessionId, { + x, + y, + button: 'left', + click_type: 'up', + }); + this.lastMousePosition = [x, y]; + } else { + const button = this.getMouseButton(action); + let numClicks = 1; + if (action === Action.DOUBLE_CLICK) { + numClicks = 2; + } else if (action === Action.TRIPLE_CLICK) { + numClicks = 3; + } + + await this.kernel.browsers.computer.clickMouse(this.sessionId, { + x, + y, + button, + click_type: 'click', + num_clicks: numClicks, + }); + this.lastMousePosition = [x, y]; + } + + await new Promise(resolve => setTimeout(resolve, 500)); + return await this.screenshot(); + } + + private async handleKeyboardAction(action: Action, text: string, duration?: number): Promise { + if (action === Action.HOLD_KEY) { + const key = this.convertToKernelKey(text); + await this.kernel.browsers.computer.pressKey(this.sessionId, { + keys: [key], + duration: duration ? duration * 1000 : undefined, + }); + } else if (action === Action.KEY) { + const key = this.convertKeyCombinationToKernel(text); + await this.kernel.browsers.computer.pressKey(this.sessionId, { + keys: [key], + }); + } else { + await this.kernel.browsers.computer.typeText(this.sessionId, { + text, + delay: TYPING_DELAY_MS, + }); + } + + await new Promise(resolve => setTimeout(resolve, 500)); + return await this.screenshot(); + } + + // Key mappings for Kernel Computer Controls API (xdotool format) + private static readonly KEY_MAP: Record = { + // Enter/Return + 'return': 'Return', + 'enter': 'Return', + 'Enter': 'Return', + // Arrow keys + 'left': 'Left', + 'right': 'Right', + 'up': 'Up', + 'down': 'Down', + 'ArrowLeft': 'Left', + 'ArrowRight': 'Right', + 'ArrowUp': 'Up', + 'ArrowDown': 'Down', + // Navigation + 'home': 'Home', + 'end': 'End', + 'pageup': 'Page_Up', + 'page_up': 'Page_Up', + 'PageUp': 'Page_Up', + 'pagedown': 'Page_Down', + 'page_down': 'Page_Down', + 'PageDown': 'Page_Down', + // Editing + 'delete': 'Delete', + 'backspace': 'BackSpace', + 'Backspace': 'BackSpace', + 'tab': 'Tab', + 'insert': 'Insert', + // Escape + 'esc': 'Escape', + 'escape': 'Escape', + // Function keys + 'f1': 'F1', + 'f2': 'F2', + 'f3': 'F3', + 'f4': 'F4', + 'f5': 'F5', + 'f6': 'F6', + 'f7': 'F7', + 'f8': 'F8', + 'f9': 'F9', + 'f10': 'F10', + 'f11': 'F11', + 'f12': 'F12', + // Misc + 'space': 'space', + 'minus': 'minus', + 'equal': 'equal', + 'plus': 'plus', + }; + + // Modifier key mappings (xdotool format) + private static readonly MODIFIER_MAP: Record = { + 'ctrl': 'ctrl', + 'control': 'ctrl', + 'Control': 'ctrl', + 'alt': 'alt', + 'Alt': 'alt', + 'shift': 'shift', + 'Shift': 'shift', + 'meta': 'super', + 'Meta': 'super', + 'cmd': 'super', + 'command': 'super', + 'win': 'super', + 'super': 'super', + }; + + private convertToKernelKey(key: string): string { + // Check modifier keys first + if (ComputerTool.MODIFIER_MAP[key]) { + return ComputerTool.MODIFIER_MAP[key]; + } + // Check special keys + if (ComputerTool.KEY_MAP[key]) { + return ComputerTool.KEY_MAP[key]; + } + // Return as-is if no mapping exists + return key; + } + + private convertKeyCombinationToKernel(combo: string): string { + // Handle key combinations (e.g., "ctrl+a", "Control+t") + if (combo.includes('+')) { + const parts = combo.split('+'); + const mappedParts = parts.map(part => this.convertToKernelKey(part.trim())); + return mappedParts.join('+'); + } + // Single key - just convert it + return this.convertToKernelKey(combo); + } + + async screenshot(): Promise { + try { + console.log('Starting screenshot...'); + await new Promise(resolve => setTimeout(resolve, this._screenshotDelay * 1000)); + const response = await this.kernel.browsers.computer.captureScreenshot(this.sessionId); + const blob = await response.blob(); + const arrayBuffer = await blob.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + console.log('Screenshot taken, size:', buffer.length, 'bytes'); + + return { + base64Image: buffer.toString('base64'), + }; + } catch (error) { + throw new ToolError(`Failed to take screenshot: ${error}`); + } + } + + async call(params: ActionParams): Promise { + const { + action, + text, + coordinate, + scrollDirection: scrollDirectionParam, + scroll_amount, + scrollAmount, + duration, + ...kwargs + } = params; + + ActionValidator.validateActionParams(params, this.mouseActions, this.keyboardActions); + + if (action === Action.SCREENSHOT) { + return await this.screenshot(); + } + + if (action === Action.CURSOR_POSITION) { + throw new ToolError('Cursor position is not available with Kernel Computer Controls API'); + } + + if (action === Action.SCROLL) { + if (this.version !== '20250124') { + throw new ToolError(`${action} is only available in version 20250124`); + } + + const scrollDirection = scrollDirectionParam || kwargs.scroll_direction; + const scrollAmountValue = scrollAmount || scroll_amount; + + if (!scrollDirection || !['up', 'down', 'left', 'right'].includes(scrollDirection)) { + throw new ToolError(`Scroll direction "${scrollDirection}" must be 'up', 'down', 'left', or 'right'`); + } + if (typeof scrollAmountValue !== 'number' || scrollAmountValue < 0) { + throw new ToolError(`Scroll amount "${scrollAmountValue}" must be a non-negative number`); + } + + const [x, y] = coordinate + ? ActionValidator.validateAndGetCoordinates(coordinate) + : this.lastMousePosition; + + let delta_x = 0; + let delta_y = 0; + // Each scroll_amount unit = ~25 pixels for fine-grained control when extracting data from tables + // This allows the model to scroll just a few rows at a time + const scrollDelta = (scrollAmountValue ?? 1) * 5; + + if (scrollDirection === 'down') { + delta_y = scrollDelta; + } else if (scrollDirection === 'up') { + delta_y = -scrollDelta; + } else if (scrollDirection === 'right') { + delta_x = scrollDelta; + } else if (scrollDirection === 'left') { + delta_x = -scrollDelta; + } + + await this.kernel.browsers.computer.scroll(this.sessionId, { + x, + y, + delta_x, + delta_y, + }); + + await new Promise(resolve => setTimeout(resolve, 500)); + return await this.screenshot(); + } + + if (action === Action.WAIT) { + if (this.version !== '20250124') { + throw new ToolError(`${action} is only available in version 20250124`); + } + await new Promise(resolve => setTimeout(resolve, duration! * 1000)); + return await this.screenshot(); + } + + if (action === Action.LEFT_CLICK_DRAG) { + if (!coordinate) { + throw new ToolError(`coordinate is required for ${action}`); + } + + const [endX, endY] = ActionValidator.validateAndGetCoordinates(coordinate); + const startCoordinate = kwargs.start_coordinate as [number, number] | undefined; + const [startX, startY] = startCoordinate + ? ActionValidator.validateAndGetCoordinates(startCoordinate) + : this.lastMousePosition; + + console.log(`Dragging from (${startX}, ${startY}) to (${endX}, ${endY})`); + + await this.kernel.browsers.computer.dragMouse(this.sessionId, { + path: [[startX, startY], [endX, endY]], + button: 'left', + }); + + this.lastMousePosition = [endX, endY]; + + await new Promise(resolve => setTimeout(resolve, 500)); + return await this.screenshot(); + } + + if (this.mouseActions.has(action)) { + if (!coordinate) { + throw new ToolError(`coordinate is required for ${action}`); + } + return await this.handleMouseAction(action, coordinate); + } + + if (this.keyboardActions.has(action)) { + if (!text) { + throw new ToolError(`text is required for ${action}`); + } + return await this.handleKeyboardAction(action, text, duration); + } + + throw new ToolError(`Invalid action: ${action}`); + } +} + +// For backward compatibility +export class ComputerTool20241022 extends ComputerTool { + constructor(kernel: Kernel, sessionId: string) { + super(kernel, sessionId, '20241022'); + } +} + +export class ComputerTool20250124 extends ComputerTool { + constructor(kernel: Kernel, sessionId: string) { + super(kernel, sessionId, '20250124'); + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/tools/types/computer.ts b/pkg/templates/typescript/regulatory-filling-monitor/tools/types/computer.ts new file mode 100644 index 0000000..60e485f --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/tools/types/computer.ts @@ -0,0 +1,64 @@ +export enum Action { + // Mouse actions + MOUSE_MOVE = 'mouse_move', + LEFT_CLICK = 'left_click', + RIGHT_CLICK = 'right_click', + MIDDLE_CLICK = 'middle_click', + DOUBLE_CLICK = 'double_click', + TRIPLE_CLICK = 'triple_click', + LEFT_CLICK_DRAG = 'left_click_drag', + LEFT_MOUSE_DOWN = 'left_mouse_down', + LEFT_MOUSE_UP = 'left_mouse_up', + + // Keyboard actions + KEY = 'key', + TYPE = 'type', + HOLD_KEY = 'hold_key', + + // System actions + SCREENSHOT = 'screenshot', + CURSOR_POSITION = 'cursor_position', + SCROLL = 'scroll', + WAIT = 'wait', +} + +// For backward compatibility +export type Action_20241022 = Action; +export type Action_20250124 = Action; + +export type MouseButton = 'left' | 'right' | 'middle'; +export type ScrollDirection = 'up' | 'down' | 'left' | 'right'; +export type Coordinate = [number, number]; +export type Duration = number; + +export interface ActionParams { + action: Action; + text?: string; + coordinate?: Coordinate; + scrollDirection?: ScrollDirection; + scroll_amount?: number; + scrollAmount?: number; + duration?: Duration; + key?: string; + [key: string]: Action | string | Coordinate | ScrollDirection | number | Duration | undefined; +} + +export interface ToolResult { + output?: string; + error?: string; + base64Image?: string; + system?: string; +} + +export interface BaseAnthropicTool { + name: string; + apiType: string; + toParams(): ActionParams; +} + +export class ToolError extends Error { + constructor(message: string) { + super(message); + this.name = 'ToolError'; + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/tools/utils/keyboard.ts b/pkg/templates/typescript/regulatory-filling-monitor/tools/utils/keyboard.ts new file mode 100644 index 0000000..0c1e384 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/tools/utils/keyboard.ts @@ -0,0 +1,88 @@ +export class KeyboardUtils { + // Only map alternative names to standard Playwright modifier keys + private static readonly modifierKeyMap: Record = { + 'ctrl': 'Control', + 'alt': 'Alt', + 'cmd': 'Meta', + 'command': 'Meta', + 'win': 'Meta', + }; + + // Essential key mappings for Playwright compatibility + private static readonly keyMap: Record = { + 'return': 'Enter', + 'space': ' ', + 'left': 'ArrowLeft', + 'right': 'ArrowRight', + 'up': 'ArrowUp', + 'down': 'ArrowDown', + 'home': 'Home', + 'end': 'End', + 'pageup': 'PageUp', + 'page_up': 'PageUp', + 'pagedown': 'PageDown', + 'page_down': 'PageDown', + 'delete': 'Delete', + 'backspace': 'Backspace', + 'tab': 'Tab', + 'esc': 'Escape', + 'escape': 'Escape', + 'insert': 'Insert', + 'super_l': 'Meta', + 'f1': 'F1', + 'f2': 'F2', + 'f3': 'F3', + 'f4': 'F4', + 'f5': 'F5', + 'f6': 'F6', + 'f7': 'F7', + 'f8': 'F8', + 'f9': 'F9', + 'f10': 'F10', + 'f11': 'F11', + 'f12': 'F12', + 'minus': '-', + 'equal': '=', + 'plus': '+', + }; + + static isModifierKey(key: string | undefined): boolean { + if (!key) return false; + const normalizedKey = this.modifierKeyMap[key.toLowerCase()] || key; + return ['Control', 'Alt', 'Shift', 'Meta'].includes(normalizedKey); + } + + static getPlaywrightKey(key: string | undefined): string { + if (!key) { + throw new Error('Key cannot be undefined'); + } + + const normalizedKey = key.toLowerCase(); + + // Handle special cases + if (normalizedKey in this.keyMap) { + return this.keyMap[normalizedKey] as string; + } + + // Normalize modifier keys + if (normalizedKey in this.modifierKeyMap) { + return this.modifierKeyMap[normalizedKey] as string; + } + + // Return the key as is - Playwright handles standard key names + return key; + } + + static parseKeyCombination(combo: string): string[] { + if (!combo) { + throw new Error('Key combination cannot be empty'); + } + return combo.toLowerCase().split('+').map(key => { + const trimmedKey = key.trim(); + if (!trimmedKey) { + throw new Error('Invalid key combination: empty key'); + } + return this.getPlaywrightKey(trimmedKey); + }); + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/tools/utils/validator.ts b/pkg/templates/typescript/regulatory-filling-monitor/tools/utils/validator.ts new file mode 100644 index 0000000..ff09cd0 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/tools/utils/validator.ts @@ -0,0 +1,67 @@ +import { Action, ToolError } from '../types/computer.ts'; +import type { ActionParams, Coordinate, Duration } from '../types/computer.ts'; + +export class ActionValidator { + static validateText(text: string | undefined, required: boolean, action: string): void { + if (required && text === undefined) { + throw new ToolError(`text is required for ${action}`); + } + if (text !== undefined && typeof text !== 'string') { + throw new ToolError(`${text} must be a string`); + } + } + + static validateCoordinate(coordinate: Coordinate | undefined, required: boolean, action: string): void { + if (required && !coordinate) { + throw new ToolError(`coordinate is required for ${action}`); + } + if (coordinate) { + this.validateAndGetCoordinates(coordinate); + } + } + + static validateDuration(duration: Duration | undefined): void { + if (duration === undefined || typeof duration !== 'number') { + throw new ToolError(`${duration} must be a number`); + } + if (duration < 0) { + throw new ToolError(`${duration} must be non-negative`); + } + if (duration > 100) { + throw new ToolError(`${duration} is too long`); + } + } + + static validateAndGetCoordinates(coordinate: Coordinate): Coordinate { + if (!Array.isArray(coordinate) || coordinate.length !== 2) { + throw new ToolError(`${coordinate} must be a tuple of length 2`); + } + if (!coordinate.every(i => typeof i === 'number' && i >= 0)) { + throw new ToolError(`${coordinate} must be a tuple of non-negative numbers`); + } + return coordinate; + } + + static validateActionParams(params: ActionParams, mouseActions: Set, keyboardActions: Set): void { + const { action, text, coordinate, duration } = params; + + // Validate text parameter + if (keyboardActions.has(action)) { + this.validateText(text, true, action); + } else { + this.validateText(text, false, action); + } + + // Validate coordinate parameter + if (mouseActions.has(action)) { + this.validateCoordinate(coordinate, true, action); + } else { + this.validateCoordinate(coordinate, false, action); + } + + // Validate duration parameter + if (action === Action.HOLD_KEY || action === Action.WAIT) { + this.validateDuration(duration); + } + } +} From 90b9a094c4099b025b9b27b84ee857d53114b8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Wed, 28 Jan 2026 17:19:09 -0300 Subject: [PATCH 3/7] feat(templates): add Anthropic API types and message utilities --- .../regulatory-filling-monitor/types/beta.ts | 58 ++++++++++++++ .../utils/message-processing.ts | 79 +++++++++++++++++++ .../utils/tool-results.ts | 49 ++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/types/beta.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/utils/message-processing.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/utils/tool-results.ts diff --git a/pkg/templates/typescript/regulatory-filling-monitor/types/beta.ts b/pkg/templates/typescript/regulatory-filling-monitor/types/beta.ts new file mode 100644 index 0000000..04cf627 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/types/beta.ts @@ -0,0 +1,58 @@ +import type { BetaMessageParam as AnthropicMessageParam, BetaMessage as AnthropicMessage, BetaContentBlock as AnthropicContentBlock } from '@anthropic-ai/sdk/resources/beta/messages/messages'; +import type { ActionParams } from '../tools/types/computer.ts'; + +// Re-export the SDK types +export type BetaMessageParam = AnthropicMessageParam; +export type BetaMessage = AnthropicMessage; +export type BetaContentBlock = AnthropicContentBlock; + +// Keep our local types for internal use +export interface BetaTextBlock { + type: 'text'; + text: string; + id?: string; + cache_control?: { type: 'ephemeral' }; +} + +export interface BetaImageBlock { + type: 'image'; + source: { + type: 'base64'; + media_type: 'image/png'; + data: string; + }; + id?: string; + cache_control?: { type: 'ephemeral' }; +} + +export interface BetaToolUseBlock { + type: 'tool_use'; + name: string; + input: ActionParams; + id?: string; + cache_control?: { type: 'ephemeral' }; +} + +export interface BetaThinkingBlock { + type: 'thinking'; + thinking: { + type: 'enabled'; + budget_tokens: number; + } | { + type: 'disabled'; + }; + signature?: string; + id?: string; + cache_control?: { type: 'ephemeral' }; +} + +export interface BetaToolResultBlock { + type: 'tool_result'; + content: (BetaTextBlock | BetaImageBlock)[] | string; + tool_use_id: string; + is_error: boolean; + id?: string; + cache_control?: { type: 'ephemeral' }; +} + +export type BetaLocalContentBlock = BetaTextBlock | BetaImageBlock | BetaToolUseBlock | BetaThinkingBlock | BetaToolResultBlock; diff --git a/pkg/templates/typescript/regulatory-filling-monitor/utils/message-processing.ts b/pkg/templates/typescript/regulatory-filling-monitor/utils/message-processing.ts new file mode 100644 index 0000000..8f9dea7 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/utils/message-processing.ts @@ -0,0 +1,79 @@ +import type { BetaMessage, BetaMessageParam, BetaToolResultBlock, BetaContentBlock, BetaLocalContentBlock } from '../types/beta.ts'; + +export function responseToParams(response: BetaMessage): BetaContentBlock[] { + return response.content.map(block => { + if (block.type === 'text' && block.text) { + return { type: 'text', text: block.text }; + } + if (block.type === 'thinking') { + const { thinking, signature, ...rest } = block; + return { ...rest, thinking, ...(signature && { signature }) }; + } + return block as BetaContentBlock; + }); +} + +export function maybeFilterToNMostRecentImages( + messages: BetaMessageParam[], + imagesToKeep: number, + minRemovalThreshold: number +): void { + if (!imagesToKeep) return; + + const toolResultBlocks = messages + .flatMap(message => Array.isArray(message?.content) ? message.content : []) + .filter((item): item is BetaToolResultBlock => + typeof item === 'object' && item.type === 'tool_result' + ); + + const totalImages = toolResultBlocks.reduce((count, toolResult) => { + if (!Array.isArray(toolResult.content)) return count; + return count + toolResult.content.filter( + content => typeof content === 'object' && content.type === 'image' + ).length; + }, 0); + + let imagesToRemove = Math.floor((totalImages - imagesToKeep) / minRemovalThreshold) * minRemovalThreshold; + + for (const toolResult of toolResultBlocks) { + if (Array.isArray(toolResult.content)) { + toolResult.content = toolResult.content.filter(content => { + if (typeof content === 'object' && content.type === 'image') { + if (imagesToRemove > 0) { + imagesToRemove--; + return false; + } + } + return true; + }); + } + } +} + +const PROMPT_CACHING_BETA_FLAG = 'prompt-caching-2024-07-31'; + +export function injectPromptCaching(messages: BetaMessageParam[]): void { + let breakpointsRemaining = 3; + + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + if (!message) continue; + if (message.role === 'user' && Array.isArray(message.content)) { + if (breakpointsRemaining > 0) { + breakpointsRemaining--; + const lastContent = message.content[message.content.length - 1]; + if (lastContent) { + (lastContent as BetaLocalContentBlock).cache_control = { type: 'ephemeral' }; + } + } else { + const lastContent = message.content[message.content.length - 1]; + if (lastContent) { + delete (lastContent as BetaLocalContentBlock).cache_control; + } + break; + } + } + } +} + +export { PROMPT_CACHING_BETA_FLAG }; diff --git a/pkg/templates/typescript/regulatory-filling-monitor/utils/tool-results.ts b/pkg/templates/typescript/regulatory-filling-monitor/utils/tool-results.ts new file mode 100644 index 0000000..6b1b149 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/utils/tool-results.ts @@ -0,0 +1,49 @@ +import type { ToolResult } from '../tools/types/computer.ts'; +import type { BetaToolResultBlock, BetaTextBlock, BetaImageBlock } from '../types/beta.ts'; + +export function makeApiToolResult( + result: ToolResult, + toolUseId: string +): BetaToolResultBlock { + const toolResultContent: (BetaTextBlock | BetaImageBlock)[] = []; + let isError = false; + + if (result.error) { + isError = true; + toolResultContent.push({ + type: 'text', + text: maybePrependSystemToolResult(result, result.error), + }); + } else { + if (result.output) { + toolResultContent.push({ + type: 'text', + text: maybePrependSystemToolResult(result, result.output), + }); + } + if (result.base64Image) { + toolResultContent.push({ + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: result.base64Image, + }, + }); + } + } + + return { + type: 'tool_result', + content: toolResultContent, + tool_use_id: toolUseId, + is_error: isError, + }; +} + +export function maybePrependSystemToolResult(result: ToolResult, resultText: string): string { + if (result.system) { + return `${result.system}\n${resultText}`; + } + return resultText; +} From 21f826052546f9f75135d07675d76b6fadf1c992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Wed, 28 Jan 2026 17:20:32 -0300 Subject: [PATCH 4/7] feat(templates): add browser session and sampling loop for regulatory-filling-monitor --- .../regulatory-filling-monitor/loop.ts | 197 ++++++++++++++++ .../regulatory-filling-monitor/session.ts | 222 ++++++++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/loop.ts create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/session.ts diff --git a/pkg/templates/typescript/regulatory-filling-monitor/loop.ts b/pkg/templates/typescript/regulatory-filling-monitor/loop.ts new file mode 100644 index 0000000..97f294e --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/loop.ts @@ -0,0 +1,197 @@ +import { Anthropic } from '@anthropic-ai/sdk'; +import { DateTime } from 'luxon'; +import type { Kernel } from '@onkernel/sdk'; +import { DEFAULT_TOOL_VERSION, TOOL_GROUPS_BY_VERSION, ToolCollection, type ToolVersion } from './tools/collection.ts'; +import { ComputerTool20241022, ComputerTool20250124 } from './tools/computer.ts'; +import type { ActionParams } from './tools/types/computer.ts'; +import { Action } from './tools/types/computer.ts'; +import type { BetaMessageParam, BetaTextBlock } from './types/beta.ts'; +import { injectPromptCaching, maybeFilterToNMostRecentImages, PROMPT_CACHING_BETA_FLAG, responseToParams } from './utils/message-processing.ts'; +import { makeApiToolResult } from './utils/tool-results.ts'; + +// System prompt optimized for the environment +const SYSTEM_PROMPT = ` +* You are utilising an Ubuntu virtual machine using ${process.arch} architecture with internet access. +* When you connect to the display, CHROMIUM IS ALREADY OPEN. The url bar is not visible but it is there. +* If you need to navigate to a new page, use ctrl+l to focus the url bar and then enter the url. +* You won't be able to see the url bar from the screenshot but ctrl-l still works. +* As the initial step click on the search bar. +* When viewing a page it can be helpful to zoom out so that you can see everything on the page. +* Either that, or make sure you scroll down to see everything before deciding something isn't available. +* When using your computer function calls, they take a while to run and send back to you. +* Where possible/feasible, try to chain multiple of these calls all into one function calls request. +* The current date is ${DateTime.now().toFormat('EEEE, MMMM d, yyyy')}. +* After each step, take a screenshot and carefully evaluate if you have achieved the right outcome. +* Explicitly show your thinking: "I have evaluated step X..." If not correct, try again. +* Only when you confirm a step was executed correctly should you move on to the next one. + + + +* When using Chromium, if a startup wizard appears, IGNORE IT. Do not even click "skip this step". +* Instead, click on the search bar on the center of the screen where it says "Search or enter address", and enter the appropriate search term or URL there. +`; + +// Add new type definitions +interface ThinkingConfig { + type: 'enabled'; + budget_tokens: number; +} + +interface ExtraBodyConfig { + thinking?: ThinkingConfig; +} + +interface ToolUseInput extends Record { + action: Action; +} + +export async function samplingLoop({ + model, + systemPromptSuffix, + messages, + apiKey, + onlyNMostRecentImages, + maxTokens = 4096, + toolVersion, + thinkingBudget, + tokenEfficientToolsBeta = false, + kernel, + sessionId, +}: { + model: string; + systemPromptSuffix?: string; + messages: BetaMessageParam[]; + apiKey: string; + onlyNMostRecentImages?: number; + maxTokens?: number; + toolVersion?: ToolVersion; + thinkingBudget?: number; + tokenEfficientToolsBeta?: boolean; + kernel: Kernel; + sessionId: string; +}): Promise { + const selectedVersion = toolVersion || DEFAULT_TOOL_VERSION; + const toolGroup = TOOL_GROUPS_BY_VERSION[selectedVersion]; + const toolCollection = new ToolCollection(...toolGroup.tools.map((Tool: typeof ComputerTool20241022 | typeof ComputerTool20250124) => new Tool(kernel, sessionId))); + + const system: BetaTextBlock = { + type: 'text', + text: `${SYSTEM_PROMPT}${systemPromptSuffix ? ' ' + systemPromptSuffix : ''}`, + }; + + while (true) { + const betas: string[] = toolGroup.beta_flag ? [toolGroup.beta_flag] : []; + + if (tokenEfficientToolsBeta) { + betas.push('token-efficient-tools-2025-02-19'); + } + + let imageTruncationThreshold = onlyNMostRecentImages || 0; + + const client = new Anthropic({ apiKey, maxRetries: 4 }); + const enablePromptCaching = true; + + if (enablePromptCaching) { + betas.push(PROMPT_CACHING_BETA_FLAG); + injectPromptCaching(messages); + onlyNMostRecentImages = 0; + (system as BetaTextBlock).cache_control = { type: 'ephemeral' }; + } + + if (onlyNMostRecentImages) { + maybeFilterToNMostRecentImages( + messages, + onlyNMostRecentImages, + imageTruncationThreshold + ); + } + + const extraBody: ExtraBodyConfig = {}; + if (thinkingBudget) { + extraBody.thinking = { type: 'enabled', budget_tokens: thinkingBudget }; + } + + const toolParams = toolCollection.toParams(); + + const response = await client.beta.messages.create({ + max_tokens: maxTokens, + messages, + model, + system: [system], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + tools: toolParams as any, + betas, + ...extraBody, + }); + + const responseParams = responseToParams(response); + + const loggableContent = responseParams.map(block => { + if (block.type === 'tool_use') { + return { + type: 'tool_use', + name: block.name, + input: block.input + }; + } + return block; + }); + console.log('=== LLM RESPONSE ==='); + console.log('Stop reason:', response.stop_reason); + console.log(loggableContent); + console.log("===") + + messages.push({ + role: 'assistant', + content: responseParams, + }); + + if (response.stop_reason === 'end_turn') { + console.log('LLM has completed its task, ending loop'); + return messages; + } + + const toolResultContent = []; + let hasToolUse = false; + + for (const contentBlock of responseParams) { + if (contentBlock.type === 'tool_use' && contentBlock.name && contentBlock.input && typeof contentBlock.input === 'object') { + const input = contentBlock.input as ToolUseInput; + if ('action' in input && typeof input.action === 'string') { + hasToolUse = true; + const toolInput: ActionParams = { + action: input.action as Action, + ...Object.fromEntries( + Object.entries(input).filter(([key]) => key !== 'action') + ) + }; + + try { + const result = await toolCollection.run( + contentBlock.name, + toolInput + ); + + const toolResult = makeApiToolResult(result, contentBlock.id!); + toolResultContent.push(toolResult); + } catch (error) { + console.error(error); + throw error; + } + } + } + } + + if (toolResultContent.length === 0 && !hasToolUse && response.stop_reason !== 'tool_use') { + console.log('No tool use or results, and not waiting for tool use, ending loop'); + return messages; + } + + if (toolResultContent.length > 0) { + messages.push({ + role: 'user', + content: toolResultContent, + }); + } + } +} diff --git a/pkg/templates/typescript/regulatory-filling-monitor/session.ts b/pkg/templates/typescript/regulatory-filling-monitor/session.ts new file mode 100644 index 0000000..06e30a6 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/session.ts @@ -0,0 +1,222 @@ +/** + * Kernel Browser Session Manager. + * + * Provides a class for managing Kernel browser lifecycle + * with optional video replay recording. + */ + +import type { Kernel } from '@onkernel/sdk'; + +export interface SessionOptions { + /** Enable stealth mode to avoid bot detection */ + stealth?: boolean; + /** Browser session timeout in seconds */ + timeoutSeconds?: number; + /** Enable replay recording (requires paid plan) */ + recordReplay?: boolean; + /** Grace period in seconds before stopping replay */ + replayGracePeriod?: number; +} + +export interface SessionInfo { + sessionId: string; + liveViewUrl: string; + replayId?: string; + replayViewUrl?: string; +} + +const DEFAULT_OPTIONS: Required = { + stealth: true, + timeoutSeconds: 300, + recordReplay: false, + replayGracePeriod: 5.0, +}; + +/** + * Manages Kernel browser lifecycle with optional replay recording. + * + * Usage: + * ```typescript + * const session = new KernelBrowserSession(kernel, options); + * await session.start(); + * try { + * // Use session.sessionId for computer controls + * } finally { + * await session.stop(); + * } + * ``` + */ +export class KernelBrowserSession { + private kernel: Kernel; + private options: Required; + + // Session state + private _sessionId: string | null = null; + private _liveViewUrl: string | null = null; + private _replayId: string | null = null; + private _replayViewUrl: string | null = null; + + constructor(kernel: Kernel, options: SessionOptions = {}) { + this.kernel = kernel; + this.options = { ...DEFAULT_OPTIONS, ...options }; + } + + get sessionId(): string { + if (!this._sessionId) { + throw new Error('Session not started. Call start() first.'); + } + return this._sessionId; + } + + get liveViewUrl(): string | null { + return this._liveViewUrl; + } + + get replayViewUrl(): string | null { + return this._replayViewUrl; + } + + get info(): SessionInfo { + return { + sessionId: this.sessionId, + liveViewUrl: this._liveViewUrl || '', + replayId: this._replayId || undefined, + replayViewUrl: this._replayViewUrl || undefined, + }; + } + + /** + * Create a Kernel browser session and optionally start recording. + */ + async start(): Promise { + // Create browser with specified settings + const browser = await this.kernel.browsers.create({ + stealth: this.options.stealth, + timeout_seconds: this.options.timeoutSeconds, + viewport: { + width: 1024, + height: 768, + refresh_rate: 60, + }, + }); + + this._sessionId = browser.session_id; + this._liveViewUrl = browser.browser_live_view_url; + + console.log(`Kernel browser created: ${this._sessionId}`); + console.log(`Live view URL: ${this._liveViewUrl}`); + + // Start replay recording if enabled + if (this.options.recordReplay) { + try { + await this.startReplay(); + } catch (error) { + console.warn(`Warning: Failed to start replay recording: ${error}`); + console.warn('Continuing without replay recording.'); + } + } + + return this.info; + } + + /** + * Start recording a replay of the browser session. + */ + private async startReplay(): Promise { + if (!this._sessionId) { + return; + } + + console.log('Starting replay recording...'); + const replay = await this.kernel.browsers.replays.start(this._sessionId); + this._replayId = replay.replay_id; + console.log(`Replay recording started: ${this._replayId}`); + } + + /** + * Stop recording and get the replay URL. + */ + private async stopReplay(): Promise { + if (!this._sessionId || !this._replayId) { + return; + } + + console.log('Stopping replay recording...'); + await this.kernel.browsers.replays.stop(this._replayId, { + id: this._sessionId, + }); + console.log('Replay recording stopped. Processing video...'); + + // Wait a moment for processing + await this.sleep(2000); + + // Poll for replay to be ready (with timeout) + const maxWait = 60000; // 60 seconds + const startTime = Date.now(); + let replayReady = false; + + while (Date.now() - startTime < maxWait) { + try { + const replays = await this.kernel.browsers.replays.list(this._sessionId); + for (const replay of replays) { + if (replay.replay_id === this._replayId) { + this._replayViewUrl = replay.replay_view_url; + replayReady = true; + break; + } + } + if (replayReady) { + break; + } + } catch { + // Ignore errors while polling + } + await this.sleep(1000); + } + + if (!replayReady) { + console.log('Warning: Replay may still be processing'); + } else if (this._replayViewUrl) { + console.log(`Replay view URL: ${this._replayViewUrl}`); + } + } + + /** + * Stop recording, and delete the browser session. + */ + async stop(): Promise { + const info = this.info; + + if (this._sessionId) { + try { + // Stop replay if recording was enabled + if (this.options.recordReplay && this._replayId) { + // Wait grace period before stopping to capture final state + if (this.options.replayGracePeriod > 0) { + console.log(`Waiting ${this.options.replayGracePeriod}s grace period...`); + await this.sleep(this.options.replayGracePeriod * 1000); + } + await this.stopReplay(); + info.replayViewUrl = this._replayViewUrl || undefined; + } + } finally { + // Always clean up the browser session, even if replay stopping fails + console.log(`Destroying browser session: ${this._sessionId}`); + await this.kernel.browsers.deleteByID(this._sessionId); + console.log('Browser session destroyed.'); + } + } + + // Reset state + this._sessionId = null; + this._liveViewUrl = null; + this._replayId = null; + this._replayViewUrl = null; + + return info; + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} From 021b659c889fe54512b928c2bae286e8863cf869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Wed, 28 Jan 2026 17:21:03 -0300 Subject: [PATCH 5/7] feat(templates): add SEC EDGAR filling monitor main application --- .../regulatory-filling-monitor/index.ts | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/index.ts diff --git a/pkg/templates/typescript/regulatory-filling-monitor/index.ts b/pkg/templates/typescript/regulatory-filling-monitor/index.ts new file mode 100644 index 0000000..f40783b --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/index.ts @@ -0,0 +1,284 @@ +import { Kernel, type KernelContext } from '@onkernel/sdk'; +import { samplingLoop } from './loop'; +import { KernelBrowserSession } from './session'; + +// ============================================================================ +// Types +// ============================================================================ + +interface SecEdgarInput { + company: string; + state?: string; + date?: string; + record_replay?: boolean; +} + +interface SecEdgarFiling { + formAndFile: string; + filedDate: string; + filingEntityPerson: string; + cik: string; + located: string; + fileNumber: string; + filmNumber: string; +} + +interface SecEdgarOutput { + filings: SecEdgarFiling[]; + replay_url?: string; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const SEC_EDGAR_SEARCH_URL = 'https://www.sec.gov/edgar/search/'; + +const FILINGS_DATA_START_MARKER = 'FILINGS_DATA:'; +const FILINGS_DATA_END_MARKER = 'END_FILINGS_DATA'; + +// ============================================================================ +// Environment +// ============================================================================ + +const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; + +if (!ANTHROPIC_API_KEY) { + throw new Error('ANTHROPIC_API_KEY is not set'); +} + +// ============================================================================ +// Prompt Builder +// ============================================================================ + +function buildTaskPrompt(company: string, state: string, date: string): string { + const stateStep = state + ? `6. Find the "Principal executive offices in" dropdown and select the state "${state}" (or its full U.S. state name)` + : '6. Skip state filtering if not needed'; + + const taskDescription = [ + `search for SEC filings for company "${company}"`, + state ? `located in ${state}` : null, + date ? `filed on ${date}` : null, + ].filter(Boolean).join(' '); + + return `You are searching the SEC EDGAR database for regulatory filings. + +TASK: Navigate to ${SEC_EDGAR_SEARCH_URL} and ${taskDescription}. + +STEPS: +1. First, use ctrl+l to focus the URL bar, then navigate to the SEC EDGAR search page: ${SEC_EDGAR_SEARCH_URL} +2. Wait for the page to load completely +3. In the main search field (labeled "Company name, ticker, or CIK number" or similar), type: ${company} +4. Click on "Show more search options" or similar to expand the search filters +5. Set the "Filed date range" to "Custom" and enter both the from and to dates as ${date} +${stateStep} +7. Click the Search button to execute the search +8. Wait for results to load +9. Before extracting data, click on "Edit columns" or the column settings button to customize visible columns. Make sure to enable/select these columns: "CIK", "File number", "Film number". Apply the changes. +10. Extract ALL filing results from the results table. For each filing, capture: + - formAndFile: The form type and file description (e.g., "10-K", "8-K", "4", "DEF 14A") + - filedDate: The date filed (YYYY-MM-DD format) + - filingEntityPerson: The company or person who filed + - cik: The CIK number + - located: The state/office location + - fileNumber: The file number + - filmNumber: The film number + +IMPORTANT: +- If a popup, survey, or feedback modal appears, close it by clicking the X button or "No thanks" before continuing +- After the search completes, note the TOTAL NUMBER OF RESULTS shown (e.g., "96 search results"). Use this count to verify you have extracted ALL filings. +- Before extracting data, zoom out the page to see more results at once. Press ctrl+minus once, then press ctrl+minus again, then press ctrl+minus a third time (each as a separate key action). This makes it easier to capture all entries. +- ALWAYS use the scroll action (with scroll_direction and scroll_amount) to navigate through results. DO NOT use Page_Down, Page_Up, Home, End, or arrow keys for scrolling - these jump too far and will cause you to miss entries. +- ALWAYS set scroll_amount: 1 for each scroll action to move through the table one row at a time. Never use scroll_amount > 1. +- Keep track of how many filings you have extracted vs. the total count shown +- If no results are found, report that clearly + +OUTPUT FORMAT (VERY IMPORTANT - follow exactly): +When you have finished extracting all the data, output the results as a valid JSON array. +Start with ${FILINGS_DATA_START_MARKER} on its own line, then output a JSON array of objects, then ${FILINGS_DATA_END_MARKER} on its own line. + +${FILINGS_DATA_START_MARKER} +[ + {"formAndFile": "10-K", "filedDate": "2025-01-15", "filingEntityPerson": "Company Name Inc", "cik": "0001234567", "located": "CA", "fileNumber": "001-12345", "filmNumber": "25123456"}, + {"formAndFile": "8-K", "filedDate": "2025-01-15", "filingEntityPerson": "Another Company", "cik": "0009876543", "located": "NY", "fileNumber": "001-98765", "filmNumber": "25654321"} +] +${FILINGS_DATA_END_MARKER} + +Rules for the JSON: +- Output must be a valid JSON array (starts with [ and ends with ]) +- Each filing is a JSON object with these exact keys: formAndFile, filedDate, filingEntityPerson, cik, located, fileNumber, filmNumber +- Use empty string "" for missing values, not null +- Dates must be in YYYY-MM-DD format +- Make sure to include ALL filings (check against the total count shown) +- ALWAYS include ${FILINGS_DATA_END_MARKER} after the JSON array + +After the JSON, provide a brief summary of what you found.`; +} + +// ============================================================================ +// Parsing Utilities +// ============================================================================ + +function isValidFiling(filing: unknown): filing is SecEdgarFiling { + if (typeof filing !== 'object' || filing === null) return false; + const f = filing as Record; + return typeof f.formAndFile === 'string' && typeof f.filedDate === 'string'; +} + +function parseFilingsFromMarkers(content: string): SecEdgarFiling[] | null { + const startIndex = content.indexOf(FILINGS_DATA_START_MARKER); + const endIndex = content.indexOf(FILINGS_DATA_END_MARKER); + + if (startIndex === -1 || endIndex === -1) return null; + + const jsonSection = content.slice(startIndex + FILINGS_DATA_START_MARKER.length, endIndex).trim(); + + // Try parsing as JSON array + try { + const parsed = JSON.parse(jsonSection); + if (Array.isArray(parsed)) { + const validFilings = parsed.filter(isValidFiling); + console.log(`Parsed ${validFilings.length} filings from JSON array`); + return validFilings; + } + } catch { + console.log('Failed to parse as JSON array, trying line-by-line'); + } + + // Fallback: parse line by line + const filings: SecEdgarFiling[] = []; + for (const line of jsonSection.split('\n')) { + const trimmedLine = line.trim().replace(/,$/, ''); + if (trimmedLine.startsWith('{') && trimmedLine.endsWith('}')) { + try { + const filing = JSON.parse(trimmedLine); + if (isValidFiling(filing)) { + filings.push(filing); + } + } catch { + // Skip malformed lines + } + } + } + + if (filings.length > 0) { + console.log(`Parsed ${filings.length} filings from line-by-line`); + return filings; + } + + return null; +} + +function parseFilingsWithRegex(content: string): SecEdgarFiling[] { + const jsonObjectRegex = /\{[^{}]*"formAndFile"[^{}]*"filedDate"[^{}]*\}/g; + const matches = content.match(jsonObjectRegex); + + if (!matches || matches.length === 0) { + console.log('No filings found in result'); + return []; + } + + const filings: SecEdgarFiling[] = []; + for (const match of matches) { + try { + const filing = JSON.parse(match); + if (isValidFiling(filing)) { + filings.push(filing); + } + } catch { + // Skip malformed objects + } + } + + console.log(`Parsed ${filings.length} filings using regex fallback`); + return filings; +} + +function parseFilingsFromResult(result: string): SecEdgarFiling[] { + return parseFilingsFromMarkers(result) ?? parseFilingsWithRegex(result); +} + +function extractTextFromMessages(messages: { content: string | { type: string; text?: string }[] }[]): string { + const lastMessage = messages[messages.length - 1]; + if (!lastMessage) { + throw new Error('Failed to get the last message from the sampling loop'); + } + + return typeof lastMessage.content === 'string' + ? lastMessage.content + : lastMessage.content + .map(block => (block.type === 'text' ? block.text : '')) + .join(''); +} + +// ============================================================================ +// Kernel App +// ============================================================================ + +const kernel = new Kernel(); +const app = kernel.app('ts-regulatory-filling-monitor'); + +// How to invoke with Kernel: +// kernel deploy index.ts --env-file .env +// kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "Apple"}' +// kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "AAPL", "state": "CA"}' +// kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "Tesla", "date": "2025-01-15"}' +// +// Parameters: +// - company (required): company name, ticker symbol, or CIK number +// - state (optional): state/office to filter, e.g. "CA", "NY" +// - date (optional): YYYY-MM-DD; defaults to today if omitted +// - record_replay (optional): true to record a video replay of the browser session + +app.action( + 'sec-edgar-task', + async (ctx: KernelContext, payload?: SecEdgarInput): Promise => { + if (!payload?.company) { + throw new Error('company is required'); + } + + const company = payload.company; + const state = payload.state ?? ''; + const date = payload.date ?? new Date().toLocaleDateString('en-CA'); + + // Create browser session with optional replay recording + const session = new KernelBrowserSession(kernel, { + stealth: true, + recordReplay: payload.record_replay ?? false, + }); + + await session.start(); + console.log('Kernel browser live view url:', session.liveViewUrl); + + try { + const taskPrompt = buildTaskPrompt(company, state, date); + + const finalMessages = await samplingLoop({ + model: 'claude-sonnet-4-5-20250929', + messages: [{ role: 'user', content: taskPrompt }], + apiKey: ANTHROPIC_API_KEY, + thinkingBudget: 1024, + kernel, + sessionId: session.sessionId, + }); + + if (finalMessages.length === 0) { + throw new Error('No messages were generated during the sampling loop'); + } + + const result = extractTextFromMessages(finalMessages); + const filings = parseFilingsFromResult(result); + const sessionInfo = await session.stop(); + + return { + filings, + replay_url: sessionInfo.replayViewUrl, + }; + } catch (error) { + console.error('Error in SEC EDGAR task:', error); + await session.stop(); + throw error; + } + }, +); From 6a80dd0bc8322e21a2b8cbf9c8e11aabbc47c0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Wed, 28 Jan 2026 17:22:00 -0300 Subject: [PATCH 6/7] docs(templates): add README for regulatory-filling-monitor template --- .../regulatory-filling-monitor/README.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 pkg/templates/typescript/regulatory-filling-monitor/README.md diff --git a/pkg/templates/typescript/regulatory-filling-monitor/README.md b/pkg/templates/typescript/regulatory-filling-monitor/README.md new file mode 100644 index 0000000..e8dc468 --- /dev/null +++ b/pkg/templates/typescript/regulatory-filling-monitor/README.md @@ -0,0 +1,113 @@ +# Kernel TypeScript Template - Regulatory Filing Monitor + +This is a Kernel application that monitors SEC EDGAR regulatory filings using Anthropic Computer Use with Kernel's Computer Controls API. + +The application navigates to the SEC EDGAR search page, applies filters for company, state, and date, and extracts detailed filing information from the results. + +## Setup + +1. Get your API keys: + - **Kernel**: [dashboard.onkernel.com](https://dashboard.onkernel.com) + - **Anthropic**: [console.anthropic.com](https://console.anthropic.com) + +2. Deploy the app: +```bash +kernel login +cp .env.example .env # Add your ANTHROPIC_API_KEY +kernel deploy index.ts --env-file .env +``` + +## Usage + +Search for SEC filings by company name, ticker, or CIK: + +```bash +# Search for Apple filings (defaults to today's date) +kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "Apple"}' + +# Search by ticker symbol with state filter +kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "AAPL", "state": "CA"}' + +# Search for Tesla filings on a specific date +kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "Tesla", "date": "2025-01-15"}' + +# Search with replay recording enabled +kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "MSFT", "record_replay": true}' +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `company` | string | Yes | Company name, ticker symbol, or CIK number | +| `state` | string | No | U.S. state abbreviation (e.g., "CA", "NY", "TX") | +| `date` | string | No | Date in YYYY-MM-DD format. Defaults to today. | +| `record_replay` | boolean | No | Set to `true` to record a video replay of the browser session. | + +### Response + +The response includes: +- `filings`: Array of extracted filing objects with: + - `formAndFile`: Filing/form type (e.g., 10-K, 8-K, 4, DEF 14A) + - `filedDate`: Date filed (YYYY-MM-DD format) + - `filingEntityPerson`: Company or person who filed + - `cik`: CIK number + - `located`: State/office location + - `fileNumber`: SEC file number + - `filmNumber`: Film number +- `replay_url`: URL to view the recorded session (if `record_replay` was enabled) + +Example response: +```json +{ + "filings": [ + { + "formAndFile": "144", + "filedDate": "2025-10-02", + "filingEntityPerson": "Apple Inc. (AAPL) COOK TIMOTHY D", + "cik": "0000320193", + "located": "Cupertino, CA", + "fileNumber": "001-36743", + "filmNumber": "251234567" + } + ], + "replay_url": "https://..." +} +``` + +## Recording Replays + +> **Note:** Replay recording is only available to Kernel users on paid plans. + +Add `"record_replay": true` to your payload to capture a video of the browser session: + +```bash +kernel invoke ts-regulatory-filling-monitor sec-edgar-task --payload '{"company": "Apple", "record_replay": true}' +``` + +## How It Works + +This application uses Anthropic's Computer Use capability to visually interact with the SEC EDGAR website: + +1. **Browser Session**: Creates a Kernel browser session with stealth mode enabled +2. **Visual Navigation**: Uses Anthropic Claude to visually navigate the SEC EDGAR search interface +3. **Search Filters**: Applies company name, state, and date filters +4. **Column Configuration**: Enables additional columns (CIK, File number, Film number) for complete data extraction +5. **Data Extraction**: Extracts filing information from the search results table +6. **Structured Output**: Parses the extracted data into a structured JSON format + +## Known Limitations + +### Cursor Position + +The `cursor_position` action is not supported with Kernel's Computer Controls API. This does not significantly impact the workflow as the model tracks cursor position through screenshots. + +### Dynamic Content + +SEC EDGAR may display dynamic content, popups, or surveys. The model is instructed to dismiss these automatically. + +## Resources + +- [Anthropic Computer Use Documentation](https://docs.anthropic.com/en/docs/build-with-claude/computer-use) +- [Kernel Documentation](https://www.kernel.sh/docs/quickstart) +- [SEC EDGAR Search](https://www.sec.gov/edgar/search/) From a8114dd98f106a9d8c98548e6722a79a56e5143b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Brito=20Malta=20Le=C3=A3o?= Date: Thu, 29 Jan 2026 10:21:56 -0300 Subject: [PATCH 7/7] chore: prompt changes to increase agent performance --- .../typescript/regulatory-filling-monitor/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/templates/typescript/regulatory-filling-monitor/index.ts b/pkg/templates/typescript/regulatory-filling-monitor/index.ts index f40783b..66c0409 100644 --- a/pkg/templates/typescript/regulatory-filling-monitor/index.ts +++ b/pkg/templates/typescript/regulatory-filling-monitor/index.ts @@ -71,7 +71,7 @@ STEPS: 2. Wait for the page to load completely 3. In the main search field (labeled "Company name, ticker, or CIK number" or similar), type: ${company} 4. Click on "Show more search options" or similar to expand the search filters -5. Set the "Filed date range" to "Custom" and enter both the from and to dates as ${date} +5. Set the "Filed date range" to "Custom" and enter both the from and to dates as ${date}. Type the date directly into the from and to fields—do NOT use the date picker or dropdown to select a date; typing is much faster. ${stateStep} 7. Click the Search button to execute the search 8. Wait for results to load @@ -86,13 +86,14 @@ ${stateStep} - filmNumber: The film number IMPORTANT: +- For date fields (Filed date range from/to): always type the date directly—do NOT use the date picker or dropdown; typing is much faster. - If a popup, survey, or feedback modal appears, close it by clicking the X button or "No thanks" before continuing - After the search completes, note the TOTAL NUMBER OF RESULTS shown (e.g., "96 search results"). Use this count to verify you have extracted ALL filings. - Before extracting data, zoom out the page to see more results at once. Press ctrl+minus once, then press ctrl+minus again, then press ctrl+minus a third time (each as a separate key action). This makes it easier to capture all entries. - ALWAYS use the scroll action (with scroll_direction and scroll_amount) to navigate through results. DO NOT use Page_Down, Page_Up, Home, End, or arrow keys for scrolling - these jump too far and will cause you to miss entries. - ALWAYS set scroll_amount: 1 for each scroll action to move through the table one row at a time. Never use scroll_amount > 1. - Keep track of how many filings you have extracted vs. the total count shown -- If no results are found, report that clearly +- If the page shows "No results found for your search!" (after you have applied filters and selected columns), do NOT try to extract data. Output an empty array [] in the FILINGS_DATA section and report that no results were found. OUTPUT FORMAT (VERY IMPORTANT - follow exactly): When you have finished extracting all the data, output the results as a valid JSON array. @@ -107,6 +108,7 @@ ${FILINGS_DATA_END_MARKER} Rules for the JSON: - Output must be a valid JSON array (starts with [ and ends with ]) +- If you see "No results found for your search!" on the page (after selecting all fields), output an empty array: [] - Each filing is a JSON object with these exact keys: formAndFile, filedDate, filingEntityPerson, cik, located, fileNumber, filmNumber - Use empty string "" for missing values, not null - Dates must be in YYYY-MM-DD format