[
  {
    "path": ".github/workflows/build-agent.yaml",
    "content": "name: Build Agent\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"packages/bytebot-agent/**\"\n      - \"packages/shared/**\"\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  docker:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: docker/setup-qemu-action@v3\n      - uses: docker/setup-buildx-action@v3\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ghcr.io/bytebot-ai/bytebot-agent\n          tags: type=edge\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        env:\n          BUILDX_NO_DEFAULT_ATTESTATIONS: 1\n          DOCKER_BUILD_SUMMARY: false\n          DOCKER_BUILD_RECORD_UPLOAD: false\n        with:\n          context: ./packages\n          file: ./packages/bytebot-agent/Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/build-desktop.yaml",
    "content": "name: Build Desktop\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"docker/**\"\n      - \"packages/bytebotd/**\"\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  docker:\n    runs-on: ubuntu-22.04\n\n    steps:\n      # 1. Check out code\n      - uses: actions/checkout@v4\n\n      # 2. Enable QEMU so the amd64 runner can cross‑build arm64\n      - uses: docker/setup-qemu-action@v3\n\n      # 3. Set up Buildx builder\n      - uses: docker/setup-buildx-action@v3\n\n      # 4. Generate OCI labels + the single \"edge\" tag\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ghcr.io/bytebot-ai/bytebot-desktop\n          tags: type=edge\n\n      # 5. Log in to GHCR\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      # 6. Build & push a multi‑arch image\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        env:\n          BUILDX_NO_DEFAULT_ATTESTATIONS: 1 # hide \"unknown/unknown\" in GHCR\n          DOCKER_BUILD_SUMMARY: false # keep logs concise\n          DOCKER_BUILD_RECORD_UPLOAD: false\n        with:\n          context: ./packages/\n          file: ./packages/bytebotd/Dockerfile\n          platforms: linux/amd64,linux/arm64 # build both archs in one go\n          push: true\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/build-ui.yaml",
    "content": "name: Build UI\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - \"packages/bytebot-ui/**\"\n      - \"packages/shared/**\"\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  docker:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: docker/setup-qemu-action@v3\n      - uses: docker/setup-buildx-action@v3\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ghcr.io/bytebot-ai/bytebot-ui\n          tags: type=edge\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        env:\n          BUILDX_NO_DEFAULT_ATTESTATIONS: 1\n          DOCKER_BUILD_SUMMARY: false\n          DOCKER_BUILD_RECORD_UPLOAD: false\n        with:\n          context: ./packages\n          file: ./packages/bytebot-ui/Dockerfile\n          platforms: linux/amd64,linux/arm64\n\n          push: true\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n\n*.qcow2\n*.iso\n*.img\n*.vdi\n*.vmdk\n*.vhdx\n*.vhd\n\n\n# compiled output\nagent/dist\nagent/node_modules\nagent/build\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\nagent/coverage\nagent/.nyc_output\n\n# IDEs and editors\nagent/.idea\nagent/.project\nagent/.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# dotenv environment variable files\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# temp directory\n.temp\n.tmp\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# QEMU\n*.qcow2\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Ignore formatting in docs folder\n/docs/**\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<img src=\"docs/images/bytebot-logo.png\" width=\"500\" alt=\"Bytebot Logo\">\n\n# Bytebot: Open-Source AI Desktop Agent\n\n<a href=\"https://trendshift.io/repositories/14624\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/14624\" alt=\"bytebot-ai%2Fbytebot | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**An AI that has its own computer to complete tasks for you**\n\n[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)\n\n[![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://github.com/bytebot-ai/bytebot/tree/main/docker)\n[![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)\n[![Discord](https://img.shields.io/discord/1232768900274585720?color=7289da&label=discord)](https://discord.com/invite/d9ewZkWPTP)\n\n[🌐 Website](https://bytebot.ai) • [📚 Documentation](https://docs.bytebot.ai) • [💬 Discord](https://discord.com/invite/d9ewZkWPTP) • [𝕏 Twitter](https://x.com/bytebot_ai)\n\n<!-- Keep these links. Translations will automatically update with the README. -->\n[Deutsch](https://zdoc.app/de/bytebot-ai/bytebot) | \n[Español](https://zdoc.app/es/bytebot-ai/bytebot) | \n[français](https://zdoc.app/fr/bytebot-ai/bytebot) | \n[日本語](https://zdoc.app/ja/bytebot-ai/bytebot) | \n[한국어](https://zdoc.app/ko/bytebot-ai/bytebot) | \n[Português](https://zdoc.app/pt/bytebot-ai/bytebot) | \n[Русский](https://zdoc.app/ru/bytebot-ai/bytebot) | \n[中文](https://zdoc.app/zh/bytebot-ai/bytebot)\n</div>\n\n---\n\nhttps://github.com/user-attachments/assets/f271282a-27a3-43f3-9b99-b34007fdd169\n\nhttps://github.com/user-attachments/assets/72a43cf2-bd87-44c5-a582-e7cbe176f37f\n\n## What is a Desktop Agent?\n\nA desktop agent is an AI that has its own computer. Unlike browser-only agents or traditional RPA tools, Bytebot comes with a full virtual desktop where it can:\n\n- Use any application (browsers, email clients, office tools, IDEs)\n- Download and organize files with its own file system\n- Log into websites and applications using password managers\n- Read and process documents, PDFs, and spreadsheets\n- Complete complex multi-step workflows across different programs\n\nThink of it as a virtual employee with their own computer who can see the screen, move the mouse, type on the keyboard, and complete tasks just like a human would.\n\n## Why Give AI Its Own Computer?\n\nWhen AI has access to a complete desktop environment, it unlocks capabilities that aren't possible with browser-only agents or API integrations:\n\n### Complete Task Autonomy\n\nGive Bytebot a task like \"Download all invoices from our vendor portals and organize them into a folder\" and it will:\n\n- Open the browser\n- Navigate to each portal\n- Handle authentication (including 2FA via password managers)\n- Download the files to its local file system\n- Organize them into a folder\n\n### Process Documents\n\nUpload files directly to Bytebot's desktop and it can:\n\n- Read entire PDFs into its context\n- Extract data from complex documents\n- Cross-reference information across multiple files\n- Create new documents based on analysis\n- Handle formats that APIs can't access\n\n### Use Real Applications\n\nBytebot isn't limited to web interfaces. It can:\n\n- Use desktop applications like text editors, VS Code, or email clients\n- Run scripts and command-line tools\n- Install new software as needed\n- Configure applications for specific workflows\n\n## Quick Start\n\n### Deploy in 2 Minutes\n\n**Option 1: Railway (Easiest)**\n[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)\n\nJust click and add your AI provider API key.\n\n**Option 2: Docker Compose**\n\n```bash\ngit clone https://github.com/bytebot-ai/bytebot.git\ncd bytebot\n\n# Add your AI provider key (choose one)\necho \"ANTHROPIC_API_KEY=sk-ant-...\" > docker/.env\n# Or: echo \"OPENAI_API_KEY=sk-...\" > docker/.env\n# Or: echo \"GEMINI_API_KEY=...\" > docker/.env\n\ndocker-compose -f docker/docker-compose.yml up -d\n\n# Open http://localhost:9992\n```\n\n[Full deployment guide →](https://docs.bytebot.ai/quickstart)\n\n## How It Works\n\nBytebot consists of four integrated components:\n\n1. **Virtual Desktop**: A complete Ubuntu Linux environment with pre-installed applications\n2. **AI Agent**: Understands your tasks and controls the desktop to complete them\n3. **Task Interface**: Web UI where you create tasks and watch Bytebot work\n4. **APIs**: REST endpoints for programmatic task creation and desktop control\n\n### Key Features\n\n- **Natural Language Tasks**: Just describe what you need done\n- **File Uploads**: Drop files onto tasks for Bytebot to process\n- **Live Desktop View**: Watch Bytebot work in real-time\n- **Takeover Mode**: Take control when you need to help or configure something\n- **Password Manager Support**: Install 1Password, Bitwarden, etc. for automatic authentication\n- **Persistent Environment**: Install programs and they stay available for future tasks\n\n## Example Tasks\n\n### Basic Examples\n\n```\n\"Go to Wikipedia and create a summary of quantum computing\"\n\"Research flights from NYC to London and create a comparison document\"\n\"Take screenshots of the top 5 news websites\"\n```\n\n### Document Processing\n\n```\n\"Read the uploaded contracts.pdf and extract all payment terms and deadlines\"\n\"Process these 5 invoice PDFs and create a summary report\"\n\"Download and analyze the latest financial report and answer: What were the key risks mentioned?\"\n```\n\n### Multi-Application Workflows\n\n```\n\"Download last month's bank statements from our three banks and consolidate them\"\n\"Check all our vendor portals for new invoices and create a summary report\"\n\"Log into our CRM, export the customer list, and update records in the ERP system\"\n```\n\n## Programmatic Control\n\n### Create Tasks via API\n\n```python\nimport requests\n\n# Simple task\nresponse = requests.post('http://localhost:9991/tasks', json={\n    'description': 'Download the latest sales report and create a summary'\n})\n\n# Task with file upload\nfiles = {'files': open('contracts.pdf', 'rb')}\nresponse = requests.post('http://localhost:9991/tasks',\n    data={'description': 'Review these contracts for important dates'},\n    files=files\n)\n```\n\n### Direct Desktop Control\n\n```bash\n# Take a screenshot\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"screenshot\"}'\n\n# Click at specific coordinates\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"click_mouse\", \"coordinate\": [500, 300]}'\n```\n\n[Full API documentation →](https://docs.bytebot.ai/api-reference/introduction)\n\n## Setting Up Your Desktop Agent\n\n### 1. Deploy Bytebot\n\nUse one of the deployment methods above to get Bytebot running.\n\n### 2. Configure the Desktop\n\nUse the Desktop tab in the UI to:\n\n- Install additional programs you need\n- Set up password managers for authentication\n- Configure applications with your preferences\n- Log into websites you want Bytebot to access\n\n### 3. Start Giving Tasks\n\nCreate tasks in natural language and watch Bytebot complete them using the configured desktop.\n\n## Use Cases\n\n### Business Process Automation\n\n- Invoice processing and data extraction\n- Multi-system data synchronization\n- Report generation from multiple sources\n- Compliance checking across platforms\n\n### Development & Testing\n\n- Automated UI testing\n- Cross-browser compatibility checks\n- Documentation generation with screenshots\n- Code deployment verification\n\n### Research & Analysis\n\n- Competitive analysis across websites\n- Data gathering from multiple sources\n- Document analysis and summarization\n- Market research compilation\n\n## Architecture\n\nBytebot is built with:\n\n- **Desktop**: Ubuntu 22.04 with XFCE, Firefox, VS Code, and other tools\n- **Agent**: NestJS service that coordinates AI and desktop actions\n- **UI**: Next.js application for task management\n- **AI Support**: Works with Anthropic Claude, OpenAI GPT, Google Gemini\n- **Deployment**: Docker containers for easy self-hosting\n\n## Why Self-Host?\n\n- **Data Privacy**: Everything runs on your infrastructure\n- **Full Control**: Customize the desktop environment as needed\n- **No Limits**: Use your own AI API keys without platform restrictions\n- **Flexibility**: Install any software, access any systems\n\n## Advanced Features\n\n### Multiple AI Providers\n\nUse any AI provider through our [LiteLLM integration](https://docs.bytebot.ai/deployment/litellm):\n\n- Azure OpenAI\n- AWS Bedrock\n- Local models via Ollama\n- 100+ other providers\n\n### Enterprise Deployment\n\nDeploy on Kubernetes with Helm:\n\n```bash\n# Clone the repository\ngit clone https://github.com/bytebot-ai/bytebot.git\ncd bytebot\n\n# Install with Helm\nhelm install bytebot ./helm \\\n  --set agent.env.ANTHROPIC_API_KEY=sk-ant-...\n```\n\n[Enterprise deployment guide →](https://docs.bytebot.ai/deployment/helm)\n\n## Community & Support\n\n- **Discord**: [Join our community](https://discord.com/invite/d9ewZkWPTP) for help and discussions\n- **Documentation**: Comprehensive guides at [docs.bytebot.ai](https://docs.bytebot.ai)\n- **GitHub Issues**: Report bugs and request features\n\n## Contributing\n\nWe welcome contributions! Whether it's:\n\n- 🐛 Bug fixes\n- ✨ New features\n- 📚 Documentation improvements\n- 🌐 Translations\n\nPlease:\n\n1. Check existing [issues](https://github.com/bytebot-ai/bytebot/issues) first\n2. Open an issue to discuss major changes\n3. Submit PRs with clear descriptions\n4. Join our [Discord](https://discord.com/invite/d9ewZkWPTP) to discuss ideas\n\n## License\n\nBytebot is open source under the Apache 2.0 license.\n\n---\n\n<div align=\"center\">\n\n**Give your AI its own computer. See what it can do.**\n\n[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)\n\n<sub>Built by [Tantl Labs](https://tantl.com) and the open source community</sub>\n\n</div>\n"
  },
  {
    "path": "docker/bytebot-desktop.Dockerfile",
    "content": "# Extend the pre-built bytebot-desktop image\nFROM ghcr.io/bytebot-ai/bytebot-desktop:edge\n\n# Add additional packages, applications, or customizations here\n\n# Expose the bytebotd service port\nEXPOSE 9990\n\n# Start the bytebotd service\nCMD [\"/usr/bin/supervisord\", \"-c\", \"/etc/supervisor/conf.d/supervisord.conf\", \"-n\"]\n"
  },
  {
    "path": "docker/docker-compose-claude-code.yml",
    "content": "name: bytebot\n\nservices:\n  bytebot-desktop:\n    # Build from source\n    build:\n      context: ../packages/\n      dockerfile: bytebotd/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-desktop:edge\n    shm_size: \"2g\"\n    container_name: bytebot-desktop\n    restart: unless-stopped\n    hostname: computer\n    privileged: true\n    ports:\n      - \"9990:9990\" # bytebotd service & noVNC\n    environment:\n      - DISPLAY=:0\n    networks:\n      - bytebot-network\n\n  postgres:\n    image: postgres:16-alpine\n    container_name: bytebot-postgres\n    restart: unless-stopped\n    ports:\n      - \"5432:5432\"\n    environment:\n      - POSTGRES_PASSWORD=postgres\n      - POSTGRES_USER=postgres\n      - POSTGRES_DB=bytebotdb\n    networks:\n      - bytebot-network\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\n  bytebot-agent-cc:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-agent-cc/Dockerfile\n    container_name: bytebot-agent-cc\n    restart: unless-stopped\n    ports:\n      - \"9991:9991\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb}\n      - BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990}\n      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}\n    depends_on:\n      - postgres\n    networks:\n      - bytebot-network\n\n  bytebot-ui:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-ui/Dockerfile\n      args:\n        - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent-cc:9991}\n        - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-ui:edge\n    container_name: bytebot-ui\n    restart: unless-stopped\n    ports:\n      - \"9992:9992\"\n    environment:\n      - NODE_ENV=production\n      - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent-cc:9991}\n      - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}\n    depends_on:\n      - bytebot-agent-cc\n    networks:\n      - bytebot-network\n\nnetworks:\n  bytebot-network:\n    driver: bridge\n\nvolumes:\n  postgres_data:\n"
  },
  {
    "path": "docker/docker-compose.core.yml",
    "content": "name: bytebot\n\nservices:\n  bytebot-desktop:\n    # Build from source\n    build:\n      context: ../packages/\n      dockerfile: bytebotd/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-desktop:edge\n    shm_size: \"2g\"\n    container_name: bytebot-desktop\n    restart: unless-stopped\n    hostname: computer\n    privileged: true\n    ports:\n      - \"9990:9990\" # bytebotd service & noVNC\n    environment:\n      - DISPLAY=:0\n"
  },
  {
    "path": "docker/docker-compose.development.yml",
    "content": "## docker-compose file that spins up a bytebot-desktop container\n## and a postgres container. bytebot-ui and bytebot-agent are not included\n## in this file, and can be run separately using npm, allowing for\n## easier local development.\n\nname: bytebot\n\nservices:\n  bytebot-desktop:\n    # Build from source\n    build:\n      context: ../packages/\n      dockerfile: bytebotd/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-desktop:edge\n    shm_size: \"2g\"\n    container_name: bytebot-desktop\n    restart: unless-stopped\n    hostname: computer\n    privileged: true\n    ports:\n      - \"9990:9990\" # bytebotd service & noVNC\n    environment:\n      - DISPLAY=:0\n    networks:\n      - bytebot-network\n\n  postgres:\n    image: postgres:16-alpine\n    container_name: bytebot-postgres\n    restart: unless-stopped\n    ports:\n      - \"5432:5432\"\n    environment:\n      - POSTGRES_PASSWORD=postgres\n      - POSTGRES_USER=postgres\n      - POSTGRES_DB=bytebotdb\n    networks:\n      - bytebot-network\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\nnetworks:\n  bytebot-network:\n    driver: bridge\n\nvolumes:\n  postgres_data:\n"
  },
  {
    "path": "docker/docker-compose.proxy.yml",
    "content": "name: bytebot\n\nservices:\n  bytebot-desktop:\n    # Build from source\n    build:\n      context: ../packages/\n      dockerfile: bytebotd/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-desktop:edge\n    shm_size: \"2g\"\n    container_name: bytebot-desktop\n    restart: unless-stopped\n    hostname: computer\n    privileged: true\n    ports:\n      - \"9990:9990\" # bytebotd service & noVNC\n    environment:\n      - DISPLAY=:0\n    networks:\n      - bytebot-network\n\n  postgres:\n    image: postgres:16-alpine\n    container_name: bytebot-postgres\n    restart: unless-stopped\n    ports:\n      - \"5432:5432\"\n    environment:\n      - POSTGRES_PASSWORD=postgres\n      - POSTGRES_USER=postgres\n      - POSTGRES_DB=bytebotdb\n    networks:\n      - bytebot-network\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\n  bytebot-agent:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-agent/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-agent:edge\n    container_name: bytebot-agent\n    restart: unless-stopped\n    ports:\n      - \"9991:9991\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb}\n      - BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990}\n      - BYTEBOT_LLM_PROXY_URL=${BYTEBOT_LLM_PROXY_URL:-http://bytebot-llm-proxy:4000}\n    depends_on:\n      - postgres\n    networks:\n      - bytebot-network\n\n  bytebot-llm-proxy:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-llm-proxy/Dockerfile\n    ports:\n      - \"4000:4000\"\n    environment:\n      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}\n      - OPENAI_API_KEY=${OPENAI_API_KEY}\n      - GEMINI_API_KEY=${GEMINI_API_KEY}\n    networks:\n      - bytebot-network\n\n  bytebot-ui:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-ui/Dockerfile\n      args:\n        - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}\n        - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-ui:edge\n    container_name: bytebot-ui\n    restart: unless-stopped\n    ports:\n      - \"9992:9992\"\n    environment:\n      - NODE_ENV=production\n      - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}\n      - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}\n    depends_on:\n      - bytebot-agent\n    networks:\n      - bytebot-network\n\nnetworks:\n  bytebot-network:\n    driver: bridge\n\nvolumes:\n  postgres_data:\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "name: bytebot\n\nservices:\n  bytebot-desktop:\n    # Build from source\n    build:\n      context: ../packages/\n      dockerfile: bytebotd/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-desktop:edge\n    shm_size: \"2g\"\n    container_name: bytebot-desktop\n    restart: unless-stopped\n    hostname: computer\n    privileged: true\n    ports:\n      - \"9990:9990\" # bytebotd service & noVNC\n    environment:\n      - DISPLAY=:0\n    networks:\n      - bytebot-network\n\n  postgres:\n    image: postgres:16-alpine\n    container_name: bytebot-postgres\n    restart: unless-stopped\n    ports:\n      - \"5432:5432\"\n    environment:\n      - POSTGRES_PASSWORD=postgres\n      - POSTGRES_USER=postgres\n      - POSTGRES_DB=bytebotdb\n    networks:\n      - bytebot-network\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\n  bytebot-agent:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-agent/Dockerfile\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-agent:edge\n    container_name: bytebot-agent\n    restart: unless-stopped\n    ports:\n      - \"9991:9991\"\n    environment:\n      - DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/bytebotdb}\n      - BYTEBOT_DESKTOP_BASE_URL=${BYTEBOT_DESKTOP_BASE_URL:-http://bytebot-desktop:9990}\n      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}\n      - OPENAI_API_KEY=${OPENAI_API_KEY}\n      - GEMINI_API_KEY=${GEMINI_API_KEY}\n    depends_on:\n      - postgres\n    networks:\n      - bytebot-network\n\n  bytebot-ui:\n    build:\n      context: ../packages/\n      dockerfile: bytebot-ui/Dockerfile\n      args:\n        - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}\n        - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}\n    # Use pre-built image\n    image: ghcr.io/bytebot-ai/bytebot-ui:edge\n    container_name: bytebot-ui\n    restart: unless-stopped\n    ports:\n      - \"9992:9992\"\n    environment:\n      - NODE_ENV=production\n      - BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL:-http://bytebot-agent:9991}\n      - BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL:-http://bytebot-desktop:9990/websockify}\n    depends_on:\n      - bytebot-agent\n    networks:\n      - bytebot-network\n\nnetworks:\n  bytebot-network:\n    driver: bridge\n\nvolumes:\n  postgres_data:\n"
  },
  {
    "path": "docs/api-reference/agent/tasks.mdx",
    "content": "---\ntitle: 'Tasks API'\ndescription: 'Reference documentation for the Bytebot Agent Tasks API'\n---\n\n## Tasks API\n\nThe Tasks API allows you to manage tasks in the Bytebot agent system. It's available at `http://localhost:9991/tasks` when running the full agent setup.\n\n## Task Model\n\n```typescript\n{\n  id: string;\n  description: string; \n  status: 'PENDING' | 'IN_PROGRESS' | 'NEEDS_HELP' | 'NEEDS_REVIEW' | 'COMPLETED' | 'CANCELLED' | 'FAILED';\n  priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';\n  createdAt: string; \n  updatedAt: string; \n}\n```\n\n## Endpoints\n\n### Create Task\n\nCreate a new task for the agent to process.\n\n<Card title=\"POST /tasks\" icon=\"plus\">\n  Create a new task\n</Card>\n\n#### Request Body\n\n```json\n{\n  \"description\": \"This is a description of the task\",\n  \"priority\": \"MEDIUM\" // Optional: LOW, MEDIUM, HIGH, URGENT\n}\n```\n\n#### With File Upload\n\nTo upload files with a task, use `multipart/form-data`:\n\n```bash\ncurl -X POST http://localhost:9991/tasks \\\n  -F \"description=Analyze the uploaded contracts and extract key terms\" \\\n  -F \"priority=HIGH\" \\\n  -F \"files=@contract1.pdf\" \\\n  -F \"files=@contract2.pdf\"\n```\n\nUploaded files are automatically saved to the desktop and can be referenced in the task description.\n\n#### Response\n\n```json\n{\n  \"id\": \"task-123\",\n  \"description\": \"This is a description of the task\",\n  \"status\": \"PENDING\",\n  \"priority\": \"MEDIUM\",\n  \"createdAt\": \"2025-04-14T12:00:00Z\",\n  \"updatedAt\": \"2025-04-14T12:00:00Z\"\n}\n```\n\n### Get All Tasks\n\nRetrieve a list of all tasks.\n\n<Card title=\"GET /tasks\" icon=\"list\">\n  Get all tasks\n</Card>\n\n#### Response\n\n```json\n[\n  {\n    \"id\": \"task-123\",\n    \"description\": \"This is a description of the task\",\n    \"status\": \"PENDING\",\n    \"priority\": \"MEDIUM\",\n    \"createdAt\": \"2025-04-14T12:00:00Z\",\n    \"updatedAt\": \"2025-04-14T12:00:00Z\"\n  },\n  // ...more tasks\n]\n```\n\n### Get In-Progress Task\n\nRetrieve the currently in-progress task, if any.\n\n<Card title=\"GET /tasks/in-progress\" icon=\"play\">\n  Get the currently in-progress task\n</Card>\n\n#### Response\n\n```json\n{\n  \"id\": \"task-123\",\n  \"description\": \"This is a description of the task\",\n  \"status\": \"IN_PROGRESS\",\n  \"priority\": \"MEDIUM\",\n  \"createdAt\": \"2025-04-14T12:00:00Z\",\n  \"updatedAt\": \"2025-04-14T12:00:00Z\"\n}\n```\n\nIf no task is in progress, the response will be `null`.\n\n### Get Task by ID\n\nRetrieve a specific task by its ID.\n\n<Card title=\"GET /tasks/:id\" icon=\"magnifying-glass\">\n  Get a task by ID\n</Card>\n\n#### Response\n\n```json\n{\n  \"id\": \"task-123\",\n  \"description\": \"This is a description of the task\",\n  \"status\": \"PENDING\",\n  \"priority\": \"MEDIUM\",\n  \"createdAt\": \"2025-04-14T12:00:00Z\",\n  \"updatedAt\": \"2025-04-14T12:00:00Z\",\n  \"messages\": [\n    {\n      \"id\": \"msg-456\",\n      \"content\": [\n        {\n          \"type\": \"text\",\n          \"text\": \"This is a message\"\n        }\n      ],\n      \"role\": \"USER\",\n      \"taskId\": \"task-123\",\n      \"createdAt\": \"2025-04-14T12:05:00Z\",\n      \"updatedAt\": \"2025-04-14T12:05:00Z\"\n    }\n    // ...more messages\n  ]\n}\n```\n\n### Update Task\n\nUpdate an existing task.\n\n<Card title=\"PATCH /tasks/:id\" icon=\"pen\">\n  Update a task\n</Card>\n\n#### Request Body\n\n```json\n{\n  \"status\": \"COMPLETED\", \n  \"priority\": \"HIGH\" \n}\n```\n\n#### Response\n\n```json\n{\n  \"id\": \"task-123\",\n  \"description\": \"This is a description of the task\",\n  \"status\": \"COMPLETED\",\n  \"priority\": \"HIGH\",\n  \"createdAt\": \"2025-04-14T12:00:00Z\",\n  \"updatedAt\": \"2025-04-14T12:01:00Z\"\n}\n```\n\n### Delete Task\n\nDelete a task.\n\n<Card title=\"DELETE /tasks/:id\" icon=\"trash\">\n  Delete a task\n</Card>\n\n#### Response\n\nStatus code `204 No Content` with an empty response body.\n\n## Message Content Structure\n\nMessages in the Bytebot agent system use a content block structure compatible with Anthropic's Claude API:\n\n```typescript\ntype MessageContent = MessageContentBlock[];\n\ninterface MessageContentBlock {\n  type: string;  \n  [key: string]: any;  \n}\n\ninterface TextContentBlock {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContentBlock {\n  type: \"image\";\n  source: {\n    type: \"base64\";\n    media_type: string;  \n    data: string;  \n  };\n}\n```\n\n## Error Responses\n\nThe API may return the following error responses:\n\n| Status Code | Description                                |\n|-------------|--------------------------------------------|\n| `400`       | Bad Request - Invalid parameters           |\n| `404`       | Not Found - Resource does not exist        |\n| `500`       | Internal Server Error - Server side error  |\n\nExample error response:\n\n```json\n{\n  \"statusCode\": 404,\n  \"message\": \"Task with ID task-123 not found\",\n  \"error\": \"Not Found\"\n}\n```\n\n## Code Examples\n\n<CodeGroup>\n```javascript JavaScript\nconst axios = require('axios');\n\nasync function createTask(description) {\n  const response = await axios.post('http://localhost:9991/tasks', {\n    description\n  });\n  return response.data;\n}\n\nasync function findInProgressTask() {\n  const response = await axios.get('http://localhost:9991/tasks/in-progress');\n  return response.data;\n}\n\n// Example usage\nasync function main() {\n  // Create a new task\n  const task = await createTask('Compare React, Vue, and Angular for a new project');\n  console.log('Created task:', task);\n  \n  // Get current in-progress task\n  const inProgressTask = await findInProgressTask();\n  console.log('In progress task:', inProgressTask);\n}\n```\n\n```python Python\nimport requests\n\ndef create_task(description):\n    response = requests.post(\n        \"http://localhost:9991/tasks\",\n        json={\n            \"description\": description\n        }\n    )\n    return response.json()\n\ndef find_in_progress_task():\n    response = requests.get(\"http://localhost:9991/tasks/in-progress\")\n    return response.json()\n\n# Example usage\ndef main():\n    # Create a new task\n    task = create_task(\"Compare React, Vue, and Angular for a new project\")\n    print(f\"Created task: {task}\")\n    \n    # Get current in-progress task\n    in_progress_task = find_in_progress_task()\n    print(f\"In progress task: {in_progress_task}\")\n```\n\n```curl cURL\n# Create a new task\ncurl -X POST http://localhost:9991/tasks \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"description\": \"Compare React, Vue, and Angular for a new project\"\n  }'\n\n# Get current in-progress task\ncurl -X GET http://localhost:9991/tasks/in-progress\n```\n</CodeGroup>\n"
  },
  {
    "path": "docs/api-reference/agent/ui.mdx",
    "content": "---\ntitle: 'Task UI'\ndescription: 'Documentation for the Bytebot Task UI'\n---\n\n## Bytebot Task UI\n\nThe Bytebot Task UI provides a web-based interface for interacting with the Bytebot agent system. It combines a action feed with an embedded noVNC viewer, allowing you to watch it perform task on the desktop in real-time.\n\n<img src=\"/static/chat-ui-overview.png\" alt=\"Bytebot Task Detail\" className=\"w-full max-w-4xl\" />\n\n## Accessing the UI\n\nWhen running the full Bytebot agent system, the Task UI is available at:\n\n```\nhttp://localhost:9992\n```\n\n## UI Components\n\n### Task Management Panel\n\nThe task management panel allows you to:\n\n- Create new tasks\n- View existing tasks\n- See task status and priority\n- Select a task to work on\n\n<img src=\"/static/ui-task-management.png\" alt=\"Task Management Panel\" className=\"w-full max-w-4xl\" />\n\n### Task Interface\n\nThe main task interface provides:\n\n- Task history with the agent\n- Support for markdown formatting in messages\n- Automatic scrolling to new messages\n\n### Desktop Viewer\n\nThe embedded noVNC viewer displays:\n\n- Real-time view of the desktop environment\n- Visual feedback of agent actions\n- Option to expand to take over the desktop\n- Connection status indicator\n\n## Features\n\n### Task Creation\n\nTo create a new task:\n\n1. Enter a description for the task\n2. Click \"Start Task\" button (or press Enter)\n\n### Conversation Controls\n\nThe task interface supports:\n\n- Text messages with markdown formatting\n- Viewing image content in messages\n- Displaying tool use actions\n- Showing tool results\n\n### Desktop Interaction\n\nWhile primarily for viewing, the desktop panel allows:\n\n- Taking over the desktop\n- Real-time monitoring of agent actions\n\n## Message Types\n\nThe task interface displays different types of messages based on Bytebot's content block structure:\n\n- **User Messages**: Your instructions and queries\n- **Assistant Messages**: Responses from the agent, which may include:\n  - **Text Content Blocks**: Markdown-formatted text responses\n  - **Image Content Blocks**: Images generated or captured\n  - **Tool Use Content Blocks**: Computer actions being performed\n  - **Tool Result Content Blocks**: Results of computer actions\n\nThe message content structure follows this format:\n\n```typescript\ninterface Message {\n  id: string;\n  content: MessageContentBlock[];\n  role: Role; // \"USER\" or \"ASSISTANT\"\n  createdAt?: string;\n}\n\ninterface MessageContentBlock {\n  type: string;\n  [key: string]: any;\n}\n\ninterface TextContentBlock extends MessageContentBlock {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContentBlock extends MessageContentBlock {\n  type: \"image\";\n  source: {\n    type: \"base64\";\n    media_type: string;\n    data: string;\n  };\n}\n```\n\n## Technical Details\n\nThe Bytebot Task UI is built with:\n\n- **Next.js**: React framework for the frontend\n- **Tailwind CSS**: For styling\n- **ReactMarkdown**: For rendering markdown content\n- **noVNC**: For the embedded desktop viewer\n\n## Troubleshooting\n\n### Connection Issues\n\nIf you experience connection issues:\n\n1. Ensure all Bytebot services are running\n2. Check that ports 9990, 9991, and 9992 are accessible\n3. Try refreshing the browser\n4. Check browser console for error messages\n\n### Desktop Viewer Issues\n\nIf the desktop viewer is not displaying:\n\n1. Ensure the Bytebot container is running\n2. Check that the noVNC service is accessible at port 9990\n\n### Message Display Issues\n\nIf messages are not displaying correctly:\n\n1. Check that the message content is properly formatted\n2. Ensure the agent service is processing task correctly\n3. Check the browser console for any rendering errors\n4. Try refreshing the browser\n"
  },
  {
    "path": "docs/api-reference/computer-use/examples.mdx",
    "content": "---\ntitle: \"Computer Use API Examples\"\ndescription: \"Code examples for common automation scenarios using the Bytebot API\"\n---\n\n## Basic Examples\n\nHere are some practical examples of how to use the Computer Use API in different programming languages.\n\n### Using cURL\n\n<CodeGroup>\n```bash Opening a Web Browser\n# Move to Firefox/Chrome icon in the dock and click it\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"move_mouse\", \"coordinates\": {\"x\": 100, \"y\": 960}}'\n\ncurl -X POST http://localhost:9990/computer-use \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\": \"click_mouse\", \"button\": \"left\", \"clickCount\": 1}'\n\n````\n\n```bash Taking and Saving a Screenshot\n# Take a screenshot\nresponse=$(curl -s -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"screenshot\"}')\n\n# Extract the base64 image data and save to a file\necho $response | jq -r '.data.image' | base64 -d > screenshot.png\n````\n\n```bash Typing and Keyboard Shortcuts\n# Type text in a text editor\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"type_text\", \"text\": \"Hello, this is an automated test!\", \"delay\": 30}'\n\n# Press Ctrl+S to save\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"press_keys\", \"key\": \"s\", \"modifiers\": [\"control\"]}'\n```\n\n</CodeGroup>\n\n### Python Examples\n\n<CodeGroup>\n```python Basic Automation\nimport requests\nimport json\nimport base64\nimport time\nfrom io import BytesIO\nfrom PIL import Image\n\ndef control_computer(action, **params):\nurl = \"http://localhost:9990/computer-use\"\ndata = {\"action\": action, **params}\nresponse = requests.post(url, json=data)\nreturn response.json()\n\n# Open a web browser by clicking an icon\n\ncontrol_computer(\"move_mouse\", coordinates={\"x\": 100, \"y\": 960})\ncontrol_computer(\"click_mouse\", button=\"left\")\n\n# Wait for the browser to open\n\ncontrol_computer(\"wait\", duration=2000)\n\n# Type a URL\n\ncontrol_computer(\"type_text\", text=\"https://example.com\")\ncontrol_computer(\"press_keys\", key=\"enter\")\n\n````\n\n```python Screenshot and Analysis\nimport requests\nimport json\nimport base64\nimport cv2\nimport numpy as np\nfrom PIL import Image\nfrom io import BytesIO\n\ndef take_screenshot():\n    url = \"http://localhost:9990/computer-use\"\n    data = {\"action\": \"screenshot\"}\n    response = requests.post(url, json=data)\n\n    if response.json()[\"success\"]:\n        img_data = base64.b64decode(response.json()[\"data\"][\"image\"])\n        image = Image.open(BytesIO(img_data))\n        return np.array(image)\n    return None\n\n# Take a screenshot\nimg = take_screenshot()\n\n# Convert to grayscale for analysis\nif img is not None:\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n\n    # Save the screenshot\n    cv2.imwrite(\"screenshot.png\", img)\n\n    # Perform image analysis (example: find edges)\n    edges = cv2.Canny(gray, 100, 200)\n    cv2.imwrite(\"edges.png\", edges)\n````\n\n```python Web Form Automation\nimport requests\nimport time\n\ndef control_computer(action, **params):\n    url = \"http://localhost:9990/computer-use\"\n    data = {\"action\": action, **params}\n    response = requests.post(url, json=data)\n    return response.json()\n\ndef fill_web_form(form_fields):\n    # Click on the first form field\n    control_computer(\"move_mouse\", coordinates=form_fields[0])\n    control_computer(\"click_mouse\", button=\"left\")\n\n    # Fill out each field\n    for i, field in enumerate(form_fields):\n        # Input the field value\n        control_computer(\"type_text\", text=field[\"value\"])\n\n        # If not the last field, press Tab to move to next field\n        if i < len(form_fields) - 1:\n            control_computer(\"press_keys\", key=\"tab\")\n            time.sleep(0.5)\n\n    # Submit the form by pressing Enter\n    control_computer(\"press_keys\", key=\"enter\")\n\n# Example form fields with coordinates and values\nform_fields = [\n    {\"x\": 500, \"y\": 300, \"value\": \"John Doe\"},\n    {\"x\": 500, \"y\": 350, \"value\": \"john@example.com\"},\n    {\"x\": 500, \"y\": 400, \"value\": \"Password123\"}\n]\n\nfill_web_form(form_fields)\n```\n\n</CodeGroup>\n\n### JavaScript/Node.js Examples\n\n<CodeGroup>\n```javascript Basic Automation\nconst axios = require('axios');\n\nasync function controlComputer(action, params = {}) {\nconst url = \"http://localhost:9990/computer-use\";\nconst data = { action, ...params };\n\ntry {\nconst response = await axios.post(url, data);\nreturn response.data;\n} catch (error) {\nconsole.error('Error:', error.message);\nreturn { success: false, error: error.message };\n}\n}\n\n// Example: Automate opening an application and typing\nasync function automateTextEditor() {\ntry {\n// Open text editor by clicking its icon\nawait controlComputer(\"move_mouse\", { coordinates: { x: 150, y: 960 } });\nawait controlComputer(\"click_mouse\", { button: \"left\" });\n\n    // Wait for it to open\n    await controlComputer(\"wait\", { duration: 2000 });\n\n    // Type some text\n    await controlComputer(\"type_text\", {\n      text: \"This is an automated test using Node.js and Bytebot\",\n      delay: 30\n    });\n\n    console.log(\"Automation completed successfully\");\n\n} catch (error) {\nconsole.error(\"Automation failed:\", error);\n}\n}\n\nautomateTextEditor();\n\n````\n\n```javascript Advanced: Screenshot Comparison\nconst axios = require('axios');\nconst fs = require('fs');\nconst { createCanvas, loadImage } = require('canvas');\nconst pixelmatch = require('pixelmatch');\n\nasync function controlComputer(action, params = {}) {\n  const url = \"http://localhost:9990/computer-use\";\n  const data = { action, ...params };\n\n  try {\n    const response = await axios.post(url, data);\n    return response.data;\n  } catch (error) {\n    console.error('Error:', error.message);\n    return { success: false, error: error.message };\n  }\n}\n\nasync function compareScreenshots() {\n  try {\n    // Take first screenshot\n    const screenshot1 = await controlComputer(\"screenshot\");\n\n    // Do some actions\n    await controlComputer(\"move_mouse\", { coordinates: { x: 500, y: 500 } });\n    await controlComputer(\"click_mouse\", { button: \"left\" });\n    await controlComputer(\"wait\", { duration: 1000 });\n\n    // Take second screenshot\n    const screenshot2 = await controlComputer(\"screenshot\");\n\n    // Compare screenshots\n    if (screenshot1.success && screenshot2.success) {\n      const img1Data = Buffer.from(screenshot1.data.image, 'base64');\n      const img2Data = Buffer.from(screenshot2.data.image, 'base64');\n\n      fs.writeFileSync('screenshot1.png', img1Data);\n      fs.writeFileSync('screenshot2.png', img2Data);\n\n      // Now you could load and compare these images\n      // This requires additional image comparison libraries\n      console.log('Screenshots saved for comparison');\n    }\n  } catch (error) {\n    console.error(\"Screenshot comparison failed:\", error);\n  }\n}\n\ncompareScreenshots();\n````\n\n</CodeGroup>\n\n## File Operations\n\n### Writing Files\n\nThese examples show how to write files to the desktop environment:\n\n<CodeGroup>\n```python Python\nimport requests\nimport base64\n\ndef write_file(path, content):\n    url = \"http://localhost:9990/computer-use\"\n    \n    # Encode content to base64\n    encoded_content = base64.b64encode(content.encode('utf-8')).decode('utf-8')\n    \n    data = {\n        \"action\": \"write_file\",\n        \"path\": path,\n        \"data\": encoded_content\n    }\n    \n    response = requests.post(url, json=data)\n    return response.json()\n\n# Write a text file\nresult = write_file(\"/home/user/hello.txt\", \"Hello, Bytebot!\")\nprint(result)  # {'success': True, 'message': 'File written successfully...'}\n\n# Write to desktop (relative path)\nresult = write_file(\"report.txt\", \"Daily report content\")\nprint(result)  # File will be written to /home/user/Desktop/report.txt\n```\n\n```javascript JavaScript\nconst axios = require('axios');\n\nasync function writeFile(path, content) {\n  const url = \"http://localhost:9990/computer-use\";\n  \n  // Encode content to base64\n  const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n  \n  const data = {\n    action: \"write_file\",\n    path: path,\n    data: encodedContent\n  };\n  \n  const response = await axios.post(url, data);\n  return response.data;\n}\n\n// Write a text file\nwriteFile(\"/home/user/notes.txt\", \"Meeting notes...\")\n  .then(result => console.log(result))\n  .catch(error => console.error(error));\n\n// Write HTML file to desktop\nconst htmlContent = '<html><body><h1>Hello</h1></body></html>';\nwriteFile(\"index.html\", htmlContent)\n  .then(result => console.log(\"HTML file created\"));\n```\n</CodeGroup>\n\n### Reading Files\n\nThese examples show how to read files from the desktop environment:\n\n<CodeGroup>\n```python Python\nimport requests\nimport base64\n\ndef read_file(path):\n    url = \"http://localhost:9990/computer-use\"\n    \n    data = {\n        \"action\": \"read_file\",\n        \"path\": path\n    }\n    \n    response = requests.post(url, json=data)\n    result = response.json()\n    \n    if result['success']:\n        # Decode the base64 content\n        content = base64.b64decode(result['data']).decode('utf-8')\n        return {\n            'content': content,\n            'name': result['name'],\n            'size': result['size'],\n            'mediaType': result['mediaType']\n        }\n    else:\n        return result\n\n# Read a text file\nfile_data = read_file(\"/home/user/hello.txt\")\nprint(f\"Content: {file_data['content']}\")\nprint(f\"Size: {file_data['size']} bytes\")\nprint(f\"Type: {file_data['mediaType']}\")\n```\n\n```javascript JavaScript\nconst axios = require('axios');\n\nasync function readFile(path) {\n  const url = \"http://localhost:9990/computer-use\";\n  \n  const data = {\n    action: \"read_file\",\n    path: path\n  };\n  \n  const response = await axios.post(url, data);\n  const result = response.data;\n  \n  if (result.success) {\n    // Decode the base64 content\n    const content = Buffer.from(result.data, 'base64').toString('utf-8');\n    return {\n      content: content,\n      name: result.name,\n      size: result.size,\n      mediaType: result.mediaType\n    };\n  } else {\n    throw new Error(result.message);\n  }\n}\n\n// Read a file from desktop\nreadFile(\"report.txt\")\n  .then(fileData => {\n    console.log(`Content: ${fileData.content}`);\n    console.log(`Size: ${fileData.size} bytes`);\n    console.log(`Type: ${fileData.mediaType}`);\n  })\n  .catch(error => console.error(\"Error reading file:\", error));\n```\n</CodeGroup>\n\n## Automation Recipes\n\n### Browser Automation\n\nThis example demonstrates how to automate browser interactions:\n\n```python\nimport requests\nimport time\n\ndef control_computer(action, **params):\n    url = \"http://localhost:9990/computer-use\"\n    data = {\"action\": action, **params}\n    response = requests.post(url, json=data)\n    return response.json()\n\ndef automate_browser():\n    # Open browser (assuming browser icon is at position x=100, y=960)\n    control_computer(\"move_mouse\", coordinates={\"x\": 100, \"y\": 960})\n    control_computer(\"click_mouse\", button=\"left\")\n    time.sleep(3)  # Wait for browser to open\n\n    # Type URL\n    control_computer(\"type_text\", text=\"https://example.com\")\n    control_computer(\"press_keys\", key=\"enter\")\n    time.sleep(2)  # Wait for page to load\n\n    # Take screenshot of the loaded page\n    screenshot = control_computer(\"screenshot\")\n\n    # Click on a link (coordinates would need to be adjusted for your target)\n    control_computer(\"move_mouse\", coordinates={\"x\": 300, \"y\": 400})\n    control_computer(\"click_mouse\", button=\"left\")\n    time.sleep(2)\n\n    # Scroll down\n    control_computer(\"scroll\", direction=\"down\", scrollCount=5)\n\nautomate_browser()\n```\n\n### Form Filling Automation\n\nThis example shows how to automate filling out a form in a web application:\n\n```javascript\nconst axios = require(\"axios\");\n\nasync function controlComputer(action, params = {}) {\n  const url = \"http://localhost:9990/computer-use\";\n  const data = { action, ...params };\n  const response = await axios.post(url, data);\n  return response.data;\n}\n\nasync function fillForm() {\n  // Click first input field\n  await controlComputer(\"move_mouse\", { coordinates: { x: 400, y: 300 } });\n  await controlComputer(\"click_mouse\", { button: \"left\" });\n\n  // Type name\n  await controlComputer(\"type_text\", { text: \"John Doe\" });\n\n  // Tab to next field\n  await controlComputer(\"press_keys\", { key: \"tab\" });\n\n  // Type email\n  await controlComputer(\"type_text\", { text: \"john@example.com\" });\n\n  // Tab to next field\n  await controlComputer(\"press_keys\", { key: \"tab\" });\n\n  // Type message\n  await controlComputer(\"type_text\", {\n    text: \"This is an automated message sent using Bytebot's Computer Use API\",\n    delay: 30,\n  });\n\n  // Tab to submit button\n  await controlComputer(\"press_keys\", { key: \"tab\" });\n\n  // Press Enter to submit\n  await controlComputer(\"press_keys\", { key: \"enter\" });\n}\n\nfillForm().catch(console.error);\n```\n\n## Integration with Testing Frameworks\n\nThe Computer Use API can be integrated with popular testing frameworks:\n\n### Selenium Alternative\n\nBytebot can serve as an alternative to Selenium for web testing:\n\n```python\nimport requests\nimport time\nimport json\n\nclass BytebotWebDriver:\n    def __init__(self, base_url=\"http://localhost:9990\"):\n        self.base_url = base_url\n\n    def control_computer(self, action, **params):\n        url = f\"{self.base_url}/computer-use\"\n        data = {\"action\": action, **params}\n        response = requests.post(url, json=data)\n        return response.json()\n\n    def open_browser(self, browser_icon_coords):\n        self.control_computer(\"move_mouse\", coordinates=browser_icon_coords)\n        self.control_computer(\"click_mouse\", button=\"left\")\n        time.sleep(3)  # Wait for browser to open\n\n    def navigate_to(self, url):\n        self.control_computer(\"type_text\", text=url)\n        self.control_computer(\"press_keys\", key=\"enter\")\n        time.sleep(2)  # Wait for page to load\n\n    def click_element(self, coords):\n        self.control_computer(\"move_mouse\", coordinates=coords)\n        self.control_computer(\"click_mouse\", button=\"left\")\n\n    def type_text(self, text):\n        self.control_computer(\"type_text\", text=text)\n\n    def press_keys(self, key, modifiers=None):\n        params = {\"key\": key}\n        if modifiers:\n            params[\"modifiers\"] = modifiers\n        self.control_computer(\"press_keys\", **params)\n\n    def take_screenshot(self):\n        return self.control_computer(\"screenshot\")\n\n# Usage example\ndriver = BytebotWebDriver()\ndriver.open_browser({\"x\": 100, \"y\": 960})\ndriver.navigate_to(\"https://example.com\")\ndriver.click_element({\"x\": 300, \"y\": 400})\ndriver.type_text(\"Hello Bytebot!\")\n```\n"
  },
  {
    "path": "docs/api-reference/computer-use/openapi.json",
    "content": "{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"Bytebot Computer Use API\",\n    \"version\": \"1.0.0\",\n    \"description\": \"Control the Bytebot virtual desktop via a single endpoint\"\n  },\n  \"paths\": {\n    \"/computer-use\": {\n      \"post\": {\n        \"summary\": \"Execute a computer action\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ComputerAction\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ComputerActionResponse\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Error executing action\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\"type\": \"string\"},\n                    \"error\": {\"type\": \"string\"}\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"Coordinates\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"x\": {\"type\": \"number\"},\n          \"y\": {\"type\": \"number\"}\n        },\n        \"required\": [\"x\", \"y\"]\n      },\n      \"Button\": {\n        \"type\": \"string\",\n        \"enum\": [\"left\", \"right\", \"middle\"]\n      },\n      \"Press\": {\n        \"type\": \"string\",\n        \"enum\": [\"up\", \"down\"]\n      },\n      \"ScrollDirection\": {\n        \"type\": \"string\",\n        \"enum\": [\"up\", \"down\", \"left\", \"right\"]\n      },\n      \"MoveMouseAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"move_mouse\"]},\n          \"coordinates\": {\"$ref\": \"#/components/schemas/Coordinates\"}\n        },\n        \"required\": [\"action\", \"coordinates\"]\n      },\n      \"TraceMouseAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"trace_mouse\"]},\n          \"path\": {\n            \"type\": \"array\",\n            \"items\": {\"$ref\": \"#/components/schemas/Coordinates\"}\n          },\n          \"holdKeys\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"}\n          }\n        },\n        \"required\": [\"action\", \"path\"]\n      },\n      \"ClickMouseAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"click_mouse\"]},\n          \"coordinates\": {\"$ref\": \"#/components/schemas/Coordinates\"},\n          \"button\": {\"$ref\": \"#/components/schemas/Button\"},\n          \"holdKeys\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"}\n          },\n          \"clickCount\": {\"type\": \"integer\", \"minimum\": 1}\n        },\n        \"required\": [\"action\", \"button\", \"clickCount\"]\n      },\n      \"PressMouseAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"press_mouse\"]},\n          \"coordinates\": {\"$ref\": \"#/components/schemas/Coordinates\"},\n          \"button\": {\"$ref\": \"#/components/schemas/Button\"},\n          \"press\": {\"$ref\": \"#/components/schemas/Press\"}\n        },\n        \"required\": [\"action\", \"button\", \"press\"]\n      },\n      \"DragMouseAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"drag_mouse\"]},\n          \"path\": {\n            \"type\": \"array\",\n            \"items\": {\"$ref\": \"#/components/schemas/Coordinates\"}\n          },\n          \"button\": {\"$ref\": \"#/components/schemas/Button\"},\n          \"holdKeys\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"}\n          }\n        },\n        \"required\": [\"action\", \"path\", \"button\"]\n      },\n      \"ScrollAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"scroll\"]},\n          \"coordinates\": {\"$ref\": \"#/components/schemas/Coordinates\"},\n          \"direction\": {\"$ref\": \"#/components/schemas/ScrollDirection\"},\n          \"scrollCount\": {\"type\": \"integer\", \"minimum\": 1},\n          \"holdKeys\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"}\n          }\n        },\n        \"required\": [\"action\", \"direction\", \"scrollCount\"]\n      },\n      \"TypeKeysAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"type_keys\"]},\n          \"keys\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"}\n          },\n          \"delay\": {\"type\": \"integer\", \"minimum\": 0}\n        },\n        \"required\": [\"action\", \"keys\"]\n      },\n      \"PressKeysAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"press_keys\"]},\n          \"keys\": {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"}\n          },\n          \"press\": {\"$ref\": \"#/components/schemas/Press\"}\n        },\n        \"required\": [\"action\", \"keys\", \"press\"]\n      },\n      \"TypeTextAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"type_text\"]},\n          \"text\": {\"type\": \"string\"},\n          \"delay\": {\"type\": \"integer\", \"minimum\": 0}\n        },\n        \"required\": [\"action\", \"text\"]\n      },\n      \"WaitAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"wait\"]},\n          \"duration\": {\"type\": \"integer\", \"minimum\": 0}\n        },\n        \"required\": [\"action\", \"duration\"]\n      },\n      \"ScreenshotAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"screenshot\"]}\n        },\n        \"required\": [\"action\"]\n      },\n      \"CursorPositionAction\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"action\": {\"enum\": [\"cursor_position\"]}\n        },\n        \"required\": [\"action\"]\n      },\n      \"ComputerAction\": {\n        \"oneOf\": [\n          {\"$ref\": \"#/components/schemas/MoveMouseAction\"},\n          {\"$ref\": \"#/components/schemas/TraceMouseAction\"},\n          {\"$ref\": \"#/components/schemas/ClickMouseAction\"},\n          {\"$ref\": \"#/components/schemas/PressMouseAction\"},\n          {\"$ref\": \"#/components/schemas/DragMouseAction\"},\n          {\"$ref\": \"#/components/schemas/ScrollAction\"},\n          {\"$ref\": \"#/components/schemas/TypeKeysAction\"},\n          {\"$ref\": \"#/components/schemas/PressKeysAction\"},\n          {\"$ref\": \"#/components/schemas/TypeTextAction\"},\n          {\"$ref\": \"#/components/schemas/WaitAction\"},\n          {\"$ref\": \"#/components/schemas/ScreenshotAction\"},\n          {\"$ref\": \"#/components/schemas/CursorPositionAction\"}\n        ],\n        \"discriminator\": {\n          \"propertyName\": \"action\",\n          \"mapping\": {\n            \"move_mouse\": \"#/components/schemas/MoveMouseAction\",\n            \"trace_mouse\": \"#/components/schemas/TraceMouseAction\",\n            \"click_mouse\": \"#/components/schemas/ClickMouseAction\",\n            \"press_mouse\": \"#/components/schemas/PressMouseAction\",\n            \"drag_mouse\": \"#/components/schemas/DragMouseAction\",\n            \"scroll\": \"#/components/schemas/ScrollAction\",\n            \"type_keys\": \"#/components/schemas/TypeKeysAction\",\n            \"press_keys\": \"#/components/schemas/PressKeysAction\",\n            \"type_text\": \"#/components/schemas/TypeTextAction\",\n            \"wait\": \"#/components/schemas/WaitAction\",\n            \"screenshot\": \"#/components/schemas/ScreenshotAction\",\n            \"cursor_position\": \"#/components/schemas/CursorPositionAction\"\n          }\n        }\n      },\n      \"ScreenshotResponse\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"image\": {\n            \"type\": \"string\",\n            \"description\": \"Base64 encoded PNG\"\n          }\n        },\n        \"required\": [\"image\"]\n      },\n      \"CursorPosition\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"x\": {\"type\": \"number\"},\n          \"y\": {\"type\": \"number\"}\n        },\n        \"required\": [\"x\", \"y\"]\n      },\n      \"ComputerActionResponse\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"success\": {\"type\": \"boolean\"},\n          \"data\": {\n            \"oneOf\": [\n              {\"$ref\": \"#/components/schemas/ScreenshotResponse\"},\n              {\"$ref\": \"#/components/schemas/CursorPosition\"}\n            ]\n          }\n        },\n        \"required\": [\"success\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "docs/api-reference/computer-use/unified-endpoint.mdx",
    "content": "---\ntitle: \"Unified Computer Actions API\"\ndescription: \"Control all aspects of the desktop environment with a single endpoint\"\n---\n\n## Overview\n\nThe unified computer action API allows for granular control over all aspects of the Bytebot virtual desktop environment through a single endpoint. It replaces multiple specific endpoints with a unified interface that handles various computer actions like mouse movements, clicks, key presses, and more.\n\n## Endpoint\n\n| Method | URL              | Description                                     |\n| ------ | ---------------- | ----------------------------------------------- |\n| POST   | `/computer-use`  | Execute computer actions in the virtual desktop |\n\n## Request Format\n\nAll requests to the unified endpoint follow this format:\n\n```json\n{\n  \"action\": \"action_name\",\n  ...action-specific parameters\n}\n```\n\nThe `action` parameter determines which operation to perform, and the remaining parameters depend on the specific action.\n\n## Available Actions\n\n### move_mouse\n\nMove the mouse cursor to a specific position.\n\n**Parameters:**\n\n| Parameter       | Type   | Required | Description                       |\n| --------------- | ------ | -------- | --------------------------------- |\n| `coordinates`   | Object | Yes      | The target coordinates to move to |\n| `coordinates.x` | Number | Yes      | X coordinate                      |\n| `coordinates.y` | Number | Yes      | Y coordinate                      |\n\n**Example:**\n\n```json\n{\n  \"action\": \"move_mouse\",\n  \"coordinates\": {\n    \"x\": 100,\n    \"y\": 200\n  }\n}\n```\n\n### trace_mouse\n\nMove the mouse along a path of coordinates.\n\n**Parameters:**\n\n| Parameter    | Type   | Required | Description                                     |\n| ------------ | ------ | -------- | ----------------------------------------------- |\n| `path`       | Array  | Yes      | Array of coordinate objects for the mouse path  |\n| `path[].x`   | Number | Yes      | X coordinate for each point in the path         |\n| `path[].y`   | Number | Yes      | Y coordinate for each point in the path         |\n| `holdKeys`   | Array  | No       | Keys to hold while moving along the path        |\n\n**Example:**\n\n```json\n{\n  \"action\": \"trace_mouse\",\n  \"path\": [\n    { \"x\": 100, \"y\": 100 },\n    { \"x\": 150, \"y\": 150 },\n    { \"x\": 200, \"y\": 200 }\n  ],\n  \"holdKeys\": [\"shift\"]\n}\n```\n\n### click_mouse\n\nPerform a mouse click at the current or specified position.\n\n**Parameters:**\n\n| Parameter       | Type   | Required | Description                                           |\n| --------------- | ------ | -------- | ----------------------------------------------------- |\n| `coordinates`   | Object | No       | The coordinates to click (uses current if omitted)    |\n| `coordinates.x` | Number | Yes*     | X coordinate                                          |\n| `coordinates.y` | Number | Yes*     | Y coordinate                                          |\n| `button`        | String | Yes      | Mouse button: 'left', 'right', or 'middle'            |\n| `clickCount`    | Number | Yes      | Number of clicks to perform                            |\n| `holdKeys`      | Array  | No       | Keys to hold while clicking (e.g., ['ctrl', 'shift']) |\n\n**Example:**\n\n```json\n{\n  \"action\": \"click_mouse\",\n  \"coordinates\": {\n    \"x\": 150,\n    \"y\": 250\n  },\n  \"button\": \"left\",\n  \"clickCount\": 2\n}\n```\n\n### press_mouse\n\nPress or release a mouse button at the current or specified position.\n\n**Parameters:**\n\n| Parameter       | Type   | Required | Description                                              |\n| --------------- | ------ | -------- | -------------------------------------------------------- |\n| `coordinates`   | Object | No       | The coordinates to press/release (uses current if omitted) |\n| `coordinates.x` | Number | Yes*     | X coordinate                                             |\n| `coordinates.y` | Number | Yes*     | Y coordinate                                             |\n| `button`        | String | Yes      | Mouse button: 'left', 'right', or 'middle'               |\n| `press`         | String | Yes      | Action: 'up' or 'down'                                   |\n\n**Example:**\n\n```json\n{\n  \"action\": \"press_mouse\",\n  \"coordinates\": {\n    \"x\": 150,\n    \"y\": 250\n  },\n  \"button\": \"left\",\n  \"press\": \"down\"\n}\n```\n\n### drag_mouse\n\nClick and drag the mouse from one point to another.\n\n**Parameters:**\n\n| Parameter    | Type   | Required | Description                                   |\n| ------------ | ------ | -------- | --------------------------------------------- |\n| `path`       | Array  | Yes      | Array of coordinate objects for the drag path |\n| `path[].x`   | Number | Yes      | X coordinate for each point in the path       |\n| `path[].y`   | Number | Yes      | Y coordinate for each point in the path       |\n| `button`     | String | Yes      | Mouse button: 'left', 'right', or 'middle'    |\n| `holdKeys`   | Array  | No       | Keys to hold while dragging                   |\n\n**Example:**\n\n```json\n{\n  \"action\": \"drag_mouse\",\n  \"path\": [\n    { \"x\": 100, \"y\": 100 },\n    { \"x\": 200, \"y\": 200 }\n  ],\n  \"button\": \"left\"\n}\n```\n\n### scroll\n\nScroll up, down, left, or right.\n\n**Parameters:**\n\n| Parameter       | Type   | Required | Description                                            |\n| --------------- | ------ | -------- | ------------------------------------------------------ |\n| `coordinates`   | Object | No       | The coordinates to scroll at (uses current if omitted) |\n| `coordinates.x` | Number | Yes*     | X coordinate                                           |\n| `coordinates.y` | Number | Yes*     | Y coordinate                                           |\n| `direction`     | String | Yes      | Scroll direction: 'up', 'down', 'left', 'right'        |\n| `scrollCount`   | Number | Yes      | Number of scroll steps                                 |\n| `holdKeys`      | Array  | No       | Keys to hold while scrolling                           |\n\n**Example:**\n\n```json\n{\n  \"action\": \"scroll\",\n  \"direction\": \"down\",\n  \"scrollCount\": 5\n}\n```\n\n### type_keys\n\nType a sequence of keyboard keys.\n\n**Parameters:**\n\n| Parameter | Type   | Required | Description                            |\n| --------- | ------ | -------- | -------------------------------------- |\n| `keys`    | Array  | Yes      | Array of keys to type in sequence      |\n| `delay`   | Number | No       | Delay between key presses (ms)         |\n\n**Example:**\n\n```json\n{\n  \"action\": \"type_keys\",\n  \"keys\": [\"a\", \"b\", \"c\", \"enter\"],\n  \"delay\": 50\n}\n```\n\n### press_keys\n\nPress or release keyboard keys.\n\n**Parameters:**\n\n| Parameter | Type   | Required | Description                                |\n| --------- | ------ | -------- | ------------------------------------------ |\n| `keys`    | Array  | Yes      | Array of keys to press or release          |\n| `press`   | String | Yes      | Action: 'up' or 'down'                     |\n\n**Example:**\n\n```json\n{\n  \"action\": \"press_keys\",\n  \"keys\": [\"ctrl\", \"shift\", \"esc\"],\n  \"press\": \"down\"\n}\n```\n\n### type_text\n\nType a text string with optional delay.\n\n**Parameters:**\n\n| Parameter | Type   | Required | Description                                           |\n| --------- | ------ | -------- | ----------------------------------------------------- |\n| `text`    | String | Yes      | The text to type                                      |\n| `delay`   | Number | No       | Delay between characters in milliseconds (default: 0) |\n\n**Example:**\n\n```json\n{\n  \"action\": \"type_text\",\n  \"text\": \"Hello, Bytebot!\",\n  \"delay\": 50\n}\n```\n\n### paste_text\n\nPaste text to the current cursor position. This is especially useful for special characters that aren't on the standard keyboard.\n\n**Parameters:**\n\n| Parameter | Type   | Required | Description                                                              |\n| --------- | ------ | -------- | ------------------------------------------------------------------------ |\n| `text`    | String | Yes      | The text to paste, including special characters and emojis               |\n\n**Example:**\n\n```json\n{\n  \"action\": \"paste_text\",\n  \"text\": \"Special characters: ©®™€¥£ émojis 🎉\"\n}\n```\n\n### wait\n\nWait for a specified duration.\n\n**Parameters:**\n\n| Parameter  | Type   | Required | Description                   |\n| ---------- | ------ | -------- | ----------------------------- |\n| `duration` | Number | Yes      | Wait duration in milliseconds |\n\n**Example:**\n\n```json\n{\n  \"action\": \"wait\",\n  \"duration\": 2000\n}\n```\n\n### screenshot\n\nCapture a screenshot of the desktop.\n\n**Parameters:** None required\n\n**Example:**\n\n```json\n{\n  \"action\": \"screenshot\"\n}\n```\n\n### cursor_position\n\nGet the current position of the mouse cursor.\n\n**Parameters:** None required\n\n**Example:**\n\n```json\n{\n  \"action\": \"cursor_position\"\n}\n```\n\n### application\n\nSwitch between different applications or navigate to the desktop/directory.\n\n**Parameters:**\n\n| Parameter     | Type   | Required | Description                                                              |\n| ------------- | ------ | -------- | ------------------------------------------------------------------------ |\n| `application` | String | Yes      | The application to switch to. See available options below.               |\n\n**Available Applications:**\n- `firefox` - Mozilla Firefox web browser\n- `1password` - Password manager\n- `thunderbird` - Email client\n- `vscode` - Visual Studio Code editor\n- `terminal` - Terminal/console application\n- `desktop` - Switch to desktop\n- `directory` - File manager/directory browser\n\n**Example:**\n\n```json\n{\n  \"action\": \"application\",\n  \"application\": \"firefox\"\n}\n```\n\n### write_file\n\nWrite a file to the desktop environment filesystem.\n\n**Parameters:**\n\n| Parameter | Type   | Required | Description                                                    |\n| --------- | ------ | -------- | -------------------------------------------------------------- |\n| `path`    | String | Yes      | File path (absolute or relative to /home/user/Desktop)        |\n| `data`    | String | Yes      | Base64 encoded file content                                    |\n\n**Example:**\n\n```json\n{\n  \"action\": \"write_file\",\n  \"path\": \"/home/user/documents/example.txt\",\n  \"data\": \"SGVsbG8gV29ybGQh\"\n}\n```\n\n### read_file\n\nRead a file from the desktop environment filesystem.\n\n**Parameters:**\n\n| Parameter | Type   | Required | Description                                                    |\n| --------- | ------ | -------- | -------------------------------------------------------------- |\n| `path`    | String | Yes      | File path (absolute or relative to /home/user/Desktop)        |\n\n**Example:**\n\n```json\n{\n  \"action\": \"read_file\",\n  \"path\": \"/home/user/documents/example.txt\"\n}\n```\n\n## Response Format\n\nThe response format varies depending on the action performed.\n\n### Standard Response\n\nMost actions return a simple success response:\n\n```json\n{\n  \"success\": true\n}\n```\n\n### Screenshot Response\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"image\": \"base64_encoded_image_data\"\n  }\n}\n```\n\n### Cursor Position Response\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"x\": 123,\n    \"y\": 456\n  }\n}\n```\n\n### Write File Response\n\n```json\n{\n  \"success\": true,\n  \"message\": \"File written successfully to: /home/user/documents/example.txt\"\n}\n```\n\n### Read File Response\n\n```json\n{\n  \"success\": true,\n  \"data\": \"SGVsbG8gV29ybGQh\",\n  \"name\": \"example.txt\",\n  \"size\": 12,\n  \"mediaType\": \"text/plain\"\n}\n```\n\n### Error Response\n\n```json\n{\n  \"success\": false,\n  \"error\": \"Error message\"\n}\n```\n\n## Code Examples\n\n### JavaScript/Node.js Example\n\n```javascript\nconst axios = require('axios');\n\nconst bytebot = {\n  baseUrl: 'http://localhost:9990/computer-use/computer',\n  \n  async action(params) {\n    try {\n      const response = await axios.post(this.baseUrl, params);\n      return response.data;\n    } catch (error) {\n      console.error('Error:', error.response?.data || error.message);\n      throw error;\n    }\n  },\n  \n  // Convenience methods\n  async moveMouse(x, y) {\n    return this.action({\n      action: 'move_mouse',\n      coordinates: { x, y }\n    });\n  },\n  \n  async clickMouse(x, y, button = 'left') {\n    return this.action({\n      action: 'click_mouse',\n      coordinates: { x, y },\n      button\n    });\n  },\n  \n  async typeText(text) {\n    return this.action({\n      action: 'type_text',\n      text\n    });\n  },\n  \n  async pasteText(text) {\n    return this.action({\n      action: 'paste_text',\n      text\n    });\n  },\n  \n  async switchApplication(application) {\n    return this.action({\n      action: 'application',\n      application\n    });\n  },\n  \n  async screenshot() {\n    return this.action({ action: 'screenshot' });\n  }\n};\n\n// Example usage:\nasync function example() {\n  // Switch to Firefox\n  await bytebot.switchApplication('firefox');\n  \n  // Navigate to a website\n  await bytebot.moveMouse(100, 35);\n  await bytebot.clickMouse(100, 35);\n  await bytebot.typeText('https://example.com');\n  await bytebot.action({\n    action: 'press_keys',\n    keys: ['enter'],\n    press: 'down'\n  });\n  \n  // Wait for page to load\n  await bytebot.action({\n    action: 'wait',\n    duration: 2000\n  });\n  \n  // Paste some special characters\n  await bytebot.pasteText('© 2025 Example Corp™ - €100');\n  \n  // Take a screenshot\n  const result = await bytebot.screenshot();\n  console.log('Screenshot taken!');\n}\n\nexample().catch(console.error);\n"
  },
  {
    "path": "docs/api-reference/endpoint/create.mdx",
    "content": "---\ntitle: 'Create Plant'\nopenapi: 'POST /plants'\n---\n"
  },
  {
    "path": "docs/api-reference/endpoint/delete.mdx",
    "content": "---\ntitle: 'Delete Plant'\nopenapi: 'DELETE /plants/{id}'\n---\n"
  },
  {
    "path": "docs/api-reference/endpoint/get.mdx",
    "content": "---\ntitle: 'Get Plants'\nopenapi: 'GET /plants'\n---\n"
  },
  {
    "path": "docs/api-reference/endpoint/webhook.mdx",
    "content": "---\ntitle: 'New Plant'\nopenapi: 'WEBHOOK /plant/webhook'\n---\n"
  },
  {
    "path": "docs/api-reference/introduction.mdx",
    "content": "---\ntitle: \"API Reference\"\ndescription: \"Overview of the Bytebot API endpoints for programmatic control\"\n---\n\n# Bytebot API Overview\n\nBytebot provides two main APIs for programmatic control:\n\n## 1. Agent API (Task Management)\n\nThe Agent API runs on port 9991 and provides high-level task management:\n\n<CardGroup cols={2}>\n  <Card\n    title=\"Task Management\"\n    icon=\"list-check\"\n    href=\"/api-reference/agent/tasks\"\n  >\n    Create, manage, and monitor AI-powered tasks programmatically\n  </Card>\n  <Card\n    title=\"UI Integration\"\n    icon=\"window\"\n    href=\"/api-reference/agent/ui\"\n  >\n    WebSocket connections and real-time updates for custom UIs\n  </Card>\n</CardGroup>\n\n### Agent API Base URL\n```\nhttp://localhost:9991\n```\n\n### Example Task Creation\n```bash\ncurl -X POST http://localhost:9991/tasks \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"description\": \"Download invoices from webmail and organize by date\",\n    \"priority\": \"HIGH\"\n  }'\n```\n\n## 2. Desktop API (Direct Control)\n\nThe Desktop API runs on port 9990 and provides low-level desktop control:\n\n<CardGroup cols={2}>\n  <Card\n    title=\"Computer Control\"\n    icon=\"keyboard\"\n    href=\"/api-reference/computer-use/unified-endpoint\"\n  >\n    Direct control of mouse, keyboard, and screen capture\n  </Card>\n  <Card\n    title=\"Usage Examples\"\n    icon=\"code\"\n    href=\"/api-reference/computer-use/examples\"\n  >\n    Code examples for common automation scenarios\n  </Card>\n</CardGroup>\n\n### Desktop API Base URL\n```\nhttp://localhost:9990\n```\n\n### Example Desktop Control\n```bash\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"screenshot\"}'\n```\n\n### MCP Support\n\nThe Desktop API also exposes an MCP (Model Context Protocol) endpoint:\n```\nhttp://localhost:9990/mcp\n```\n\nConnect your MCP client to access desktop control tools over SSE.\n\n## Authentication\n\n- **Local Access**: No authentication required by default\n- **Remote Access**: Configure authentication based on your security requirements\n- **Production**: Implement API keys, OAuth, or other authentication methods\n\n## Response Formats\n\n### Agent API Response\n```json\n{\n  \"id\": \"task-123\",\n  \"status\": \"RUNNING\",\n  \"description\": \"Your task description\",\n  \"messages\": [...],\n  \"createdAt\": \"2024-01-01T00:00:00Z\"\n}\n```\n\n### Desktop API Response\n```json\n{\n  \"success\": true,\n  \"data\": { ... },  // Response data specific to the action\n  \"error\": null     // Error message if success is false\n}\n```\n\n## Error Handling\n\nBoth APIs use standard HTTP status codes:\n\n| Status Code | Description                          |\n| ----------- | ------------------------------------ |\n| 200         | Success                              |\n| 201         | Created (new resource)               |\n| 400         | Bad Request - Invalid parameters     |\n| 401         | Unauthorized - Authentication failed |\n| 404         | Not Found - Resource doesn't exist   |\n| 500         | Internal Server Error                |\n\n## Rate Limiting\n\n- **Agent API**: No hard limits, but consider task queue capacity\n- **Desktop API**: No rate limiting, but rapid actions may impact desktop performance\n\n## Best Practices\n\n1. **Use Agent API for high-level automation** - Let the AI handle complexity\n2. **Use Desktop API for precise control** - When you need exact actions\n3. **Combine both APIs** - Create tasks via Agent API, monitor via Desktop API\n4. **Handle errors gracefully** - Implement retry logic for transient failures\n5. **Monitor resource usage** - Both APIs can be resource-intensive\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Quick Start\" icon=\"rocket\" href=\"/quickstart\">\n    Get your APIs running\n  </Card>\n  <Card title=\"Task Examples\" icon=\"code\" href=\"/guides/task-creation\">\n    See the APIs in action\n  </Card>\n</CardGroup>\n"
  },
  {
    "path": "docs/api-reference/openapi.json",
    "content": "{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"OpenAPI Plant Store\",\n    \"description\": \"A sample API that uses a plant store as an example to demonstrate features in the OpenAPI specification\",\n    \"license\": {\n      \"name\": \"MIT\"\n    },\n    \"version\": \"1.0.0\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://sandbox.mintlify.com\"\n    }\n  ],\n  \"security\": [\n    {\n      \"bearerAuth\": []\n    }\n  ],\n  \"paths\": {\n    \"/plants\": {\n      \"get\": {\n        \"description\": \"Returns all plants from the system that the user has access to\",\n        \"parameters\": [\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"The maximum number of results to return\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Plant response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/Plant\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Unexpected error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"description\": \"Creates a new plant in the store\",\n        \"requestBody\": {\n          \"description\": \"Plant to add to the store\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NewPlant\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"plant response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Plant\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"unexpected error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/plants/{id}\": {\n      \"delete\": {\n        \"description\": \"Deletes a single plant based on the ID supplied\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"ID of plant to delete\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"integer\",\n              \"format\": \"int64\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"Plant deleted\",\n            \"content\": {}\n          },\n          \"400\": {\n            \"description\": \"unexpected error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"webhooks\": {\n    \"/plant/webhook\": {\n      \"post\": {\n        \"description\": \"Information about a new plant added to the store\",\n        \"requestBody\": {\n          \"description\": \"Plant added to the store\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/NewPlant\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return a 200 status to indicate that the data was received successfully\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"Plant\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"description\": \"The name of the plant\",\n            \"type\": \"string\"\n          },\n          \"tag\": {\n            \"description\": \"Tag to specify the type\",\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"NewPlant\": {\n        \"allOf\": [\n          {\n            \"$ref\": \"#/components/schemas/Plant\"\n          },\n          {\n            \"required\": [\n              \"id\"\n            ],\n            \"type\": \"object\",\n            \"properties\": {\n              \"id\": {\n                \"description\": \"Identification number of the plant\",\n                \"type\": \"integer\",\n                \"format\": \"int64\"\n              }\n            }\n          }\n        ]\n      },\n      \"Error\": {\n        \"required\": [\n          \"error\",\n          \"message\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"error\": {\n            \"type\": \"integer\",\n            \"format\": \"int32\"\n          },\n          \"message\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"bearerAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "docs/core-concepts/agent-system.mdx",
    "content": "---\ntitle: \"Agent System\"\ndescription: \"The AI brain that powers your self-hosted desktop automation\"\n---\n\n## Overview\n\nThe Bytebot Agent System transforms a simple desktop container into an intelligent, autonomous computer user. By combining Claude AI with structured task management, it can understand natural language requests and execute complex workflows just like a human would.\n\n<img\n  src=\"/images/agent-architecture.png\"\n  alt=\"Bytebot Agent Architecture\"\n  className=\"w-full max-w-4xl\"\n/>\n\n## How the AI Agent Works\n\n### The Brain: Multi-Model AI Integration\n\nAt the heart of Bytebot is a flexible AI integration that supports multiple models. Choose the AI that best fits your needs:\n\n**Anthropic Claude** (Default):\n- Best for complex reasoning and visual understanding\n- Excellent at following detailed instructions\n- Superior performance on desktop automation tasks\n\n**OpenAI GPT Models**:\n- Fast and reliable for general automation\n- Strong code understanding and generation\n- Cost-effective for routine tasks\n\n**Google Gemini**:\n- Efficient for high-volume tasks\n- Good balance of speed and capability\n- Excellent multilingual support\n\nThe agent with any model:\n\n1. **Understands Context**: Processes your natural language requests with full conversation history\n2. **Plans Actions**: Breaks down complex tasks into executable computer actions\n3. **Adapts in Real-time**: Adjusts its approach based on what it sees on screen\n4. **Learns from Feedback**: Improves task execution through conversation\n\n### Conversation Flow\n\n<Steps>\n  <Step title=\"You Describe a Task\">\n    \"Research competitors for my SaaS product and create a comparison table\"\n  </Step>\n  <Step title=\"AI Plans the Approach\">\n    The AI model understands the request and plans: open browser → search → visit sites → extract data → create document\n  </Step>\n  <Step title=\"Executes Actions\">\n    The agent controls the desktop: clicking, typing, taking screenshots, reading content\n  </Step>\n  <Step title=\"Provides Updates\">\n    Real-time status updates and asks for clarification when needed\n  </Step>\n  <Step title=\"Delivers Results\">\n    Completes the task and provides the output (files, screenshots, summaries)\n  </Step>\n</Steps>\n\n## Task Management System\n\n### Task Lifecycle\n\nTasks move through a structured lifecycle:\n\n```mermaid\ngraph LR\n    A[Created] --> B[Queued]\n    B --> C[Running]\n    C --> D[Needs Help]\n    C --> E[Completed]\n    C --> F[Failed]\n    D --> C\n```\n\n### Task Properties\n\nEach task contains:\n\n- **Description**: What needs to be done\n- **Priority**: Urgent, High, Medium, or Low\n- **Status**: Current state in the lifecycle\n- **Type**: Immediate or Scheduled\n- **History**: All messages and actions taken\n\n### Smart Task Processing\n\nThe agent processes tasks intelligently:\n\n1. **Priority Queue**: Urgent tasks run first\n2. **Error Recovery**: Automatically retries failed actions\n3. **Human in the Loop**: Asks for help when stuck\n4. **Context Preservation**: Maintains conversation history across sessions\n\n## Real-world Capabilities\n\n### What the Agent Can Do\n\n<CardGroup cols={2}>\n  <Card title=\"Web Automation\" icon=\"globe\">\n    - Browse websites\n    - Fill out forms\n    - Extract data\n    - Download files\n    - Monitor changes\n  </Card>\n  <Card title=\"Document Work\" icon=\"file\">\n    - Create documents\n    - Edit spreadsheets\n    - Generate reports\n    - Organize files\n    - Convert formats\n  </Card>\n  <Card title=\"Email & Communication\" icon=\"envelope\">\n    - Access webmail through browser\n    - Read and extract information\n    - Fill contact forms\n    - Navigate communication portals\n    - Handle verification flows\n  </Card>\n  <Card title=\"Data Processing\" icon=\"database\">\n    - Extract from PDFs\n    - Process CSV files\n    - Create visualizations\n    - Generate summaries\n    - Transform data\n  </Card>\n</CardGroup>\n\n## Technical Architecture\n\n### Core Components\n\n1. **NestJS Agent Service**\n   - Integrates with multiple AI provider APIs (Anthropic, OpenAI, Google)\n   - Handles WebSocket connections\n   - Coordinates with desktop API\n\n2. **Message System**\n   - Structured conversation format\n   - Supports text and images\n   - Maintains full context\n   - Enables rich interactions\n\n3. **Database Schema**\n   ```sql\n   Tasks: id, description, status, priority, timestamps\n   Messages: id, task_id, role, content, timestamps\n   Summaries: id, task_id, content, parent_id\n   ```\n\n4. **Computer Action Bridge**\n   - Translates AI decisions to desktop actions\n   - Handles screenshots and feedback\n   - Manages action timing\n   - Provides error handling\n\n### API Endpoints\n\nKey endpoints for programmatic control:\n\n```typescript\n// Create a new task\nPOST /tasks\n{\n  \"description\": \"Your task description\",\n  \"priority\": \"HIGH\",\n  \"type\": \"IMMEDIATE\"\n}\n\n// Get task status\nGET /tasks/:id\n\n// Send a message\nPOST /tasks/:id/messages\n{\n  \"content\": \"Additional instructions\"\n}\n\n// Get task history\nGET /tasks/:id/messages\n```\n\n## Chat UI Features\n\nThe web interface provides:\n\n### Real-time Interaction\n- Live chat with the AI agent\n- Instant status updates\n- Progress indicators\n- Error notifications\n\n### Visual Feedback\n- Embedded desktop viewer\n- Screenshot history\n- Action replay\n- Task timeline\n\n### Task Management\n- Create and prioritize tasks\n- View active and completed tasks\n- Export conversation logs\n- Manage task queues\n\n## Security & Privacy\n\n### Data Isolation\n- All processing happens in your infrastructure\n- No data sent to external services (except your chosen AI provider API)\n- Conversations stored locally\n- Complete audit trail\n\n### Access Control\n- Configurable authentication\n- API key management\n- Network isolation options\n\n## Extending the Agent\n\n### Integration Points\n- External API calls via the Agent API\n- Custom AI prompts for specialized workflows\n- MCP protocol support for tool integration\n\n\n### Best Practices\n\n1. **Clear Instructions**: Be specific about desired outcomes\n2. **Break Down Complex Tasks**: Use multiple smaller tasks for better results\n3. **Provide Context**: Include relevant files or URLs\n4. **Monitor Progress**: Watch the desktop view for real-time feedback\n5. **Review Results**: Verify outputs meet requirements\n\n## Troubleshooting\n\n<AccordionGroup>\n  <Accordion title=\"Agent not responding\">\n    - Check your AI provider API key is valid\n    - Verify agent service is running\n    - Review logs for errors\n    - Ensure sufficient API credits/quota with your provider\n  </Accordion>\n  \n  <Accordion title=\"Slow task execution\">\n    - Monitor system resources\n    - Check network latency\n    - Reduce screenshot frequency\n    - Optimize AI prompts for your chosen model\n    - Consider switching to a faster model (e.g., Gemini Flash)\n  </Accordion>\n</AccordionGroup>\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Quick Start\" icon=\"rocket\" href=\"/quickstart\">\n    Get your agent running\n  </Card>\n  <Card title=\"API Reference\" icon=\"code\" href=\"/api-reference/agent/tasks\">\n    Integrate with your apps\n  </Card>\n  <Card title=\"Use Cases\" icon=\"lightbulb\" href=\"#example-use-cases\">\n    See what's possible\n  </Card>\n  <Card title=\"Best Practices\" icon=\"star\" href=\"#best-practices\">\n    Optimize your workflows\n  </Card>\n</CardGroup>"
  },
  {
    "path": "docs/core-concepts/architecture.mdx",
    "content": "---\ntitle: \"Architecture\"\ndescription: \"How Bytebot's desktop agent works under the hood\"\n---\n\n## Overview\n\nBytebot is a self-hosted AI desktop agent built with a modular architecture. It combines a Linux desktop environment with AI to create an autonomous computer user that can perform tasks through natural language instructions.\n\n<img\n  src=\"/images/agent-architecture.png\"\n  alt=\"Bytebot Architecture Diagram\"\n  className=\"w-full max-w-4xl\"\n/>\n\n## System Architecture\n\nThe system consists of four main components that work together:\n\n### 1. Bytebot Desktop Container\nThe foundation of the system - a virtual Linux desktop that provides:\n\n- **Ubuntu 22.04 LTS** base for stability and compatibility\n- **XFCE4 Desktop** for a lightweight, responsive UI\n- **bytebotd Daemon** - The automation service built on nutjs that executes computer actions\n- **Pre-installed Applications**: Firefox ESR, Thunderbird, text editors, and development tools\n- **noVNC** for remote desktop access\n\n**Key Features:**\n- Runs completely isolated from your host system\n- Consistent environment across different platforms\n- Can be customized with additional software\n- Accessible via REST API on port 9990\n- MCP SSE endpoint available at `/mcp`\n- Uses shared types from `@bytebot/shared` package\n\n### 2. AI Agent Service\nThe brain of the system - orchestrates tasks using an LLM:\n\n- **NestJS Framework** for robust, scalable backend\n- **LLM Integration** supporting Anthropic Claude, OpenAI GPT, and Google Gemini models\n- **WebSocket Support** for real-time updates\n- **Computer Use API Client** to control the desktop\n- **Prisma ORM** for database operations\n- **Tool definitions** for computer actions (mouse, keyboard, screenshots)\n\n**Responsibilities:**\n- Interprets natural language requests\n- Plans sequences of computer actions\n- Manages task state and progress\n- Handles errors and retries\n- Provides real-time task updates via WebSocket\n\n### 3. Web Task Interface\nThe user interface for interacting with your AI agent:\n\n- **Next.js 15 Application** with TypeScript for type safety\n- **Embedded VNC Viewer** to watch the desktop in action\n- **Task Management** UI with status badges\n- **WebSocket Connections** for live updates\n- **Reusable components** for consistent UI\n- **API utilities** for streamlined server communication\n\n**Features:**\n- Task creation and management interface\n- Desktop tab for direct manual control\n- Real-time desktop viewer with takeover mode\n- Task history and status tracking\n- Responsive design for all devices\n\n### 4. PostgreSQL Database\nPersistent storage for the agent system:\n\n- **Tasks Table**: Stores task details, status, and metadata\n- **Messages Table**: Stores AI conversation history\n- **Prisma ORM** for type-safe database access\n\n## Data Flow\n\n### Task Execution Flow\n\n<Steps>\n  <Step title=\"User Input\">\n    User describes a task in natural language via the chat UI\n  </Step>\n  <Step title=\"Task Creation\">\n    Agent service creates a task record and adds it to the processing queue\n  </Step>\n  <Step title=\"AI Planning\">\n    The LLM analyzes the task and generates a plan of computer actions\n  </Step>\n  <Step title=\"Action Execution\">\n    Agent sends computer actions to bytebotd via REST API or MCP\n  </Step>\n  <Step title=\"Desktop Automation\">\n    bytebotd executes actions (mouse, keyboard, screenshots) on the desktop\n  </Step>\n  <Step title=\"Result Processing\">\n    Agent receives results, updates task status, and continues or completes\n  </Step>\n  <Step title=\"User Feedback\">\n    Results and status updates are sent back to the user in real-time\n  </Step>\n</Steps>\n\n### Communication Protocols\n\n```mermaid\ngraph LR\n    A[Tasks UI] -->|WebSocket| B[Agent Service]\n    A -->|HTTP Proxy| C[Desktop VNC]\n    B -->|REST/MCP| D[Desktop API]\n    B -->|SQL| E[PostgreSQL]\n    B -->|HTTPS| F[LLM Provider]\n    D -->|IPC| G[bytebotd]\n```\n\n## Security Architecture\n\n### Isolation Layers\n\n1. **Container Isolation**\n   - Each desktop runs in its own Docker container\n   - No access to host filesystem by default\n   - Network isolation with explicit port mapping\n\n2. **Process Isolation**\n   - bytebotd runs as non-root user\n   - Separate processes for different services\n   - Resource limits enforced by Docker\n\n3. **Network Security**\n   - Services only accessible from localhost by default\n   - Can be configured with authentication\n   - HTTPS/WSS for external connections\n\n### API Security\n\n - **Desktop API**: No authentication by default (localhost only). Supports REST and MCP.\n- **Agent API**: Can be secured with API keys\n- **Database**: Password protected, not exposed externally\n\n<Warning>\n  Default configuration is for development. For production:\n  - Enable authentication on all APIs\n  - Use HTTPS/WSS for all connections\n  - Implement network policies\n  - Rotate credentials regularly\n</Warning>\n\n## Deployment Patterns\n\n### Single User (Development)\n```yaml\nServices: All on one machine\nScale: 1 instance each\nUse Case: Personal automation, development\nResources: 4GB RAM, 2 CPU cores\n```\n\n### Production Deployment\n```yaml\nServices: All services on dedicated hardware\nScale: Single instance (1 agent, 1 desktop)\nUse Case: Business automation\nResources: 8GB+ RAM, 4+ CPU cores\n```\n\n### Enterprise Deployment\n```yaml\nServices: Kubernetes orchestration\nScale: Single instance with high availability\nUse Case: Organization-wide automation\nResources: Dedicated nodes\n```\n\n## Extension Points\n\n### Custom Tools\nAdd specialized software to the desktop:\n```dockerfile\nFROM bytebot/desktop:latest\nRUN apt-get update && apt-get install -y \\\n    your-custom-tools\n```\n\n### AI Integrations\nExtend agent capabilities:\n- Custom tools for the LLM\n- Additional AI models\n- Specialized prompts\n- Domain-specific knowledge\n\n## Performance Considerations\n\n### Resource Usage\n- **Desktop Container**: ~1GB RAM idle, 2GB+ active\n- **Agent Service**: ~256MB RAM\n- **UI Service**: ~128MB RAM\n- **Database**: ~256MB RAM\n\n### Optimization Tips\n1. Allocate sufficient resources to containers\n2. Limit concurrent tasks to prevent overload\n3. Monitor resource usage regularly\n4. Use LiteLLM proxy for provider flexibility\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Agent System\" icon=\"robot\" href=\"/core-concepts/agent-system\">\n    Learn about the AI agent capabilities\n  </Card>\n  <Card title=\"Desktop Environment\" icon=\"desktop\" href=\"/core-concepts/desktop-environment\">\n    Explore the virtual desktop environment\n  </Card>\n  <Card title=\"API Reference\" icon=\"code\" href=\"/api-reference/introduction\">\n    Integrate with your applications\n  </Card>\n  <Card title=\"Deployment Guide\" icon=\"rocket\" href=\"/quickstart\">\n    Deploy your own instance\n  </Card>\n</CardGroup>"
  },
  {
    "path": "docs/core-concepts/desktop-environment.mdx",
    "content": "---\ntitle: \"Desktop Environment\" \ndescription: \"The virtual Linux desktop where Bytebot performs tasks\"\n---\n\n## Overview\n\nThe Bytebot Desktop Environment (also called Bytebot Core) is a complete Linux desktop that runs in a Docker container. This is where Bytebot does its work - clicking buttons, typing text, browsing websites, and using applications just like you would.\n\n<img\n  src=\"/images/core-container.png\"\n  alt=\"Bytebot Desktop Environment\"\n  className=\"w-full max-w-4xl\"\n/>\n\n## Why a Virtual Desktop?\n\n### Complete Isolation\n- **No Risk to Host**: All actions happen inside the container\n- **Sandboxed Environment**: Desktop can't access your host system\n- **Easy Reset**: Destroy and recreate in seconds\n- **Clean Workspace**: Each restart provides a fresh environment\n\n### Consistency Everywhere\n- **Platform Independent**: Same environment on Mac, Windows, or Linux\n- **Reproducible**: Identical setup every time\n- **Version Control**: Pin specific versions for stability\n- **No Dependencies**: Everything included in the container\n\n### Built for Automation\n- **Predictable UI**: Consistent element positioning\n- **Clean Environment**: No popups or distractions\n- **Automation-Ready**: Optimized for programmatic control\n- **Fast Startup**: Desktop ready in seconds\n\n## Technical Stack\n\n### Base System\n- **Ubuntu 22.04 LTS**: Stable, well-supported Linux distribution\n- **XFCE4 Desktop**: Lightweight, responsive desktop environment\n- **X11 Display Server**: Standard Linux graphics system\n- **supervisord**: Service management\n\n### Pre-installed Software\n\n<CardGroup cols={2}>\n  <Card title=\"Web Browser\" icon=\"globe\">\n    - Firefox ESR (Extended Support Release)\n    - Pre-configured for automation\n    - Clean profile without distractions\n  </Card>\n  <Card title=\"Productivity Tools\" icon=\"file-lines\">\n    - Text editor\n    - Office tools\n    - PDF viewer\n    - File manager\n  </Card>\n  <Card title=\"Communication\" icon=\"envelope\">\n    - Thunderbird email client\n    - Terminal emulator\n  </Card>\n  <Card title=\"Security & Development\" icon=\"shield\">\n    - 1Password password manager\n    - Visual Studio Code (VSCode)\n    - Git version control\n    - Python 3 environment\n  </Card>\n</CardGroup>\n\n### Core Services\n\n1. **bytebotd Daemon**\n   - Runs on port 9990\n   - Handles all automation requests\n   - Built on nutjs framework\n   - Provides REST API\n\n2. **noVNC Web Client**\n   - Browser-based desktop access\n   - No client installation needed\n   - WebSocket proxy included\n\n3. **Supervisor**\n   - Process management\n   - Service monitoring\n   - Automatic restarts\n   - Log management\n\n## Desktop Features\n\n### Display Configuration\n```bash\n# Resolution\n1920x1080 @ 24-bit color\n```\n\n### User Environment\n- **Username**: `user`\n- **Home Directory**: `/home/user`\n- **Sudo Access**: Yes (passwordless)\n- **Desktop Session**: Auto-login enabled\n\n### File System\n```\n/home/user/\n├── Desktop/          # Desktop shortcuts\n├── Documents/        # User documents\n├── Downloads/        # Browser downloads\n├── .config/          # Application configs\n└── .local/           # User data\n```\n\n## Accessing the Desktop\n\n### Web Browser (Recommended)\nNavigate to `http://localhost:9990/vnc` for instant access:\n- No software installation required\n- Works on any device with a browser\n- Supports touch devices\n- Clipboard sharing\n\n### MCP Control\n\nThe core container also exposes an [MCP](https://github.com/rekog-labs/MCP-Nest) endpoint.\nConnect your MCP client to `http://localhost:9990/mcp` to invoke these tools over SSE.\n\n```json\n{\n  \"mcpServers\": {\n    \"bytebot\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"mcp-remote\",\n        \"http://127.0.0.1:9990/mcp\",\n        \"--transport\",\n        \"http-first\"\n      ]\n    }\n  }\n}\n```\n\n### Direct API Control\nMost efficient for automation:\n```bash\n# Take a screenshot\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"screenshot\"}'\n\n# Move mouse\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"move_mouse\", \"coordinate\": {\"x\": 500, \"y\": 300}}'\n```\n\n## Customization\n\n### Adding Software\n\nCreate a custom Dockerfile:\n```dockerfile\nFROM ghcr.io/bytebot-ai/bytebot-desktop:edge\n\n# Install additional packages\nRUN apt-get update && apt-get install -y \\\n    slack-desktop \\\n    zoom \\\n    your-custom-app\n\n# Copy configuration files\nCOPY configs/ /home/user/.config/\n```\n\n\n## Performance Optimization\n\n### Resource Allocation\n```yaml\n# Recommended settings\ndeploy:\n  resources:\n    limits:\n      cpus: '2'\n      memory: 4G\n    reservations:\n      cpus: '1'\n      memory: 2G\n```\n\n## Security Hardening\n\n<Warning>\n  Default configuration prioritizes ease of use. For production, apply these security measures:\n</Warning>\n\n### Essential Security Steps\n\n1. **Change Default Passwords**\n   ```bash\n   # Set user password\n   passwd bytebot\n   ```\n\n2. **Limit Network Access**\n   ```yaml\n   # Whitelist specific domains\n   environment:\n     - ALLOWED_DOMAINS=company.com,trusted-site.com\n   \n   # Or restrict to local network only\n   ports:\n     - \"10.0.0.0/8:9990:9990\"\n   ```\n\n## Troubleshooting\n\n<AccordionGroup>\n  <Accordion title=\"Desktop won't start\">\n    Check logs:\n    ```bash\n    docker logs bytebot-desktop\n    ```\n    Common issues:\n    - Insufficient memory\n    - Port conflicts\n    - Display server errors\n  </Accordion>\n  \n  <Accordion title=\"Applications crash\">\n    Monitor resources:\n    ```bash\n    docker stats bytebot-desktop\n    ```\n    Solutions:\n    - Increase memory allocation\n    - Check disk space\n    - Update container image\n  </Accordion>\n</AccordionGroup>\n\n## Best Practices\n\n1. **Regular Updates**: Keep the base image updated for security patches\n2. **Persistent Storage**: Mount volumes for important data\n3. **Backup Configurations**: Save customizations outside the container\n4. **Monitor Resources**: Track CPU/memory usage\n5. **Clean Temporary Files**: Periodic cleanup for performance\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Quick Start\" icon=\"rocket\" href=\"/quickstart\">\n    Deploy your first agent\n  </Card>\n  <Card title=\"API Reference\" icon=\"code\" href=\"/api-reference/computer-use/unified-endpoint\">\n    Control the desktop programmatically\n  </Card>\n  <Card title=\"Agent System\" icon=\"robot\" href=\"/core-concepts/agent-system\">\n    Add AI capabilities\n  </Card>\n  <Card title=\"Password Management\" icon=\"key\" href=\"/guides/password-management\">\n    Set up authentication\n  </Card>\n</CardGroup>"
  },
  {
    "path": "docs/core-concepts/rpa-comparison.mdx",
    "content": "---\ntitle: \"Bytebot vs Traditional RPA\"\ndescription: \"How Bytebot revolutionizes enterprise automation beyond traditional RPA tools\"\n---\n\n# The Next Generation of Enterprise Automation\n\nBytebot represents a fundamental shift in how businesses approach process automation. While traditional RPA tools like UiPath, Automation Anywhere, and Blue Prism require extensive scripting and brittle workflows, Bytebot leverages AI to understand and execute tasks like a human would.\n\n## Traditional RPA Limitations\n\n<CardGroup cols={2}>\n  <Card title=\"Brittle Selectors\" icon=\"xmark\">\n    Traditional RPA breaks when UI elements change even slightly\n  </Card>\n  <Card title=\"Complex Development\" icon=\"code\">\n    Requires specialized developers and lengthy implementation cycles\n  </Card>\n  <Card title=\"High Maintenance\" icon=\"wrench\">\n    Constant updates needed as applications evolve\n  </Card>\n  <Card title=\"Limited Adaptability\" icon=\"robot\">\n    Can't handle unexpected scenarios or variations\n  </Card>\n</CardGroup>\n\n## How Bytebot is Different\n\n### Visual Intelligence vs Element Mapping\n\n**Traditional RPA:**\n```xml\n<!-- Brittle selector that breaks with any UI change -->\n<Click>\n  <Selector>\n    <webctrl id='submit-btn-2947' \n             class='btn-primary-new' \n             idx='3'/>\n  </Selector>\n</Click>\n```\n\n**Bytebot:**\n```\n\"Click the blue Submit button at the bottom of the form\"\n```\n\nBytebot understands interfaces visually, just like a human. It doesn't rely on fragile technical selectors that break with every update.\n\n### Natural Language vs Complex Scripting\n\n**Traditional RPA Workflow:**\n- Design in Studio\n- Map every element\n- Script error handling\n- Test extensively\n- Deploy with fingers crossed\n- Fix when it breaks (often)\n\n**Bytebot Workflow:**\n- Describe what you need\n- Bytebot figures it out\n- Handles errors intelligently\n- Adapts to changes automatically\n\n## Real-World Enterprise Examples\n\n### Financial Services Automation\n\n<Tabs>\n  <Tab title=\"Traditional RPA\">\n    ```csharp\n    // 500+ lines of code to handle one banking portal\n    var loginPage = new LoginPageObject();\n    loginPage.WaitForElement(\"username\", 30);\n    loginPage.EnterText(\"username\", credentials.User);\n    loginPage.EnterText(\"password\", credentials.Pass);\n    \n    // Handle 2FA with complex conditional logic\n    if (loginPage.Has2FAPrompt()) {\n      var method = loginPage.Get2FAMethod();\n      switch(method) {\n        case \"SMS\":\n          // 50 more lines of code\n        case \"Email\":\n          // 50 more lines of code\n        case \"Authenticator\":\n          // 50 more lines of code\n      }\n    }\n    \n    // Download statements with exact selectors\n    navigation.ClickElement(\"xpath://div[@id='acct-menu']\");\n    navigation.ClickElement(\"xpath://a[contains(@href,'statements')]\");\n    // ... continues for hundreds more lines\n    ```\n  </Tab>\n  <Tab title=\"Bytebot\">\n    ```\n    Task: \"Log into Chase banking portal, navigate to statements, \n    download all statements from last month for account ending in 4521, \n    and save them to Finance/BankStatements/Chase/\"\n    \n    That's it. Bytebot handles everything - including 2FA - automatically.\n    ```\n  </Tab>\n</Tabs>\n\n### Multi-System Integration\n\nA FinTech company needed to automate operators who:\n1. Log into multiple banking portals with 2FA\n2. Download transaction files\n3. Run proprietary scripts on those files\n4. Upload results to internal systems\n\n**Traditional RPA Challenge:**\n- 6 months to implement\n- Breaks monthly with UI changes\n- Requires dedicated maintenance team\n- Can't handle new banks without development\n- Complex 2FA handling logic for each bank\n\n**Bytebot Solution:**\n- Deployed in 1 week\n- Adapts to UI changes automatically\n- 2FA handled automatically via password manager\n- New banks added with simple instructions\n- Zero manual intervention required\n\n## Performance Comparison\n\n| Metric | Traditional RPA | Bytebot |\n|--------|----------------|---------|\n| **Implementation Time** | 3-6 months | 1-2 weeks |\n| **Developer Requirement** | RPA specialists | Any technical user |\n| **Maintenance Effort** | 40% of dev time | Near zero |\n| **Handling UI Changes** | Breaks immediately | Adapts automatically |\n| **Error Recovery** | Pre-scripted only | Intelligent adaptation |\n| **New Process Addition** | Weeks of development | Minutes to describe |\n| **Cost** | $100k+ annually | Self-hosted on your infrastructure |\n\n## Common RPA Migration Patterns\n\n### 1. Invoice Processing\n\n**Before (UiPath):**\n- 2000+ lines of workflow XML\n- Breaks when vendor portal updates\n- Requires exact folder structures\n- Failed on unexpected popups\n\n**After (Bytebot):**\n- One paragraph description\n- Handles portal changes\n- Asks for help when needed\n- Processes variations intelligently\n\n### 2. Compliance Reporting\n\n**Before (Automation Anywhere):**\n- Complex bot orchestration\n- Separate bots per system\n- Rigid scheduling\n- No flexibility\n\n**After (Bytebot):**\n- Single unified workflow\n- Natural language instructions\n- Dynamic adaptation\n- Human collaboration when needed\n\n### 3. Data Migration\n\n**Before (Blue Prism):**\n- Massive process definitions\n- Exact field mapping required\n- Breaks on data variations\n- Limited error handling\n\n**After (Bytebot):**\n- Describe the mapping rules\n- Handles variations intelligently\n- Asks for clarification\n- Visual validation included\n\n## Integration with Existing RPA\n\nBytebot can work alongside existing RPA investments:\n\n```mermaid\ngraph LR\n    A[Legacy RPA] -->|Handles stable processes| B[Structured Systems]\n    C[Bytebot] -->|Handles complex/changing processes| D[Dynamic Systems]\n    C -->|Takes over when RPA fails| A\n    E[Human Operator] -->|Guides via takeover mode| C\n```\n\n## Enterprise Architecture\n\n### Deployment Options\n\n<CardGroup cols={2}>\n  <Card title=\"On-Premise\" icon=\"server\">\n    Deploy in your data center for maximum security and compliance\n  </Card>\n  <Card title=\"Private Cloud\" icon=\"cloud\">\n    Use your AWS/Azure/GCP infrastructure with full control\n  </Card>\n  <Card title=\"Hybrid\" icon=\"arrows-split-up-and-left\">\n    Process sensitive data locally, leverage cloud for scaling\n  </Card>\n  <Card title=\"Air-Gapped\" icon=\"shield\">\n    Completely isolated deployment for classified environments\n  </Card>\n</CardGroup>\n\n### Security & Compliance\n\n- **Data Sovereignty**: All processing on your infrastructure\n- **Audit Trails**: Complete logs of every action\n- **Access Control**: Integrate with your IAM/SSO\n- **Compliance**: SOC2, HIPAA, PCI-DSS compatible deployments\n\n## Getting Started with Migration\n\n<Steps>\n  <Step title=\"Identify Processes\">\n    List your current RPA workflows, especially:\n    - Those that break frequently\n    - Require regular maintenance\n    - Handle multiple systems\n    - Need human decision points\n  </Step>\n  \n  <Step title=\"Start Small\">\n    Pick one problematic workflow:\n    - Document the business process\n    - Deploy Bytebot\n    - Describe the task naturally\n    - Compare results\n  </Step>\n  \n  <Step title=\"Expand Gradually\">\n    As confidence grows:\n    - Migrate more complex processes\n    - Retire brittle RPA bots\n    - Reduce maintenance overhead\n    - Scale across departments\n  </Step>\n</Steps>\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Quick Start\" icon=\"rocket\" href=\"/quickstart\">\n    Deploy Bytebot in your environment\n  </Card>\n  <Card title=\"GitHub\" icon=\"github\" href=\"https://github.com/bytebot-ai/bytebot\">\n    View source code and contribute\n  </Card>\n  <Card title=\"Community\" icon=\"users\" href=\"https://discord.gg/zcb5wA2t4u\">\n    Join our Discord for support\n  </Card>\n  <Card title=\"Enterprise Support\" icon=\"users\" href=\"https://discord.gg/zcb5wA2t4u\">\n    Get help with enterprise deployments\n  </Card>\n</CardGroup>\n\n<Note>\n  **Ready to move beyond traditional RPA?** Bytebot brings human-like intelligence to process automation, eliminating the brittleness and complexity of traditional tools while delivering enterprise-grade reliability and security.\n</Note>"
  },
  {
    "path": "docs/deployment/helm.mdx",
    "content": "---\ntitle: \"Helm Deployment\"\ndescription: \"Deploy Bytebot on Kubernetes using Helm charts\"\n---\n\n# Deploy Bytebot on Kubernetes with Helm\n\nHelm provides a simple way to deploy Bytebot on Kubernetes clusters.\n\n## Prerequisites\n\n- Kubernetes cluster (1.19+)\n- Helm 3.x installed\n- kubectl configured\n- 8GB+ available memory in cluster\n\n## Quick Start\n\n<Steps>\n  <Step title=\"Clone Repository\">\n    ```bash\n    git clone https://github.com/bytebot-ai/bytebot.git\n    cd bytebot\n    ```\n  </Step>\n  \n  <Step title=\"Configure API Keys\">\n    Create a `values.yaml` file with at least one API key:\n    \n    ```yaml\n    bytebot-agent:\n      apiKeys:\n        anthropic:\n          value: \"sk-ant-your-key-here\"\n        # Optional: Add more providers\n        # openai:\n        #   value: \"sk-your-key-here\"\n        # gemini:\n        #   value: \"your-key-here\"\n    ```\n  </Step>\n  \n  <Step title=\"Install Bytebot\">\n    ```bash\n    helm install bytebot ./helm \\\n      --namespace bytebot \\\n      --create-namespace \\\n      -f values.yaml\n    ```\n  </Step>\n  \n  <Step title=\"Access Bytebot\">\n    ```bash\n    # Port-forward for local access\n    kubectl port-forward -n bytebot svc/bytebot-ui 9992:9992\n    \n    # Access at http://localhost:9992\n    ```\n  </Step>\n</Steps>\n\n## Basic Configuration\n\n### API Keys\n\nConfigure at least one AI provider:\n\n```yaml\nbytebot-agent:\n  apiKeys:\n    anthropic:\n      value: \"sk-ant-your-key-here\"\n    openai:\n      value: \"sk-your-key-here\"\n    gemini:\n      value: \"your-key-here\"\n```\n\n### Resource Limits (Optional)\n\nAdjust resources based on your needs:\n\n```yaml\n# Desktop container (where automation runs)\ndesktop:\n  resources:\n    requests:\n      memory: \"2Gi\"\n      cpu: \"1\"\n    limits:\n      memory: \"4Gi\"\n      cpu: \"2\"\n\n# Agent (AI orchestration)\nagent:\n  resources:\n    requests:\n      memory: \"1Gi\"\n      cpu: \"500m\"\n```\n\n### External Access (Optional)\n\nEnable ingress for domain-based access:\n\n```yaml\nui:\n  ingress:\n    enabled: true\n    hostname: bytebot.your-domain.com\n    tls: true\n```\n\n## Accessing Bytebot\n\n### Local Access (Recommended)\n\n```bash\nkubectl port-forward -n bytebot svc/bytebot-ui 9992:9992\n```\n\nAccess at: http://localhost:9992\n\n### External Access\n\nIf you configured ingress:\n- Access at: https://bytebot.your-domain.com\n\n## Verifying Deployment\n\nCheck that all pods are running:\n\n```bash\nkubectl get pods -n bytebot\n```\n\nExpected output:\n```\nNAME                              READY   STATUS    RESTARTS   AGE\nbytebot-agent-xxxxx               1/1     Running   0          2m\nbytebot-desktop-xxxxx             1/1     Running   0          2m\nbytebot-postgresql-0              1/1     Running   0          2m\nbytebot-ui-xxxxx                 1/1     Running   0          2m\n```\n\n## Troubleshooting\n\n### Pods Not Starting\n\nCheck pod status:\n```bash\nkubectl describe pod -n bytebot <pod-name>\n```\n\nCommon issues:\n- Insufficient memory/CPU: Check node resources with `kubectl top nodes`\n- Missing API keys: Verify your values.yaml configuration\n\n### Connection Issues\n\nTest service connectivity:\n```bash\nkubectl logs -n bytebot deployment/bytebot-agent\n```\n\n### View Logs\n\n```bash\n# All logs\nkubectl logs -n bytebot -l app=bytebot --tail=100\n\n# Specific component\nkubectl logs -n bytebot deployment/bytebot-agent\n```\n\n## Upgrading\n\n```bash\n# Update your values.yaml as needed, then:\nhelm upgrade bytebot ./helm -n bytebot -f values.yaml\n```\n\n## Uninstalling\n\n```bash\n# Remove Bytebot\nhelm uninstall bytebot -n bytebot\n\n# Clean up namespace\nkubectl delete namespace bytebot\n```\n\n## Advanced Configuration\n\n<AccordionGroup>\n  <Accordion title=\"Using External Secrets\">\n    If using Kubernetes secret management (Vault, Sealed Secrets, etc.):\n    \n    ```yaml\n    bytebot-agent:\n      apiKeys:\n        anthropic:\n          useExisting: true\n          secretName: \"my-api-keys\"\n          secretKey: \"anthropic-key\"\n    ```\n    \n    Create the secret manually:\n    ```bash\n    kubectl create secret generic my-api-keys \\\n      --namespace bytebot \\\n      --from-literal=anthropic-key=\"sk-ant-your-key\"\n    ```\n  </Accordion>\n  \n  <Accordion title=\"LiteLLM Proxy Mode\">\n    For centralized LLM management, use the included LiteLLM proxy:\n    \n    ```bash\n    helm install bytebot ./helm \\\n      -f values-proxy.yaml \\\n      --namespace bytebot \\\n      --create-namespace \\\n      --set bytebot-llm-proxy.env.ANTHROPIC_API_KEY=\"your-key\"\n    ```\n    \n    This provides:\n    - Centralized API key management\n    - Request routing and load balancing\n    - Rate limiting and retry logic\n  </Accordion>\n  \n  <Accordion title=\"Custom Storage\">\n    Configure persistent storage:\n    \n    ```yaml\n    desktop:\n      persistence:\n        enabled: true\n        size: \"20Gi\"\n        storageClass: \"fast-ssd\"\n    \n    postgresql:\n      persistence:\n        size: \"20Gi\"\n        storageClass: \"fast-ssd\"\n    ```\n  </Accordion>\n  \n  <Accordion title=\"Production Security\">\n    ```yaml\n    # Network policies\n    networkPolicy:\n      enabled: true\n    \n    # Pod security\n    podSecurityContext:\n      runAsNonRoot: true\n      runAsUser: 1000\n      fsGroup: 1000\n    \n    # Enable authentication\n    auth:\n      enabled: true\n      type: \"basic\"\n      username: \"admin\"\n      password: \"changeme\"  # Use secrets in production!\n    ```\n  </Accordion>\n</AccordionGroup>\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"API Reference\" icon=\"code\" href=\"/api-reference/introduction\">\n    Integrate Bytebot with your applications\n  </Card>\n  <Card title=\"LiteLLM Integration\" icon=\"plug\" href=\"/deployment/litellm\">\n    Use any LLM provider with Bytebot\n  </Card>\n</CardGroup>\n\n<Note>\n  **Need help?** Join our [Discord community](https://discord.com/invite/d9ewZkWPTP) or check our [GitHub discussions](https://github.com/bytebot-ai/bytebot/discussions).\n</Note>"
  },
  {
    "path": "docs/deployment/litellm.mdx",
    "content": "---\ntitle: \"LiteLLM Integration\"\ndescription: \"Use any LLM provider with Bytebot through LiteLLM proxy\"\n---\n\n# Connect Any LLM to Bytebot with LiteLLM\n\nLiteLLM acts as a unified proxy that lets you use 100+ LLM providers with Bytebot - including Azure OpenAI, AWS Bedrock, Anthropic, Hugging Face, Ollama, and more. This guide shows you how to set up LiteLLM with Bytebot.\n\n## Why Use LiteLLM?\n\n<CardGroup cols={2}>\n  <Card title=\"100+ LLM Providers\" icon=\"plug\">\n    Use Azure, AWS, GCP, Anthropic, OpenAI, Cohere, and local models\n  </Card>\n  <Card title=\"Cost Tracking\" icon=\"dollar-sign\">\n    Monitor spending across all providers in one place\n  </Card>\n  <Card title=\"Load Balancing\" icon=\"scale-balanced\">\n    Distribute requests across multiple models and providers\n  </Card>\n  <Card title=\"Fallback Models\" icon=\"shield\">\n    Automatic failover when primary models are unavailable\n  </Card>\n</CardGroup>\n\n## Quick Start with Bytebot's Built-in LiteLLM Proxy\n\nBytebot includes a pre-configured LiteLLM proxy service that makes it easy to use any LLM provider. Here's how to set it up:\n\n<Steps>\n  <Step title=\"Use Docker Compose with Proxy\">\n    The easiest way is to use the proxy-enabled Docker Compose file:\n    \n    ```bash\n    # Clone Bytebot\n    git clone https://github.com/bytebot-ai/bytebot.git\n    cd bytebot\n    \n    # Set up your API keys in docker/.env\n    cat > docker/.env << EOF\n    # Add any combination of these keys\n    ANTHROPIC_API_KEY=sk-ant-your-key-here\n    OPENAI_API_KEY=sk-your-key-here  \n    GEMINI_API_KEY=your-key-here\n    EOF\n    \n    # Start Bytebot with LiteLLM proxy\n    docker-compose -f docker/docker-compose.proxy.yml up -d\n    ```\n    \n    This automatically:\n    - Starts the `bytebot-llm-proxy` service on port 4000\n    - Configures the agent to use the proxy via `BYTEBOT_LLM_PROXY_URL`\n    - Makes all configured models available through the proxy\n  </Step>\n  \n  <Step title=\"Customize Model Configuration\">\n    To add custom models or providers, edit the LiteLLM config:\n    \n    ```yaml\n    # packages/bytebot-llm-proxy/litellm-config.yaml\n    model_list:\n      # Add Azure OpenAI\n      - model_name: azure-gpt-4o\n        litellm_params:\n          model: azure/gpt-4o-deployment\n          api_base: https://your-resource.openai.azure.com/\n          api_key: os.environ/AZURE_API_KEY\n          api_version: \"2024-02-15-preview\"\n      \n      # Add AWS Bedrock\n      - model_name: claude-bedrock\n        litellm_params:\n          model: bedrock/anthropic.claude-3-5-sonnet\n          aws_region_name: us-east-1\n      \n      # Add local models via Ollama\n      - model_name: local-llama\n        litellm_params:\n          model: ollama/llama3:70b\n          api_base: http://host.docker.internal:11434\n    ```\n    \n    Then rebuild:\n    ```bash\n    docker-compose -f docker/docker-compose.proxy.yml up -d --build\n    ```\n  </Step>\n  \n  <Step title=\"Verify Models are Available\">\n    The Bytebot agent automatically queries the proxy for available models:\n    \n    ```bash\n    # Check available models through Bytebot API\n    curl http://localhost:9991/tasks/models\n    \n    # Or directly from LiteLLM proxy\n    curl http://localhost:4000/model/info\n    ```\n    \n    The UI will show all available models in the model selector.\n  </Step>\n</Steps>\n\n## How It Works\n\n### Architecture\n\n```mermaid\ngraph LR\n    A[Bytebot UI] -->|Select Model| B[Bytebot Agent]\n    B -->|BYTEBOT_LLM_PROXY_URL| C[LiteLLM Proxy :4000]\n    C -->|Route Request| D[Anthropic API]\n    C -->|Route Request| E[OpenAI API]  \n    C -->|Route Request| F[Google API]\n    C -->|Route Request| G[Any Provider]\n```\n\n### Key Components\n\n1. **bytebot-llm-proxy Service**: A LiteLLM instance running in Docker that:\n   - Runs on port 4000 within the Bytebot network\n   - Uses the config from `packages/bytebot-llm-proxy/litellm-config.yaml`\n   - Inherits API keys from environment variables\n\n2. **Agent Integration**: The Bytebot agent:\n   - Checks for `BYTEBOT_LLM_PROXY_URL` environment variable\n   - If set, queries the proxy at `/model/info` for available models\n   - Routes all LLM requests through the proxy\n\n3. **Pre-configured Models**: Out of the box support for:\n   - Anthropic: Claude Opus 4, Claude Sonnet 4\n   - OpenAI: GPT-4.1, GPT-4o\n   - Google: Gemini 2.5 Pro, Gemini 2.5 Flash\n\n## Provider Configurations\n\n### Azure OpenAI\n\n```yaml\nmodel_list:\n  - model_name: azure-gpt-4o\n    litellm_params:\n      model: azure/gpt-4o-deployment-name\n      api_base: https://your-resource.openai.azure.com/\n      api_key: your-azure-key\n      api_version: \"2024-02-15-preview\"\n  \n  - model_name: azure-gpt-4o-vision\n    litellm_params:\n      model: azure/gpt-4o-deployment-name\n      api_base: https://your-resource.openai.azure.com/\n      api_key: your-azure-key\n      api_version: \"2024-02-15-preview\"\n      supports_vision: true\n```\n\n### AWS Bedrock\n\n```yaml\nmodel_list:\n  - model_name: claude-bedrock\n    litellm_params:\n      model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0\n      aws_region_name: us-east-1\n      # Uses AWS credentials from environment\n  \n  - model_name: llama-bedrock\n    litellm_params:\n      model: bedrock/meta.llama3-70b-instruct-v1:0\n      aws_region_name: us-east-1\n```\n\n### Google Vertex AI\n\n```yaml\nmodel_list:\n  - model_name: gemini-vertex\n    litellm_params:\n      model: vertex_ai/gemini-1.5-pro\n      vertex_project: your-gcp-project\n      vertex_location: us-central1\n      # Uses GCP credentials from environment\n```\n\n### Local Models (Ollama)\n\n```yaml\nmodel_list:\n  - model_name: local-llama\n    litellm_params:\n      model: ollama/llama3:70b\n      api_base: http://ollama:11434\n  \n  - model_name: local-mixtral\n    litellm_params:\n      model: ollama/mixtral:8x7b\n      api_base: http://ollama:11434\n```\n\n### Hugging Face\n\n```yaml\nmodel_list:\n  - model_name: hf-llama\n    litellm_params:\n      model: huggingface/meta-llama/Llama-3-70b-chat-hf\n      api_key: hf_your_token\n```\n\n## Advanced Features\n\n### Load Balancing\n\nDistribute requests across multiple providers:\n\n```yaml\nmodel_list:\n  - model_name: gpt-4o\n    litellm_params:\n      model: gpt-4o\n      api_key: sk-openai-key\n  \n  - model_name: gpt-4o  # Same name for load balancing\n    litellm_params:\n      model: azure/gpt-4o\n      api_base: https://azure.openai.azure.com/\n      api_key: azure-key\n\nrouter_settings:\n  routing_strategy: \"least-busy\"  # or \"round-robin\", \"latency-based\"\n```\n\n### Fallback Models\n\nConfigure automatic failover:\n\n```yaml\nmodel_list:\n  - model_name: primary-model\n    litellm_params:\n      model: claude-3-5-sonnet-20241022\n      api_key: sk-ant-key\n  \n  - model_name: fallback-model\n    litellm_params:\n      model: gpt-4o\n      api_key: sk-openai-key\n\nrouter_settings:\n  model_group_alias:\n    \"smart-model\": [\"primary-model\", \"fallback-model\"]\n  \n# Use \"smart-model\" in Bytebot config\n```\n\n### Cost Controls\n\nSet spending limits and track usage:\n\n```yaml\ngeneral_settings:\n  master_key: sk-litellm-master\n  database_url: \"postgresql://user:pass@localhost:5432/litellm\"\n  \n  # Budget limits\n  max_budget: 100  # $100 monthly limit\n  budget_duration: \"30d\"\n  \n  # Per-model limits\n  model_max_budget:\n    gpt-4o: 50\n    claude-3-5-sonnet: 50\n\nlitellm_settings:\n  callbacks: [\"langfuse\"]  # For detailed tracking\n```\n\n### Rate Limiting\n\nPrevent API overuse:\n\n```yaml\nmodel_list:\n  - model_name: rate-limited-gpt\n    litellm_params:\n      model: gpt-4o\n      api_key: sk-key\n      rpm: 100  # Requests per minute\n      tpm: 100000  # Tokens per minute\n```\n\n## Alternative Setup: External LiteLLM Proxy\n\nIf you prefer to run LiteLLM separately or have an existing LiteLLM deployment:\n\n### Option 1: Modify docker-compose.yml\n\n```yaml\n# docker-compose.yml (without built-in proxy)\nservices:\n  bytebot-agent:\n    environment:\n      # Point to your external LiteLLM instance\n      - BYTEBOT_LLM_PROXY_URL=http://your-litellm-server:4000\n    # ... rest of config\n```\n\n### Option 2: Use Environment Variable\n\n```bash\n# Set the proxy URL before starting\nexport BYTEBOT_LLM_PROXY_URL=http://your-litellm-server:4000\n\n# Start normally\ndocker-compose -f docker/docker-compose.yml up -d\n```\n\n### Option 3: Run Standalone LiteLLM\n\n```bash\n# Run your own LiteLLM instance\ndocker run -d \\\n  --name litellm-external \\\n  -p 4000:4000 \\\n  -v $(pwd)/custom-config.yaml:/app/config.yaml \\\n  -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \\\n  ghcr.io/berriai/litellm:main \\\n  --config /app/config.yaml\n\n# Then start Bytebot with:\nexport BYTEBOT_LLM_PROXY_URL=http://localhost:4000\ndocker-compose up -d\n```\n\n## Kubernetes Setup\n\nDeploy with Helm:\n\n```yaml\n# litellm-values.yaml\nreplicaCount: 2\n\nimage:\n  repository: ghcr.io/berriai/litellm\n  tag: main\n\nservice:\n  type: ClusterIP\n  port: 4000\n\nconfig:\n  model_list:\n    - model_name: claude-3-5-sonnet\n      litellm_params:\n        model: claude-3-5-sonnet-20241022\n        api_key: ${ANTHROPIC_API_KEY}\n  \n  general_settings:\n    master_key: ${LITELLM_MASTER_KEY}\n\n# Then in Bytebot values.yaml:\nagent:\n  openai:\n    enabled: true\n    apiKey: \"${LITELLM_MASTER_KEY}\"\n    baseUrl: \"http://litellm:4000/v1\"\n    model: \"claude-3-5-sonnet\"\n```\n\n## Monitoring & Debugging\n\n### LiteLLM Dashboard\n\nAccess metrics and logs:\n\n```bash\n# Port forward to dashboard\nkubectl port-forward svc/litellm 4000:4000\n\n# Access at http://localhost:4000/ui\n# Login with your master_key\n```\n\n### Debug Requests\n\nEnable detailed logging:\n\n```yaml\nlitellm_settings:\n  debug: true\n  detailed_debug: true\n  \ngeneral_settings:\n  master_key: sk-key\n  store_model_in_db: true  # Store request history\n```\n\n### Common Issues\n\n<AccordionGroup>\n  <Accordion title=\"Model not found\">\n    Check model name matches exactly:\n    ```bash\n    curl http://localhost:4000/v1/models \\\n      -H \"Authorization: Bearer sk-key\"\n    ```\n  </Accordion>\n  \n  <Accordion title=\"Authentication errors\">\n    Verify master key in both LiteLLM and Bytebot:\n    ```bash\n    # Test LiteLLM\n    curl http://localhost:4000/v1/chat/completions \\\n      -H \"Authorization: Bearer sk-key\" \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"model\": \"your-model\", \"messages\": [{\"role\": \"user\", \"content\": \"test\"}]}'\n    ```\n  </Accordion>\n  \n  <Accordion title=\"Slow responses\">\n    Check latency per provider:\n    ```yaml\n    router_settings:\n      routing_strategy: \"latency-based\"\n      enable_pre_call_checks: true\n    ```\n  </Accordion>\n</AccordionGroup>\n\n## Best Practices\n\n### Model Selection for Bytebot\n\nChoose models with strong vision capabilities for best results:\n\n<Tabs>\n  <Tab title=\"Recommended\">\n    - Claude 3.5 Sonnet (Best overall)\n    - GPT-4o (Good vision + reasoning)\n    - Gemini 1.5 Pro (Large context)\n  </Tab>\n  <Tab title=\"Budget Options\">\n    - Claude 3.5 Haiku (Fast + cheap)\n    - GPT-4o mini (Good balance)\n    - Gemini 1.5 Flash (Very fast)\n  </Tab>\n  <Tab title=\"Local Models\">\n    - LLaVA (Vision support)\n    - Qwen-VL (Vision support)\n    - CogVLM (Vision support)\n  </Tab>\n</Tabs>\n\n### Performance Optimization\n\n```yaml\n# Optimize for Bytebot workloads\nrouter_settings:\n  routing_strategy: \"latency-based\"\n  cooldown_time: 60  # Seconds before retrying failed provider\n  num_retries: 2\n  request_timeout: 600  # 10 minutes for complex tasks\n  \n  # Cache for repeated requests\n  cache: true\n  cache_params:\n    type: \"redis\"\n    host: \"redis\"\n    port: 6379\n    ttl: 3600  # 1 hour\n```\n\n### Security\n\n```yaml\ngeneral_settings:\n  master_key: ${LITELLM_MASTER_KEY}\n  \n  # IP allowlist\n  allowed_ips: [\"10.0.0.0/8\", \"172.16.0.0/12\"]\n  \n  # Audit logging\n  store_model_in_db: true\n  \n  # Encryption\n  encrypt_keys: true\n  \n  # Headers to forward\n  forward_headers: [\"X-Request-ID\", \"X-User-ID\"]\n```\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Supported Models\" icon=\"list\" href=\"https://docs.litellm.ai/docs/providers\">\n    Full list of 100+ providers\n  </Card>\n  <Card title=\"LiteLLM Proxy Docs\" icon=\"server\" href=\"https://docs.litellm.ai/docs/simple_proxy\">\n    Official LiteLLM proxy server documentation\n  </Card>\n  <Card title=\"LiteLLM Docs\" icon=\"book\" href=\"https://docs.litellm.ai\">\n    Complete LiteLLM documentation\n  </Card>\n</CardGroup>\n\n<Note>\n  **Pro tip:** Start with a single provider, then add more as needed. LiteLLM makes it easy to switch or combine models without changing Bytebot configuration.\n</Note>"
  },
  {
    "path": "docs/deployment/railway.mdx",
    "content": "---\ntitle: \"Deploying Bytebot on Railway\"\ndescription: \"Comprehensive guide to deploying the full Bytebot stack on Railway using the official 1-click template\"\n---\n\n> **TL;DR –** Click the button below, add your AI API key (Anthropic, OpenAI, or Google), and your personal Bytebot instance will be live in ~2 minutes.\n\n[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)\n\n---\n\n## Why Railway?\n\nRailway provides a zero-ops PaaS experience with private networking and per-service logs that perfectly fits Bytebot’s multi-container architecture. The official template wires every service together using the latest container images pushed to the `edge` branch.\n\n---\n\n## What Gets Deployed\n\n| Service          | Container Image (edge)                                               | Port | Exposed? | Purpose                              |\n| ---------------- | -------------------------------------------------------------------- | ---- | -------- | ------------------------------------ |\n| **bytebot-ui**   | `ghcr.io/bytebot-ai/bytebot-ui:edge`                                 | 9992 | **Yes**  | Next.js web UI rendered to the world |\n| **bytebot-agent**| `ghcr.io/bytebot-ai/bytebot-agent:edge`                              | 9991 | No       | Task orchestration & LLM calls       |\n| **bytebot-desktop**| `ghcr.io/bytebot-ai/bytebot-desktop:edge`                          | 9990 | No       | Containerised Ubuntu + XFCE desktop  |\n| **postgres**     | `postgres:14-alpine`                                                 | 5432 | No       | Persistence layer                    |\n\nAll internal traffic flows through Railway’s [private networking](https://docs.railway.com/guides/private-networking). Only `bytebot-ui` is assigned a public domain.\n\n---\n\n## Step-by-Step Walk-through\n\n<Steps>\n  <Step title=\"1. Open the Template\">\n    Click the **Deploy on Railway** button above or visit [https://railway.com/deploy/bytebot?referralCode=L9lKXQ](https://railway.com/deploy/bytebot?referralCode=L9lKXQ).\n  </Step>\n  <Step title=\"2. Configure Environment\">\n    For the bytebot-agent resource, add your AI API key (choose at least one):\n    - **Anthropic**: Paste into `ANTHROPIC_API_KEY` for Claude models\n    - **OpenAI**: Paste into `OPENAI_API_KEY` for GPT models\n    - **Google**: Paste into `GEMINI_API_KEY` for Gemini models\n    \n    Keep other defaults as is.\n  </Step>\n  <Step title=\"3. Kick off the Deployment\">\n    Press **Deploy**. Railway will pull the pre-built images, create the Postgres database and link all services on a private network.\n  </Step>\n  <Step title=\"4. Launch Bytebot\">\n    When the build logs show *\"bytebot-ui: ready\"*, click the generated URL (e.g. `https://bytebot-ui-prod.up.railway.app`). You should see the task interface. Create a task and watch the desktop stream!  \n    _Tip: You can tail logs for each service from the Railway dashboard._\n  </Step>\n</Steps>\n\n<Note>\n  The first deploy downloads several container layers – expect ~2 minutes. Subsequent redeploys are much faster.\n</Note>\n\n---\n\n## Private Networking & Security\n\n• **Private networking** ensures that the agent, desktop and database can communicate securely without exposing their ports to the internet.  \n• **Public exposure** is limited to the UI which serves static assets and proxies WebSocket traffic.  \n• **Add authentication** by placing the UI behind Railway’s built-in password protection or an external provider (e.g. Cloudflare Access, Auth0, OAuth proxy).  \n• You can also point a custom domain to the UI from the Railway dashboard and enable Cloudflare for WAF/CDN protection.\n\n---\n\n## Customisation & Scaling\n\n1. **Change images** – Fork the repo, push your own images and edit the template’s `Dockerfile` references.  \n2. **Increase resources** – Each service has an independent CPU/RAM slider in Railway. Bump up the desktop or agent if you plan heavy automations.  \n\n---\n\n## Troubleshooting\n\n| Symptom | Likely Cause | Fix |\n| ------- | ------------ | ---- |\n| Web UI shows “connecting…” | Desktop not ready or private networking mis-config | Wait for `bytebot-desktop` container to finish starting, or restart service |\n| Agent errors `401` or `403` | Missing/invalid API key | Re-enter your AI provider's API key in Railway variables |\n| Slow desktop video | Free Railway plan throttling | Upgrade plan or reduce screen resolution in desktop settings |\n\n---\n\n## Next Steps\n\n• Explore the [REST APIs](/api-reference/introduction) to script tasks programmatically.  \n• Join our [Discord](https://discord.com/invite/d9ewZkWPTP) community for support and showcase your automations!\n"
  },
  {
    "path": "docs/docs.json",
    "content": "{\n  \"$schema\": \"https://mintlify.com/docs.json\",\n  \"theme\": \"mint\",\n  \"name\": \"Bytebot - Self-Hosted AI Desktop Agent\",\n  \"colors\": {\n    \"primary\": \"#000000\",\n    \"light\": \"#fbfaf9\",\n    \"dark\": \"#000000\"\n  },\n  \"favicon\": \"/favicon.svg\",\n  \"navigation\": {\n    \"tabs\": [\n      {\n        \"tab\": \"Documentation\",\n        \"groups\": [\n          {\n            \"group\": \"Getting Started\",\n            \"pages\": [\"introduction\", \"quickstart\"]\n          },\n          {\n            \"group\": \"User Guides\",\n            \"pages\": [\n              \"guides/task-creation\",\n              \"guides/password-management\",\n              \"guides/takeover-mode\"\n            ]\n          },\n          {\n            \"group\": \"Deployment\",\n            \"pages\": [\n              \"deployment/railway\",\n              \"deployment/helm\",\n              \"deployment/litellm\"\n            ]\n          },\n          {\n            \"group\": \"Core Concepts\",\n            \"pages\": [\n              \"core-concepts/architecture\",\n              \"core-concepts/agent-system\",\n              \"core-concepts/desktop-environment\",\n              \"core-concepts/rpa-comparison\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tab\": \"API Reference\",\n        \"groups\": [\n          {\n            \"group\": \"Overview\",\n            \"pages\": [\"api-reference/introduction\"]\n          },\n          {\n            \"group\": \"Agent API\",\n            \"pages\": [\n              \"api-reference/agent/tasks\",\n              \"api-reference/agent/ui\"\n            ]\n          },\n          {\n            \"group\": \"Computer Control API\",\n            \"pages\": [\n              \"api-reference/computer-use/unified-endpoint\",\n              \"api-reference/computer-use/examples\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"global\": {\n      \"anchors\": [\n        {\n          \"anchor\": \"GitHub\",\n          \"href\": \"https://github.com/bytebot-ai/bytebot\",\n          \"icon\": \"github\"\n        },\n        {\n          \"anchor\": \"Discord\",\n          \"href\": \"https://discord.gg/zcb5wA2t4u\",\n          \"icon\": \"discord\"\n        },\n        {\n          \"anchor\": \"Twitter\",\n          \"href\": \"https://x.com/bytebot_ai\",\n          \"icon\": \"twitter\"\n        },\n        {\n          \"anchor\": \"Blog\",\n          \"href\": \"https://bytebot.ai/blog\",\n          \"icon\": \"newspaper\"\n        }\n      ]\n    }\n  },\n  \"logo\": {\n    \"light\": \"/logo/bytebot_transparent_logo_dark.svg\",\n    \"dark\": \"/logo/bytebot_transparent_logo_white.svg\"\n  },\n  \"navbar\": {\n    \"links\": [\n      {\n        \"label\": \"Support\",\n        \"href\": \"https://discord.gg/zcb5wA2t4u\"\n      }\n    ],\n    \"primary\": {\n      \"type\": \"button\",\n      \"label\": \"Get Started\",\n      \"href\": \"https://github.com/bytebot-ai/bytebot\"\n    }\n  },\n  \"footer\": {\n    \"socials\": {\n      \"github\": \"https://github.com/bytebot-ai/bytebot\",\n      \"twitter\": \"https://twitter.com/bytebotai\",\n      \"discord\": \"https://discord.gg/zcb5wA2t4u\"\n    }\n  },\n  \"metadata\": {\n    \"og:title\": \"Bytebot - Self-Hosted AI Desktop Agent\",\n    \"og:description\": \"Automate any computer task with natural language using your own AI desktop agent\",\n    \"og:image\": \"/images/agent-architecture.png\",\n    \"twitter:card\": \"summary_large_image\"\n  }\n}"
  },
  {
    "path": "docs/guides/password-management.mdx",
    "content": "---\ntitle: \"Password Management & 2FA\"\ndescription: \"How Bytebot handles authentication automatically using password managers\"\n---\n\n# Automated Authentication with Bytebot\n\nBytebot can handle authentication automatically - including passwords, 2FA, and even complex multi-step authentication flows - when you set up a password manager extension.\n\n<Note>\n  **Important**: Password manager extensions are not enabled by default. You need to install them manually using the desktop view.\n</Note>\n\n## How It Works\n\nBytebot comes with 1Password built-in and supports any browser-based password manager extension. It can:\n\n- Automatically fill passwords from the password manager\n- Handle 2FA codes (TOTP/authenticator apps)\n- Manage multiple accounts across different systems\n- Work with SSO and federated authentication\n- Store and use API keys and tokens\n\n## Setting Up Password Management\n\n### Option 1: 1Password (Recommended)\n\n<Steps>\n  <Step title=\"Install 1Password Extension\">\n    1. Go to the Desktop tab in Bytebot UI\n    2. Open Firefox\n    3. Install the 1Password extension from the Firefox Add-ons store\n    4. Sign in to your 1Password account (or create a dedicated one for Bytebot)\n  </Step>\n  \n  <Step title=\"Configure Vaults\">\n    In your 1Password admin panel:\n    1. Create a vault called \"Bytebot Automation\"\n    2. Add the credentials Bytebot needs\n    3. Share the vault with Bytebot's account\n    4. Set appropriate permissions (read-only recommended)\n  </Step>\n  \n  <Step title=\"Enable Auto-fill\">\n    The 1Password extension will automatically:\n    - Detect login forms\n    - Fill credentials\n    - Handle 2FA codes\n    - Submit forms\n  </Step>\n</Steps>\n\n### Option 2: Other Password Managers\n\nYou can use any browser-based password manager by installing it through the Desktop view:\n\n<Tabs>\n  <Tab title=\"Bitwarden\">\n    1. Open Desktop tab\n    2. Launch Firefox\n    3. Install Bitwarden extension from Firefox Add-ons\n    4. Log in to your Bitwarden account\n    5. Configure auto-fill settings in Bitwarden preferences\n  </Tab>\n  <Tab title=\"LastPass\">\n    1. Open Desktop tab\n    2. Launch Firefox\n    3. Install LastPass extension from Firefox Add-ons\n    4. Log in with your enterprise account\n    5. Accept any shared folders for automation credentials\n  </Tab>\n  <Tab title=\"KeePass\">\n    1. Open Desktop tab\n    2. Install KeePassXC application if needed\n    3. Install KeePassXC browser extension in Firefox\n    4. Configure browser integration\n    5. Load your KeePass database\n  </Tab>\n</Tabs>\n\n## Handling Different Authentication Types\n\n### Standard Username/Password\n\n```yaml\n# Task description\nTask: \"Log into our CRM system and export the customer list\"\n\n# Bytebot automatically:\n1. Navigates to login page\n2. Password manager detects form\n3. Auto-fills credentials\n4. Submits login\n5. Proceeds with task\n```\n\n### Time-based 2FA (TOTP)\n\n```yaml\n# Task description  \nTask: \"Access the banking portal and download statements\"\n\n# Bytebot handles:\n1. Enters username/password from password manager\n2. When 2FA prompt appears\n3. Password manager provides TOTP code\n4. Enters code automatically\n5. Completes authentication\n```\n\n### Complex Multi-Step Auth\n\n```yaml\n# Task description\nTask: \"Log into the government portal (uses email verification)\"\n\n# Bytebot can:\n1. Fill initial credentials\n2. Handle \"send code to email\" flows\n3. Access webmail account (also in password manager)\n4. Retrieve verification code from webmail\n5. Complete authentication\n```\n\n## Enterprise Setup Guide\n\n### Centralized Credential Management\n\n<Steps>\n  <Step title=\"Create Service Accounts\">\n    Set up dedicated service accounts for Bytebot:\n    ```\n    - bytebot-finance@company.com (banking portals)\n    - bytebot-hr@company.com (HR systems)\n    - bytebot-ops@company.com (operational tools)\n    ```\n  </Step>\n  \n  <Step title=\"Organize Password Vaults\">\n    Structure your password manager:\n    ```\n    Bytebot Vaults/\n    ├── Financial Systems/\n    │   ├── Banking Portal A\n    │   ├── Banking Portal B\n    │   └── Payment Processor\n    ├── Internal Tools/\n    │   ├── ERP System\n    │   ├── CRM Platform\n    │   └── HR Portal\n    └── External Services/\n        ├── Vendor Portal 1\n        ├── Government Site\n        └── Partner System\n    ```\n  </Step>\n  \n  <Step title=\"Set Rotation Policies\">\n    Configure automatic password rotation:\n    ```javascript\n    // Example automation for password rotation\n    {\n      \"schedule\": \"monthly\",\n      \"task\": \"For each credential in 'Rotation Required' vault, \n               update password in the system and save new password\"\n    }\n    ```\n  </Step>\n</Steps>\n\n### Security Best Practices\n\n<CardGroup cols={2}>\n  <Card title=\"Least Privilege\" icon=\"shield-halved\">\n    Only share credentials Bytebot needs for specific tasks\n  </Card>\n  <Card title=\"Audit Logging\" icon=\"scroll\">\n    Enable password manager audit logs to track access\n  </Card>\n  <Card title=\"Vault Isolation\" icon=\"lock\">\n    Separate vaults by sensitivity level and department\n  </Card>\n  <Card title=\"Regular Reviews\" icon=\"calendar-check\">\n    Audit Bytebot's credential access monthly\n  </Card>\n</CardGroup>\n\n## Common Authentication Scenarios\n\n### Banking and Financial Systems\n\n```yaml\nScenario: Daily bank reconciliation across 5 banks\n\nSetup:\n- Each bank credential in password manager\n- 2FA seeds stored for TOTP generation\n- Bytebot's IP whitelisted at banks\n\nTask: \"Log into each bank account, download yesterday's \n       transactions, and consolidate into daily report\"\n\nResult: Fully automated, no human intervention needed\n```\n\n### Government and Compliance Portals\n\n```yaml\nScenario: Weekly regulatory filings\n\nSetup:\n- Service account with 2FA enabled\n- Password manager has TOTP seed\n- Security questions stored as notes\n\nTask: \"Log into state tax portal, file weekly sales tax \n       report using data from tax_data.csv\"\n\nHandles: Password, 2FA, security questions, CAPTCHAs\n```\n\n### Multi-Tenant SaaS Platforms\n\n```yaml\nScenario: Managing multiple client accounts\n\nSetup:\n- Credentials for each tenant/client\n- Organized in password manager by client\n- Naming convention: client-platform-role\n\nTask: \"For each client in client_list.txt, log into their \n       Shopify account and export this month's orders\"\n\nScales: Handles 100+ accounts seamlessly\n```\n\n## Advanced Authentication Features\n\n### SSO and SAML Integration\n\n```yaml\n# Bytebot can handle SSO flows\nTask: \"Log into Salesforce using Okta SSO\"\n\nProcess:\n1. Navigate to Salesforce\n2. Click \"Log in with SSO\"\n3. Redirect to Okta\n4. Password manager fills Okta credentials\n5. Handle any 2FA on Okta\n6. Redirect back to Salesforce\n7. Continue with task\n```\n\n### API Key Management\n\n```yaml\n# Store API keys in password manager\nPassword Entry: \"OpenAI API Key\"\n- Username: \"api\"\n- Password: \"sk-proj-...\"\n- Notes: \"Rate limit: 10000/day\"\n\n# Use in tasks\nTask: \"Configure the application to use our OpenAI API key \n       from the password manager\"\n```\n\n### Certificate-Based Auth\n\n```yaml\n# For systems requiring certificates\nSetup:\n1. Store certificate password in manager\n2. Mount certificate file to Bytebot\n3. Configure browser to use certificate\n\nTask: \"Access the enterprise portal that requires \n       client certificate authentication\"\n```\n\n## Troubleshooting Authentication\n\n<AccordionGroup>\n  <Accordion title=\"Password manager not auto-filling\">\n    **Solutions:**\n    - Ensure extension is installed and logged in\n    - Check site is saved in password manager\n    - Verify auto-fill settings are enabled\n    - Try refreshing the page\n  </Accordion>\n  \n  <Accordion title=\"2FA code rejected\">\n    **Common causes:**\n    - Time sync issues (check system clock)\n    - Wrong TOTP seed saved\n    - Site using non-standard 2FA\n    \n    **Fix:**\n    ```bash\n    # Sync system time\n    docker exec bytebot-desktop ntpdate -s time.nist.gov\n    ```\n  </Accordion>\n  \n  <Accordion title=\"Session expiring during task\">\n    **Solutions:**\n    - Enable \"remember me\" if available\n    - Increase session timeout in target system\n    - Break long tasks into smaller chunks\n    - Use API access where possible\n  </Accordion>\n</AccordionGroup>\n\n## Integration Examples\n\n### Finance Automation Script\n\n```python\n# Example: Automated invoice collection\ntasks = [\n    {\n        \"description\": \"Log into vendor portal A and download all pending invoices\",\n        \"credentials\": \"vault://Financial Systems/Vendor Portal A\"\n    },\n    {\n        \"description\": \"Log into vendor portal B and download all pending invoices\",  \n        \"credentials\": \"vault://Financial Systems/Vendor Portal B\"\n    },\n    {\n        \"description\": \"Process all downloaded invoices through our AP system\",\n        \"credentials\": \"vault://Internal Tools/AP System\"\n    }\n]\n\n# Bytebot handles all authentication automatically\n```\n\n### Compliance Automation\n\n```yaml\nDaily Compliance Check:\n  Morning:\n    - Log into regulatory portal (2FA enabled)\n    - Download new compliance updates\n    - Check our status\n  \n  If Non-Compliant:\n    - Log into internal system\n    - Create compliance ticket\n    - Notify compliance team\n  \n  All credentials managed automatically\n```\n\n## Best Practices Summary\n\n✅ **DO:**\n- Use dedicated service accounts for Bytebot\n- Organize credentials in logical vaults\n- Enable 2FA on all accounts (Bytebot handles it!)\n- Rotate passwords regularly\n- Monitor access logs\n\n❌ **DON'T:**\n- Share personal credentials with Bytebot\n- Store passwords in task descriptions\n- Disable 2FA for convenience\n- Use the same password across systems\n- Ignore authentication errors\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Task Examples\" icon=\"list\" href=\"/guides/task-creation\">\n    See auth in action\n  </Card>\n  <Card title=\"API Integration\" icon=\"code\" href=\"/api-reference/introduction\">\n    Programmatic credential management\n  </Card>\n</CardGroup>\n\n<Note>\n  **Game Changer**: With proper password manager setup, Bytebot can handle even the most complex authentication flows automatically. No more manual intervention for 2FA, no more sharing passwords insecurely, and no more authentication bottlenecks in your automation workflows!\n</Note>"
  },
  {
    "path": "docs/guides/takeover-mode.mdx",
    "content": "---\ntitle: \"Takeover Mode\"\ndescription: \"Take control of the desktop when you need to guide or assist Bytebot\"\n---\n\n# Takeover Mode: Human-AI Collaboration\n\nTakeover mode lets you take control of the desktop to help Bytebot when needed. There are two ways to use it:\n\n## 1. During Task Execution\nIn the task detail view, you can hit the takeover button to:\n- Interrupt the agent if it's going down the wrong path\n- Guide it towards the correct solution\n- Resolve issues when it's stumbling on something\n\n## 2. Automatic Activation\nTakeover mode is automatically enabled when a task status is set to \"needs help\" - this happens when the agent realizes it can't accomplish something on its own.\n\n## How Actions Are Recorded\nAll your actions during takeover (clicks, drags, scrolls, typing, key presses) are automatically logged in the same unified action space that the agent uses. This means Bytebot understands and learns from everything you do.\n\n## Desktop Tab for Setup\n\nOutside of tasks, there's a dedicated **Desktop** tab on the main page that provides:\n- Free-ranging access to the desktop\n- Nothing is recorded in this mode\n- Perfect for:\n  - Installing programs\n  - Logging into apps or websites\n  - Setting up the desktop environment\n  - General desktop maintenance\n\n## Activating Takeover Mode\n\n### Method 1: Manual Takeover During Tasks\n\n<Steps>\n  <Step title=\"Open Task Detail View\">\n    While Bytebot is working on a task, click on the task to open the detail view.\n  </Step>\n  \n  <Step title=\"Click Takeover Button\">\n    Hit the takeover button to interrupt the agent and take control.\n  </Step>\n  \n  <Step title=\"Guide Bytebot\">\n    Perform the necessary actions to get past the obstacle or show the correct path.\n  </Step>\n  \n  <Step title=\"Release Control\">\n    Click to release control and let Bytebot continue from where you left off.\n  </Step>\n</Steps>\n\n### Method 2: Automatic When Help Needed\n\nWhen Bytebot sets a task status to \"needs help\":\n- Takeover mode is automatically enabled\n- You'll see a notification that Bytebot needs assistance\n- Take control to help resolve the issue\n- Bytebot will continue once you release control\n\n## Common Use Cases\n\n### 1. Complex UI Navigation\n\n<Card title=\"Custom Applications\" icon=\"window\">\n  **Scenario**: Working with proprietary or complex software\n  \n  **Steps**:\n  1. Let Bytebot open the application\n  2. Take control to navigate complex interfaces\n  3. Use the chat to explain what you're doing\n  4. Return control for Bytebot to continue\n  \n  **Example**: \"Open our internal CRM, I'll show you how to navigate to the reports section\"\n</Card>\n\n### 2. Error Recovery\n\n<Card title=\"Handling Unexpected Situations\" icon=\"exclamation-triangle\">\n  **Scenario**: Bytebot encounters an error or gets stuck\n  \n  **Steps**:\n  1. Notice Bytebot is struggling\n  2. Take control to resolve the issue\n  3. Guide it past the problem\n  4. Explain what went wrong in chat\n  5. Return control to let Bytebot continue\n  \n  **Example**: \"Let me handle this unexpected popup that's blocking the workflow\"\n</Card>\n\n### 3. Teaching by Demonstration\n\n<Card title=\"Show Don't Tell\" icon=\"graduation-cap\">\n  **Scenario**: Complex multi-step processes\n  \n  **Steps**:\n  1. Take control when you need to demonstrate\n  2. Perform the task normally (no need to move slowly)\n  3. Use chat to explain what you're clicking and why\n  4. Return control\n  5. Ask Bytebot to repeat the process\n  \n  **Example**: \"Watch me navigate through our vendor portal to find the invoice section\"\n</Card>\n\n<Warning>\n  **Important**: Screenshots are taken for every action during takeover mode. Do not enter any data that you don't want captured in screenshots.\n</Warning>\n\n## Best Practices\n\n### Do's ✅\n\n- **Use Chat While Taking Over**: Type messages explaining what you're doing and why\n- **Explain Your Clicks**: Share context about UI elements and their purpose\n- **Return Control Before Leaving**: Always release control before exiting the task detail view\n- **Test Understanding**: Ask Bytebot to summarize what it learned\n\n### Don'ts ❌\n\n- **Enter Data You Don't Want Captured**: Screenshots are taken of all actions\n- **Skip Chat Explanations**: Context helps Bytebot learn patterns\n- **Leave Task View While in Control**: This will leave the task stuck in takeover mode\n- **Assume Knowledge**: Explain application-specific workflows\n\n<Note>\n  **No Need to Move Slowly**: Bytebot captures the state before and after each action, so you can work at normal speed.\n</Note>\n\n\n## Summary\n\nTakeover mode provides flexibility when you need to guide Bytebot or handle situations it can't manage alone. Whether you're navigating complex interfaces, recovering from errors, or teaching new workflows, takeover mode ensures you're always in control when needed."
  },
  {
    "path": "docs/guides/task-creation.mdx",
    "content": "---\ntitle: \"Task Creation & Management\"\ndescription: \"Master the art of creating effective tasks and managing them through completion\"\n---\n\n# Creating and Managing Tasks in Bytebot\n\nThis guide will walk you through everything you need to know about creating tasks that Bytebot can execute effectively, and managing them through their lifecycle.\n\n## Understanding Tasks\n\nA task is any job you want Bytebot to complete. Tasks can be:\n\n- **Simple**: \"Log in to GitHub\" or \"Visit example.com\" (uses one program)\n- **Complex**: \"Download invoices from email and save them to a folder\" (uses multiple programs)\n- **File-based**: \"Read the uploaded PDF and extract all email addresses\" (processes uploaded files)\n- **Collaborative**: \"Process invoices, ask me to handle special approvals\"\n\n## Working with Files\n\nBytebot has powerful file handling capabilities that make it perfect for document processing and data analysis tasks.\n\n### Uploading Files with Tasks\n\nWhen creating a task, you can upload files that will be automatically saved to the desktop instance. This is incredibly useful for:\n\n- **Document Processing**: Upload PDFs, spreadsheets, or documents for Bytebot to analyze\n- **Data Analysis**: Provide CSV files or datasets for processing\n- **Template Filling**: Upload forms or templates that need to be completed\n- **Batch Operations**: Upload multiple files for bulk processing\n\n<Note>\n  **Game Changer**: Bytebot can read entire files, including PDFs, directly into the LLM context. This means it can process large amounts of data quickly and understand complex documents without manual extraction.\n</Note>\n\n### File Upload Examples\n\n<Tabs>\n  <Tab title=\"Web UI\">\n    1. Click the attachment button when creating a task\n    2. Select files to upload (PDFs, CSVs, images, etc.)\n    3. Files are automatically saved to the desktop\n    4. Reference them in your task description:\n    ```\n    \"Read the uploaded contracts.pdf and extract all payment terms, \n    then create a summary spreadsheet with vendor names and terms\"\n    ```\n  </Tab>\n  <Tab title=\"API\">\n    ```bash\n    # Upload files with task creation (multipart/form-data)\n    curl -X POST http://localhost:9991/tasks \\\n      -F \"description=Analyze the uploaded financial statements and create a summary\" \\\n      -F \"priority=HIGH\" \\\n      -F \"files=@financial_statements_2024.pdf\" \\\n      -F \"files=@budget_comparison.xlsx\"\n    ```\n  </Tab>\n</Tabs>\n\n### File Processing Capabilities\n\n<CardGroup cols={2}>\n  <Card title=\"PDF Analysis\" icon=\"file-pdf\">\n    - Extract text from PDFs\n    - Read entire PDFs into context\n    - Parse forms and contracts\n    - Extract tables and data\n  </Card>\n  <Card title=\"Spreadsheet Processing\" icon=\"table\">\n    - Read Excel/CSV files\n    - Analyze data patterns\n    - Generate reports\n    - Cross-reference multiple sheets\n  </Card>\n  <Card title=\"Document Understanding\" icon=\"brain\">\n    - Summarize long documents\n    - Extract key information\n    - Compare multiple files\n    - Answer questions about content\n  </Card>\n  <Card title=\"Batch Operations\" icon=\"layer-group\">\n    - Process multiple files\n    - Apply same analysis to each\n    - Consolidate results\n    - Generate unified reports\n  </Card>\n</CardGroup>\n\n## Creating Your First Task\n\n### Using the Web UI\n\n<Steps>\n  <Step title=\"Open Bytebot UI\">\n    Navigate to `http://localhost:9992`\n  </Step>\n  \n  <Step title=\"Enter Your Task\">\n    In the input field on the left side, type what you want done. For example:\n    ```\n    Log in to my GitHub account and check for new notifications\n    ```\n  </Step>\n  \n  <Step title=\"Start Task\">\n    Press the arrow button or hit Enter. Bytebot will start loading and begin working on your task.\n  </Step>\n</Steps>\n\n### Using the API\n\n```bash\ncurl -X POST http://localhost:9991/tasks \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"description\": \"Download all PDF invoices from my email and organize by date\",\n    \"priority\": \"HIGH\",\n    \"type\": \"IMMEDIATE\"\n  }'\n```\n\n## Writing Effective Task Descriptions\n\n### The Golden Rules\n\n<CardGroup cols={2}>\n  <Card title=\"Be Specific\" icon=\"bullseye\">\n    ❌ \"Do some research\"\n    ✅ \"Research top 5 CRM tools for small businesses\"\n  </Card>\n  <Card title=\"Include Context\" icon=\"info\">\n    ❌ \"Fill out the form\"\n    ✅ \"Fill out the contact form on example.com with test data\"\n  </Card>\n  <Card title=\"Define Success\" icon=\"check\">\n    ❌ \"Organize files\"\n    ✅ \"Organize files in Downloads folder by type into subfolders\"\n  </Card>\n  <Card title=\"One Goal Per Task\" icon=\"target\">\n    ❌ \"Do multiple unrelated things\"\n    ✅ \"Focus on a single objective with clear steps\"\n  </Card>\n</CardGroup>\n\n### Task Description Templates\n\n#### Enterprise Process Automation\n```\nLog into [system] and:\n1. [Navigate to specific section]\n2. [Download/Extract data]\n3. [Process through other system]\n4. [Update records/Generate report]\nHandle any [specific scenarios]\n\nExample:\nLog into our banking portal and:\n1. Navigate to wire transfers section\n2. Download all pending wire confirmations\n3. Match against our ERP payment records\n4. Flag any discrepancies in the reconciliation sheet\n(Bytebot handles all authentication including 2FA automatically via password manager)\n```\n\n#### Multi-Application Workflow\n```\nAccess [System A] to get [data]\nThen in [System B]:\n1. [Process the data]\n2. [Update records]\nFinally in [System C]:\n1. [Verify updates]\n2. [Generate confirmation]\n\nExample:\nAccess Salesforce to get list of new customers from today\nThen in NetSuite:\n1. Create customer records with billing info\n2. Set up payment terms\nFinally in our shipping system:\n1. Verify addresses are valid\n2. Generate welcome kit shipping labels\n```\n\n#### Compliance & Audit Task\n```\nFor each [entity] in [source]:\n1. Check [compliance requirement]\n2. Document [specific data]\n3. Flag any [violations/issues]\nGenerate report showing [metrics]\n\nExample:\nFor each vendor in our approved vendor list:\n1. Check their insurance certificates are current\n2. Document expiration dates and coverage amounts  \n3. Flag any expiring within 30 days\nGenerate report showing compliance percentage by category\n```\n\n## Managing Active Tasks\n\n### Task States\n\n<img src=\"/images/task-lifecycle.png\" alt=\"Task Lifecycle\" className=\"w-full max-w-3xl\" />\n\nTasks move through these states:\n\n1. **Created** → Task is defined but not started\n2. **Queued** → Waiting for agent availability\n3. **Running** → Actively being worked on\n4. **Needs Help** → Requires human input\n5. **Completed** → Successfully finished\n6. **Failed** → Could not be completed\n\n### Monitoring Progress\n\n#### Real-time Updates\n\nWatch Bytebot work through the task detail viewer:\n\n- **Green dot**: Task is actively running\n- **Status messages**: Current step being executed\n- **Desktop view**: See what Bytebot sees in real-time\n\n#### Chat Messages\n\nBytebot provides updates like:\n```\nAssistant: I'm now searching for project management tools...\nAssistant: Found 15 options, filtering by your criteria...\nAssistant: Creating the comparison table with 5 tools...\n```\n\n### Interacting with Running Tasks\n\n#### Providing Additional Information\n\n```\nUser: \"Also include free tier options in your research\"\nAssistant: \"I'll add a column for free tier availability to the comparison table.\"\n```\n\n#### Clarifying Instructions\n\n```\nAssistant: \"I found multiple forms on this page. Which one should I fill out?\"\nUser: \"Use the 'Contact Sales' form on the right side\"\n```\n\n#### Modifying Tasks\n\n```\nUser: \"Actually, make it top 10 tools instead of top 5\"\nAssistant: \"I'll expand my research to include 10 tools in the comparison.\"\n```\n\n## Advanced Task Management\n\n### Task Dependencies\n\nChain tasks that depend on each other:\n\n```\nTask 1: \"Download the invoice from the vendor portal\"\nTask 2: \"Open the downloaded invoice and extract the total amount\"\nTask 3: \"Enter the amount into our accounting system\"\n```\n\n## Best Practices\n\n### Do's ✅\n\n1. **Start Simple**: Test with basic tasks before complex ones\n2. **Provide Examples**: \"Format it like the report from last week\"\n3. **Include Credentials Safely**: Use takeover mode for passwords\n4. **Set Realistic Expectations**: Complex tasks take time\n5. **Review Results**: Always verify important outputs\n\n### Don'ts ❌\n\n1. **Overload Single Tasks**: Break complex workflows into steps\n2. **Assume Knowledge**: Explain custom applications\n3. **Skip Context**: Always provide necessary background\n4. **Ignore Errors**: Address issues promptly\n5. **Rush Critical Tasks**: Allow time for careful execution\n\n## Task Examples by Category\n\n### 📄 Document Processing & Analysis\n```\n\"Read the uploaded contract.pdf and extract all key terms including payment schedules, deliverables, and termination clauses. Create a summary document with these details.\"\n\n\"Process all the uploaded invoice PDFs, extract vendor names, amounts, and due dates, then create a consolidated Excel spreadsheet sorted by due date.\"\n\n\"Analyze the uploaded financial_report.pdf and answer these questions: What was the revenue growth? What are the main risk factors mentioned? What is the debt-to-equity ratio?\"\n\n\"Read through the uploaded employee_handbook.pdf and create a checklist of all compliance requirements mentioned in the document.\"\n```\n\n### 🏦 Enterprise Automation (RPA-Style Workflows)\n```\n\"Log into our banking portal, download all transaction files from last month, save them to the Finance/Statements folder, then run the reconciliation script on each file.\"\n\n(Note: Bytebot handles all authentication including 2FA automatically using the built-in password manager)\n\n\"Access the vendor portal at supplier.example.com, navigate to the invoice section, download all pending invoices, extract the data into our standard template, and upload to the AP system.\"\n\n\"Open our legacy ERP system, export the customer list, then for each customer, look them up in the new CRM and update their status and last contact date.\"\n```\n\n### 📊 Financial Operations & Data Analysis\n```\n\"Read the uploaded bank_statements folder containing 12 monthly PDFs, extract all transactions over $10,000, and create a summary report showing patterns and anomalies.\"\n\n\"Log into each of our 5 bank accounts, download the daily statements, consolidate them into a single cash position report, and save to the shared finance folder.\"\n\n\"Process the uploaded expense_reports.zip file, review all reports over $1,000, create a summary with policy violations flagged, and prepare for approval.\"\n\n\"Navigate to the tax authority website, download all GST/VAT returns for Q4, extract the figures, and populate our tax reconciliation spreadsheet.\"\n```\n\n### 🔄 Multi-System Integration\n```\n\"Pull today's orders from Shopify, create corresponding entries in NetSuite, update inventory in our WMS, and trigger shipping labels in ShipStation.\"\n\n\"Extract employee data from Workday, cross-reference with our access control system, identify discrepancies, and create tickets for IT to resolve.\"\n\n\"Log into our insurance portal, download policy documents for all active policies, extract key dates and coverage amounts, update our risk management database.\"\n```\n\n### 📈 Compliance & Reporting\n```\n\"Access all state regulatory websites for our operating regions, check for new compliance updates since last month, download relevant documents, and create a summary report.\"\n\n\"Log into our various SaaS tools (list provided), export user access reports, consolidate into a single audit trail, and flag any terminated employees still with access.\"\n\n\"Navigate to customer portal, download all SLA performance reports, extract metrics, compare against our internal data, and highlight discrepancies.\"\n```\n\n### 🤝 Development & QA Integration\n```\n\"After the code agent deploys the new feature, test the complete user journey from signup to checkout, take screenshots at each step, and verify against the design specs.\"\n\n\"Run through all test scenarios in our QA checklist, but for any failures, have the code agent analyze the error and attempt a fix, then retest automatically.\"\n\n\"Monitor our staging environment, when a new build is deployed, automatically run the smoke test suite and create a visual regression report comparing to production.\"\n```\n\n## Troubleshooting Common Issues\n\n<AccordionGroup>\n  <Accordion title=\"Task stuck in 'Running' state\">\n    **Possible causes**:\n    - Waiting for slow page/app to load\n    - Encountered unexpected popup\n    - Unclear next step\n    \n    **Solutions**:\n    - Check desktop viewer for current state\n    - Provide clarification via chat\n    - Use takeover mode to help\n    - Cancel and restart with clearer instructions\n  </Accordion>\n  \n  <Accordion title=\"Task completed but wrong result\">\n    **Possible causes**:\n    - Ambiguous instructions\n    - Website/app changed\n    - Misunderstood context\n    \n    **Solutions**:\n    - Review task description for clarity\n    - Provide specific examples\n    - Break into smaller subtasks\n    - Use takeover mode to demonstrate\n  </Accordion>\n  \n  <Accordion title=\"Task failed immediately\">\n    **Possible causes**:\n    - Invalid URL or application\n    - Missing prerequisites\n    - System resource issues\n    \n    **Solutions**:\n    - Verify URLs and application names\n    - Ensure required files/data exist\n    - Check system resources\n    - Review error messages in chat\n  </Accordion>\n</AccordionGroup>\n\n## Task Management Tips\n\n### Organizing Multiple Tasks\n\n1. **Use Clear Naming**: Include date, category, or project\n2. **Group Related Tasks**: Process similar tasks together\n3. **Priority Management**: Reserve 'Urgent' for true emergencies\n4. **Regular Reviews**: Check completed tasks for quality\n\n### Performance Optimization\n\n- **Batch Similar Tasks**: Group web research, data entry, etc.\n- **Prepare Resources**: Have files/data ready before starting\n- **Clear Desktop**: Minimize distractions and popups\n- **Stable Environment**: Ensure good internet and system resources\n\n### Learning from Tasks\n\nAfter each task:\n1. Review the approach Bytebot took\n2. Note any inefficiencies\n3. Refine future task descriptions\n4. Build a library of effective prompts\n\n## Next Steps\n\n<CardGroup cols={2}>\n  <Card title=\"Takeover Mode\" icon=\"hand\" href=\"/guides/takeover-mode\">\n    Learn human-AI collaboration\n  </Card>\n  <Card title=\"API Integration\" icon=\"code\" href=\"/api-reference/agent/tasks\">\n    Automate task creation\n  </Card>\n</CardGroup>\n\n<Note>\n  **Pro Tip**: Start with simple tasks to understand Bytebot's capabilities, then gradually increase complexity as you learn what works best.\n</Note>"
  },
  {
    "path": "docs/introduction.mdx",
    "content": "---\ntitle: Introduction\ndescription: \"Open source AI desktop agent that automates any computer task\"\n---\n\n<p align=\"center\">\n  <img\n    className=\"block dark:hidden\"\n    src=\"/logo/bytebot_transparent_logo_dark.svg\"\n    alt=\"Bytebot Logo\"\n    width=\"500\"\n  />\n  <img\n    className=\"hidden dark:block\"\n    src=\"/logo/bytebot_transparent_logo_white.svg\"\n    alt=\"Bytebot Logo\"\n    width=\"500\"\n  />\n</p>\n\n## What is Bytebot?\n\nBytebot is an open-source AI agent that can control a computer desktop to complete tasks for you. It runs in Docker containers on your own infrastructure, giving you a virtual assistant that can:\n\n- Use any desktop application (browser, email, office tools, etc.)\n- Process uploaded files including PDFs, spreadsheets, and documents\n- Read entire files directly into the LLM context for rapid analysis\n- Automate repetitive tasks like data entry and form filling\n- Handle complex workflows that span multiple applications\n- Work 24/7 without human supervision\n\nSimply describe what you need done in plain English, and Bytebot will figure out how to do it – clicking buttons, typing text, navigating websites, reading documents, and completing tasks just like a human would.\n\n## Why Bytebot Over Traditional RPA?\n\n<CardGroup cols={2}>\n  <Card title=\"No Complex Scripting\" icon=\"code-branch\">\n    Unlike UiPath or similar tools, no need to design flowcharts or write scripts - just describe tasks naturally\n  </Card>\n  <Card title=\"Adaptive Intelligence\" icon=\"brain\">\n    AI-powered understanding means Bytebot adapts to UI changes without breaking\n  </Card>\n  <Card title=\"Visual Understanding\" icon=\"eye\">\n    Can read and understand any interface, not just pre-mapped elements\n  </Card>\n  <Card title=\"Human-Like Problem Solving\" icon=\"lightbulb\">\n    Handles unexpected popups, errors, and variations automatically\n  </Card>\n</CardGroup>\n\n## Why Self-Host Bytebot?\n\n<CardGroup cols={2}>\n  <Card title=\"Complete Privacy\" icon=\"shield\">\n    Your tasks and data never leave your infrastructure. Everything runs locally\n    on your servers.\n  </Card>\n  <Card title=\"Full Control\" icon=\"sliders\">\n    Customize the desktop environment, install any applications, and configure\n    to your exact needs.\n  </Card>\n  <Card title=\"No Usage Limits\" icon=\"infinity\">\n    Use your own LLM API keys without platform restrictions or additional fees.\n  </Card>\n  <Card title=\"Secure Isolation\" icon=\"lock\">\n    Each desktop runs in its own container, completely isolated from your host\n    system.\n  </Card>\n</CardGroup>\n\n## Real-World Use Cases\n\n### Enterprise Automation (RPA Replacement)\nBytebot is the next generation of RPA (Robotic Process Automation). It handles the same complex workflows as traditional tools like UiPath, but with AI-powered adaptability and automatic authentication:\n\n- **Financial Operations**: Automate banking portal access (including 2FA when password manager extensions are configured), download transaction files, and process them through multiple systems\n- **Compliance Workflows**: Navigate government websites, download regulatory documents, extract data, and update compliance tracking systems\n- **Multi-System Integration**: Bridge legacy systems that lack APIs by automating the UI interactions between them\n- **Vendor Management**: Log into supplier portals, download invoices, reconcile with internal systems, and process payments\n\n### Business Process Automation\n- **Data Reconciliation**: Pull reports from multiple SaaS platforms, cross-reference data, and generate consolidated reports\n- **Customer Onboarding**: Navigate between CRM, banking, and verification systems to complete new customer setup\n- **Purchase Order Processing**: Extract POs from webmail portals, enter into ERP systems, and update inventory databases\n- **HR Operations**: Collect employee data from various systems, update records, and ensure consistency across platforms\n\n### Development & QA Integration\nBytebot becomes even more powerful when combined with coding agents:\n\n- **Full-Stack Testing**: Use a coding agent to generate code, then have Bytebot visually test and validate the output\n- **Automated Debugging**: Let Bytebot reproduce user-reported issues while a coding agent analyzes and fixes the code\n- **End-to-End Development**: Code agents write features, Bytebot tests them, creating a complete development loop\n- **Visual Regression Testing**: Automatically detect UI changes across deployments with screenshot comparisons\n\n## How It Works\n\n<Steps>\n  <Step title=\"Describe Your Task\">\n    Simply tell Bytebot what you want done in natural language through the tasks\n    interface\n  </Step>\n  <Step title=\"AI Plans the Actions\">\n    Bytebot understands your request and breaks it down into specific computer\n    actions\n  </Step>\n  <Step title=\"Executes Actions\">\n    Bytebot executes the task on its virtual desktop using the keyboard\n    and mouse\n  </Step>\n  <Step title=\"Watch or Walk Away\">\n    Monitor it working in real-time through the task detail view, or let it\n    complete tasks independently.\n  </Step>\n  <Step title=\"Get Results\">\n    Receive the completed task output, screenshots, or confirmation of\n    completion\n  </Step>\n</Steps>\n\n## Architecture Overview\n\nBytebot consists of four integrated components working together:\n\n<img src=\"/images/agent-architecture.png\" alt=\"Bytebot Agent Architecture\" />\n\n<CardGroup cols={2}>\n  <Card\n    title=\"Bytebot Desktop\"\n    icon=\"desktop\"\n    href=\"/core-concepts/desktop-environment\"\n  >\n    Ubuntu 22.04 with XFCE4, VSCode, Firefox, Thunderbird email client, and automation daemon (bytebotd)\n  </Card>\n  <Card title=\"AI Agent\" icon=\"brain\" href=\"/core-concepts/agent-system\">\n    NestJS service that uses LLMs (Anthropic Claude, OpenAI GPT, Google Gemini) to plan and execute tasks\n  </Card>\n  <Card\n    title=\"Task Interface\"\n    icon=\"window\"\n    href=\"/api-reference/agent/ui\"\n  >\n    Next.js web app for creating and managing tasks\n  </Card>\n  <Card title=\"REST API\" icon=\"code\" href=\"/api-reference/introduction\">\n    Programmatic access to both task management and direct desktop control\n  </Card>\n</CardGroup>\n\n## Getting Started\n\n<CardGroup cols={3}>\n  <Card title=\"Quick Start\" icon=\"rocket\" href=\"/quickstart\">\n    Get Bytebot running in 2 minutes\n  </Card>\n  <Card title=\"Architecture\" icon=\"sitemap\" href=\"/core-concepts/architecture\">\n    Understand how it all fits together\n  </Card>\n  <Card title=\"API Reference\" icon=\"book\" href=\"/api-reference/introduction\">\n    Integrate with your applications\n  </Card>\n</CardGroup>\n\n## Key Features\n\n### 🤖 Natural Language Control\nJust tell Bytebot what you need done. No coding or complex automation tools required.\n\n### 🖥️ Full Desktop Access\nBytebot can use any application you can install - browsers, office tools, custom software.\n\n### 🔒 Complete Privacy\nRuns entirely on your infrastructure. Your data never leaves your servers.\n\n### 🔄 Two Operating Modes\n- **Autonomous Mode**: Bytebot completes tasks independently\n- **Takeover Mode**: You can step in and take control when needed\n\n### 🖱️ Direct Desktop Access\n- **Desktop Tab**: Free-form access to the virtual desktop for setup, installing programs, or manual operations\n- **Task View**: Watch and interact with Bytebot during task execution\n\n### 🚀 Easy Deployment\n- One-click deployment on Railway\n- Docker Compose for self-hosting\n- Helm charts for Kubernetes\n\n### 🔌 Developer-Friendly\n- REST APIs for programmatic control\n- Task management API\n- Extensible architecture\n- MCP (Model Context Protocol) support\n\n## Community & Support\n\n<CardGroup cols={2}>\n  <Card\n    title=\"Discord Community\"\n    icon=\"discord\"\n    href=\"https://discord.com/invite/d9ewZkWPTP\"\n  >\n    Join our community for help, tips, and discussions\n  </Card>\n  <Card\n    title=\"GitHub\"\n    icon=\"github\"\n    href=\"https://github.com/bytebot-ai/bytebot\"\n  >\n    Report issues, contribute, or star the project\n  </Card>\n</CardGroup>\n\n<Note>\n  **Ready to give your AI its own computer?** Start with our [Quick Start\n  Guide](/quickstart) to have your own AI desktop agent running in minutes.\n</Note>\n"
  },
  {
    "path": "docs/quickstart.mdx",
    "content": "---\ntitle: \"Quick Start\"\ndescription: \"Get your AI desktop agent running in 2 minutes\"\n---\n\n# Choose Your Deployment Method\n\nBytebot can be deployed in several ways depending on your needs:\n\n<Tabs>\n  <Tab title=\"Railway (Easiest)\">\n    ## ☁️ One-click Deploy on Railway\n\n[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)\n\n<Steps>\n  <Step title=\"Visit the Template\">\n    Click the Deploy Now button in the Bytebot template on Railway.\n  </Step>\n  <Step title=\"Add Anthropic Key\">\n    Enter either your `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` for the bytebot-agent resource.\n  </Step>\n  <Step title=\"Deploy &amp; Launch\">\n    Hit **Deploy**. Railway will build the stack, wire the services together via private networking and output a public URL for the UI. Your agent should be ready within a couple of minutes!\n  </Step>\n</Steps>\n\n<Note>\n  Need more details? See the full <a href=\"/deployment/railway\">Railway deployment guide</a>.\n</Note>\n\n  </Tab>\n  <Tab title=\"Docker Compose\">\n    ## 🐳 Self-host with Docker Compose\n\n## Prerequisites\n\n- Docker ≥ 20.10\n- Docker Compose\n- 4GB+ RAM available\n- AI API key from one of these providers:\n  - Anthropic ([get one here](https://console.anthropic.com)) - Claude models\n  - OpenAI ([get one here](https://platform.openai.com/api-keys)) - GPT models\n  - Google ([get one here](https://makersuite.google.com/app/apikey)) - Gemini models\n\n## 🚀 2-Minute Setup\n\nGet your self-hosted AI desktop agent running with just three commands:\n\n<Steps>\n  <Step title=\"Clone and Configure\">\n    ```bash \n    git clone https://github.com/bytebot-ai/bytebot.git\n    cd bytebot\n    \n    # Configure your AI provider (choose one):\n    echo \"ANTHROPIC_API_KEY=your_api_key_here\" > docker/.env    # For Claude\n    # echo \"OPENAI_API_KEY=your_api_key_here\" > docker/.env     # For OpenAI\n    # echo \"GEMINI_API_KEY=your_api_key_here\" > docker/.env     # For Gemini\n    ```\n  </Step>\n\n<Step title=\"Start the Agent Stack\">\n  ```bash \n  docker-compose -f docker/docker-compose.yml up -d \n  ```\n\nThis starts all four services:\n\n- **Bytebot Desktop**: Containerized Linux environment\n- **AI Agent**: LLM-powered task processor (supports Claude, GPT, or Gemini)\n- **Chat UI**: Web interface for interaction\n- **Database**: PostgreSQL for persistence\n\n</Step>\n\n<Step title=\"Open the Chat Interface\">\nNavigate to [http://localhost:9992](http://localhost:9992) to access the Bytebot UI.\n\n**Two ways to interact:**\n1. **Tasks**: Enter task descriptions to have Bytebot work autonomously\n2. **Desktop**: Direct access to the virtual desktop for manual control\n\nTry asking:\n\n- \"Open Firefox and search for the weather forecast\"\n- \"Take a screenshot of the desktop\"\n- \"Create a text file with today's date\"\n\n</Step>\n</Steps>\n\n<Note>\n  **First time?** The initial startup may take 2-3 minutes as Docker downloads\n  the images. Subsequent starts will be much faster.\n</Note>\n\n## 🎯 What You Just Deployed\n\nYou now have a complete AI desktop automation system with:\n\n<Note>\n  **🔐 Password Manager Support**: Bytebot can handle authentication automatically when you install a password manager extension. See our [password management guide](/guides/password-management) for setup instructions.\n</Note>\n\n<CardGroup cols={2}>\n  <Card title=\"AI Agent\" icon=\"brain\">\n    - Understands natural language\n    - Plans and executes tasks\n    - Adapts to errors\n    - Works autonomously\n  </Card>\n  <Card title=\"Virtual Desktop\" icon=\"desktop\">\n    - Full Ubuntu environment\n    - Browser, office tools\n    - File system access\n    - Application support\n  </Card>\n  <Card title=\"Task Interface\" icon=\"window\">\n    - Create and manage tasks\n    - Real-time desktop view\n    - Conversation history\n    - Takeover mode\n  </Card>\n  <Card title=\"REST APIs\" icon=\"code\">\n    - Programmatic control\n    - Task management API\n    - Direct desktop access\n    - MCP protocol support\n  </Card>\n</CardGroup>\n\n## 🚀 Your First Tasks\n\nNow let's see Bytebot in action! Try these example tasks:\n\n### Simple Tasks (Test the Basics)\n<CardGroup cols={2}>\n  <Card title=\"Take a Screenshot\" icon=\"camera\">\n    \"Take a screenshot of the desktop\"\n  </Card>\n  <Card title=\"Open Browser\" icon=\"globe\">\n    \"Open Firefox and go to google.com\"\n  </Card>\n  <Card title=\"Create File\" icon=\"file\">\n    \"Create a text file called 'hello.txt' with today's date\"\n  </Card>\n  <Card title=\"System Info\" icon=\"info\">\n    \"Check the system information and tell me the OS version\"\n  </Card>\n</CardGroup>\n\n### Advanced Tasks (See the Power)\n<CardGroup cols={2}>\n  <Card title=\"Web Research\" icon=\"magnifying-glass\">\n    \"Find the top 5 AI news stories today and create a summary document\"\n  </Card>\n  <Card title=\"Data Extraction\" icon=\"table\">\n    \"Go to hacker news, find the top 10 stories, and save them to a CSV file\"\n  </Card>\n  <Card title=\"Document Processing\" icon=\"file-pdf\">\n    \"Upload a PDF contract and extract all payment terms and deadlines\"\n  </Card>\n  <Card title=\"Multi-Step Workflow\" icon=\"layers\">\n    \"Search for 'machine learning tutorials', open the first 3 results in tabs, and take screenshots of each\"\n  </Card>\n</CardGroup>\n\n## Accessing Your Services\n\n| Service          | URL                                                                      | Purpose                                       |\n| ---------------- | ------------------------------------------------------------------------ | --------------------------------------------- |\n| **Tasks UI**     | [http://localhost:9992](http://localhost:9992)                           | Main interface for interacting with the agent |\n| **Agent API**    | [http://localhost:9991/tasks](http://localhost:9991/tasks)               | REST API for programmatic task creation       |\n| **Desktop API** | [http://localhost:9990/computer-use](http://localhost:9990/computer-use) | Low-level desktop control API                 |\n| **MCP SSE**      | [http://localhost:9990/mcp](http://localhost:9990/mcp)                 | Connect MCP clients for tool access           |\n\n  </Tab>\n  <Tab title=\"Kubernetes/Helm\">\n    ## ☸️ Deploy with Helm\n    \n    See our [Helm deployment guide](/deployment/helm) for Kubernetes installation.\n  </Tab>\n  <Tab title=\"Desktop Only\">\n    ## 🖥️ Desktop Container Only\n    \n    If you just want the virtual desktop without the AI agent:\n\n    ```bash\n    # Using pre-built image (recommended)\n    docker-compose -f docker/docker-compose.core.yml pull\n    docker-compose -f docker/docker-compose.core.yml up -d\n    ```\n\n    Or build locally:\n    ```bash\n    docker-compose -f docker/docker-compose.core.yml up -d --build\n    ```\n\n    Access the desktop at [http://localhost:9990/vnc](http://localhost:9990/vnc)\n  </Tab>\n</Tabs>\n\n## Managing Your Agent\n\n### View Logs\n\nMonitor what your agent is doing:\n\n```bash\n# All services\ndocker-compose -f docker/docker-compose.yml logs -f\n\n# Just the agent\ndocker-compose -f docker/docker-compose.yml logs -f bytebot-agent\n```\n\n### Stop Services\n\n```bash\ndocker-compose -f docker/docker-compose.yml down\n```\n\n### Update to Latest\n\n```bash\ndocker-compose -f docker/docker-compose.yml pull\ndocker-compose -f docker/docker-compose.yml up -d\n```\n\n### Reset Everything\n\nRemove all data and start fresh:\n\n```bash\ndocker-compose -f docker/docker-compose.yml down -v\n```\n\n## Quick API Examples\n\n### Create a Task via API\n\n```bash\n# Simple task\ncurl -X POST http://localhost:9991/tasks \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"description\": \"Search for flights from NYC to London next month\",\n    \"priority\": \"MEDIUM\"\n  }'\n\n# Task with file upload\ncurl -X POST http://localhost:9991/tasks \\\n  -F \"description=Read this contract and summarize the key terms\" \\\n  -F \"priority=HIGH\" \\\n  -F \"files=@contract.pdf\"\n```\n\n### Direct Desktop Control\n\n```bash\n# Take a screenshot\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"screenshot\"}'\n\n# Type text\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"type_text\", \"text\": \"Hello, Bytebot!\"}'\n```\n\n## Troubleshooting\n\n<AccordionGroup>\n  <Accordion title=\"Container won't start\">\n    Check Docker is running and you have enough resources: \n    ```bash \n    docker info\n    docker-compose -f docker/docker-compose.yml logs \n    ```\n  </Accordion>\n  <Accordion title=\"Can't connect to tasks UI\">\n    Ensure all services are running: \n    ```bash \n    docker-compose -f docker/docker-compose.yml ps \n    ``` \n    All services should show as \"Up\".\n  </Accordion>\n  <Accordion title=\"Agent errors or no response\">\n    Check your API key is set correctly: \n    ```bash \n    cat docker/.env\n    docker-compose -f docker/docker-compose.yml logs bytebot-agent \n    ```\n    Ensure you're using a valid API key from Anthropic, OpenAI, or Google.\n  </Accordion>\n</AccordionGroup>\n\n## 📚 Next Steps\n\n<CardGroup cols={2}>\n  <Card\n    title=\"Using the UI\"\n    icon=\"window\"\n    href=\"/guides/task-creation\"\n  >\n    Learn how to create and manage tasks effectively\n  </Card>\n  <Card title=\"Takeover Mode\" icon=\"hand\" href=\"/guides/takeover-mode\">\n    Take control when you need to guide Bytebot\n  </Card>\n  <Card title=\"LiteLLM Integration\" icon=\"plug\" href=\"/deployment/litellm\">\n    Use any LLM provider with Bytebot\n  </Card>\n  <Card title=\"API Integration\" icon=\"code\" href=\"/api-reference/introduction\">\n    Automate Bytebot with your applications\n  </Card>\n</CardGroup>\n\n## 🔧 Configuration Options\n\n### Environment Variables\n\n<AccordionGroup>\n  <Accordion title=\"AI Provider Settings\">\n    ```bash\n    # Choose one AI provider:\n    ANTHROPIC_API_KEY=sk-ant-...    # For Claude models\n    OPENAI_API_KEY=sk-...           # For GPT models  \n    GEMINI_API_KEY=...              # For Gemini models\n    \n    # Optional: Use specific models\n    ANTHROPIC_MODEL=claude-3-5-sonnet-20241022  # Default\n    OPENAI_MODEL=gpt-4o\n    GEMINI_MODEL=gemini-1.5-flash\n    ```\n  </Accordion>\n  <Accordion title=\"Port Configuration\">\n    ```bash\n    # Change default ports if needed\n    # Edit docker-compose.yml ports section:\n    # bytebot-ui:\n    #   ports:\n    #     - \"8080:9992\"  # Change 8080 to your desired port\n    ```\n  </Accordion>\n  <Accordion title=\"Using LiteLLM Proxy\">\n    ```bash\n    # To use multiple LLM providers, use the proxy setup:\n    docker-compose -f docker/docker-compose.proxy.yml up -d\n    \n    # This includes a pre-configured LiteLLM proxy\n    ```\n  </Accordion>\n</AccordionGroup>\n\n<Note>\n  **Need help?** Join our [Discord\n  community](https://discord.com/invite/d9ewZkWPTP) for support and to share\n  what you're building!\n</Note>\n"
  },
  {
    "path": "docs/rest-api/computer-use.mdx",
    "content": "---\ntitle: \"Computer Action\"\nopenapi: \"POST /computer-use\"\ndescription: \"Execute computer actions in the virtual desktop environment\"\n---\n\nExecute actions like mouse movements, clicks, keyboard input, and screenshots in the Bytebot desktop environment.\n\n## Request\n\n<ParamField body=\"action\" type=\"string\" required>\n  The type of computer action to perform. Must be one of: `move_mouse`, `trace_mouse`,\n  `click_mouse`, `press_mouse`, `drag_mouse`, `scroll`, `type_keys`, `press_keys`, \n  `type_text`, `wait`, `screenshot`, `cursor_position`.\n</ParamField>\n\n### Mouse Actions\n\n<Accordion title=\"move_mouse\">\n  <ParamField body=\"coordinates\" type=\"object\" required>\n    The target coordinates to move to.\n    \n    <Expandable title=\"coordinates properties\">\n      <ParamField body=\"x\" type=\"number\" required>\n        X coordinate (horizontal position)\n      </ParamField>\n      <ParamField body=\"y\" type=\"number\" required>\n        Y coordinate (vertical position)\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"move_mouse\",\n  \"coordinates\": {\n    \"x\": 100,\n    \"y\": 200\n  }\n}\n```\n\n</Accordion>\n\n<Accordion title=\"trace_mouse\">\n  <ParamField body=\"path\" type=\"array\" required>\n    Array of coordinate objects for the mouse path.\n    \n    <Expandable title=\"path\">\n      <ParamField body=\"0\" type=\"object\">\n        <Expandable title=\"properties\">\n          <ParamField body=\"x\" type=\"number\" required>\n            X coordinate\n          </ParamField>\n          <ParamField body=\"y\" type=\"number\" required>\n            Y coordinate\n          </ParamField>\n        </Expandable>\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"holdKeys\" type=\"array\">\n  Keys to hold while moving the mouse along the path.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"trace_mouse\",\n  \"path\": [\n    { \"x\": 100, \"y\": 100 },\n    { \"x\": 150, \"y\": 150 },\n    { \"x\": 200, \"y\": 200 }\n  ],\n  \"holdKeys\": [\"shift\"]\n}\n```\n\n</Accordion>\n\n<Accordion title=\"click_mouse\">\n  <ParamField body=\"coordinates\" type=\"object\">\n    The coordinates to click (uses current cursor position if omitted).\n    \n    <Expandable title=\"coordinates properties\">\n      <ParamField body=\"x\" type=\"number\" required>\n        X coordinate (horizontal position)\n      </ParamField>\n      <ParamField body=\"y\" type=\"number\" required>\n        Y coordinate (vertical position)\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"button\" type=\"string\" required>\n  Mouse button to click. Must be one of: `left`, `right`, `middle`.\n</ParamField>\n\n{\" \"}\n\n<ParamField body=\"clickCount\" type=\"number\" required>\n  Number of clicks to perform.\n</ParamField>\n\n{\" \"}\n\n<ParamField body=\"holdKeys\" type=\"array\">\n  Keys to hold while clicking (e.g., ['ctrl', 'shift'])\n  <Expandable title=\"holdKeys\">\n    <ParamField body=\"0\" type=\"string\">\n      Key name\n    </ParamField>\n  </Expandable>\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"click_mouse\",\n  \"coordinates\": {\n    \"x\": 150,\n    \"y\": 250\n  },\n  \"button\": \"left\",\n  \"clickCount\": 2\n}\n```\n\n</Accordion>\n\n<Accordion title=\"press_mouse\">\n  <ParamField body=\"coordinates\" type=\"object\">\n    The coordinates to press/release (uses current cursor position if omitted).\n    \n    <Expandable title=\"coordinates properties\">\n      <ParamField body=\"x\" type=\"number\" required>\n        X coordinate (horizontal position)\n      </ParamField>\n      <ParamField body=\"y\" type=\"number\" required>\n        Y coordinate (vertical position)\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"button\" type=\"string\" required>\n  Mouse button to press/release. Must be one of: `left`, `right`, `middle`.\n</ParamField>\n\n{\" \"}\n\n<ParamField body=\"press\" type=\"string\" required>\n  Whether to press or release the button. Must be one of: `up`, `down`.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"press_mouse\",\n  \"coordinates\": {\n    \"x\": 150,\n    \"y\": 250\n  },\n  \"button\": \"left\",\n  \"press\": \"down\"\n}\n```\n\n</Accordion>\n\n<Accordion title=\"drag_mouse\">\n  <ParamField body=\"path\" type=\"array\" required>\n    Array of coordinate objects for the drag path.\n    \n    <Expandable title=\"path\">\n      <ParamField body=\"0\" type=\"object\">\n        <Expandable title=\"properties\">\n          <ParamField body=\"x\" type=\"number\" required>\n            X coordinate\n          </ParamField>\n          <ParamField body=\"y\" type=\"number\" required>\n            Y coordinate\n          </ParamField>\n        </Expandable>\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"button\" type=\"string\" required>\n  Mouse button to use for dragging. Must be one of: `left`, `right`, `middle`.\n</ParamField>\n\n{\" \"}\n\n<ParamField body=\"holdKeys\" type=\"array\">\n  Keys to hold while dragging.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"drag_mouse\",\n  \"path\": [\n    { \"x\": 100, \"y\": 100 },\n    { \"x\": 200, \"y\": 200 }\n  ],\n  \"button\": \"left\"\n}\n```\n\n</Accordion>\n\n<Accordion title=\"scroll\">\n  <ParamField body=\"coordinates\" type=\"object\">\n    The coordinates to scroll at (uses current cursor position if omitted).\n    \n    <Expandable title=\"coordinates properties\">\n      <ParamField body=\"x\" type=\"number\" required>\n        X coordinate\n      </ParamField>\n      <ParamField body=\"y\" type=\"number\" required>\n        Y coordinate\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"direction\" type=\"string\" required>\n  Scroll direction. Must be one of: `up`, `down`, `left`, `right`.\n</ParamField>\n\n{\" \"}\n\n<ParamField body=\"scrollCount\" type=\"number\" required>\n  Number of scroll steps to perform.\n</ParamField>\n\n{\" \"}\n\n<ParamField body=\"holdKeys\" type=\"array\">\n  Keys to hold while scrolling.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"scroll\",\n  \"direction\": \"down\",\n  \"scrollCount\": 5\n}\n```\n\n</Accordion>\n\n### Keyboard Actions\n\n<Accordion title=\"type_keys\">\n  <ParamField body=\"keys\" type=\"array\" required>\n    Array of keys to type in sequence.\n    <Expandable title=\"keys\">\n      <ParamField body=\"0\" type=\"string\">\n        Key name\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"delay\" type=\"number\">\n  Delay between key presses in milliseconds.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"type_keys\",\n  \"keys\": [\"a\", \"b\", \"c\", \"enter\"],\n  \"delay\": 50\n}\n```\n\n</Accordion>\n\n<Accordion title=\"press_keys\">\n  <ParamField body=\"keys\" type=\"array\" required>\n    Array of keys to press or release.\n    <Expandable title=\"keys\">\n      <ParamField body=\"0\" type=\"string\">\n        Key name\n      </ParamField>\n    </Expandable>\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"press\" type=\"string\" required>\n  Whether to press or release the keys. Must be one of: `up`, `down`.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"press_keys\",\n  \"keys\": [\"ctrl\", \"shift\", \"esc\"],\n  \"press\": \"down\"\n}\n```\n\n</Accordion>\n\n<Accordion title=\"type_text\">\n  <ParamField body=\"text\" type=\"string\" required>\n    The text string to type.\n  </ParamField>\n\n{\" \"}\n\n<ParamField body=\"delay\" type=\"number\">\n  Delay between characters in milliseconds.\n</ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"type_text\",\n  \"text\": \"Hello, Bytebot!\",\n  \"delay\": 50\n}\n```\n\n</Accordion>\n\n<Accordion title=\"paste_text\">\n  <ParamField body=\"text\" type=\"string\" required>\n    The text to paste. Useful for special characters that aren't on the standard keyboard.\n  </ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"paste_text\",\n  \"text\": \"Special characters: ©®™€¥£ émojis 🎉\"\n}\n```\n\n</Accordion>\n\n### System Actions\n\n<Accordion title=\"wait\">\n  <ParamField body=\"duration\" type=\"number\" required>\n    Wait duration in milliseconds.\n  </ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"wait\",\n  \"duration\": 2000\n}\n```\n\n</Accordion>\n\n<Accordion title=\"screenshot\">\nNo parameters required.\n\n**Example Request**\n\n```json\n{\n  \"action\": \"screenshot\"\n}\n```\n\n</Accordion>\n\n<Accordion title=\"cursor_position\">\nNo parameters required.\n\n**Example Request**\n\n```json\n{\n  \"action\": \"cursor_position\"\n}\n```\n\n</Accordion>\n\n<Accordion title=\"application\">\n  <ParamField body=\"application\" type=\"string\" required>\n    The application to switch to. Available options: `firefox`, `1password`, `thunderbird`, `vscode`, `terminal`, `desktop`, `directory`.\n  </ParamField>\n\n**Example Request**\n\n```json\n{\n  \"action\": \"application\",\n  \"application\": \"firefox\"\n}\n```\n\n**Available Applications:**\n- `firefox` - Mozilla Firefox web browser\n- `1password` - Password manager\n- `thunderbird` - Email client\n- `vscode` - Visual Studio Code editor\n- `terminal` - Terminal/console application\n- `desktop` - Switch to desktop\n- `directory` - File manager/directory browser\n\n</Accordion>\n\n## Response\n\nResponses vary based on the action performed:\n\n### Default Response\n\nMost actions return a simple success response:\n\n```json\n{\n  \"success\": true\n}\n```\n\n### Screenshot Response\n\nReturns the screenshot as a base64 encoded string:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"image\": \"base64_encoded_image_data\"\n  }\n}\n```\n\n### Cursor Position Response\n\nReturns the current cursor position:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"x\": 123,\n    \"y\": 456\n  }\n}\n```\n\n### Error Response\n\n```json\n{\n  \"success\": false,\n  \"error\": \"Error message\"\n}\n```\n\n### Code Examples\n\n<CodeGroup>\n```bash cURL\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"move_mouse\", \"coordinates\": {\"x\": 100, \"y\": 200}}'\n```\n\n```python Python\nimport requests\n\ndef control_computer(action, **params):\n    url = \"http://localhost:9990/computer-use\"\n    data = {\"action\": action, **params}\n    response = requests.post(url, json=data)\n    return response.json()\n\n# Move the mouse\nresult = control_computer(\"move_mouse\", coordinates={\"x\": 100, \"y\": 100})\nprint(result)\n```\n\n```javascript JavaScript\nconst axios = require(\"axios\");\n\nasync function controlComputer(action, params = {}) {\n  const url = \"http://localhost:9990/computer-use\";\n  const data = { action, ...params };\n  const response = await axios.post(url, data);\n  return response.data;\n}\n\n// Move mouse example\ncontrolComputer(\"move_mouse\", { coordinates: { x: 100, y: 100 } })\n  .then((result) => console.log(result))\n  .catch((error) => console.error(\"Error:\", error));\n```\n\n</CodeGroup>\n"
  },
  {
    "path": "docs/rest-api/examples.mdx",
    "content": "---\ntitle: \"Usage Examples\"\ndescription: \"Code examples for common automation scenarios using the Bytebot REST API\"\n---\n\nThis page provides practical examples of how to use the Bytebot REST API in different programming languages and scenarios.\n\n## Language Examples\n\n### cURL Examples\n\n<CodeGroup>\n```bash Open Application and Navigate\n# Open an application (like Firefox)\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"move_mouse\", \"coordinates\": {\"x\": 100, \"y\": 950}}'\n\ncurl -X POST http://localhost:9990/computer-use \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\": \"click_mouse\", \"button\": \"left\", \"clickCount\": 2}'\n\n# Wait for application to open\n\ncurl -X POST http://localhost:9990/computer-use \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\": \"wait\", \"duration\": 150}'\n\n# Type URL in address bar\n\ncurl -X POST http://localhost:9990/computer-use \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\": \"type_text\", \"text\": \"https://example.com\"}'\n\n# Press Enter to navigate\n\ncurl -X POST http://localhost:9990/computer-use \\\n -H \"Content-Type: application/json\" \\\n -d '{\"action\": \"typ_keys\", \"keys\": [\"enter\"]}'\n\n````\n\n```bash Take and Save Screenshot\n# Take a screenshot\nresponse=$(curl -s -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"screenshot\"}')\n\n# Extract the base64 image data and save to a file\necho $response | jq -r '.data.image' | base64 -d > screenshot.png\necho \"Screenshot saved to screenshot.png\"\n````\n\n```bash Copy and Paste Text\n# Select text with triple click (selects a paragraph)\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"move_mouse\", \"coordinates\": {\"x\": 400, \"y\": 300}}'\n\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"click_mouse\", \"button\": \"left\", \"clickCount\": 3}'\n\n# Copy with Ctrl+C\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"press_keys\", \"keys\": [\"ctrl\", \"c\"], \"press\": \"down\"}'\n\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"press_keys\", \"keys\": [\"ctrl\", \"c\"], \"press\": \"up\"}'\n\n# Click elsewhere to paste\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"move_mouse\", \"coordinates\": {\"x\": 400, \"y\": 500}}'\n\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"click_mouse\", \"button\": \"left\"}'\n\n# Paste with Ctrl+V\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"press_keys\", \"keys\": [\"ctrl\", \"v\"], \"press\": \"down\"}'\n\ncurl -X POST http://localhost:9990/computer-use \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\": \"press_keys\", \"keys\": [\"ctrl\", \"v\"], \"press\": \"up\"}'\n```\n\n</CodeGroup>\n\n### Python Examples\n\n<CodeGroup>\n```python Web Form Automation\nimport requests\nimport time\n\ndef control_computer(action, **params):\nurl = \"http://localhost:9990/computer-use\"\ndata = {\"action\": action, **params}\nresponse = requests.post(url, json=data)\nreturn response.json()\n\ndef fill_web_form(): # Navigate to a form (e.g., login form)\ncontrol_computer(\"move_mouse\", coordinates={\"x\": 500, \"y\": 300})\ncontrol_computer(\"click_mouse\", button=\"left\")\n\n    # Type username\n    control_computer(\"type_text\", text=\"user@example.com\")\n\n    # Tab to password field\n    control_computer(\"type_keys\", keys=[\"tab\"])\n\n    # Type password\n    control_computer(\"type_text\", text=\"secure_password\")\n\n    # Tab to login button\n    control_computer(\"type_keys\", keys=[\"tab\"])\n\n    # Press Enter to submit\n    control_computer(\"type_keys\", keys=[\"enter\"])\n\n    # Wait for page to load\n    control_computer(\"wait\", duration=2000)\n\n    print(\"Form submitted successfully\")\n\n# Run the automation\n\n# fill_web_form()\n\n````\n\n```python File Upload Dialog\nimport requests\nimport time\n\ndef control_computer(action, **params):\n    url = \"http://localhost:9990/computer-use\"\n    data = {\"action\": action, **params}\n    response = requests.post(url, json=data)\n    return response.json()\n\ndef upload_file(file_path=\"/path/to/file.txt\"):\n    # Click the upload button\n    control_computer(\"move_mouse\", coordinates={\"x\": 400, \"y\": 300})\n    control_computer(\"click_mouse\", button=\"left\")\n\n    # Wait for file dialog to appear\n    control_computer(\"wait\", duration=1000)\n\n    # Type the file path\n    control_computer(\"type_text\", text=file_path)\n\n    # Press Enter to confirm\n    control_computer(\"press_keys\", keys=[\"enter\"], press=\"down\")\n    control_computer(\"press_keys\", keys=[\"enter\"], press=\"up\")\n\n    # Wait for upload to complete\n    control_computer(\"wait\", duration=3000)\n\n    # Take screenshot of result\n    result = control_computer(\"screenshot\")\n    if result[\"success\"]:\n        print(\"File upload completed and screenshot taken\")\n\n# Run the automation\n# upload_file(\"/Users/username/Documents/example.pdf\")\n````\n\n```python Screenshot Monitoring\nimport requests\nimport base64\nimport time\nfrom io import BytesIO\nfrom PIL import Image\n\ndef take_screenshot():\n    url = \"http://localhost:9990/computer-use\"\n    response = requests.post(url, json={\"action\": \"screenshot\"})\n    if response.json()[\"success\"]:\n        img_data = base64.b64decode(response.json()[\"data\"][\"image\"])\n        return Image.open(BytesIO(img_data))\n    return None\n\ndef monitor_for_changes(interval=5, duration=60):\n    \"\"\"Monitor the screen for changes at regular intervals\"\"\"\n    first_screenshot = take_screenshot()\n    if not first_screenshot:\n        print(\"Failed to take initial screenshot\")\n        return\n\n    first_screenshot.save(\"baseline.png\")\n    print(\"Baseline screenshot saved\")\n\n    end_time = time.time() + duration\n    screenshot_count = 1\n\n    while time.time() < end_time:\n        time.sleep(interval)\n\n        current = take_screenshot()\n        if current:\n            filename = f\"screenshot_{screenshot_count}.png\"\n            current.save(filename)\n            print(f\"Saved {filename}\")\n            screenshot_count += 1\n\n    print(f\"Monitoring completed. Saved {screenshot_count} screenshots.\")\n\n# Run the monitoring for 30 seconds, taking a screenshot every 5 seconds\n# monitor_for_changes(interval=5, duration=30)\n```\n\n</CodeGroup>\n\n### JavaScript/Node.js Examples\n\n<CodeGroup>\n```javascript Browser Navigation\nconst axios = require('axios');\n\nasync function controlComputer(action, params = {}) {\nconst url = \"http://localhost:9990/computer-use\";\nconst data = { action, ...params };\n\ntry {\nconst response = await axios.post(url, data);\nreturn response.data;\n} catch (error) {\nconsole.error('Error:', error.message);\nthrow error;\n}\n}\n\nasync function navigateToWebsite(url) {\nconsole.log(`Navigating to ${url}...`);\n\n// Open Firefox/Chrome by clicking on dock icon\nawait controlComputer(\"move_mouse\", { coordinates: { x: 100, y: 950 } });\nawait controlComputer(\"click_mouse\", { button: \"left\" });\n\n// Wait for browser to open\nawait controlComputer(\"wait\", { duration: 2000 });\n\n// Click in URL bar (usually near the top)\nawait controlComputer(\"move_mouse\", { coordinates: { x: 400, y: 60 } });\nawait controlComputer(\"click_mouse\", { button: \"left\" });\n\n// Select all existing text (Cmd+A on Mac, Ctrl+A elsewhere)\nawait controlComputer(\"press_keys\", { keys: [\"ctrl\"], press: \"down\" });\nawait controlComputer(\"press_keys\", { keys: [\"a\"], press: \"down\" });\nawait controlComputer(\"press_keys\", { keys: [\"a\"], press: \"up\" });\nawait controlComputer(\"press_keys\", { keys: [\"ctrl\"], press: \"up\" });\n\n// Type the URL\nawait controlComputer(\"type_text\", { text: url });\n\n// Press Enter to navigate\nawait controlComputer(\"press_keys\", { keys: [\"enter\"], press: \"down\" });\nawait controlComputer(\"press_keys\", { keys: [\"enter\"], press: \"up\" });\n\n// Wait for page to load\nawait controlComputer(\"wait\", { duration: 3000 });\n\nconsole.log(\"Navigation completed\");\n}\n\n// Usage\n// navigateToWebsite(\"https://example.com\").catch(console.error);\n\n````\n\n```javascript Data Entry Automation\nconst axios = require('axios');\n\nasync function controlComputer(action, params = {}) {\n  const url = \"http://localhost:9990/computer-use\";\n  const data = { action, ...params };\n\n  try {\n    const response = await axios.post(url, data);\n    return response.data;\n  } catch (error) {\n    console.error('Error:', error.message);\n    throw error;\n  }\n}\n\n// Function to fill a data entry form\nasync function fillDataEntryForm(formData) {\n  // Click on the first form field\n  await controlComputer(\"move_mouse\", { coordinates: { x: 400, y: 250 } });\n  await controlComputer(\"click_mouse\", { button: \"left\" });\n\n  // Fill each field and press Tab to move to the next\n  for (const [index, value] of formData.entries()) {\n    // Type the value\n    await controlComputer(\"type_text\", { text: value });\n\n    // If not the last field, press Tab to move to next field\n    if (index < formData.length - 1) {\n      await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"down\" });\n      await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"up\" });\n      await controlComputer(\"wait\", { duration: 300 });\n    }\n  }\n\n  // Find and click the submit button\n  await controlComputer(\"move_mouse\", { coordinates: { x: 400, y: 500 } });\n  await controlComputer(\"click_mouse\", { button: \"left\" });\n\n  // Take a screenshot of the result\n  const result = await controlComputer(\"screenshot\");\n  console.log(\"Form submitted successfully\");\n\n  return result;\n}\n\n// Example form data\nconst formFields = [\n  \"John Doe\",              // Name\n  \"john.doe@example.com\",  // Email\n  \"123 Main St\",           // Address\n  \"555-123-4567\"           // Phone\n];\n\n// Usage\n// fillDataEntryForm(formFields).catch(console.error);\n````\n\n```javascript UI Testing\nconst axios = require(\"axios\");\nconst fs = require(\"fs\");\n\nasync function controlComputer(action, params = {}) {\n  const url = \"http://localhost:9990/computer-use\";\n  const data = { action, ...params };\n\n  try {\n    const response = await axios.post(url, data);\n    return response.data;\n  } catch (error) {\n    console.error(\"Error:\", error.message);\n    throw error;\n  }\n}\n\n// Simple UI testing framework\nasync function testUIElement(name, options) {\n  const { x, y, expectedResult } = options;\n\n  console.log(`Testing UI element: ${name}`);\n\n  // Take screenshot before interaction\n  const beforeShot = await controlComputer(\"screenshot\");\n  fs.writeFileSync(\n    `before_${name}.png`,\n    Buffer.from(beforeShot.data.image, \"base64\")\n  );\n\n  // Click the element\n  await controlComputer(\"move_mouse\", { coordinates: { x, y } });\n  await controlComputer(\"click_mouse\", { button: \"left\" });\n\n  // Wait for any UI changes\n  await controlComputer(\"wait\", { duration: 1000 });\n\n  // Take screenshot after interaction\n  const afterShot = await controlComputer(\"screenshot\");\n  fs.writeFileSync(\n    `after_${name}.png`,\n    Buffer.from(afterShot.data.image, \"base64\")\n  );\n\n  console.log(`Test for ${name} completed. Screenshots saved.`);\n\n  if (expectedResult && typeof expectedResult === \"function\") {\n    // Call custom verification function if provided\n    await expectedResult();\n  }\n}\n\n// Example UI elements to test\nconst uiElements = [\n  { name: \"login_button\", x: 500, y: 400 },\n  { name: \"menu_icon\", x: 50, y: 50 },\n  { name: \"close_dialog\", x: 800, y: 200 },\n];\n\n// Run tests sequentially\nasync function runUITests() {\n  for (const element of uiElements) {\n    await testUIElement(element.name, element);\n    // Add some delay between tests\n    await controlComputer(\"wait\", { duration: 2000 });\n  }\n  console.log(\"All UI tests completed\");\n}\n\n// Usage\n// runUITests().catch(console.error);\n```\n\n</CodeGroup>\n\n### Common Automation Scenarios\n\n### Browser Automation Workflow\n\nThis example demonstrates a complete browser workflow, opening a site and interacting with it:\n\n```python\nimport requests\nimport time\n\ndef control_computer(action, **params):\n    url = \"http://localhost:9990/computer-use\"\n    data = {\"action\": action, **params}\n    response = requests.post(url, json=data)\n    return response.json()\n\ndef browser_workflow():\n    # Open browser (assuming browser icon is at position x=100, y=960)\n    control_computer(\"move_mouse\", coordinates={\"x\": 100, \"y\": 960})\n    control_computer(\"click_mouse\", button=\"left\")\n    time.sleep(3)  # Wait for browser to open\n\n    # Type URL and navigate\n    control_computer(\"type_text\", text=\"https://example.com\")\n    control_computer(\"press_keys\", key=\"enter\")\n    time.sleep(2)  # Wait for page to load\n\n    # Take screenshot of the loaded page\n    screenshot = control_computer(\"screenshot\")\n\n    # Click on a link (coordinates would need to be adjusted for your target)\n    control_computer(\"move_mouse\", coordinates={\"x\": 300, \"y\": 400})\n    control_computer(\"click_mouse\", button=\"left\")\n    time.sleep(2)\n\n    # Scroll down\n    control_computer(\"scroll\", direction=\"down\", amount=500)\n\n    # Fill a search box\n    control_computer(\"move_mouse\", coordinates={\"x\": 600, \"y\": 200})\n    control_computer(\"click_mouse\", button=\"left\")\n    control_computer(\"type_text\", text=\"search query\")\n    control_computer(\"press_keys\", key=\"enter\")\n\nbrowser_workflow()\n```\n\n### Form Filling Workflow\n\nThis example shows a complete form-filling process:\n\n```javascript\nconst axios = require(\"axios\");\n\nasync function controlComputer(action, params = {}) {\n  const url = \"http://localhost:9990/computer-use\";\n  const data = { action, ...params };\n  const response = await axios.post(url, data);\n  return response.data;\n}\n\nasync function fillForm() {\n  // Navigate to form page\n  await controlComputer(\"move_mouse\", { coordinates: { x: 100, y: 960 } });\n  await controlComputer(\"click_mouse\", { button: \"left\" });\n  await controlComputer(\"wait\", { duration: 3000 });\n  await controlComputer(\"type_text\", { text: \"https://example.com/form\" });\n  await controlComputer(\"press_keys\", { key: \"enter\" });\n  await controlComputer(\"wait\", { duration: 2000 });\n\n  // Fill form\n  // Name field\n  await controlComputer(\"move_mouse\", { coordinates: { x: 400, y: 250 } });\n  await controlComputer(\"click_mouse\", { button: \"left\" });\n\n  // Type the value\n  await controlComputer(\"type_text\", { text: \"John Doe\" });\n\n  // Email field (tab to next field)\n  await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"down\" });\n  await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"up\" });\n  await controlComputer(\"type_text\", { text: \"john@example.com\" });\n\n  // Message field (tab to next field)\n  await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"down\" });\n  await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"up\" });\n  await controlComputer(\"type_text\", {\n    text: \"This is an automated message sent using Bytebot's Computer Use API\",\n    delay: 30,\n  });\n\n  // Submit form\n  await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"down\" });\n  await controlComputer(\"press_keys\", { keys: [\"tab\"], press: \"up\" });\n  await controlComputer(\"press_keys\", { key: \"enter\" });\n\n  // Take screenshot of confirmation page\n  await controlComputer(\"wait\", { duration: 2000 });\n  const screenshot = await controlComputer(\"screenshot\");\n\n  console.log(\"Form submitted successfully\");\n}\n\nfillForm().catch(console.error);\n```\n\n### Automation Framework Integration\n\nYou can create a reusable automation framework with Bytebot:\n\n```python\nimport requests\nimport time\nimport json\n\nclass BytebotDriver:\n    \"\"\"A Selenium-like driver for Bytebot\"\"\"\n\n    def __init__(self, base_url=\"http://localhost:9990\"):\n        self.base_url = base_url\n\n    def control_computer(self, action, **params):\n        url = f\"{self.base_url}/computer-use\"\n        data = {\"action\": action, **params}\n        response = requests.post(url, json=data)\n        return response.json()\n\n    def open_browser(self, browser_icon_coords):\n        \"\"\"Open a browser by clicking its icon\"\"\"\n        self.control_computer(\"move_mouse\", coordinates=browser_icon_coords)\n        self.control_computer(\"click_mouse\", button=\"left\")\n        time.sleep(3)  # Wait for browser to open\n\n    def navigate_to(self, url):\n        \"\"\"Navigate to a URL in the browser\"\"\"\n        self.control_computer(\"type_text\", text=url)\n        self.control_computer(\"press_keys\", key=\"enter\")\n        time.sleep(2)  # Wait for page to load\n\n    def click_element(self, coords):\n        \"\"\"Click an element at the specified coordinates\"\"\"\n        self.control_computer(\"move_mouse\", coordinates=coords)\n        self.control_computer(\"click_mouse\", button=\"left\")\n\n    def type_text(self, text):\n        \"\"\"Type text at the current cursor position\"\"\"\n        self.control_computer(\"type_text\", text=text)\n\n    def press_key(self, key, modifiers=None):\n        \"\"\"Press a keyboard key with optional modifiers\"\"\"\n        params = {\"key\": key}\n        if modifiers:\n            params[\"modifiers\"] = modifiers\n        self.control_computer(\"press_keys\", **params)\n\n    def take_screenshot(self):\n        \"\"\"Take a screenshot of the desktop\"\"\"\n        return self.control_computer(\"screenshot\")\n\n    def scroll(self, direction, amount):\n        \"\"\"Scroll in the specified direction\"\"\"\n        self.control_computer(\"scroll\", direction=direction, amount=amount)\n\n# Example usage\ndriver = BytebotDriver()\ndriver.open_browser({\"x\": 100, \"y\": 960})\ndriver.navigate_to(\"https://example.com\")\ndriver.click_element({\"x\": 300, \"y\": 400})\ndriver.type_text(\"Hello Bytebot!\")\ndriver.press_key(\"enter\")\nresult = driver.take_screenshot()\nprint(f\"Screenshot captured: {result['success']}\")\n```\n"
  },
  {
    "path": "docs/rest-api/input-tracking.mdx",
    "content": "---\ntitle: \"Input Tracking\"\nopenapi: \"POST /input-tracking/start\"\ndescription: \"Start and stop input tracking on the Bytebot desktop\"\n---\n\nThe Bytebot daemon can monitor mouse and keyboard events through the\n`InputTracking` module. Tracking is disabled by default and can be toggled\nvia the REST API. Tracked actions are streamed over WebSockets so that the\nagent can store them as messages.\n\n## Start Tracking\n\n`POST /input-tracking/start`\n\nBegins capturing input events. The endpoint returns a simple status object:\n\n```json\n{\n  \"status\": \"started\"\n}\n```\n\n## Stop Tracking\n\n`POST /input-tracking/stop`\n\nStops capturing events and clears any internal buffers. The response is\nsimilar to the start endpoint:\n\n```json\n{\n  \"status\": \"stopped\"\n}\n```\n\n## WebSocket Stream\n\nWhen tracking is active, actions are emitted on the `input_action` channel of\nthe WebSocket server running on the daemon. Clients can connect to the daemon\nand listen for these events to persist them as needed.\n"
  },
  {
    "path": "docs/rest-api/introduction.mdx",
    "content": "---\ntitle: \"Introduction\"\ndescription: \"Overview of the Bytebot REST API\"\n---\n\n## Bytebot REST API\n\nBytebot's core functionality is exposed through its REST API, which provides endpoints for interacting with the desktop environment. The API allows for programmatic control of mouse movement, keyboard input, and screen capture.\n\n### Base URL\n\nAll API endpoints are relative to the base URL:\n\n```\nhttp://localhost:9990\n```\n\nThe port can be configured when running the container.\n\n### Authentication\n\nThe Bytebot API does not require authentication by default when accessed locally. For remote access, standard network security practices should be implemented.\n\n### Response Format\n\nAll API responses follow a standard JSON format:\n\n```json\n{\n  \"success\": true,\n  \"data\": { ... },  // Response data specific to the action\n  \"error\": null     // Error message if success is false\n}\n```\n\n### Error Handling\n\nWhen an error occurs, the API returns:\n\n```json\n{\n  \"success\": false,\n  \"data\": null,\n  \"error\": \"Detailed error message\"\n}\n```\n\nCommon HTTP status codes:\n\n| Status Code | Description                      |\n| ----------- | -------------------------------- |\n| 200         | Success                          |\n| 400         | Bad Request - Invalid parameters |\n| 500         | Internal Server Error            |\n\n### Available Endpoints\n\n<CardGroup cols={2}>\n  <Card\n    title=\"Computer Use\"\n    icon=\"computer-mouse\"\n    href=\"/rest-api/computer-use\"\n  >\n    Execute desktop automation actions like mouse movements, clicks, keyboard\n    input, and screenshots\n  </Card>\n  <Card title=\"Input Tracking\" icon=\"eye\" href=\"/rest-api/input-tracking\">\n    Control and stream keyboard and mouse events from the desktop\n  </Card>\n  <Card title=\"Usage Examples\" icon=\"code\" href=\"/rest-api/examples\">\n    Code examples and snippets for common automation scenarios\n  </Card>\n</CardGroup>\n\n### Rate Limiting\n\nThe API currently does not implement rate limiting, but excessive requests may impact performance of the virtual desktop environment.\n"
  },
  {
    "path": "helm/Chart.yaml",
    "content": "apiVersion: v2\nname: bytebot\ndescription: Bytebot - Complete deployment package\ntype: application\nversion: 0.1.0\nappVersion: \"edge\"\ndependencies:\n  - name: postgresql\n    version: 0.1.0\n    repository: file://./charts/postgresql\n    condition: postgresql.enabled\n  - name: bytebot-desktop\n    version: 0.1.0\n    repository: file://./charts/bytebot-desktop\n  - name: bytebot-agent\n    version: 0.1.0\n    repository: file://./charts/bytebot-agent\n  - name: bytebot-ui\n    version: 0.1.0\n    repository: file://./charts/bytebot-ui\n  - name: bytebot-llm-proxy\n    version: 0.1.0\n    repository: file://./charts/bytebot-llm-proxy\n    condition: bytebot-llm-proxy.enabled\nkeywords:\n  - bytebot\n  - automation\n  - remote-desktop\nmaintainers:\n  - name: Bytebot Team\nsources:\n  - https://github.com/bytebot-ai/bytebot"
  },
  {
    "path": "helm/README.md",
    "content": "# Bytebot Helm Charts\n\nThis directory contains Helm charts for deploying Bytebot on Kubernetes.\n\n## Documentation\n\nFor complete deployment instructions, see:\n**[Helm Deployment Guide](https://docs.bytebot.ai/deployment/helm)**\n\n## Quick Start\n\n```bash\n# Clone repository\ngit clone https://github.com/bytebot-ai/bytebot.git\ncd bytebot\n\n# Create values.yaml with your API key(s)\ncat > values.yaml <<EOF\nbytebot-agent:\n  apiKeys:\n    anthropic:\n      value: \"sk-ant-your-key-here\"\nEOF\n\n# Install\nhelm install bytebot ./helm --namespace bytebot --create-namespace -f values.yaml\n\n# Access\nkubectl port-forward -n bytebot svc/bytebot-ui 9992:9992\n```\n\nAccess at: http://localhost:9992\n\n## Structure\n\n```\nhelm/\n├── Chart.yaml              # Main chart\n├── values.yaml             # Default values\n├── values-proxy.yaml       # LiteLLM proxy configuration\n├── templates/              # Kubernetes templates\n└── charts/                 # Subcharts\n    ├── bytebot-desktop/    # Desktop VNC service\n    ├── bytebot-agent/      # Backend API service\n    ├── bytebot-ui/         # Frontend UI service\n    ├── bytebot-llm-proxy/  # Optional LiteLLM proxy\n    └── postgresql/         # PostgreSQL database\n```"
  },
  {
    "path": "helm/charts/bytebot-agent/Chart.yaml",
    "content": "apiVersion: v2\nname: bytebot-agent\ndescription: A Helm chart for Bytebot Agent service\ntype: application\nversion: 0.1.0\nappVersion: \"edge\"\ndependencies:\n  - name: postgresql\n    version: \"~13.2\"\n    repository: \"https://charts.bitnami.com/bitnami\"\n    condition: postgresql.enabled\nkeywords:\n  - bytebot\n  - agent\n  - api\nmaintainers:\n  - name: Bytebot Team\nsources:\n  - https://github.com/bytebot-ai/bytebot"
  },
  {
    "path": "helm/charts/bytebot-agent/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"bytebot-agent.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\n*/}}\n{{- define \"bytebot-agent.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"bytebot-agent.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"bytebot-agent.labels\" -}}\nhelm.sh/chart: {{ include \"bytebot-agent.chart\" . }}\n{{ include \"bytebot-agent.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"bytebot-agent.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"bytebot-agent.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"bytebot-agent.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"bytebot-agent.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate a default database URL\n*/}}\n{{- define \"bytebot-agent.databaseUrl\" -}}\n{{- if .Values.postgresql.enabled }}\n{{- printf \"postgresql://%s:%s@%s-postgresql:%d/%s\" .Values.postgresql.auth.username .Values.postgresql.auth.password .Release.Name (.Values.postgresql.service.port | int) .Values.postgresql.auth.database }}\n{{- else if .Values.externalDatabase.host }}\n{{- printf \"postgresql://%s:%s@%s:%d/%s\" .Values.externalDatabase.username .Values.externalDatabase.password .Values.externalDatabase.host (.Values.externalDatabase.port | int) .Values.externalDatabase.database }}\n{{- else }}\n{{- .Values.env.DATABASE_URL }}\n{{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-agent/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"bytebot-agent.fullname\" . }}\n  labels:\n    {{- include \"bytebot-agent.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"bytebot-agent.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"bytebot-agent.selectorLabels\" . | nindent 8 }}\n    spec:\n      containers:\n      - name: {{ .Chart.Name }}\n        image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n        imagePullPolicy: {{ .Values.image.pullPolicy }}\n        ports:\n        - name: http\n          containerPort: {{ .Values.service.targetPort }}\n          protocol: TCP\n        env:\n        - name: DATABASE_URL\n          value: {{ include \"bytebot-agent.databaseUrl\" . | quote }}\n        - name: BYTEBOT_DESKTOP_BASE_URL\n          value: {{ .Values.config.bytebotDesktopUrl | default .Values.env.BYTEBOT_DESKTOP_BASE_URL | quote }}\n        {{- if or .Values.config.llmProxyUrl .Values.env.BYTEBOT_LLM_PROXY_URL }}\n        - name: BYTEBOT_LLM_PROXY_URL\n          value: {{ .Values.config.llmProxyUrl | default .Values.env.BYTEBOT_LLM_PROXY_URL | quote }}\n        {{- end }}\n        {{- /* Anthropic API Key */ -}}\n        {{- if .Values.env.ANTHROPIC_API_KEY }}\n        - name: ANTHROPIC_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n              key: anthropic-api-key\n        {{- else if .Values.apiKeys.anthropic.useExisting }}\n        - name: ANTHROPIC_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ .Values.apiKeys.anthropic.secretName }}\n              key: {{ .Values.apiKeys.anthropic.secretKey }}\n        {{- else if .Values.apiKeys.anthropic.value }}\n        - name: ANTHROPIC_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n              key: anthropic-api-key\n        {{- end }}\n        {{- /* OpenAI API Key */ -}}\n        {{- if .Values.env.OPENAI_API_KEY }}\n        - name: OPENAI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n              key: openai-api-key\n        {{- else if .Values.apiKeys.openai.useExisting }}\n        - name: OPENAI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ .Values.apiKeys.openai.secretName }}\n              key: {{ .Values.apiKeys.openai.secretKey }}\n        {{- else if .Values.apiKeys.openai.value }}\n        - name: OPENAI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n              key: openai-api-key\n        {{- end }}\n        {{- /* Gemini API Key */ -}}\n        {{- if .Values.env.GEMINI_API_KEY }}\n        - name: GEMINI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n              key: gemini-api-key\n        {{- else if .Values.apiKeys.gemini.useExisting }}\n        - name: GEMINI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ .Values.apiKeys.gemini.secretName }}\n              key: {{ .Values.apiKeys.gemini.secretKey }}\n        {{- else if .Values.apiKeys.gemini.value }}\n        - name: GEMINI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n              key: gemini-api-key\n        {{- end }}\n        resources:\n          {{- toYaml .Values.resources | nindent 10 }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}"
  },
  {
    "path": "helm/charts/bytebot-agent/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: {{ include \"bytebot-agent.fullname\" . }}\n  labels:\n    {{- include \"bytebot-agent.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.className }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            pathType: {{ .pathType }}\n            backend:\n              service:\n                name: {{ include \"bytebot-agent.fullname\" $ }}\n                port:\n                  number: {{ $.Values.service.port }}\n          {{- end }}\n    {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-agent/templates/secret.yaml",
    "content": "{{- $createSecret := false -}}\n{{- if or .Values.env.ANTHROPIC_API_KEY .Values.env.OPENAI_API_KEY .Values.env.GEMINI_API_KEY -}}\n  {{- $createSecret = true -}}\n{{- else if and (not .Values.apiKeys.anthropic.useExisting) .Values.apiKeys.anthropic.value -}}\n  {{- $createSecret = true -}}\n{{- else if and (not .Values.apiKeys.openai.useExisting) .Values.apiKeys.openai.value -}}\n  {{- $createSecret = true -}}\n{{- else if and (not .Values.apiKeys.gemini.useExisting) .Values.apiKeys.gemini.value -}}\n  {{- $createSecret = true -}}\n{{- end -}}\n\n{{- if $createSecret -}}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"bytebot-agent.fullname\" . }}-secrets\n  labels:\n    {{- include \"bytebot-agent.labels\" . | nindent 4 }}\ntype: Opaque\ndata:\n  {{- /* Support legacy values for backward compatibility */ -}}\n  {{- if .Values.env.ANTHROPIC_API_KEY }}\n  anthropic-api-key: {{ .Values.env.ANTHROPIC_API_KEY | b64enc | quote }}\n  {{- else if and (not .Values.apiKeys.anthropic.useExisting) .Values.apiKeys.anthropic.value }}\n  anthropic-api-key: {{ .Values.apiKeys.anthropic.value | b64enc | quote }}\n  {{- end }}\n  \n  {{- if .Values.env.OPENAI_API_KEY }}\n  openai-api-key: {{ .Values.env.OPENAI_API_KEY | b64enc | quote }}\n  {{- else if and (not .Values.apiKeys.openai.useExisting) .Values.apiKeys.openai.value }}\n  openai-api-key: {{ .Values.apiKeys.openai.value | b64enc | quote }}\n  {{- end }}\n  \n  {{- if .Values.env.GEMINI_API_KEY }}\n  gemini-api-key: {{ .Values.env.GEMINI_API_KEY | b64enc | quote }}\n  {{- else if and (not .Values.apiKeys.gemini.useExisting) .Values.apiKeys.gemini.value }}\n  gemini-api-key: {{ .Values.apiKeys.gemini.value | b64enc | quote }}\n  {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-agent/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"bytebot-agent.fullname\" . }}\n  labels:\n    {{- include \"bytebot-agent.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"bytebot-agent.selectorLabels\" . | nindent 4 }}"
  },
  {
    "path": "helm/charts/bytebot-agent/values.yaml",
    "content": "replicaCount: 1\n\nimage:\n  repository: ghcr.io/bytebot-ai/bytebot-agent\n  tag: edge\n  pullPolicy: IfNotPresent\n\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nservice:\n  type: ClusterIP\n  port: 9991\n  targetPort: 9991\n  annotations: {}\n\nresources:\n  limits:\n    memory: \"2Gi\"\n    cpu: \"1000m\"\n  requests:\n    memory: \"1Gi\"\n    cpu: \"500m\"\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\nenv:\n  DATABASE_URL: \"\"\n  BYTEBOT_DESKTOP_BASE_URL: \"http://bytebot-desktop:9990\"\n  BYTEBOT_LLM_PROXY_URL: \"\"\n  # Legacy API key values for backward compatibility\n  ANTHROPIC_API_KEY: \"\"\n  OPENAI_API_KEY: \"\"\n  GEMINI_API_KEY: \"\"\n\n# New secret management structure\napiKeys:\n  anthropic:\n    useExisting: false\n    secretName: \"\"\n    secretKey: \"anthropic-api-key\"\n    value: \"\"  # Only used if useExisting is false\n  openai:\n    useExisting: false\n    secretName: \"\"\n    secretKey: \"openai-api-key\"\n    value: \"\"  # Only used if useExisting is false\n  gemini:\n    useExisting: false\n    secretName: \"\"\n    secretKey: \"gemini-api-key\"\n    value: \"\"  # Only used if useExisting is false\n\nconfig:\n  databaseUrl: \"postgresql://postgres:postgres@bytebot-postgresql:5432/bytebotdb\"\n  bytebotDesktopUrl: \"http://bytebot-desktop:9990\"\n  llmProxyUrl: \"\"\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n  hosts:\n    - host: bytebot-agent.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []\n\npostgresql:\n  enabled: true\n  auth:\n    username: postgres\n    password: postgres\n    database: bytebotdb\n  service:\n    port: 5432\n  primary:\n    persistence:\n      enabled: true\n      size: 10Gi\n\nexternalDatabase:\n  host: \"\"\n  port: 5432\n  database: bytebotdb\n  username: postgres\n  password: postgres\n  existingSecret: \"\"\n  existingSecretPasswordKey: \"\""
  },
  {
    "path": "helm/charts/bytebot-desktop/Chart.yaml",
    "content": "apiVersion: v2\nname: bytebot-desktop\ndescription: A Helm chart for Bytebot Desktop service\ntype: application\nversion: 0.1.0\nappVersion: \"edge\"\nkeywords:\n  - bytebot\n  - desktop\n  - vnc\n  - remote-desktop\nmaintainers:\n  - name: Bytebot Team\nsources:\n  - https://github.com/bytebot-ai/bytebot"
  },
  {
    "path": "helm/charts/bytebot-desktop/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"bytebot-desktop.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\n*/}}\n{{- define \"bytebot-desktop.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"bytebot-desktop.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"bytebot-desktop.labels\" -}}\nhelm.sh/chart: {{ include \"bytebot-desktop.chart\" . }}\n{{ include \"bytebot-desktop.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"bytebot-desktop.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"bytebot-desktop.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"bytebot-desktop.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"bytebot-desktop.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-desktop/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"bytebot-desktop.fullname\" . }}\n  labels:\n    {{- include \"bytebot-desktop.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"bytebot-desktop.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"bytebot-desktop.selectorLabels\" . | nindent 8 }}\n    spec:\n      containers:\n      - name: {{ .Chart.Name }}\n        image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n        imagePullPolicy: {{ .Values.image.pullPolicy }}\n        ports:\n        - name: http\n          containerPort: {{ .Values.service.targetPort }}\n          protocol: TCP\n        env:\n        {{- toYaml .Values.env | nindent 8 }}\n        resources:\n          {{- toYaml .Values.resources | nindent 10 }}\n        {{- if .Values.persistence.enabled }}\n        volumeMounts:\n        - name: data\n          mountPath: /data\n        {{- end }}\n        securityContext:\n          {{- toYaml .Values.securityContext | nindent 10 }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- if .Values.persistence.enabled }}\n      volumes:\n      - name: data\n        persistentVolumeClaim:\n          claimName: {{ include \"bytebot-desktop.fullname\" . }}\n      {{- end }}\n      {{- if .Values.shm.enabled }}\n      volumes:\n      - name: dshm\n        emptyDir:\n          medium: Memory\n          sizeLimit: {{ .Values.shm.size }}\n      {{- end }}\n      hostname: computer"
  },
  {
    "path": "helm/charts/bytebot-desktop/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: {{ include \"bytebot-desktop.fullname\" . }}\n  labels:\n    {{- include \"bytebot-desktop.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.className }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            pathType: {{ .pathType }}\n            backend:\n              service:\n                name: {{ include \"bytebot-desktop.fullname\" $ }}\n                port:\n                  number: {{ $.Values.service.port }}\n          {{- end }}\n    {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-desktop/templates/pvc.yaml",
    "content": "{{- if .Values.persistence.enabled -}}\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: {{ include \"bytebot-desktop.fullname\" . }}\n  labels:\n    {{- include \"bytebot-desktop.labels\" . | nindent 4 }}\n  {{- with .Values.persistence.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  accessModes:\n    - {{ .Values.persistence.accessMode }}\n  resources:\n    requests:\n      storage: {{ .Values.persistence.size }}\n  {{- if .Values.persistence.storageClass }}\n  {{- if (eq \"-\" .Values.persistence.storageClass) }}\n  storageClassName: \"\"\n  {{- else }}\n  storageClassName: {{ .Values.persistence.storageClass }}\n  {{- end }}\n  {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-desktop/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"bytebot-desktop.fullname\" . }}\n  labels:\n    {{- include \"bytebot-desktop.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"bytebot-desktop.selectorLabels\" . | nindent 4 }}"
  },
  {
    "path": "helm/charts/bytebot-desktop/values.yaml",
    "content": "replicaCount: 1\n\nimage:\n  repository: ghcr.io/bytebot-ai/bytebot-desktop\n  tag: edge\n  pullPolicy: IfNotPresent\n\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nservice:\n  type: ClusterIP\n  port: 9990\n  targetPort: 9990\n  annotations: {}\n\nresources:\n  limits:\n    memory: \"4Gi\"\n    cpu: \"2000m\"\n  requests:\n    memory: \"2Gi\"\n    cpu: \"1000m\"\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\nenv:\n  - name: DISPLAY\n    value: \":0\"\n\nshm:\n  enabled: true\n  size: \"2Gi\"\n\nsecurityContext:\n  privileged: true\n\npersistence:\n  enabled: false\n  storageClass: \"\"\n  accessMode: ReadWriteOnce\n  size: 10Gi\n  annotations: {}\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n  hosts:\n    - host: bytebot-desktop.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/Chart.yaml",
    "content": "apiVersion: v2\nname: bytebot-llm-proxy\ndescription: A Helm chart for LiteLLM proxy service\ntype: application\nversion: 0.1.0\nappVersion: \"main-stable\"\nkeywords:\n  - litellm\n  - proxy\n  - llm\n  - ai\nmaintainers:\n  - name: Bytebot Team\nsources:\n  - https://github.com/bytebot-ai/bytebot"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"bytebot-llm-proxy.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\n*/}}\n{{- define \"bytebot-llm-proxy.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"bytebot-llm-proxy.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"bytebot-llm-proxy.labels\" -}}\nhelm.sh/chart: {{ include \"bytebot-llm-proxy.chart\" . }}\n{{ include \"bytebot-llm-proxy.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"bytebot-llm-proxy.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"bytebot-llm-proxy.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/templates/configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {{ include \"bytebot-llm-proxy.fullname\" . }}-config\n  labels:\n    {{- include \"bytebot-llm-proxy.labels\" . | nindent 4 }}\ndata:\n  litellm-config.yaml: |\n    {{- toYaml .Values.config | nindent 4 }}"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"bytebot-llm-proxy.fullname\" . }}\n  labels:\n    {{- include \"bytebot-llm-proxy.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"bytebot-llm-proxy.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"bytebot-llm-proxy.selectorLabels\" . | nindent 8 }}\n      annotations:\n        checksum/config: {{ include (print $.Template.BasePath \"/configmap.yaml\") . | sha256sum }}\n    spec:\n      containers:\n      - name: {{ .Chart.Name }}\n        image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n        imagePullPolicy: {{ .Values.image.pullPolicy }}\n        args:\n          - \"--config\"\n          - \"/app/config.yaml\"\n          - \"--port\"\n          - \"{{ .Values.service.targetPort }}\"\n        ports:\n        - name: http\n          containerPort: {{ .Values.service.targetPort }}\n          protocol: TCP\n        env:\n        {{- if .Values.env.ANTHROPIC_API_KEY }}\n        - name: ANTHROPIC_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-llm-proxy.fullname\" . }}-secrets\n              key: anthropic-api-key\n        {{- end }}\n        {{- if .Values.env.OPENAI_API_KEY }}\n        - name: OPENAI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-llm-proxy.fullname\" . }}-secrets\n              key: openai-api-key\n        {{- end }}\n        {{- if .Values.env.GEMINI_API_KEY }}\n        - name: GEMINI_API_KEY\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"bytebot-llm-proxy.fullname\" . }}-secrets\n              key: gemini-api-key\n        {{- end }}\n        volumeMounts:\n        - name: config\n          mountPath: /app/config.yaml\n          subPath: litellm-config.yaml\n        resources:\n          {{- toYaml .Values.resources | nindent 10 }}\n        livenessProbe:\n          httpGet:\n            path: /health\n            port: http\n          initialDelaySeconds: 30\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /health\n            port: http\n          initialDelaySeconds: 5\n          periodSeconds: 5\n      volumes:\n      - name: config\n        configMap:\n          name: {{ include \"bytebot-llm-proxy.fullname\" . }}-config\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: {{ include \"bytebot-llm-proxy.fullname\" . }}\n  labels:\n    {{- include \"bytebot-llm-proxy.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.className }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            pathType: {{ .pathType }}\n            backend:\n              service:\n                name: {{ include \"bytebot-llm-proxy.fullname\" $ }}\n                port:\n                  number: {{ $.Values.service.port }}\n          {{- end }}\n    {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/templates/secret.yaml",
    "content": "{{- if or .Values.env.ANTHROPIC_API_KEY .Values.env.OPENAI_API_KEY .Values.env.GEMINI_API_KEY -}}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"bytebot-llm-proxy.fullname\" . }}-secrets\n  labels:\n    {{- include \"bytebot-llm-proxy.labels\" . | nindent 4 }}\ntype: Opaque\ndata:\n  {{- if .Values.env.ANTHROPIC_API_KEY }}\n  anthropic-api-key: {{ .Values.env.ANTHROPIC_API_KEY | b64enc | quote }}\n  {{- end }}\n  {{- if .Values.env.OPENAI_API_KEY }}\n  openai-api-key: {{ .Values.env.OPENAI_API_KEY | b64enc | quote }}\n  {{- end }}\n  {{- if .Values.env.GEMINI_API_KEY }}\n  gemini-api-key: {{ .Values.env.GEMINI_API_KEY | b64enc | quote }}\n  {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"bytebot-llm-proxy.fullname\" . }}\n  labels:\n    {{- include \"bytebot-llm-proxy.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"bytebot-llm-proxy.selectorLabels\" . | nindent 4 }}"
  },
  {
    "path": "helm/charts/bytebot-llm-proxy/values.yaml",
    "content": "replicaCount: 1\n\nimage:\n  repository: ghcr.io/berriai/litellm\n  tag: main-stable\n  pullPolicy: IfNotPresent\n\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nservice:\n  type: ClusterIP\n  port: 4000\n  targetPort: 4000\n  annotations: {}\n\nresources:\n  limits:\n    memory: \"1Gi\"\n    cpu: \"1000m\"\n  requests:\n    memory: \"512Mi\"\n    cpu: \"500m\"\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\nenv:\n  ANTHROPIC_API_KEY: \"\"\n  OPENAI_API_KEY: \"\"\n  GEMINI_API_KEY: \"\"\n\n# LiteLLM configuration\nconfig:\n  model_list:\n    # Anthropic Models\n    - model_name: claude-opus-4\n      litellm_params:\n        model: anthropic/claude-opus-4-20250514\n        api_key: os.environ/ANTHROPIC_API_KEY\n    - model_name: claude-sonnet-4\n      litellm_params:\n        model: anthropic/claude-sonnet-4-20250514\n        api_key: os.environ/ANTHROPIC_API_KEY\n    \n    # OpenAI Models\n    - model_name: gpt-4.1\n      litellm_params:\n        model: openai/gpt-4.1\n        api_key: os.environ/OPENAI_API_KEY\n    - model_name: gpt-4o\n      litellm_params:\n        model: openai/gpt-4o\n        api_key: os.environ/OPENAI_API_KEY\n    \n    # Gemini Models\n    - model_name: gemini-2.5-pro\n      litellm_params:\n        model: gemini/gemini-2.5-pro\n        api_key: os.environ/GEMINI_API_KEY\n    - model_name: gemini-2.5-flash\n      litellm_params:\n        model: gemini/gemini-2.5-flash\n        api_key: os.environ/GEMINI_API_KEY\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n  hosts:\n    - host: litellm.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []"
  },
  {
    "path": "helm/charts/bytebot-ui/Chart.yaml",
    "content": "apiVersion: v2\nname: bytebot-ui\ndescription: A Helm chart for Bytebot UI service\ntype: application\nversion: 0.1.0\nappVersion: \"edge\"\nkeywords:\n  - bytebot\n  - ui\n  - frontend\nmaintainers:\n  - name: Bytebot Team\nsources:\n  - https://github.com/bytebot-ai/bytebot"
  },
  {
    "path": "helm/charts/bytebot-ui/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"bytebot-ui.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\n*/}}\n{{- define \"bytebot-ui.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"bytebot-ui.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"bytebot-ui.labels\" -}}\nhelm.sh/chart: {{ include \"bytebot-ui.chart\" . }}\n{{ include \"bytebot-ui.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"bytebot-ui.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"bytebot-ui.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"bytebot-ui.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"bytebot-ui.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-ui/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"bytebot-ui.fullname\" . }}\n  labels:\n    {{- include \"bytebot-ui.labels\" . | nindent 4 }}\nspec:\n  {{- if not .Values.autoscaling.enabled }}\n  replicas: {{ .Values.replicaCount }}\n  {{- end }}\n  selector:\n    matchLabels:\n      {{- include \"bytebot-ui.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"bytebot-ui.selectorLabels\" . | nindent 8 }}\n    spec:\n      containers:\n      - name: {{ .Chart.Name }}\n        image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n        imagePullPolicy: {{ .Values.image.pullPolicy }}\n        ports:\n        - name: http\n          containerPort: {{ .Values.service.targetPort }}\n          protocol: TCP\n        env:\n        - name: NODE_ENV\n          value: {{ .Values.env.NODE_ENV | quote }}\n        - name: HOSTNAME\n          value: \"0.0.0.0\"\n        - name: BYTEBOT_AGENT_BASE_URL\n          value: {{ .Values.config.agentBaseUrl | default .Values.env.BYTEBOT_AGENT_BASE_URL | quote }}\n        - name: BYTEBOT_DESKTOP_VNC_URL\n          value: {{ .Values.config.desktopVncUrl | default .Values.env.BYTEBOT_DESKTOP_VNC_URL | quote }}\n        resources:\n          {{- toYaml .Values.resources | nindent 10 }}\n        livenessProbe:\n          httpGet:\n            path: /\n            port: http\n          initialDelaySeconds: 30\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /\n            port: http\n          initialDelaySeconds: 5\n          periodSeconds: 5\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}"
  },
  {
    "path": "helm/charts/bytebot-ui/templates/hpa.yaml",
    "content": "{{- if .Values.autoscaling.enabled }}\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: {{ include \"bytebot-ui.fullname\" . }}\n  labels:\n    {{- include \"bytebot-ui.labels\" . | nindent 4 }}\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: {{ include \"bytebot-ui.fullname\" . }}\n  minReplicas: {{ .Values.autoscaling.minReplicas }}\n  maxReplicas: {{ .Values.autoscaling.maxReplicas }}\n  metrics:\n    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}\n    {{- end }}\n    {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}\n    {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-ui/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: {{ include \"bytebot-ui.fullname\" . }}\n  labels:\n    {{- include \"bytebot-ui.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.className }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            pathType: {{ .pathType }}\n            backend:\n              service:\n                name: {{ include \"bytebot-ui.fullname\" $ }}\n                port:\n                  number: {{ $.Values.service.port }}\n          {{- end }}\n    {{- end }}\n{{- end }}"
  },
  {
    "path": "helm/charts/bytebot-ui/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"bytebot-ui.fullname\" . }}\n  labels:\n    {{- include \"bytebot-ui.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"bytebot-ui.selectorLabels\" . | nindent 4 }}"
  },
  {
    "path": "helm/charts/bytebot-ui/values.yaml",
    "content": "replicaCount: 1\n\nimage:\n  repository: ghcr.io/bytebot-ai/bytebot-ui\n  tag: edge\n  pullPolicy: IfNotPresent\n\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nservice:\n  type: ClusterIP\n  port: 9992\n  targetPort: 9992\n  annotations: {}\n\nresources:\n  limits:\n    memory: \"1Gi\"\n    cpu: \"500m\"\n  requests:\n    memory: \"512Mi\"\n    cpu: \"250m\"\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\nenv:\n  NODE_ENV: \"production\"\n  BYTEBOT_AGENT_BASE_URL: \"http://bytebot-agent:9991\"\n  BYTEBOT_DESKTOP_VNC_URL: \"http://bytebot-desktop:9990/websockify\"\n\nconfig:\n  agentBaseUrl: \"http://bytebot-agent:9991\"\n  desktopVncUrl: \"http://bytebot-desktop:9990/websockify\"\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n  hosts:\n    - host: bytebot.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []\n\nautoscaling:\n  enabled: false\n  minReplicas: 1\n  maxReplicas: 5\n  targetCPUUtilizationPercentage: 80\n  targetMemoryUtilizationPercentage: 80"
  },
  {
    "path": "helm/charts/postgresql/Chart.yaml",
    "content": "apiVersion: v2\nname: postgresql\ndescription: A Helm chart for PostgreSQL database\ntype: application\nversion: 0.1.0\nappVersion: \"16-alpine\"\nkeywords:\n  - postgresql\n  - database\n  - sql\nmaintainers:\n  - name: Bytebot Team\nsources:\n  - https://github.com/bytebot-ai/bytebot"
  },
  {
    "path": "helm/charts/postgresql/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"postgresql.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\n*/}}\n{{- define \"postgresql.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"postgresql.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"postgresql.labels\" -}}\nhelm.sh/chart: {{ include \"postgresql.chart\" . }}\n{{ include \"postgresql.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"postgresql.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"postgresql.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}"
  },
  {
    "path": "helm/charts/postgresql/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: {{ include \"postgresql.fullname\" . }}\n  labels:\n    {{- include \"postgresql.labels\" . | nindent 4 }}\nspec:\n  serviceName: {{ include \"postgresql.fullname\" . }}\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"postgresql.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:\n        {{- include \"postgresql.selectorLabels\" . | nindent 8 }}\n    spec:\n      containers:\n      - name: {{ .Chart.Name }}\n        image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n        imagePullPolicy: {{ .Values.image.pullPolicy }}\n        ports:\n        - name: postgresql\n          containerPort: {{ .Values.service.targetPort }}\n          protocol: TCP\n        env:\n        - name: POSTGRES_USER\n          value: {{ .Values.auth.username | quote }}\n        - name: POSTGRES_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: {{ include \"postgresql.fullname\" . }}-secret\n              key: postgres-password\n        - name: POSTGRES_DB\n          value: {{ .Values.auth.database | quote }}\n        - name: PGDATA\n          value: /var/lib/postgresql/data/pgdata\n        resources:\n          {{- toYaml .Values.resources | nindent 10 }}\n        livenessProbe:\n          exec:\n            command:\n              - /bin/sh\n              - -c\n              - exec pg_isready -U {{ .Values.auth.username | quote }} -h 127.0.0.1 -p 5432\n          initialDelaySeconds: 30\n          periodSeconds: 10\n        readinessProbe:\n          exec:\n            command:\n              - /bin/sh\n              - -c\n              - exec pg_isready -U {{ .Values.auth.username | quote }} -h 127.0.0.1 -p 5432\n          initialDelaySeconds: 5\n          periodSeconds: 5\n        volumeMounts:\n        - name: data\n          mountPath: /var/lib/postgresql/data\n      {{- if .Values.metrics.enabled }}\n      - name: metrics\n        image: \"{{ .Values.metrics.image.repository }}:{{ .Values.metrics.image.tag }}\"\n        imagePullPolicy: {{ .Values.metrics.image.pullPolicy }}\n        env:\n        - name: DATA_SOURCE_NAME\n          value: \"postgresql://{{ .Values.auth.username }}:{{ .Values.auth.password }}@localhost:5432/{{ .Values.auth.database }}?sslmode=disable\"\n        ports:\n        - name: metrics\n          containerPort: {{ .Values.metrics.service.port }}\n          protocol: TCP\n      {{- end }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n  volumeClaimTemplates:\n  - metadata:\n      name: data\n      labels:\n        {{- include \"postgresql.labels\" . | nindent 8 }}\n    spec:\n      accessModes:\n        - {{ .Values.persistence.accessMode }}\n      resources:\n        requests:\n          storage: {{ .Values.persistence.size }}\n      {{- if .Values.persistence.storageClass }}\n      {{- if (eq \"-\" .Values.persistence.storageClass) }}\n      storageClassName: \"\"\n      {{- else }}\n      storageClassName: {{ .Values.persistence.storageClass }}\n      {{- end }}\n      {{- end }}"
  },
  {
    "path": "helm/charts/postgresql/templates/secret.yaml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"postgresql.fullname\" . }}-secret\n  labels:\n    {{- include \"postgresql.labels\" . | nindent 4 }}\ntype: Opaque\ndata:\n  postgres-password: {{ .Values.auth.password | b64enc | quote }}"
  },
  {
    "path": "helm/charts/postgresql/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"postgresql.fullname\" . }}\n  labels:\n    {{- include \"postgresql.labels\" . | nindent 4 }}\n  {{- with .Values.service.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: postgresql\n      protocol: TCP\n      name: postgresql\n    {{- if .Values.metrics.enabled }}\n    - port: {{ .Values.metrics.service.port }}\n      targetPort: metrics\n      protocol: TCP\n      name: metrics\n    {{- end }}\n  selector:\n    {{- include \"postgresql.selectorLabels\" . | nindent 4 }}"
  },
  {
    "path": "helm/charts/postgresql/values.yaml",
    "content": "replicaCount: 1\n\nimage:\n  repository: postgres\n  tag: 16-alpine\n  pullPolicy: IfNotPresent\n\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nservice:\n  type: ClusterIP\n  port: 5432\n  targetPort: 5432\n  annotations: {}\n\npersistence:\n  enabled: true\n  storageClass: \"\"\n  accessMode: ReadWriteOnce\n  size: 10Gi\n  annotations: {}\n\nresources:\n  limits:\n    memory: \"1Gi\"\n    cpu: \"1000m\"\n  requests:\n    memory: \"512Mi\"\n    cpu: \"500m\"\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\nauth:\n  username: postgres\n  password: postgres\n  database: bytebotdb\n\nconfig:\n  maxConnections: 100\n  sharedBuffers: \"256MB\"\n\nmetrics:\n  enabled: false\n  image:\n    repository: wrouesnel/postgres_exporter\n    tag: latest\n    pullPolicy: IfNotPresent\n  service:\n    port: 9187"
  },
  {
    "path": "helm/templates/NOTES.txt",
    "content": "Thank you for installing {{ .Chart.Name }}!\n\nTo access Bytebot:\n\n1. Port-forward the UI service:\n   kubectl port-forward -n {{ .Release.Namespace }} service/bytebot-ui 9992:9992\n\n2. Open your browser:\n   http://localhost:9992\n\n{{- if .Values.ingress.enabled }}\n\nExternal access is configured at:\nhttp{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.ingress.host }}\n{{- end }}\n\nTo check deployment status:\nkubectl get pods -n {{ .Release.Namespace }}\n\nFor documentation:\nhttps://docs.bytebot.ai/deployment/helm"
  },
  {
    "path": "helm/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: {{ .Release.Name }}-bytebot\n  labels:\n    app.kubernetes.io/name: bytebot\n    app.kubernetes.io/instance: {{ .Release.Name }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.className }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls.enabled }}\n  tls:\n    - hosts:\n        - {{ .Values.ingress.host }}\n      secretName: {{ .Values.ingress.tls.secretName }}\n  {{- end }}\n  rules:\n    - host: {{ .Values.ingress.host }}\n      http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: bytebot-ui\n                port:\n                  number: 9992\n          - path: /api\n            pathType: Prefix\n            backend:\n              service:\n                name: bytebot-agent\n                port:\n                  number: 9991\n          - path: /vnc\n            pathType: Prefix\n            backend:\n              service:\n                name: bytebot-desktop\n                port:\n                  number: 9990\n{{- end }}"
  },
  {
    "path": "helm/values-proxy.yaml",
    "content": "# Proxy configuration for Bytebot with LiteLLM\n# This values file enables the LiteLLM proxy and configures the agent to use it\n# Usage: helm install bytebot . -f values-proxy.yaml\n\n# Enable LiteLLM proxy\nbytebot-llm-proxy:\n  enabled: true\n  fullnameOverride: \"bytebot-llm-proxy\"\n  env:\n    # Set your API keys here\n    ANTHROPIC_API_KEY: \"\"\n    OPENAI_API_KEY: \"\"\n    GEMINI_API_KEY: \"\"\n  service:\n    type: ClusterIP\n    port: 4000\n  resources:\n    limits:\n      memory: \"1Gi\"\n      cpu: \"1000m\"\n    requests:\n      memory: \"512Mi\"\n      cpu: \"500m\"\n\n# Configure agent to use the proxy\nbytebot-agent:\n  config:\n    llmProxyUrl: \"http://bytebot-llm-proxy:4000\"\n  # Don't set API keys on the agent when using proxy\n  env:\n    ANTHROPIC_API_KEY: \"\"\n    OPENAI_API_KEY: \"\"\n    GEMINI_API_KEY: \"\"\n\n# All other services remain the same\npostgresql:\n  enabled: true\n\nbytebot-desktop:\n  enabled: true\n\nbytebot-ui:\n  enabled: true\n\n# Optional: Enable ingress for external access\ningress:\n  enabled: false\n  className: \"nginx\"\n  host: bytebot.example.com\n  tls:\n    enabled: true\n    secretName: bytebot-tls"
  },
  {
    "path": "helm/values-simple.yaml",
    "content": "# Simple Bytebot configuration example\n# Copy this file and add your API key(s)\n\nbytebot-agent:\n  # Configure at least one API key\n  apiKeys:\n    anthropic:\n      value: \"sk-ant-your-key-here\"\n    \n    # Optional: Add more providers\n    # openai:\n    #   value: \"sk-your-key-here\"\n    # gemini:\n    #   value: \"your-key-here\"\n\n# That's it! The defaults handle everything else.\n# \n# For more options, see the full values.yaml file."
  },
  {
    "path": "helm/values.yaml",
    "content": "global:\n  storageClass: \"\"\n  \npostgresql:\n  enabled: true\n  fullnameOverride: \"bytebot-postgresql\"\n  auth:\n    username: postgres\n    password: postgres\n    database: bytebotdb\n  service:\n    port: 5432\n  persistence:\n    enabled: true\n    size: 10Gi\n\nbytebot-desktop:\n  fullnameOverride: \"bytebot-desktop\"\n  image:\n    repository: ghcr.io/bytebot-ai/bytebot-desktop\n    tag: edge\n  service:\n    type: ClusterIP\n    port: 9990\n  resources:\n    limits:\n      memory: \"4Gi\"\n      cpu: \"2000m\"\n    requests:\n      memory: \"2Gi\"\n      cpu: \"1000m\"\n  shm:\n    enabled: true\n    size: \"2Gi\"\n\nbytebot-agent:\n  fullnameOverride: \"bytebot-agent\"\n  image:\n    repository: ghcr.io/bytebot-ai/bytebot-agent\n    tag: edge\n  service:\n    type: ClusterIP\n    port: 9991\n  config:\n    bytebotDesktopUrl: \"http://bytebot-desktop:9990\"\n  postgresql:\n    enabled: false\n  externalDatabase:\n    host: \"bytebot-postgresql\"\n    port: 5432\n    database: bytebotdb\n    username: postgres\n    password: postgres\n  # Legacy API key configuration (for backward compatibility)\n  env:\n    ANTHROPIC_API_KEY: \"\"\n    OPENAI_API_KEY: \"\"\n    GEMINI_API_KEY: \"\"\n  \n  # New secret management structure - use this for better security\n  apiKeys:\n    anthropic:\n      useExisting: false\n      secretName: \"\"\n      secretKey: \"anthropic-api-key\"\n      value: \"\"  # Only used if useExisting is false\n    openai:\n      useExisting: false\n      secretName: \"\"\n      secretKey: \"openai-api-key\"\n      value: \"\"  # Only used if useExisting is false\n    gemini:\n      useExisting: false\n      secretName: \"\"\n      secretKey: \"gemini-api-key\"\n      value: \"\"  # Only used if useExisting is false\n  resources:\n    limits:\n      memory: \"2Gi\"\n      cpu: \"1000m\"\n    requests:\n      memory: \"1Gi\"\n      cpu: \"500m\"\n\nbytebot-ui:\n  fullnameOverride: \"bytebot-ui\"\n  image:\n    repository: ghcr.io/bytebot-ai/bytebot-ui\n    tag: edge\n  service:\n    type: ClusterIP\n    port: 9992\n  config:\n    agentBaseUrl: \"http://bytebot-agent:9991\"\n    desktopVncUrl: \"http://bytebot-desktop:9990/websockify\"\n  resources:\n    limits:\n      memory: \"1Gi\"\n      cpu: \"500m\"\n    requests:\n      memory: \"512Mi\"\n      cpu: \"250m\"\n\n# LiteLLM proxy is disabled by default\n# Enable it by using -f values-proxy.yaml or --set bytebot-llm-proxy.enabled=true\nbytebot-llm-proxy:\n  enabled: false\n\ningress:\n  enabled: false\n  className: \"nginx\"\n  annotations:\n    cert-manager.io/cluster-issuer: \"letsencrypt-prod\"\n  host: bytebot.example.com\n  tls:\n    enabled: true\n    secretName: bytebot-tls"
  },
  {
    "path": "packages/bytebot-agent/.dockerignore",
    "content": "**/node_modules\n**/dist\n**/.git\n**/.vscode\n**/.env*\n**/npm-debug.log\n**/yarn-debug.log\n**/yarn-error.log\n**/package-lock.json"
  },
  {
    "path": "packages/bytebot-agent/.gitignore",
    "content": "# compiled output\n/dist\n/node_modules\n/build\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# temp directory\n.temp\n.tmp\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n"
  },
  {
    "path": "packages/bytebot-agent/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}"
  },
  {
    "path": "packages/bytebot-agent/Dockerfile",
    "content": "# Base image\nFROM node:20-alpine\n\n# Create app directory\nWORKDIR /app\n\n# Copy app source\nCOPY ./shared ./shared\nCOPY ./bytebot-agent/ ./bytebot-agent/\n\nWORKDIR /app/bytebot-agent\n\n# Install dependencies\nRUN npm install\n\n\nRUN npm run build\n\n# Run the application\nCMD [\"npm\", \"run\", \"start:prod\"] \n\n\n"
  },
  {
    "path": "packages/bytebot-agent/eslint.config.mjs",
    "content": "// @ts-check\nimport eslint from '@eslint/js';\nimport eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';\nimport globals from 'globals';\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n  {\n    ignores: ['eslint.config.mjs'],\n  },\n  eslint.configs.recommended,\n  ...tseslint.configs.recommendedTypeChecked,\n  eslintPluginPrettierRecommended,\n  {\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest,\n      },\n      ecmaVersion: 5,\n      sourceType: 'module',\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n  },\n  {\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-floating-promises': 'warn',\n      '@typescript-eslint/no-unsafe-argument': 'warn'\n    },\n  },\n);"
  },
  {
    "path": "packages/bytebot-agent/nest-cli.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/nest-cli\",\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\",\n  \"compilerOptions\": {\n    \"deleteOutDir\": true\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/package.json",
    "content": "{\n  \"name\": \"bytebot-agent\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"author\": \"\",\n  \"private\": true,\n  \"license\": \"UNLICENSED\",\n  \"scripts\": {\n    \"prisma:dev\": \"npx prisma migrate dev && npx prisma generate\",\n    \"prisma:prod\": \"npx prisma migrate deploy && npx prisma generate\",\n    \"build\": \"npm run build --prefix ../shared && npx prisma generate && nest build\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"start\": \"npm run build --prefix ../shared && nest start\",\n    \"start:dev\": \"npm run build --prefix ../shared && nest start --watch\",\n    \"start:debug\": \"npm run build --prefix ../shared && nest start --debug --watch\",\n    \"start:prod\": \"npm run build --prefix ../shared && npx prisma migrate deploy && npx prisma generate && node dist/main\",\n    \"lint\": \"eslint \\\"{src,apps,libs,test}/**/*.ts\\\" --fix\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:cov\": \"jest --coverage\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^0.39.0\",\n    \"@bytebot/shared\": \"../shared\",\n    \"@google/genai\": \"^1.8.0\",\n    \"@nestjs/common\": \"^11.0.1\",\n    \"@nestjs/config\": \"^4.0.2\",\n    \"@nestjs/core\": \"^11.0.1\",\n    \"@nestjs/event-emitter\": \"^3.0.1\",\n    \"@nestjs/platform-express\": \"^11.1.5\",\n    \"@nestjs/platform-socket.io\": \"^11.1.1\",\n    \"@nestjs/schedule\": \"^6.0.0\",\n    \"@nestjs/websockets\": \"^11.1.1\",\n    \"@prisma/client\": \"^6.16.1\",\n    \"@thallesp/nestjs-better-auth\": \"^1.0.0\",\n    \"class-transformer\": \"^0.5.1\",\n    \"class-validator\": \"^0.14.2\",\n    \"openai\": \"^5.8.2\",\n    \"reflect-metadata\": \"^0.2.2\",\n    \"rxjs\": \"^7.8.1\",\n    \"socket.io\": \"^4.8.1\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"zod\": \"^4.0.5\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.2.0\",\n    \"@eslint/js\": \"^9.18.0\",\n    \"@nestjs/cli\": \"^11.0.0\",\n    \"@nestjs/schematics\": \"^11.0.0\",\n    \"@nestjs/testing\": \"^11.0.1\",\n    \"@swc/cli\": \"^0.6.0\",\n    \"@swc/core\": \"^1.10.7\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.10.7\",\n    \"@types/supertest\": \"^6.0.2\",\n    \"eslint\": \"^9.18.0\",\n    \"eslint-config-prettier\": \"^10.0.1\",\n    \"eslint-plugin-prettier\": \"^5.2.2\",\n    \"globals\": \"^15.14.0\",\n    \"jest\": \"^29.7.0\",\n    \"prettier\": \"^3.4.2\",\n    \"prisma\": \"^6.16.1\",\n    \"source-map-support\": \"^0.5.21\",\n    \"supertest\": \"^7.0.0\",\n    \"ts-jest\": \"^29.2.5\",\n    \"ts-loader\": \"^9.5.2\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^4.2.0\",\n    \"typescript\": \"^5.7.3\",\n    \"typescript-eslint\": \"^8.20.0\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\n      \"js\",\n      \"json\",\n      \"ts\"\n    ],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".*\\\\.spec\\\\.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"collectCoverageFrom\": [\n      \"**/*.(t|j)s\"\n    ],\n    \"coverageDirectory\": \"../coverage\",\n    \"testEnvironment\": \"node\"\n  },\n  \"overrides\": {\n    \"openai\": {\n      \"zod\": \"^4.0.5\"\n    }\n  },\n  \"engines\": {\n    \"node\": \"20\"\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250328022708_initial_migration/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"TaskStatus\" AS ENUM ('PENDING', 'IN_PROGRESS', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED');\n\n-- CreateEnum\nCREATE TYPE \"TaskPriority\" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');\n\n-- CreateEnum\nCREATE TYPE \"MessageType\" AS ENUM ('USER', 'ASSISTANT');\n\n-- CreateTable\nCREATE TABLE \"Task\" (\n    \"id\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"status\" \"TaskStatus\" NOT NULL DEFAULT 'PENDING',\n    \"priority\" \"TaskPriority\" NOT NULL DEFAULT 'MEDIUM',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Task_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Summary\" (\n    \"id\" TEXT NOT NULL,\n    \"content\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"taskId\" TEXT NOT NULL,\n    \"parentId\" TEXT,\n\n    CONSTRAINT \"Summary_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Message\" (\n    \"id\" TEXT NOT NULL,\n    \"content\" JSONB NOT NULL,\n    \"type\" \"MessageType\" NOT NULL DEFAULT 'ASSISTANT',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"taskId\" TEXT NOT NULL,\n    \"summaryId\" TEXT,\n\n    CONSTRAINT \"Message_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"Summary\" ADD CONSTRAINT \"Summary_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Summary\" ADD CONSTRAINT \"Summary_parentId_fkey\" FOREIGN KEY (\"parentId\") REFERENCES \"Summary\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_summaryId_fkey\" FOREIGN KEY (\"summaryId\") REFERENCES \"Summary\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250413053912_message_role/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `type` on the `Message` table. All the data in the column will be lost.\n\n*/\n-- CreateEnum\nCREATE TYPE \"MessageRole\" AS ENUM ('USER', 'ASSISTANT');\n\n-- AlterTable\nALTER TABLE \"Message\" DROP COLUMN \"type\",\nADD COLUMN     \"role\" \"MessageRole\" NOT NULL DEFAULT 'ASSISTANT';\n\n-- DropEnum\nDROP TYPE \"MessageType\";\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250522200556_updated_task_structure/migration.sql",
    "content": "\n-- CreateEnum\nCREATE TYPE \"Role\" AS ENUM ('USER', 'ASSISTANT');\n\n-- CreateEnum\nCREATE TYPE \"TaskType\" AS ENUM ('IMMEDIATE', 'SCHEDULED');\n\n-- AlterEnum\nBEGIN;\nCREATE TYPE \"TaskStatus_new\" AS ENUM ('PENDING', 'RUNNING', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED');\nALTER TABLE \"Task\" ALTER COLUMN \"status\" DROP DEFAULT;\nALTER TABLE \"Task\" ALTER COLUMN \"status\" TYPE \"TaskStatus_new\" USING (CASE \"status\"::text WHEN 'IN_PROGRESS' THEN 'RUNNING' ELSE \"status\"::text END::\"TaskStatus_new\");\nALTER TYPE \"TaskStatus\" RENAME TO \"TaskStatus_old\";\nALTER TYPE \"TaskStatus_new\" RENAME TO \"TaskStatus\";\nDROP TYPE \"TaskStatus_old\";\nALTER TABLE \"Task\" ALTER COLUMN \"status\" SET DEFAULT 'PENDING';\nCOMMIT;\n\n-- DropForeignKey\nALTER TABLE \"Message\" DROP CONSTRAINT \"Message_taskId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"Summary\" DROP CONSTRAINT \"Summary_taskId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Message\" ADD COLUMN \"new_role\" \"Role\" NOT NULL DEFAULT 'ASSISTANT';\nUPDATE \"Message\"\nSET \"new_role\" = CASE\n    WHEN lower(\"role\"::text) = 'user' THEN 'USER'::\"Role\"\n    WHEN lower(\"role\"::text) = 'assistant' THEN 'ASSISTANT'::\"Role\"\n    ELSE 'ASSISTANT'::\"Role\"\nEND;\n\n-- Step 3: Drop the old 'role' column.\nALTER TABLE \"Message\" DROP COLUMN \"role\";\n\n-- Step 4: Rename 'new_role' to 'role'.\nALTER TABLE \"Message\" RENAME COLUMN \"new_role\" TO \"role\";\n\n-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"completedAt\" TIMESTAMP(3),\nADD COLUMN     \"createdBy\" \"Role\" NOT NULL DEFAULT 'USER',\nADD COLUMN     \"error\" TEXT,\nADD COLUMN     \"executedAt\" TIMESTAMP(3),\nADD COLUMN     \"result\" JSONB,\nADD COLUMN     \"type\" \"TaskType\" NOT NULL DEFAULT 'IMMEDIATE';\n\n-- DropEnum\nDROP TYPE \"MessageRole\";\n\n-- AddForeignKey\nALTER TABLE \"Summary\" ADD CONSTRAINT \"Summary_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250523162632_add_scheduling/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"queuedAt\" TIMESTAMP(3),\nADD COLUMN     \"scheduledFor\" TIMESTAMP(3);\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250529003255_tasks_control/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"control\" \"Role\" NOT NULL DEFAULT 'USER';\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250530012753_tasks_control/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ALTER COLUMN \"control\" SET DEFAULT 'ASSISTANT';\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250619013027_add_better_auth_schema/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Message\" ADD COLUMN     \"userId\" TEXT;\n\n-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT,\n    \"email\" TEXT NOT NULL,\n    \"emailVerified\" BOOLEAN NOT NULL DEFAULT false,\n    \"image\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"User_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Session\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"ipAddress\" TEXT,\n    \"userAgent\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Session_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Account\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"accountId\" TEXT NOT NULL,\n    \"providerId\" TEXT NOT NULL,\n    \"accessToken\" TEXT,\n    \"refreshToken\" TEXT,\n    \"accessTokenExpiresAt\" TIMESTAMP(3),\n    \"refreshTokenExpiresAt\" TIMESTAMP(3),\n    \"scope\" TEXT,\n    \"idToken\" TEXT,\n    \"password\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Account_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Verification\" (\n    \"id\" TEXT NOT NULL,\n    \"identifier\" TEXT NOT NULL,\n    \"value\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Verification_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Session_token_key\" ON \"Session\"(\"token\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Account_providerId_accountId_key\" ON \"Account\"(\"providerId\", \"accountId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Verification_identifier_value_key\" ON \"Verification\"(\"identifier\", \"value\");\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Session\" ADD CONSTRAINT \"Session_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Account\" ADD CONSTRAINT \"Account_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250622195148_add_user_to_task/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"userId\" TEXT;\n\n-- AddForeignKey\nALTER TABLE \"Task\" ADD CONSTRAINT \"Task_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250706223912_model_picker/migration.sql",
    "content": "-- AlterTable: add `model` column as JSONB (nullable initially)\nALTER TABLE \"Task\" ADD COLUMN \"model\" JSONB;\n\n-- Backfill existing tasks with the default Anthropic Claude Opus 4 model\nUPDATE \"Task\"\nSET \"model\" = jsonb_build_object(\n  'provider', 'anthropic',\n  'name', 'claude-opus-4-20250514',\n  'title', 'Claude Opus 4'\n)\nWHERE \"model\" IS NULL;\n\n-- Enforce NOT NULL constraint now that data is populated\nALTER TABLE \"Task\" ALTER COLUMN \"model\" SET NOT NULL;\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250722041608_files/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"File\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"type\" TEXT NOT NULL,\n    \"size\" INTEGER NOT NULL,\n    \"data\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"taskId\" TEXT NOT NULL,\n\n    CONSTRAINT \"File_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"File\" ADD CONSTRAINT \"File_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/20250820172813_remove_auth/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `userId` on the `Message` table. All the data in the column will be lost.\n  - You are about to drop the column `userId` on the `Task` table. All the data in the column will be lost.\n  - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `Verification` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"public\".\"Account\" DROP CONSTRAINT \"Account_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"public\".\"Message\" DROP CONSTRAINT \"Message_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"public\".\"Session\" DROP CONSTRAINT \"Session_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"public\".\"Task\" DROP CONSTRAINT \"Task_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"public\".\"Message\" DROP COLUMN \"userId\";\n\n-- AlterTable\nALTER TABLE \"public\".\"Task\" DROP COLUMN \"userId\";\n\n-- DropTable\nDROP TABLE \"public\".\"Account\";\n\n-- DropTable\nDROP TABLE \"public\".\"Session\";\n\n-- DropTable\nDROP TABLE \"public\".\"User\";\n\n-- DropTable\nDROP TABLE \"public\".\"Verification\";\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (e.g., Git)\nprovider = \"postgresql\"\n"
  },
  {
    "path": "packages/bytebot-agent/prisma/schema.prisma",
    "content": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\nenum TaskStatus {\n  PENDING\n  RUNNING\n  NEEDS_HELP\n  NEEDS_REVIEW\n  COMPLETED\n  CANCELLED\n  FAILED\n}\n\nenum TaskPriority {\n  LOW\n  MEDIUM\n  HIGH\n  URGENT\n}\n\nenum Role {\n  USER\n  ASSISTANT\n}\n\nenum TaskType {\n  IMMEDIATE\n  SCHEDULED\n}\n\nmodel Task {\n  id            String        @id @default(uuid())\n  description   String\n  type          TaskType      @default(IMMEDIATE)\n  status        TaskStatus    @default(PENDING)\n  priority      TaskPriority  @default(MEDIUM)\n  control       Role          @default(ASSISTANT)\n  createdAt     DateTime      @default(now())\n  createdBy     Role          @default(USER)\n  scheduledFor  DateTime?\n  updatedAt     DateTime      @updatedAt\n  executedAt    DateTime?\n  completedAt   DateTime?\n  queuedAt      DateTime?\n  error         String?\n  result        Json?\n  // Example: \n  // { \"provider\": \"anthropic\", \"name\": \"claude-opus-4-20250514\", \"title\": \"Claude Opus 4\" }\n  model         Json\n  messages      Message[]\n  summaries     Summary[]\n  files         File[]\n}\n\nmodel Summary {\n  id             String     @id @default(uuid())\n  content        String\n  createdAt      DateTime   @default(now())\n  updatedAt      DateTime   @updatedAt\n  messages       Message[]  // One-to-many relationship: one Summary has many Messages\n\n  task      Task        @relation(fields: [taskId], references: [id], onDelete: Cascade)\n  taskId    String\n  \n  // Self-referential relationship\n  parentSummary  Summary?   @relation(\"SummaryHierarchy\", fields: [parentId], references: [id])\n  parentId       String?\n  childSummaries Summary[]  @relation(\"SummaryHierarchy\")\n}\n\nmodel Message {\n  id        String      @id @default(uuid())\n  // Content field follows Anthropic's content blocks structure\n  // Example: \n  // [\n  //   {\"type\": \"text\", \"text\": \"Hello world\"},\n  //   {\"type\": \"image\", \"source\": {\"type\": \"base64\", \"media_type\": \"image/jpeg\", \"data\": \"...\"}}\n  // ]\n  content   Json\n  role      Role @default(ASSISTANT)\n  createdAt DateTime    @default(now())\n  updatedAt DateTime    @updatedAt\n  task      Task        @relation(fields: [taskId], references: [id], onDelete: Cascade)\n  taskId    String\n  summary   Summary?    @relation(fields: [summaryId], references: [id])\n  summaryId String?     // Optional foreign key to Summary\n}\n\nmodel File {\n  id            String      @id @default(uuid())\n  name          String\n  type          String      // MIME type\n  size          Int         // Size in bytes\n  data          String      // Base64 encoded file data\n  createdAt     DateTime    @default(now())\n  updatedAt     DateTime    @updatedAt\n  \n  // Relations\n  task          Task        @relation(fields: [taskId], references: [id], onDelete: Cascade)\n  taskId        String\n}\n\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.analytics.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { ConfigService } from '@nestjs/config';\nimport { TasksService } from '../tasks/tasks.service';\nimport { MessagesService } from '../messages/messages.service';\n\n@Injectable()\nexport class AgentAnalyticsService {\n  private readonly logger = new Logger(AgentAnalyticsService.name);\n  private readonly endpoint?: string;\n\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly messagesService: MessagesService,\n    configService: ConfigService,\n  ) {\n    this.endpoint = configService.get<string>('BYTEBOT_ANALYTICS_ENDPOINT');\n    if (!this.endpoint) {\n      this.logger.warn(\n        'BYTEBOT_ANALYTICS_ENDPOINT is not set. Analytics service disabled.',\n      );\n    }\n  }\n\n  @OnEvent('task.cancel')\n  @OnEvent('task.failed')\n  @OnEvent('task.completed')\n  async handleTaskEvent(payload: { taskId: string }) {\n    if (!this.endpoint) return;\n\n    try {\n      const task = await this.tasksService.findById(payload.taskId);\n      const messages = await this.messagesService.findEvery(payload.taskId);\n\n      await fetch(this.endpoint, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ ...task, messages }),\n      });\n    } catch (error: any) {\n      this.logger.error(\n        `Failed to send analytics for task ${payload.taskId}: ${error.message}`,\n        error.stack,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.computer-use.ts",
    "content": "import {\n  Button,\n  Coordinates,\n  Press,\n  ComputerToolUseContentBlock,\n  ToolResultContentBlock,\n  MessageContentType,\n  isScreenshotToolUseBlock,\n  isCursorPositionToolUseBlock,\n  isMoveMouseToolUseBlock,\n  isTraceMouseToolUseBlock,\n  isClickMouseToolUseBlock,\n  isPressMouseToolUseBlock,\n  isDragMouseToolUseBlock,\n  isScrollToolUseBlock,\n  isTypeKeysToolUseBlock,\n  isPressKeysToolUseBlock,\n  isTypeTextToolUseBlock,\n  isWaitToolUseBlock,\n  isApplicationToolUseBlock,\n  isPasteTextToolUseBlock,\n  isReadFileToolUseBlock,\n} from '@bytebot/shared';\nimport { Logger } from '@nestjs/common';\n\nconst BYTEBOT_DESKTOP_BASE_URL = process.env.BYTEBOT_DESKTOP_BASE_URL as string;\n\nexport async function handleComputerToolUse(\n  block: ComputerToolUseContentBlock,\n  logger: Logger,\n): Promise<ToolResultContentBlock> {\n  logger.debug(\n    `Handling computer tool use: ${block.name}, tool_use_id: ${block.id}`,\n  );\n\n  if (isScreenshotToolUseBlock(block)) {\n    logger.debug('Processing screenshot request');\n    try {\n      logger.debug('Taking screenshot');\n      const image = await screenshot();\n      logger.debug('Screenshot captured successfully');\n\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Image,\n            source: {\n              data: image,\n              media_type: 'image/png',\n              type: 'base64',\n            },\n          },\n        ],\n      };\n    } catch (error) {\n      logger.error(`Screenshot failed: ${error.message}`, error.stack);\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Text,\n            text: 'ERROR: Failed to take screenshot',\n          },\n        ],\n        is_error: true,\n      };\n    }\n  }\n\n  if (isCursorPositionToolUseBlock(block)) {\n    logger.debug('Processing cursor position request');\n    try {\n      logger.debug('Getting cursor position');\n      const position = await cursorPosition();\n      logger.debug(`Cursor position obtained: ${position.x}, ${position.y}`);\n\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Text,\n            text: `Cursor position: ${position.x}, ${position.y}`,\n          },\n        ],\n      };\n    } catch (error) {\n      logger.error(\n        `Getting cursor position failed: ${error.message}`,\n        error.stack,\n      );\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Text,\n            text: 'ERROR: Failed to get cursor position',\n          },\n        ],\n        is_error: true,\n      };\n    }\n  }\n\n  try {\n    if (isMoveMouseToolUseBlock(block)) {\n      await moveMouse(block.input);\n    }\n    if (isTraceMouseToolUseBlock(block)) {\n      await traceMouse(block.input);\n    }\n    if (isClickMouseToolUseBlock(block)) {\n      await clickMouse(block.input);\n    }\n    if (isPressMouseToolUseBlock(block)) {\n      await pressMouse(block.input);\n    }\n    if (isDragMouseToolUseBlock(block)) {\n      await dragMouse(block.input);\n    }\n    if (isScrollToolUseBlock(block)) {\n      await scroll(block.input);\n    }\n    if (isTypeKeysToolUseBlock(block)) {\n      await typeKeys(block.input);\n    }\n    if (isPressKeysToolUseBlock(block)) {\n      await pressKeys(block.input);\n    }\n    if (isTypeTextToolUseBlock(block)) {\n      await typeText(block.input);\n    }\n    if (isPasteTextToolUseBlock(block)) {\n      await pasteText(block.input);\n    }\n    if (isWaitToolUseBlock(block)) {\n      await wait(block.input);\n    }\n    if (isApplicationToolUseBlock(block)) {\n      await application(block.input);\n    }\n    if (isReadFileToolUseBlock(block)) {\n      logger.debug(`Reading file: ${block.input.path}`);\n      const result = await readFile(block.input);\n\n      if (result.success && result.data) {\n        // Return document content block\n        return {\n          type: MessageContentType.ToolResult,\n          tool_use_id: block.id,\n          content: [\n            {\n              type: MessageContentType.Document,\n              source: {\n                type: 'base64',\n                media_type: result.mediaType || 'application/octet-stream',\n                data: result.data,\n              },\n              name: result.name || 'file',\n              size: result.size,\n            },\n          ],\n        };\n      } else {\n        // Return error message\n        return {\n          type: MessageContentType.ToolResult,\n          tool_use_id: block.id,\n          content: [\n            {\n              type: MessageContentType.Text,\n              text: result.message || 'Error reading file',\n            },\n          ],\n          is_error: true,\n        };\n      }\n    }\n\n    let image: string | null = null;\n    try {\n      // Wait before taking screenshot to allow UI to settle\n      const delayMs = 750; // 750ms delay\n      logger.debug(`Waiting ${delayMs}ms before taking screenshot`);\n      await new Promise((resolve) => setTimeout(resolve, delayMs));\n\n      logger.debug('Taking screenshot');\n      image = await screenshot();\n      logger.debug('Screenshot captured successfully');\n    } catch (error) {\n      logger.error('Failed to take screenshot', error);\n    }\n\n    logger.debug(`Tool execution successful for tool_use_id: ${block.id}`);\n    const toolResult: ToolResultContentBlock = {\n      type: MessageContentType.ToolResult,\n      tool_use_id: block.id,\n      content: [\n        {\n          type: MessageContentType.Text,\n          text: 'Tool executed successfully',\n        },\n      ],\n    };\n\n    if (image) {\n      toolResult.content.push({\n        type: MessageContentType.Image,\n        source: {\n          data: image,\n          media_type: 'image/png',\n          type: 'base64',\n        },\n      });\n    }\n\n    return toolResult;\n  } catch (error) {\n    logger.error(\n      `Error executing ${block.name} tool: ${error.message}`,\n      error.stack,\n    );\n    return {\n      type: MessageContentType.ToolResult,\n      tool_use_id: block.id,\n      content: [\n        {\n          type: MessageContentType.Text,\n          text: `Error executing ${block.name} tool: ${error.message}`,\n        },\n      ],\n      is_error: true,\n    };\n  }\n}\n\nasync function moveMouse(input: { coordinates: Coordinates }): Promise<void> {\n  const { coordinates } = input;\n  console.log(\n    `Moving mouse to coordinates: [${coordinates.x}, ${coordinates.y}]`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'move_mouse',\n        coordinates,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in move_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function traceMouse(input: {\n  path: Coordinates[];\n  holdKeys?: string[];\n}): Promise<void> {\n  const { path, holdKeys } = input;\n  console.log(\n    `Tracing mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'trace_mouse',\n        path,\n        holdKeys,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in trace_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function clickMouse(input: {\n  coordinates?: Coordinates;\n  button: Button;\n  holdKeys?: string[];\n  clickCount: number;\n}): Promise<void> {\n  const { coordinates, button, holdKeys, clickCount } = input;\n  console.log(\n    `Clicking mouse ${button} ${clickCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}] ` : ''} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'click_mouse',\n        coordinates,\n        button,\n        holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,\n        clickCount,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in click_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function pressMouse(input: {\n  coordinates?: Coordinates;\n  button: Button;\n  press: Press;\n}): Promise<void> {\n  const { coordinates, button, press } = input;\n  console.log(\n    `Pressing mouse ${button} ${press} ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'press_mouse',\n        coordinates,\n        button,\n        press,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in press_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function dragMouse(input: {\n  path: Coordinates[];\n  button: Button;\n  holdKeys?: string[];\n}): Promise<void> {\n  const { path, button, holdKeys } = input;\n  console.log(\n    `Dragging mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'drag_mouse',\n        path,\n        button,\n        holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in drag_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function scroll(input: {\n  coordinates?: Coordinates;\n  direction: 'up' | 'down' | 'left' | 'right';\n  scrollCount: number;\n  holdKeys?: string[];\n}): Promise<void> {\n  const { coordinates, direction, scrollCount, holdKeys } = input;\n  console.log(\n    `Scrolling ${direction} ${scrollCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'scroll',\n        coordinates,\n        direction,\n        scrollCount,\n        holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in scroll action:', error);\n    throw error;\n  }\n}\n\nasync function typeKeys(input: {\n  keys: string[];\n  delay?: number;\n}): Promise<void> {\n  const { keys, delay } = input;\n  console.log(`Typing keys: ${keys}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'type_keys',\n        keys,\n        delay,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in type_keys action:', error);\n    throw error;\n  }\n}\n\nasync function pressKeys(input: {\n  keys: string[];\n  press: Press;\n}): Promise<void> {\n  const { keys, press } = input;\n  console.log(`Pressing keys: ${keys}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'press_keys',\n        keys,\n        press,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in press_keys action:', error);\n    throw error;\n  }\n}\n\nasync function typeText(input: {\n  text: string;\n  delay?: number;\n}): Promise<void> {\n  const { text, delay } = input;\n  console.log(`Typing text: ${text}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'type_text',\n        text,\n        delay,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in type_text action:', error);\n    throw error;\n  }\n}\n\nasync function pasteText(input: { text: string }): Promise<void> {\n  const { text } = input;\n  console.log(`Pasting text: ${text}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'paste_text',\n        text,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in paste_text action:', error);\n    throw error;\n  }\n}\n\nasync function wait(input: { duration: number }): Promise<void> {\n  const { duration } = input;\n  console.log(`Waiting for ${duration}ms`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'wait',\n        duration,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in wait action:', error);\n    throw error;\n  }\n}\n\nasync function cursorPosition(): Promise<Coordinates> {\n  console.log('Getting cursor position');\n\n  try {\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'cursor_position',\n      }),\n    });\n\n    const data = await response.json();\n    return { x: data.x, y: data.y };\n  } catch (error) {\n    console.error('Error in cursor_position action:', error);\n    throw error;\n  }\n}\n\nasync function screenshot(): Promise<string> {\n  console.log('Taking screenshot');\n\n  try {\n    const requestBody = {\n      action: 'screenshot',\n    };\n\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(requestBody),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to take screenshot: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n\n    if (!data.image) {\n      throw new Error('Failed to take screenshot: No image data received');\n    }\n\n    return data.image; // Base64 encoded image\n  } catch (error) {\n    console.error('Error in screenshot action:', error);\n    throw error;\n  }\n}\n\nasync function application(input: { application: string }): Promise<void> {\n  const { application } = input;\n  console.log(`Opening application: ${application}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'application',\n        application,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in application action:', error);\n    throw error;\n  }\n}\n\nasync function readFile(input: { path: string }): Promise<{\n  success: boolean;\n  data?: string;\n  name?: string;\n  size?: number;\n  mediaType?: string;\n  message?: string;\n}> {\n  const { path } = input;\n  console.log(`Reading file: ${path}`);\n\n  try {\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'read_file',\n        path,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to read file: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n    return data;\n  } catch (error) {\n    console.error('Error in read_file action:', error);\n    return {\n      success: false,\n      message: `Error reading file: ${error.message}`,\n    };\n  }\n}\n\nexport async function writeFile(input: {\n  path: string;\n  content: string;\n}): Promise<{ success: boolean; message?: string }> {\n  const { path, content } = input;\n  console.log(`Writing file: ${path}`);\n\n  try {\n    // Content is always base64 encoded\n    const base64Data = content;\n\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'write_file',\n        path,\n        data: base64Data,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to write file: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n    return data;\n  } catch (error) {\n    console.error('Error in write_file action:', error);\n    return {\n      success: false,\n      message: `Error writing file: ${error.message}`,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.constants.ts",
    "content": "export const DEFAULT_DISPLAY_SIZE = {\n  width: 1280,\n  height: 960,\n};\n\nexport const SUMMARIZATION_SYSTEM_PROMPT = `You are a helpful assistant that summarizes conversations for long-running tasks.\nYour job is to create concise summaries that preserve all important information, tool usage, and key decisions.\nFocus on:\n- Task progress and completed actions\n- Important tool calls and their results\n- Key decisions made\n- Any errors or issues encountered\n- Current state and what remains to be done\n\nProvide a structured summary that can be used as context for continuing the task.`;\n\nexport const AGENT_SYSTEM_PROMPT = `\nYou are **Bytebot**, a highly-reliable AI engineer operating a virtual computer whose display measures ${DEFAULT_DISPLAY_SIZE.width} x ${DEFAULT_DISPLAY_SIZE.height} pixels.\n\nThe current date is ${new Date().toLocaleDateString()}. The current time is ${new Date().toLocaleTimeString()}. The current timezone is ${Intl.DateTimeFormat().resolvedOptions().timeZone}.\n\n────────────────────────\nAVAILABLE APPLICATIONS\n────────────────────────\n\nOn the computer, the following applications are available:\n\nFirefox Browser -- The default web browser, use it to navigate to websites.\nThunderbird -- The default email client, use it to send and receive emails (if you have an account).\n1Password -- The password manager, use it to store and retrieve your passwords (if you have an account).\nVisual Studio Code -- The default code editor, use it to create and edit files.\nTerminal -- The default terminal, use it to run commands.\nFile Manager -- The default file manager, use it to navigate and manage files.\nTrash -- The default trash\n\nALL APPLICATIONS ARE GUI BASED, USE THE COMPUTER TOOLS TO INTERACT WITH THEM. ONLY ACCESS THE APPLICATIONS VIA THEIR DESKTOP ICONS.\n\n*Never* use keyboard shortcuts to switch between applications, only use \\`computer_application\\` to switch between the default applications. \n\n────────────────────────\nCORE WORKING PRINCIPLES\n────────────────────────\n1. **Observe First** - *Always* invoke \\`computer_screenshot\\` before your first action **and** whenever the UI may have changed. Screenshot before every action when filling out forms. Never act blindly. When opening documents or PDFs, scroll through at least the first page to confirm it is the correct document. \n2. **Navigate applications**  = *Always* invoke \\`computer_application\\` to switch between the default applications.\n3. **Human-Like Interaction**\n   • Move in smooth, purposeful paths; click near the visual centre of targets.  \n   • Double-click desktop icons to open them.  \n   • Type realistic, context-appropriate text with \\`computer_type_text\\` (for short strings) or \\`computer_paste_text\\` (for long strings), or shortcuts with \\`computer_type_keys\\`.\n4. **Valid Keys Only** - \n   Use **exactly** the identifiers listed in **VALID KEYS** below when supplying \\`keys\\` to \\`computer_type_keys\\` or \\`computer_press_keys\\`. All identifiers come from nut-tree's \\`Key\\` enum; they are case-sensitive and contain *no spaces*.\n5. **Verify Every Step** - After each action:  \n   a. Take another screenshot.  \n   b. Confirm the expected state before continuing. If it failed, retry sensibly (try again, and then try 2 different methods) before calling \\`set_task_status\\` with \\`\"status\":\"needs_help\"\\`.\n6. **Efficiency & Clarity** - Combine related key presses; prefer scrolling or dragging over many small moves; minimise unnecessary waits.\n7. **Stay Within Scope** - Do nothing the user didn't request; don't suggest unrelated tasks. For form and login fields, don't fill in random data, unless explicitly told to do so.\n8. **Security** - If you see a password, secret key, or other sensitive information (or the user shares it with you), do not repeat it in conversation. When typing sensitive information, use \\`computer_type_text\\` with \\`isSensitive\\` set to \\`true\\`.\n9. **Consistency & Persistence** - Even if the task is repetitive, do not end the task until the user's goal is completely met. For bulk operations, maintain focus and continue until all items are processed.\n\n────────────────────────\nREPETITIVE TASK HANDLING\n────────────────────────\nWhen performing repetitive tasks (e.g., \"visit each profile\", \"process all items\"):\n\n1. **Track Progress** - Maintain a mental count of:\n   • Total items to process (if known)\n   • Items completed so far\n   • Current item being processed\n   • Any errors encountered\n\n2. **Batch Processing** - For large sets:\n   • Process in groups of 10-20 items\n   • Take brief pauses between batches to prevent system overload\n   • Continue until ALL items are processed\n\n3. **Error Recovery** - If an item fails:\n   • Note the error but continue with the next item\n   • Keep a list of failed items to report at the end\n   • Don't let one failure stop the entire operation\n\n4. **Progress Updates** - Every 10-20 items:\n   • Brief status: \"Processed 20/100 profiles, continuing...\"\n   • No need for detailed reports unless requested\n\n5. **Completion Criteria** - The task is NOT complete until:\n   • All items in the set are processed, OR\n   • You reach a clear endpoint (e.g., \"No more profiles to load\"), OR\n   • The user explicitly tells you to stop\n\n6. **State Management** - If the task might span multiple tabs/pages:\n   • Save progress to a file periodically\n   • Include timestamps and item identifiers\n\n────────────────────────\nTASK LIFECYCLE TEMPLATE\n────────────────────────\n1. **Prepare** - Initial screenshot → plan → estimate scope if possible.  \n2. **Execute Loop** - For each sub-goal: Screenshot → Think → Act → Verify.\n3. **Batch Loop** - For repetitive tasks:\n   • While items remain:\n     - Process batch of 10-20 items\n     - Update progress counter\n     - Check for stop conditions\n     - Brief status update\n   • Continue until ALL done\n\n4. **Switch Applications** - If you need to switch between the default applications, reach the home directory, or return to the desktop, invoke          \n   \\`\\`\\`json\n   { \"name\": \"computer_application\", \"input\": { \"application\": \"application name\" } }\n   \\`\\`\\` \n   It will open (or focus if it is already open) the application, in fullscreen.\n   The application name must be one of the following: firefox, thunderbird, 1password, vscode, terminal, directory, desktop.\n5. **Create other tasks** - If you need to create additional separate tasks, invoke          \n   \\`\\`\\`json\n   { \"name\": \"create_task\", \"input\": { \"description\": \"Subtask description\", \"type\": \"IMMEDIATE\", \"priority\": \"MEDIUM\" } }\n   \\`\\`\\` \n   The other tasks will be executed in the order they are created, after the current task is completed. Only create separate tasks if they are not related to the current task.\n6. **Schedule future tasks** - If you need to schedule a task to run in the future, invoke          \n   \\`\\`\\`json\n{ \"name\": \"create_task\", \"input\": { \"description\": \"Subtask description\", \"type\": \"SCHEDULED\", \"scheduledFor\": <ISO Date>, \"priority\": \"MEDIUM\" } }\n   \\`\\`\\` \n   Only schedule tasks if they must be run in the future. Do not schedule tasks that can be run immediately.\n7. **Read Files** - If you need to read file contents, invoke\n   \\`\\`\\`json\n   { \"name\": \"computer_read_file\", \"input\": { \"path\": \"/path/to/file\" } }\n   \\`\\`\\`\n   This tool reads files and returns them as document content blocks with base64 data, supporting various file types including documents (PDF, DOCX, TXT, etc.) and images (PNG, JPG, etc.).\n8. **Ask for Help** - If you need clarification, or if you are unable to fully complete the task, invoke          \n   \\`\\`\\`json\n   { \"name\": \"set_task_status\", \"input\": { \"status\": \"needs_help\", \"description\": \"Summary of help or clarification needed\" } }\n   \\`\\`\\`  \n9. **Cleanup** - When the user's goal is met:  \n   • Close every window, file, or app you opened so the desktop is tidy.  \n   • Return to an idle desktop/background.  \n10. **Terminate** - ONLY ONCE THE USER'S GOAL IS COMPLETELY MET, As your final tool call and message, invoke          \n   \\`\\`\\`json\n   { \"name\": \"set_task_status\", \"input\": { \"status\": \"completed\", \"description\": \"Summary of the task\" } }\n   \\`\\`\\`  \n   No further actions or messages will follow this call.\n\n**IMPORTANT**: For bulk operations like \"visit each profile in the directory\":\n- Do NOT mark as completed after just a few profiles\n- Continue until you've processed ALL profiles or reached a clear end\n- If there are 100+ profiles, process them ALL\n- Only stop when explicitly told or when there are genuinely no more items\n\n────────────────────────\nVALID KEYS\n────────────────────────\nA, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp,  \nB, Backslash, Backspace,  \nC, CapsLock, Clear, Comma,  \nD, Decimal, Delete, Divide, Down,  \nE, End, Enter, Equal, Escape, F,  \nF1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,  \nFn,  \nG, Grave,  \nH, Home,  \nI, Insert,  \nJ, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin,  \nM, Menu, Minus, Multiply,  \nN, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock,  \nNumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9,  \nO, P, PageDown, PageUp, Pause, Period, Print,  \nQ, Quote,  \nR, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin,  \nS, ScrollLock, Semicolon, Slash, Space, Subtract,  \nT, Tab,  \nU, Up,  \nV, W, X, Y, Z\n\nRemember: **accuracy over speed, clarity and consistency over cleverness**.  \nThink before each move, keep the desktop clean when you're done, and **always** finish with \\`set_task_status\\`. Don't ask follow-up questions after completing the task.\n\n**For repetitive tasks**: Persistence is key. Continue until ALL items are processed, not just the first few.\n`;\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TasksModule } from '../tasks/tasks.module';\nimport { MessagesModule } from '../messages/messages.module';\nimport { AnthropicModule } from '../anthropic/anthropic.module';\nimport { AgentProcessor } from './agent.processor';\nimport { ConfigModule } from '@nestjs/config';\nimport { AgentScheduler } from './agent.scheduler';\nimport { InputCaptureService } from './input-capture.service';\nimport { OpenAIModule } from '../openai/openai.module';\nimport { GoogleModule } from '../google/google.module';\nimport { SummariesModule } from 'src/summaries/summaries.modue';\nimport { AgentAnalyticsService } from './agent.analytics';\nimport { ProxyModule } from 'src/proxy/proxy.module';\n\n@Module({\n  imports: [\n    ConfigModule,\n    TasksModule,\n    MessagesModule,\n    SummariesModule,\n    AnthropicModule,\n    OpenAIModule,\n    GoogleModule,\n    ProxyModule,\n  ],\n  providers: [\n    AgentProcessor,\n    AgentScheduler,\n    InputCaptureService,\n    AgentAnalyticsService,\n  ],\n  exports: [AgentProcessor],\n})\nexport class AgentModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.processor.ts",
    "content": "import { TasksService } from '../tasks/tasks.service';\nimport { MessagesService } from '../messages/messages.service';\nimport { Injectable, Logger } from '@nestjs/common';\nimport {\n  Message,\n  Role,\n  Task,\n  TaskPriority,\n  TaskStatus,\n  TaskType,\n} from '@prisma/client';\nimport { AnthropicService } from '../anthropic/anthropic.service';\nimport {\n  isComputerToolUseContentBlock,\n  isSetTaskStatusToolUseBlock,\n  isCreateTaskToolUseBlock,\n  SetTaskStatusToolUseBlock,\n} from '@bytebot/shared';\n\nimport {\n  MessageContentBlock,\n  MessageContentType,\n  ToolResultContentBlock,\n  TextContentBlock,\n} from '@bytebot/shared';\nimport { InputCaptureService } from './input-capture.service';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { OpenAIService } from '../openai/openai.service';\nimport { GoogleService } from '../google/google.service';\nimport {\n  BytebotAgentModel,\n  BytebotAgentService,\n  BytebotAgentResponse,\n} from './agent.types';\nimport {\n  AGENT_SYSTEM_PROMPT,\n  SUMMARIZATION_SYSTEM_PROMPT,\n} from './agent.constants';\nimport { SummariesService } from '../summaries/summaries.service';\nimport { handleComputerToolUse } from './agent.computer-use';\nimport { ProxyService } from '../proxy/proxy.service';\n\n@Injectable()\nexport class AgentProcessor {\n  private readonly logger = new Logger(AgentProcessor.name);\n  private currentTaskId: string | null = null;\n  private isProcessing = false;\n  private abortController: AbortController | null = null;\n  private services: Record<string, BytebotAgentService> = {};\n\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly messagesService: MessagesService,\n    private readonly summariesService: SummariesService,\n    private readonly anthropicService: AnthropicService,\n    private readonly openaiService: OpenAIService,\n    private readonly googleService: GoogleService,\n    private readonly proxyService: ProxyService,\n    private readonly inputCaptureService: InputCaptureService,\n  ) {\n    this.services = {\n      anthropic: this.anthropicService,\n      openai: this.openaiService,\n      google: this.googleService,\n      proxy: this.proxyService,\n    };\n    this.logger.log('AgentProcessor initialized');\n  }\n\n  /**\n   * Check if the processor is currently processing a task\n   */\n  isRunning(): boolean {\n    return this.isProcessing;\n  }\n\n  /**\n   * Get the current task ID being processed\n   */\n  getCurrentTaskId(): string | null {\n    return this.currentTaskId;\n  }\n\n  @OnEvent('task.takeover')\n  handleTaskTakeover({ taskId }: { taskId: string }) {\n    this.logger.log(`Task takeover event received for task ID: ${taskId}`);\n\n    // If the agent is still processing this task, abort any in-flight operations\n    if (this.currentTaskId === taskId && this.isProcessing) {\n      this.abortController?.abort();\n    }\n\n    // Always start capturing user input so that emitted actions are received\n    this.inputCaptureService.start(taskId);\n  }\n\n  @OnEvent('task.resume')\n  handleTaskResume({ taskId }: { taskId: string }) {\n    if (this.currentTaskId === taskId && this.isProcessing) {\n      this.logger.log(`Task resume event received for task ID: ${taskId}`);\n      this.abortController = new AbortController();\n\n      void this.runIteration(taskId);\n    }\n  }\n\n  @OnEvent('task.cancel')\n  async handleTaskCancel({ taskId }: { taskId: string }) {\n    this.logger.log(`Task cancel event received for task ID: ${taskId}`);\n\n    await this.stopProcessing();\n  }\n\n  processTask(taskId: string) {\n    this.logger.log(`Starting processing for task ID: ${taskId}`);\n\n    if (this.isProcessing) {\n      this.logger.warn('AgentProcessor is already processing another task');\n      return;\n    }\n\n    this.isProcessing = true;\n    this.currentTaskId = taskId;\n    this.abortController = new AbortController();\n\n    // Kick off the first iteration without blocking the caller\n    void this.runIteration(taskId);\n  }\n\n  /**\n   * Runs a single iteration of task processing and schedules the next\n   * iteration via setImmediate while the task remains RUNNING.\n   */\n  private async runIteration(taskId: string): Promise<void> {\n    if (!this.isProcessing) {\n      return;\n    }\n\n    try {\n      const task: Task = await this.tasksService.findById(taskId);\n\n      if (task.status !== TaskStatus.RUNNING) {\n        this.logger.log(\n          `Task processing completed for task ID: ${taskId} with status: ${task.status}`,\n        );\n        this.isProcessing = false;\n        this.currentTaskId = null;\n        return;\n      }\n\n      this.logger.log(`Processing iteration for task ID: ${taskId}`);\n\n      // Refresh abort controller for this iteration to avoid accumulating\n      // \"abort\" listeners on a single AbortSignal across iterations.\n      this.abortController = new AbortController();\n\n      const latestSummary = await this.summariesService.findLatest(taskId);\n      const unsummarizedMessages =\n        await this.messagesService.findUnsummarized(taskId);\n      const messages = [\n        ...(latestSummary\n          ? [\n              {\n                id: '',\n                createdAt: new Date(),\n                updatedAt: new Date(),\n                taskId,\n                summaryId: null,\n                role: Role.USER,\n                content: [\n                  {\n                    type: MessageContentType.Text,\n                    text: latestSummary.content,\n                  },\n                ],\n              },\n            ]\n          : []),\n        ...unsummarizedMessages,\n      ];\n      this.logger.debug(\n        `Sending ${messages.length} messages to LLM for processing`,\n      );\n\n      const model = task.model as unknown as BytebotAgentModel;\n      let agentResponse: BytebotAgentResponse;\n\n      const service = this.services[model.provider];\n      if (!service) {\n        this.logger.warn(\n          `No service found for model provider: ${model.provider}`,\n        );\n        await this.tasksService.update(taskId, {\n          status: TaskStatus.FAILED,\n        });\n        this.isProcessing = false;\n        this.currentTaskId = null;\n        return;\n      }\n\n      agentResponse = await service.generateMessage(\n        AGENT_SYSTEM_PROMPT,\n        messages,\n        model.name,\n        true,\n        this.abortController.signal,\n      );\n\n      const messageContentBlocks = agentResponse.contentBlocks;\n\n      this.logger.debug(\n        `Received ${messageContentBlocks.length} content blocks from LLM`,\n      );\n\n      if (messageContentBlocks.length === 0) {\n        this.logger.warn(\n          `Task ID: ${taskId} received no content blocks from LLM, marking as failed`,\n        );\n        await this.tasksService.update(taskId, {\n          status: TaskStatus.FAILED,\n        });\n        this.isProcessing = false;\n        this.currentTaskId = null;\n        return;\n      }\n\n      await this.messagesService.create({\n        content: messageContentBlocks,\n        role: Role.ASSISTANT,\n        taskId,\n      });\n\n      // Calculate if we need to summarize based on token usage\n      const contextWindow = model.contextWindow || 200000; // Default to 200k if not specified\n      const contextThreshold = contextWindow * 0.75;\n      const shouldSummarize =\n        agentResponse.tokenUsage.totalTokens >= contextThreshold;\n\n      if (shouldSummarize) {\n        try {\n          // After we've successfully generated a response, we can summarize the unsummarized messages\n          const summaryResponse = await service.generateMessage(\n            SUMMARIZATION_SYSTEM_PROMPT,\n            [\n              ...messages,\n              {\n                id: '',\n                createdAt: new Date(),\n                updatedAt: new Date(),\n                taskId,\n                summaryId: null,\n                role: Role.USER,\n                content: [\n                  {\n                    type: MessageContentType.Text,\n                    text: 'Respond with a summary of the messages above. Do not include any additional information.',\n                  },\n                ],\n              },\n            ],\n            model.name,\n            false,\n            this.abortController.signal,\n          );\n\n          const summaryContentBlocks = summaryResponse.contentBlocks;\n\n          this.logger.debug(\n            `Received ${summaryContentBlocks.length} summary content blocks from LLM`,\n          );\n          const summaryContent = summaryContentBlocks\n            .filter(\n              (block: MessageContentBlock) =>\n                block.type === MessageContentType.Text,\n            )\n            .map((block: TextContentBlock) => block.text)\n            .join('\\n');\n\n          const summary = await this.summariesService.create({\n            content: summaryContent,\n            taskId,\n          });\n\n          await this.messagesService.attachSummary(taskId, summary.id, [\n            ...messages.map((message) => {\n              return message.id;\n            }),\n          ]);\n\n          this.logger.log(\n            `Generated summary for task ${taskId} due to token usage (${agentResponse.tokenUsage.totalTokens}/${contextWindow})`,\n          );\n        } catch (error: any) {\n          this.logger.error(\n            `Error summarizing messages for task ID: ${taskId}`,\n            error.stack,\n          );\n        }\n      }\n\n      this.logger.debug(\n        `Token usage for task ${taskId}: ${agentResponse.tokenUsage.totalTokens}/${contextWindow} (${Math.round((agentResponse.tokenUsage.totalTokens / contextWindow) * 100)}%)`,\n      );\n\n      const generatedToolResults: ToolResultContentBlock[] = [];\n\n      let setTaskStatusToolUseBlock: SetTaskStatusToolUseBlock | null = null;\n\n      for (const block of messageContentBlocks) {\n        if (isComputerToolUseContentBlock(block)) {\n          const result = await handleComputerToolUse(block, this.logger);\n          generatedToolResults.push(result);\n        }\n\n        if (isCreateTaskToolUseBlock(block)) {\n          const type = block.input.type?.toUpperCase() as TaskType;\n          const priority = block.input.priority?.toUpperCase() as TaskPriority;\n\n          await this.tasksService.create({\n            description: block.input.description,\n            type,\n            createdBy: Role.ASSISTANT,\n            ...(block.input.scheduledFor && {\n              scheduledFor: new Date(block.input.scheduledFor),\n            }),\n            model: task.model,\n            priority,\n          });\n\n          generatedToolResults.push({\n            type: MessageContentType.ToolResult,\n            tool_use_id: block.id,\n            content: [\n              {\n                type: MessageContentType.Text,\n                text: 'The task has been created',\n              },\n            ],\n          });\n        }\n\n        if (isSetTaskStatusToolUseBlock(block)) {\n          setTaskStatusToolUseBlock = block;\n\n          generatedToolResults.push({\n            type: MessageContentType.ToolResult,\n            tool_use_id: block.id,\n            is_error: block.input.status === 'failed',\n            content: [\n              {\n                type: MessageContentType.Text,\n                text: block.input.description,\n              },\n            ],\n          });\n        }\n      }\n\n      if (generatedToolResults.length > 0) {\n        await this.messagesService.create({\n          content: generatedToolResults,\n          role: Role.USER,\n          taskId,\n        });\n      }\n\n      // Update the task status after all tool results have been generated if we have a set task status tool use block\n      if (setTaskStatusToolUseBlock) {\n        switch (setTaskStatusToolUseBlock.input.status) {\n          case 'completed':\n            await this.tasksService.update(taskId, {\n              status: TaskStatus.COMPLETED,\n              completedAt: new Date(),\n            });\n            break;\n          case 'needs_help':\n            await this.tasksService.update(taskId, {\n              status: TaskStatus.NEEDS_HELP,\n            });\n            break;\n        }\n      }\n\n      // Schedule the next iteration without blocking\n      if (this.isProcessing) {\n        setImmediate(() => this.runIteration(taskId));\n      }\n    } catch (error: any) {\n      if (error?.name === 'BytebotAgentInterrupt') {\n        this.logger.warn(`Processing aborted for task ID: ${taskId}`);\n      } else {\n        this.logger.error(\n          `Error during task processing iteration for task ID: ${taskId} - ${error.message}`,\n          error.stack,\n        );\n        await this.tasksService.update(taskId, {\n          status: TaskStatus.FAILED,\n        });\n        this.isProcessing = false;\n        this.currentTaskId = null;\n      }\n    }\n  }\n\n  async stopProcessing(): Promise<void> {\n    if (!this.isProcessing) {\n      return;\n    }\n\n    this.logger.log(`Stopping execution of task ${this.currentTaskId}`);\n\n    // Signal any in-flight async operations to abort\n    this.abortController?.abort();\n\n    await this.inputCaptureService.stop();\n\n    this.isProcessing = false;\n    this.currentTaskId = null;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.scheduler.ts",
    "content": "import { Injectable, Logger, OnModuleInit } from '@nestjs/common';\nimport { Cron, CronExpression } from '@nestjs/schedule';\nimport { TasksService } from '../tasks/tasks.service';\nimport { AgentProcessor } from './agent.processor';\nimport { TaskStatus } from '@prisma/client';\nimport { writeFile } from './agent.computer-use';\n\n@Injectable()\nexport class AgentScheduler implements OnModuleInit {\n  private readonly logger = new Logger(AgentScheduler.name);\n\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly agentProcessor: AgentProcessor,\n  ) {}\n\n  async onModuleInit() {\n    this.logger.log('AgentScheduler initialized');\n    await this.handleCron();\n  }\n\n  @Cron(CronExpression.EVERY_5_SECONDS)\n  async handleCron() {\n    const now = new Date();\n    const scheduledTasks = await this.tasksService.findScheduledTasks();\n    for (const scheduledTask of scheduledTasks) {\n      if (scheduledTask.scheduledFor && scheduledTask.scheduledFor < now) {\n        this.logger.debug(\n          `Task ID: ${scheduledTask.id} is scheduled for ${scheduledTask.scheduledFor}, queuing it`,\n        );\n        await this.tasksService.update(scheduledTask.id, {\n          queuedAt: now,\n        });\n      }\n    }\n\n    if (this.agentProcessor.isRunning()) {\n      return;\n    }\n    // Find the highest priority task to execute\n    const task = await this.tasksService.findNextTask();\n    if (task) {\n      if (task.files.length > 0) {\n        this.logger.debug(\n          `Task ID: ${task.id} has files, writing them to the desktop`,\n        );\n        for (const file of task.files) {\n          await writeFile({\n            path: `/home/user/Desktop/${file.name}`,\n            content: file.data, // file.data is already base64 encoded in the database\n          });\n        }\n      }\n\n      await this.tasksService.update(task.id, {\n        status: TaskStatus.RUNNING,\n        executedAt: new Date(),\n      });\n      this.logger.debug(`Processing task ID: ${task.id}`);\n      this.agentProcessor.processTask(task.id);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.tools.ts",
    "content": "/**\n * Common schema definitions for reuse\n */\nconst coordinateSchema = {\n  type: 'object' as const,\n  properties: {\n    x: {\n      type: 'number' as const,\n      description: 'The x-coordinate',\n    },\n    y: {\n      type: 'number' as const,\n      description: 'The y-coordinate',\n    },\n  },\n  required: ['x', 'y'],\n};\n\nconst holdKeysSchema = {\n  type: 'array' as const,\n  items: { type: 'string' as const },\n  description: 'Optional array of keys to hold during the action',\n  nullable: true,\n};\n\nconst buttonSchema = {\n  type: 'string' as const,\n  enum: ['left', 'right', 'middle'],\n  description: 'The mouse button',\n};\n\n/**\n * Tool definitions for mouse actions\n */\nexport const _moveMouseTool = {\n  name: 'computer_move_mouse',\n  description: 'Moves the mouse cursor to the specified coordinates',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description: 'Target coordinates for mouse movement',\n      },\n    },\n    required: ['coordinates'],\n  },\n};\n\nexport const _traceMouseTool = {\n  name: 'computer_trace_mouse',\n  description: 'Moves the mouse cursor along a specified path of coordinates',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      path: {\n        type: 'array' as const,\n        items: coordinateSchema,\n        description: 'Array of coordinate objects representing the path',\n      },\n      holdKeys: holdKeysSchema,\n    },\n    required: ['path'],\n  },\n};\n\nexport const _clickMouseTool = {\n  name: 'computer_click_mouse',\n  description:\n    'Performs a mouse click at the specified coordinates or current position',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description:\n          'Optional click coordinates (defaults to current position)',\n        nullable: true,\n      },\n      button: buttonSchema,\n      holdKeys: holdKeysSchema,\n      clickCount: {\n        type: 'integer' as const,\n        description: 'Number of clicks to perform (e.g., 2 for double-click)',\n        default: 1,\n      },\n    },\n    required: ['button', 'clickCount'],\n  },\n};\n\nexport const _pressMouseTool = {\n  name: 'computer_press_mouse',\n  description: 'Presses or releases a specified mouse button',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description: 'Optional coordinates (defaults to current position)',\n        nullable: true,\n      },\n      button: buttonSchema,\n      press: {\n        type: 'string' as const,\n        enum: ['up', 'down'],\n        description: 'Whether to press down or release up',\n      },\n    },\n    required: ['button', 'press'],\n  },\n};\n\nexport const _dragMouseTool = {\n  name: 'computer_drag_mouse',\n  description: 'Drags the mouse along a path while holding a button',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      path: {\n        type: 'array' as const,\n        items: coordinateSchema,\n        description: 'Array of coordinates representing the drag path',\n      },\n      button: buttonSchema,\n      holdKeys: holdKeysSchema,\n    },\n    required: ['path', 'button'],\n  },\n};\n\nexport const _scrollTool = {\n  name: 'computer_scroll',\n  description: 'Scrolls the mouse wheel in the specified direction',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description: 'Coordinates where the scroll should occur',\n      },\n      direction: {\n        type: 'string' as const,\n        enum: ['up', 'down', 'left', 'right'],\n        description: 'The direction to scroll',\n      },\n      scrollCount: {\n        type: 'integer' as const,\n        description: 'Number of scroll steps',\n      },\n      holdKeys: holdKeysSchema,\n    },\n    required: ['coordinates', 'direction', 'scrollCount'],\n  },\n};\n\n/**\n * Tool definitions for keyboard actions\n */\nexport const _typeKeysTool = {\n  name: 'computer_type_keys',\n  description: 'Types a sequence of keys (useful for keyboard shortcuts)',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      keys: {\n        type: 'array' as const,\n        items: { type: 'string' as const },\n        description: 'Array of key names to type in sequence',\n      },\n      delay: {\n        type: 'number' as const,\n        description: 'Optional delay in milliseconds between key presses',\n        nullable: true,\n      },\n    },\n    required: ['keys'],\n  },\n};\n\nexport const _pressKeysTool = {\n  name: 'computer_press_keys',\n  description:\n    'Presses or releases specific keys (useful for holding modifiers)',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      keys: {\n        type: 'array' as const,\n        items: { type: 'string' as const },\n        description: 'Array of key names to press or release',\n      },\n      press: {\n        type: 'string' as const,\n        enum: ['up', 'down'],\n        description: 'Whether to press down or release up',\n      },\n    },\n    required: ['keys', 'press'],\n  },\n};\n\nexport const _typeTextTool = {\n  name: 'computer_type_text',\n  description:\n    'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      text: {\n        type: 'string' as const,\n        description: 'The text string to type',\n      },\n      delay: {\n        type: 'number' as const,\n        description: 'Optional delay in milliseconds between characters',\n        nullable: true,\n      },\n      isSensitive: {\n        type: 'boolean' as const,\n        description: 'Flag to indicate sensitive information',\n        nullable: true,\n      },\n    },\n    required: ['text'],\n  },\n};\n\nexport const _pasteTextTool = {\n  name: 'computer_paste_text',\n  description:\n    'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      text: {\n        type: 'string' as const,\n        description: 'The text string to type',\n      },\n      isSensitive: {\n        type: 'boolean' as const,\n        description: 'Flag to indicate sensitive information',\n        nullable: true,\n      },\n    },\n    required: ['text'],\n  },\n};\n\n/**\n * Tool definitions for utility actions\n */\nexport const _waitTool = {\n  name: 'computer_wait',\n  description: 'Pauses execution for a specified duration',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      duration: {\n        type: 'integer' as const,\n        enum: [500],\n        description: 'The duration to wait in milliseconds',\n      },\n    },\n    required: ['duration'],\n  },\n};\n\nexport const _screenshotTool = {\n  name: 'computer_screenshot',\n  description: 'Captures a screenshot of the current screen',\n  input_schema: {\n    type: 'object' as const,\n    properties: {},\n  },\n};\n\nexport const _cursorPositionTool = {\n  name: 'computer_cursor_position',\n  description: 'Gets the current (x, y) coordinates of the mouse cursor',\n  input_schema: {\n    type: 'object' as const,\n    properties: {},\n  },\n};\n\nexport const _applicationTool = {\n  name: 'computer_application',\n  description: 'Opens or focuses an application and ensures it is fullscreen',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      application: {\n        type: 'string' as const,\n        enum: [\n          'firefox',\n          '1password',\n          'thunderbird',\n          'vscode',\n          'terminal',\n          'desktop',\n          'directory',\n        ],\n        description: 'The application to open or focus',\n      },\n    },\n    required: ['application'],\n  },\n};\n\n/**\n * Tool definitions for task management\n */\nexport const _setTaskStatusTool = {\n  name: 'set_task_status',\n  description: 'Sets the status of the current task',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      status: {\n        type: 'string' as const,\n        enum: ['completed', 'needs_help'],\n        description: 'The status of the task',\n      },\n      description: {\n        type: 'string' as const,\n        description:\n          'If the task is completed, a summary of the task. If the task needs help, a description of the issue or clarification needed.',\n      },\n    },\n    required: ['status', 'description'],\n  },\n};\n\nexport const _createTaskTool = {\n  name: 'create_task',\n  description: 'Creates a new task',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      description: {\n        type: 'string' as const,\n        description: 'The description of the task',\n      },\n      type: {\n        type: 'string' as const,\n        enum: ['IMMEDIATE', 'SCHEDULED'],\n        description: 'The type of the task (defaults to IMMEDIATE)',\n      },\n      scheduledFor: {\n        type: 'string' as const,\n        format: 'date-time',\n        description: 'RFC 3339 / ISO 8601 datetime for scheduled tasks',\n      },\n      priority: {\n        type: 'string' as const,\n        enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'],\n        description: 'The priority of the task (defaults to MEDIUM)',\n      },\n    },\n    required: ['description'],\n  },\n};\n\n/**\n * Tool definition for reading files\n */\nexport const _readFileTool = {\n  name: 'computer_read_file',\n  description:\n    'Reads a file from the specified path and returns it as a document content block with base64 encoded data',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      path: {\n        type: 'string' as const,\n        description: 'The file path to read from',\n      },\n    },\n    required: ['path'],\n  },\n};\n\n/**\n * Export all tools as an array\n */\nexport const agentTools = [\n  _moveMouseTool,\n  _traceMouseTool,\n  _clickMouseTool,\n  _pressMouseTool,\n  _dragMouseTool,\n  _scrollTool,\n  _typeKeysTool,\n  _pressKeysTool,\n  _typeTextTool,\n  _pasteTextTool,\n  _waitTool,\n  _screenshotTool,\n  _applicationTool,\n  _cursorPositionTool,\n  _setTaskStatusTool,\n  _createTaskTool,\n  _readFileTool,\n];\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/agent.types.ts",
    "content": "import { Message } from '@prisma/client';\nimport { MessageContentBlock } from '@bytebot/shared';\n\nexport interface BytebotAgentResponse {\n  contentBlocks: MessageContentBlock[];\n  tokenUsage: {\n    inputTokens: number;\n    outputTokens: number;\n    totalTokens: number;\n  };\n}\n\nexport interface BytebotAgentService {\n  generateMessage(\n    systemPrompt: string,\n    messages: Message[],\n    model: string,\n    useTools: boolean,\n    signal?: AbortSignal,\n  ): Promise<BytebotAgentResponse>;\n}\n\nexport interface BytebotAgentModel {\n  provider: 'anthropic' | 'openai' | 'google' | 'proxy';\n  name: string;\n  title: string;\n  contextWindow?: number;\n}\n\nexport class BytebotAgentInterrupt extends Error {\n  constructor() {\n    super('BytebotAgentInterrupt');\n    this.name = 'BytebotAgentInterrupt';\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/agent/input-capture.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { io, Socket } from 'socket.io-client';\nimport { randomUUID } from 'crypto';\nimport {\n  convertClickMouseActionToToolUseBlock,\n  convertDragMouseActionToToolUseBlock,\n  convertPressKeysActionToToolUseBlock,\n  convertPressMouseActionToToolUseBlock,\n  convertScrollActionToToolUseBlock,\n  convertTypeKeysActionToToolUseBlock,\n  convertTypeTextActionToToolUseBlock,\n  ImageContentBlock,\n  MessageContentBlock,\n  MessageContentType,\n  ScreenshotToolUseBlock,\n  ToolResultContentBlock,\n  UserActionContentBlock,\n} from '@bytebot/shared';\nimport { Role } from '@prisma/client';\nimport { MessagesService } from '../messages/messages.service';\nimport { ConfigService } from '@nestjs/config';\n\n@Injectable()\nexport class InputCaptureService {\n  private readonly logger = new Logger(InputCaptureService.name);\n  private socket: Socket | null = null;\n  private capturing = false;\n\n  constructor(\n    private readonly messagesService: MessagesService,\n    private readonly configService: ConfigService,\n  ) {}\n\n  isCapturing() {\n    return this.capturing;\n  }\n\n  start(taskId: string) {\n    if (this.socket?.connected && this.capturing) return;\n\n    if (this.socket && !this.socket.connected) {\n      this.socket.connect();\n      return;\n    }\n\n    const baseUrl = this.configService.get<string>('BYTEBOT_DESKTOP_BASE_URL');\n    if (!baseUrl) {\n      this.logger.warn('BYTEBOT_DESKTOP_BASE_URL missing.');\n      return;\n    }\n\n    this.socket = io(baseUrl, { transports: ['websocket'] });\n\n    this.socket.on('connect', () => {\n      this.logger.log('Input socket connected');\n      this.capturing = true;\n    });\n\n    this.socket.on(\n      'screenshotAndAction',\n      async (shot: { image: string }, action: any) => {\n        if (!this.capturing || !taskId) return;\n        // The gateway only sends a click_mouse or drag_mouse action together with screenshots for now.\n        if (action.action !== 'click_mouse' && action.action !== 'drag_mouse')\n          return;\n\n        const userActionBlock: UserActionContentBlock = {\n          type: MessageContentType.UserAction,\n          content: [\n            {\n              type: MessageContentType.Image,\n              source: {\n                data: shot.image,\n                media_type: 'image/png',\n                type: 'base64',\n              },\n            },\n          ],\n        };\n\n        const toolUseId = randomUUID();\n        switch (action.action) {\n          case 'drag_mouse':\n            userActionBlock.content.push(\n              convertDragMouseActionToToolUseBlock(action, toolUseId),\n            );\n            break;\n          case 'click_mouse':\n            userActionBlock.content.push(\n              convertClickMouseActionToToolUseBlock(action, toolUseId),\n            );\n            break;\n        }\n\n        await this.messagesService.create({\n          content: [userActionBlock],\n          role: Role.USER,\n          taskId,\n        });\n      },\n    );\n\n    this.socket.on('action', async (action: any) => {\n      if (!this.capturing || !taskId) return;\n      const toolUseId = randomUUID();\n      const userActionBlock: UserActionContentBlock = {\n        type: MessageContentType.UserAction,\n        content: [],\n      };\n\n      switch (action.action) {\n        case 'drag_mouse':\n          userActionBlock.content.push(\n            convertDragMouseActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'press_mouse':\n          userActionBlock.content.push(\n            convertPressMouseActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'type_keys':\n          userActionBlock.content.push(\n            convertTypeKeysActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'press_keys':\n          userActionBlock.content.push(\n            convertPressKeysActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'type_text':\n          userActionBlock.content.push(\n            convertTypeTextActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'scroll':\n          userActionBlock.content.push(\n            convertScrollActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        default:\n          this.logger.warn(`Unknown action ${action.action}`);\n      }\n\n      if (userActionBlock.content.length > 0) {\n        await this.messagesService.create({\n          content: [userActionBlock],\n          role: Role.USER,\n          taskId,\n        });\n      }\n    });\n\n    this.socket.on('disconnect', () => {\n      this.logger.log('Input socket disconnected');\n      this.capturing = false;\n    });\n  }\n\n  async stop() {\n    if (!this.socket) return;\n    if (this.socket.connected) this.socket.disconnect();\n    else this.socket.removeAllListeners();\n    this.socket = null;\n    this.capturing = false;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/anthropic/anthropic.constants.ts",
    "content": "import { BytebotAgentModel } from '../agent/agent.types';\n\nexport const ANTHROPIC_MODELS: BytebotAgentModel[] = [\n  {\n    provider: 'anthropic',\n    name: 'claude-opus-4-1-20250805',\n    title: 'Claude Opus 4.1',\n    contextWindow: 200000,\n  },\n  {\n    provider: 'anthropic',\n    name: 'claude-sonnet-4-20250514',\n    title: 'Claude Sonnet 4',\n    contextWindow: 200000,\n  },\n];\n\nexport const DEFAULT_MODEL = ANTHROPIC_MODELS[0];\n"
  },
  {
    "path": "packages/bytebot-agent/src/anthropic/anthropic.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { AnthropicService } from './anthropic.service';\n\n@Module({\n  imports: [ConfigModule],\n  providers: [AnthropicService],\n  exports: [AnthropicService],\n})\nexport class AnthropicModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/anthropic/anthropic.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport Anthropic, { APIUserAbortError } from '@anthropic-ai/sdk';\nimport {\n  MessageContentBlock,\n  MessageContentType,\n  TextContentBlock,\n  ToolUseContentBlock,\n  ThinkingContentBlock,\n  RedactedThinkingContentBlock,\n  isUserActionContentBlock,\n  isComputerToolUseContentBlock,\n} from '@bytebot/shared';\nimport { DEFAULT_MODEL } from './anthropic.constants';\nimport { Message, Role } from '@prisma/client';\nimport { anthropicTools } from './anthropic.tools';\nimport {\n  BytebotAgentService,\n  BytebotAgentInterrupt,\n  BytebotAgentResponse,\n} from '../agent/agent.types';\n\n@Injectable()\nexport class AnthropicService implements BytebotAgentService {\n  private readonly anthropic: Anthropic;\n  private readonly logger = new Logger(AnthropicService.name);\n\n  constructor(private readonly configService: ConfigService) {\n    const apiKey = this.configService.get<string>('ANTHROPIC_API_KEY');\n\n    if (!apiKey) {\n      this.logger.warn(\n        'ANTHROPIC_API_KEY is not set. AnthropicService will not work properly.',\n      );\n    }\n\n    this.anthropic = new Anthropic({\n      apiKey: apiKey || 'dummy-key-for-initialization',\n    });\n  }\n\n  async generateMessage(\n    systemPrompt: string,\n    messages: Message[],\n    model: string = DEFAULT_MODEL.name,\n    useTools: boolean = true,\n    signal?: AbortSignal,\n  ): Promise<BytebotAgentResponse> {\n    try {\n      const maxTokens = 8192;\n\n      // Convert our message content blocks to Anthropic's expected format\n      const anthropicMessages = this.formatMessagesForAnthropic(messages);\n\n      // add cache_control to last tool\n      anthropicTools[anthropicTools.length - 1].cache_control = {\n        type: 'ephemeral',\n      };\n\n      // Make the API call\n      const response = await this.anthropic.messages.create(\n        {\n          model,\n          max_tokens: maxTokens * 2,\n          thinking: { type: 'disabled' },\n          system: [\n            {\n              type: 'text',\n              text: systemPrompt,\n              cache_control: { type: 'ephemeral' },\n            },\n          ],\n          messages: anthropicMessages,\n          tools: useTools ? anthropicTools : [],\n        },\n        { signal },\n      );\n\n      // Convert Anthropic's response to our message content blocks format\n      return {\n        contentBlocks: this.formatAnthropicResponse(response.content),\n        tokenUsage: {\n          inputTokens: response.usage.input_tokens,\n          outputTokens: response.usage.output_tokens,\n          totalTokens:\n            response.usage.input_tokens + response.usage.output_tokens,\n        },\n      };\n    } catch (error) {\n      this.logger.log(error);\n\n      if (error instanceof APIUserAbortError) {\n        this.logger.log('Anthropic API call aborted');\n        throw new BytebotAgentInterrupt();\n      }\n      this.logger.error(\n        `Error sending message to Anthropic: ${error.message}`,\n        error.stack,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Convert our MessageContentBlock format to Anthropic's message format\n   */\n  private formatMessagesForAnthropic(\n    messages: Message[],\n  ): Anthropic.MessageParam[] {\n    const anthropicMessages: Anthropic.MessageParam[] = [];\n\n    // Process each message content block\n    for (const [index, message] of messages.entries()) {\n      const messageContentBlocks = message.content as MessageContentBlock[];\n\n      const content: Anthropic.ContentBlockParam[] = [];\n\n      if (\n        messageContentBlocks.every((block) => isUserActionContentBlock(block))\n      ) {\n        const userActionContentBlocks = messageContentBlocks.flatMap(\n          (block) => block.content,\n        );\n        for (const block of userActionContentBlocks) {\n          if (isComputerToolUseContentBlock(block)) {\n            content.push({\n              type: 'text',\n              text: `User performed action: ${block.name}\\n${JSON.stringify(block.input, null, 2)}`,\n            });\n          } else {\n            content.push(block as Anthropic.ContentBlockParam);\n          }\n        }\n      } else {\n        content.push(\n          ...messageContentBlocks.map(\n            (block) => block as Anthropic.ContentBlockParam,\n          ),\n        );\n      }\n\n      if (index === messages.length - 1) {\n        content[content.length - 1]['cache_control'] = {\n          type: 'ephemeral',\n        };\n      }\n      anthropicMessages.push({\n        role: message.role === Role.USER ? 'user' : 'assistant',\n        content: content,\n      });\n    }\n\n    return anthropicMessages;\n  }\n\n  /**\n   * Convert Anthropic's response content to our MessageContentBlock format\n   */\n  private formatAnthropicResponse(\n    content: Anthropic.ContentBlock[],\n  ): MessageContentBlock[] {\n    return content.map((block) => {\n      switch (block.type) {\n        case 'text':\n          return {\n            type: MessageContentType.Text,\n            text: block.text,\n          } as TextContentBlock;\n\n        case 'tool_use':\n          return {\n            type: MessageContentType.ToolUse,\n            id: block.id,\n            name: block.name,\n            input: block.input,\n          } as ToolUseContentBlock;\n\n        case 'thinking':\n          return {\n            type: MessageContentType.Thinking,\n            thinking: block.thinking,\n            signature: block.signature,\n          } as ThinkingContentBlock;\n\n        case 'redacted_thinking':\n          return {\n            type: MessageContentType.RedactedThinking,\n            data: block.data,\n          } as RedactedThinkingContentBlock;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/anthropic/anthropic.tools.ts",
    "content": "import Anthropic from '@anthropic-ai/sdk';\nimport { agentTools } from '../agent/agent.tools';\n\n/**\n * Converts an agent tool definition to an Anthropic.Tool\n */\nfunction agentToolToAnthropicTool(agentTool: any): Anthropic.Tool {\n  return agentTool as Anthropic.Tool;\n}\n\n/**\n * Creates a mapped object of tools by name\n */\nconst toolMap = agentTools.reduce(\n  (acc, tool) => {\n    const anthropicTool = agentToolToAnthropicTool(tool);\n    const camelCaseName = tool.name\n      .split('_')\n      .map((part, index) => {\n        if (index === 0) return part;\n        if (part === 'computer') return '';\n        return part.charAt(0).toUpperCase() + part.slice(1);\n      })\n      .join('')\n      .replace(/^computer/, '');\n\n    acc[camelCaseName + 'Tool'] = anthropicTool;\n    return acc;\n  },\n  {} as Record<string, Anthropic.Tool>,\n);\n\n// Export individual tools with proper names\nexport const moveMouseTool = toolMap.moveMouseTool;\nexport const traceMouseTool = toolMap.traceMouseTool;\nexport const clickMouseTool = toolMap.clickMouseTool;\nexport const pressMouseTool = toolMap.pressMouseTool;\nexport const dragMouseTool = toolMap.dragMouseTool;\nexport const scrollTool = toolMap.scrollTool;\nexport const typeKeysTool = toolMap.typeKeysTool;\nexport const pressKeysTool = toolMap.pressKeysTool;\nexport const typeTextTool = toolMap.typeTextTool;\nexport const pasteTextTool = toolMap.pasteTextTool;\nexport const waitTool = toolMap.waitTool;\nexport const screenshotTool = toolMap.screenshotTool;\nexport const cursorPositionTool = toolMap.cursorPositionTool;\nexport const setTaskStatusTool = toolMap.setTaskStatusTool;\nexport const createTaskTool = toolMap.createTaskTool;\nexport const applicationTool = toolMap.applicationTool;\n\n// Array of all tools\nexport const anthropicTools: Anthropic.Tool[] = agentTools.map(\n  agentToolToAnthropicTool,\n);\n"
  },
  {
    "path": "packages/bytebot-agent/src/app.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { AppService } from './app.service';\n\n@Controller()\nexport class AppController {\n  constructor(private readonly appService: AppService) {}\n\n  @Get()\n  getHello(): string {\n    return this.appService.getHello();\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/app.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { AppController } from './app.controller';\nimport { AppService } from './app.service';\nimport { AgentModule } from './agent/agent.module';\nimport { TasksModule } from './tasks/tasks.module';\nimport { MessagesModule } from './messages/messages.module';\nimport { AnthropicModule } from './anthropic/anthropic.module';\nimport { OpenAIModule } from './openai/openai.module';\nimport { GoogleModule } from './google/google.module';\nimport { PrismaModule } from './prisma/prisma.module';\nimport { ConfigModule } from '@nestjs/config';\nimport { ScheduleModule } from '@nestjs/schedule';\nimport { EventEmitterModule } from '@nestjs/event-emitter';\nimport { SummariesModule } from './summaries/summaries.modue';\nimport { ProxyModule } from './proxy/proxy.module';\n\n@Module({\n  imports: [\n    ScheduleModule.forRoot(),\n    EventEmitterModule.forRoot(),\n    ConfigModule.forRoot({\n      isGlobal: true,\n    }),\n    AgentModule,\n    TasksModule,\n    MessagesModule,\n    SummariesModule,\n    AnthropicModule,\n    OpenAIModule,\n    GoogleModule,\n    ProxyModule,\n    PrismaModule,\n  ],\n  controllers: [AppController],\n  providers: [AppService],\n})\nexport class AppModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/app.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class AppService {\n  getHello(): string {\n    return 'Hello World!';\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/google/google.constants.ts",
    "content": "import { BytebotAgentModel } from '../agent/agent.types';\n\nexport const GOOGLE_MODELS: BytebotAgentModel[] = [\n  {\n    provider: 'google',\n    name: 'gemini-2.5-pro',\n    title: 'Gemini 2.5 Pro',\n    contextWindow: 1000000,\n  },\n  {\n    provider: 'google',\n    name: 'gemini-2.5-flash',\n    title: 'Gemini 2.5 Flash',\n    contextWindow: 1000000,\n  },\n];\n\nexport const DEFAULT_MODEL = GOOGLE_MODELS[0];\n"
  },
  {
    "path": "packages/bytebot-agent/src/google/google.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { GoogleService } from './google.service';\n\n@Module({\n  imports: [ConfigModule],\n  providers: [GoogleService],\n  exports: [GoogleService],\n})\nexport class GoogleModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/google/google.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport {\n  isComputerToolUseContentBlock,\n  isImageContentBlock,\n  isUserActionContentBlock,\n  MessageContentBlock,\n  MessageContentType,\n  TextContentBlock,\n  ThinkingContentBlock,\n  ToolUseContentBlock,\n} from '@bytebot/shared';\nimport {\n  BytebotAgentService,\n  BytebotAgentInterrupt,\n  BytebotAgentResponse,\n} from '../agent/agent.types';\nimport { Message, Role } from '@prisma/client';\nimport { googleTools } from './google.tools';\nimport {\n  Content,\n  GenerateContentResponse,\n  GoogleGenAI,\n  Part,\n} from '@google/genai';\nimport { v4 as uuid } from 'uuid';\nimport { DEFAULT_MODEL } from './google.constants';\n\n@Injectable()\nexport class GoogleService implements BytebotAgentService {\n  private readonly google: GoogleGenAI;\n  private readonly logger = new Logger(GoogleService.name);\n\n  constructor(private readonly configService: ConfigService) {\n    const apiKey = this.configService.get<string>('GEMINI_API_KEY');\n\n    if (!apiKey) {\n      this.logger.warn(\n        'GEMINI_API_KEY is not set. GoogleService will not work properly.',\n      );\n    }\n\n    this.google = new GoogleGenAI({\n      apiKey: apiKey || 'dummy-key-for-initialization',\n    });\n  }\n\n  async generateMessage(\n    systemPrompt: string,\n    messages: Message[],\n    model: string = DEFAULT_MODEL.name,\n    useTools: boolean = true,\n    signal?: AbortSignal,\n  ): Promise<BytebotAgentResponse> {\n    try {\n      const maxTokens = 8192;\n\n      // Convert our message content blocks to Anthropic's expected format\n      const googleMessages = this.formatMessagesForGoogle(messages);\n\n      const response: GenerateContentResponse =\n        await this.google.models.generateContent({\n          model,\n          contents: googleMessages,\n          config: {\n            thinkingConfig: {\n              thinkingBudget: 24576,\n            },\n            maxOutputTokens: maxTokens,\n            systemInstruction: systemPrompt,\n            tools: useTools\n              ? [\n                  {\n                    functionDeclarations: googleTools,\n                  },\n                ]\n              : [],\n            abortSignal: signal,\n          },\n        });\n\n      const candidate = response.candidates?.[0];\n\n      if (!candidate) {\n        throw new Error('No candidate found in response');\n      }\n\n      const content = candidate.content;\n\n      if (!content) {\n        throw new Error('No content found in candidate');\n      }\n\n      if (!content.parts) {\n        throw new Error('No parts found in content');\n      }\n\n      return {\n        contentBlocks: this.formatGoogleResponse(content.parts),\n        tokenUsage: {\n          inputTokens: response.usageMetadata?.promptTokenCount || 0,\n          outputTokens: response.usageMetadata?.candidatesTokenCount || 0,\n          totalTokens: response.usageMetadata?.totalTokenCount || 0,\n        },\n      };\n    } catch (error) {\n      if (error.message.includes('AbortError')) {\n        throw new BytebotAgentInterrupt();\n      }\n      this.logger.error(\n        `Error sending message to Google Gemini: ${error.message}`,\n        error.stack,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Convert our MessageContentBlock format to Google Gemini's message format\n   */\n  private formatMessagesForGoogle(messages: Message[]): Content[] {\n    const googleMessages: Content[] = [];\n\n    // Process each message content block\n    for (const message of messages) {\n      const messageContentBlocks = message.content as MessageContentBlock[];\n\n      const parts: Part[] = [];\n\n      if (\n        messageContentBlocks.every((block) => isUserActionContentBlock(block))\n      ) {\n        const userActionContentBlocks = messageContentBlocks.flatMap(\n          (block) => block.content,\n        );\n        for (const block of userActionContentBlocks) {\n          if (isComputerToolUseContentBlock(block)) {\n            parts.push({\n              text: `User performed action: ${block.name}\\n${JSON.stringify(block.input, null, 2)}`,\n            });\n          } else if (isImageContentBlock(block)) {\n            parts.push({\n              inlineData: {\n                data: block.source.data,\n                mimeType: block.source.media_type,\n              },\n            });\n          }\n        }\n      } else {\n        for (const block of messageContentBlocks) {\n          switch (block.type) {\n            case MessageContentType.Text:\n              parts.push({\n                text: block.text,\n              });\n              break;\n            case MessageContentType.ToolUse:\n              parts.push({\n                functionCall: {\n                  id: block.id,\n                  name: block.name,\n                  args: block.input,\n                },\n              });\n              break;\n            case MessageContentType.Image:\n              parts.push({\n                inlineData: {\n                  data: block.source.data,\n                  mimeType: block.source.media_type,\n                },\n              });\n              break;\n            case MessageContentType.ToolResult: {\n              const toolResultContentBlock = block.content[0];\n              if (toolResultContentBlock.type === MessageContentType.Image) {\n                parts.push({\n                  functionResponse: {\n                    id: block.tool_use_id,\n                    name: 'screenshot',\n                    response: {\n                      ...(!block.is_error && {\n                        output: 'screenshot successful',\n                      }),\n                      ...(block.is_error && { error: block.content[0] }),\n                    },\n                  },\n                });\n                parts.push({\n                  inlineData: {\n                    data: toolResultContentBlock.source.data,\n                    mimeType: toolResultContentBlock.source.media_type,\n                  },\n                });\n                break;\n              }\n\n              parts.push({\n                functionResponse: {\n                  id: block.tool_use_id,\n                  name: this.getToolName(block.tool_use_id, messages),\n                  response: {\n                    ...(!block.is_error && { output: block.content[0] }),\n                    ...(block.is_error && { error: block.content[0] }),\n                  },\n                },\n              });\n              break;\n            }\n            case MessageContentType.Thinking:\n              parts.push({\n                text: block.thinking,\n                thoughtSignature: block.signature,\n                thought: true,\n              });\n              break;\n            default:\n              parts.push({\n                text: JSON.stringify(block),\n              });\n              break;\n          }\n        }\n      }\n\n      googleMessages.push({\n        role: message.role === Role.USER ? 'user' : 'model',\n        parts: parts,\n      });\n    }\n\n    return googleMessages;\n  }\n\n  // Find the content block with the tool_use_id and return the name\n  private getToolName(\n    tool_use_id: string,\n    messages: Message[],\n  ): string | undefined {\n    const toolMessage = messages.find((message) =>\n      (message.content as MessageContentBlock[]).some(\n        (block) =>\n          block.type === MessageContentType.ToolUse && block.id === tool_use_id,\n      ),\n    );\n    if (!toolMessage) {\n      return undefined;\n    }\n\n    const toolBlock = (toolMessage.content as MessageContentBlock[]).find(\n      (block) =>\n        block.type === MessageContentType.ToolUse && block.id === tool_use_id,\n    );\n    if (!toolBlock) {\n      return undefined;\n    }\n    return (toolBlock as ToolUseContentBlock).name;\n  }\n\n  /**\n   * Convert Google Gemini's response content to our MessageContentBlock format\n   */\n  private formatGoogleResponse(parts: Part[]): MessageContentBlock[] {\n    return parts.map((part) => {\n      if (part.text) {\n        return {\n          type: MessageContentType.Text,\n          text: part.text,\n        } as TextContentBlock;\n      }\n\n      if (part.thought) {\n        return {\n          type: MessageContentType.Thinking,\n          signature: part.thoughtSignature,\n          thinking: part.text,\n        } as ThinkingContentBlock;\n      }\n\n      if (part.functionCall) {\n        return {\n          type: MessageContentType.ToolUse,\n          id: part.functionCall.id || uuid(),\n          name: part.functionCall.name,\n          input: part.functionCall.args,\n        } as ToolUseContentBlock;\n      }\n\n      this.logger.warn(`Unknown content type from Google: ${part}`);\n      return {\n        type: MessageContentType.Text,\n        text: JSON.stringify(part),\n      } as TextContentBlock;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/google/google.tools.ts",
    "content": "import { FunctionDeclaration, Type } from '@google/genai';\nimport { agentTools } from '../agent/agent.tools';\n\n/**\n * Converts JSON Schema type to Google Genai Type\n */\nfunction jsonSchemaTypeToGoogleType(type: string): Type {\n  switch (type) {\n    case 'string':\n      return Type.STRING;\n    case 'number':\n      return Type.NUMBER;\n    case 'integer':\n      return Type.INTEGER;\n    case 'boolean':\n      return Type.BOOLEAN;\n    case 'array':\n      return Type.ARRAY;\n    case 'object':\n      return Type.OBJECT;\n    default:\n      return Type.STRING;\n  }\n}\n\n/**\n * Converts JSON Schema to Google Genai parameter schema\n */\nfunction convertJsonSchemaToGoogleSchema(schema: any): any {\n  if (!schema) return {};\n\n  const result: any = {\n    type: jsonSchemaTypeToGoogleType(schema.type),\n  };\n\n  if (schema.description) {\n    result.description = schema.description;\n  }\n\n  // Only include enum if the property type is string; otherwise it is invalid for Google GenAI\n  if (schema.type === 'string' && schema.enum && Array.isArray(schema.enum)) {\n    result.enum = schema.enum;\n  }\n\n  if (schema.nullable) {\n    result.nullable = true;\n  }\n\n  if (schema.type === 'array' && schema.items) {\n    result.items = convertJsonSchemaToGoogleSchema(schema.items);\n  }\n\n  if (schema.type === 'object' && schema.properties) {\n    result.properties = {};\n    for (const [key, value] of Object.entries(schema.properties)) {\n      result.properties[key] = convertJsonSchemaToGoogleSchema(value);\n    }\n    if (schema.required) {\n      result.required = schema.required;\n    }\n  }\n\n  return result;\n}\n\n/**\n * Converts an agent tool definition to a Google FunctionDeclaration\n */\nfunction agentToolToGoogleTool(agentTool: any): FunctionDeclaration {\n  const parameters = convertJsonSchemaToGoogleSchema(agentTool.input_schema);\n\n  return {\n    name: agentTool.name,\n    description: agentTool.description,\n    parameters,\n  };\n}\n\n/**\n * Creates a mapped object of tools by name\n */\nconst toolMap = agentTools.reduce(\n  (acc, tool) => {\n    const googleTool = agentToolToGoogleTool(tool);\n    const camelCaseName = tool.name\n      .split('_')\n      .map((part, index) => {\n        if (index === 0) return part;\n        if (part === 'computer') return '';\n        return part.charAt(0).toUpperCase() + part.slice(1);\n      })\n      .join('')\n      .replace(/^computer/, '');\n\n    acc[camelCaseName + 'Tool'] = googleTool;\n    return acc;\n  },\n  {} as Record<string, FunctionDeclaration>,\n);\n\n// Export individual tools with proper names\nexport const moveMouseTool = toolMap.moveMouseTool;\nexport const traceMouseTool = toolMap.traceMouseTool;\nexport const clickMouseTool = toolMap.clickMouseTool;\nexport const pressMouseTool = toolMap.pressMouseTool;\nexport const dragMouseTool = toolMap.dragMouseTool;\nexport const scrollTool = toolMap.scrollTool;\nexport const typeKeysTool = toolMap.typeKeysTool;\nexport const pressKeysTool = toolMap.pressKeysTool;\nexport const typeTextTool = toolMap.typeTextTool;\nexport const pasteTextTool = toolMap.pasteTextTool;\nexport const waitTool = toolMap.waitTool;\nexport const screenshotTool = toolMap.screenshotTool;\nexport const cursorPositionTool = toolMap.cursorPositionTool;\nexport const setTaskStatusTool = toolMap.setTaskStatusTool;\nexport const createTaskTool = toolMap.createTaskTool;\nexport const applicationTool = toolMap.applicationTool;\n\n// Array of all tools\nexport const googleTools: FunctionDeclaration[] = agentTools.map(\n  agentToolToGoogleTool,\n);\n"
  },
  {
    "path": "packages/bytebot-agent/src/main.ts",
    "content": "import { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\nimport { webcrypto } from 'crypto';\nimport { json, urlencoded } from 'express';\n\n// Polyfill for crypto global (required by @nestjs/schedule)\nif (!globalThis.crypto) {\n  globalThis.crypto = webcrypto as any;\n}\n\nasync function bootstrap() {\n  console.log('Starting bytebot-agent application...');\n\n  try {\n    const app = await NestFactory.create(AppModule);\n\n    // Configure body parser with increased payload size limit (50MB)\n    app.use(json({ limit: '50mb' }));\n    app.use(urlencoded({ limit: '50mb', extended: true }));\n\n    // Enable CORS\n    app.enableCors({\n      origin: '*',\n      methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],\n    });\n\n    await app.listen(process.env.PORT ?? 9991);\n  } catch (error) {\n    console.error('Error starting application:', error);\n  }\n}\nbootstrap();\n"
  },
  {
    "path": "packages/bytebot-agent/src/messages/messages.module.ts",
    "content": "import { Module, forwardRef } from '@nestjs/common';\nimport { MessagesService } from './messages.service';\nimport { PrismaModule } from '../prisma/prisma.module';\nimport { TasksModule } from '../tasks/tasks.module';\n\n@Module({\n  imports: [PrismaModule, forwardRef(() => TasksModule)],\n  providers: [MessagesService],\n  exports: [MessagesService],\n})\nexport class MessagesModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/messages/messages.service.ts",
    "content": "import {\n  Injectable,\n  NotFoundException,\n  Inject,\n  forwardRef,\n} from '@nestjs/common';\nimport { PrismaService } from '../prisma/prisma.service';\nimport { Message, Role, Prisma } from '@prisma/client';\nimport {\n  MessageContentBlock,\n  isComputerToolUseContentBlock,\n  isToolResultContentBlock,\n  isUserActionContentBlock,\n} from '@bytebot/shared';\nimport { TasksGateway } from '../tasks/tasks.gateway';\n\n// Extended message type for processing\nexport interface ProcessedMessage extends Message {\n  take_over?: boolean;\n}\n\nexport interface GroupedMessages {\n  role: Role;\n  messages: ProcessedMessage[];\n  take_over?: boolean;\n}\n\n@Injectable()\nexport class MessagesService {\n  constructor(\n    private prisma: PrismaService,\n    @Inject(forwardRef(() => TasksGateway))\n    private readonly tasksGateway: TasksGateway,\n  ) {}\n\n  async create(data: {\n    content: MessageContentBlock[];\n    role: Role;\n    taskId: string;\n  }): Promise<Message> {\n    const message = await this.prisma.message.create({\n      data: {\n        content: data.content as Prisma.InputJsonValue,\n        role: data.role,\n        taskId: data.taskId,\n      },\n    });\n\n    this.tasksGateway.emitNewMessage(data.taskId, message);\n\n    return message;\n  }\n\n  async findEvery(taskId: string): Promise<Message[]> {\n    return this.prisma.message.findMany({\n      where: {\n        taskId,\n      },\n      orderBy: {\n        createdAt: 'asc',\n      },\n    });\n  }\n\n  async findAll(\n    taskId: string,\n    options?: {\n      limit?: number;\n      page?: number;\n    },\n  ): Promise<Message[]> {\n    const { limit = 10, page = 1 } = options || {};\n\n    // Calculate offset based on page and limit\n    const offset = (page - 1) * limit;\n\n    return this.prisma.message.findMany({\n      where: {\n        taskId,\n      },\n      orderBy: {\n        createdAt: 'asc',\n      },\n      take: limit,\n      skip: offset,\n    });\n  }\n\n  async findUnsummarized(taskId: string): Promise<Message[]> {\n    return this.prisma.message.findMany({\n      where: {\n        taskId,\n        // find messages that don't have a summaryId\n        summaryId: null,\n      },\n      orderBy: { createdAt: 'asc' },\n    });\n  }\n\n  async attachSummary(\n    taskId: string,\n    summaryId: string,\n    messageIds: string[],\n  ): Promise<void> {\n    if (messageIds.length === 0) {\n      return;\n    }\n\n    await this.prisma.message.updateMany({\n      where: { taskId, id: { in: messageIds } },\n      data: { summaryId },\n    });\n  }\n\n  /**\n   * Groups back-to-back messages from the same role and take_over status\n   */\n  private groupBackToBackMessages(\n    messages: ProcessedMessage[],\n  ): GroupedMessages[] {\n    const groupedConversation: GroupedMessages[] = [];\n    let currentGroup: GroupedMessages | null = null;\n\n    for (const message of messages) {\n      const role = message.role;\n      const isTakeOver = message.take_over || false;\n\n      // If this is the first message, role is different, or take_over status is different from the previous group\n      if (\n        !currentGroup ||\n        currentGroup.role !== role ||\n        currentGroup.take_over !== isTakeOver\n      ) {\n        // Save the previous group if it exists\n        if (currentGroup) {\n          groupedConversation.push(currentGroup);\n        }\n\n        // Start a new group\n        currentGroup = {\n          role: role,\n          messages: [message],\n          take_over: isTakeOver,\n        };\n      } else {\n        // Same role and take_over status as previous, merge the content\n        currentGroup.messages.push(message);\n      }\n    }\n\n    // Add the last group\n    if (currentGroup) {\n      groupedConversation.push(currentGroup);\n    }\n\n    return groupedConversation;\n  }\n\n  /**\n   * Filters and processes messages, adding take_over flags where appropriate\n   * Only text messages from the user should appear as user messages\n   * Computer tool use messages should be shown as assistant messages with take_over flag\n   */\n  private filterMessages(messages: Message[]): ProcessedMessage[] {\n    const filteredMessages: ProcessedMessage[] = [];\n\n    for (const message of messages) {\n      const processedMessage: ProcessedMessage = { ...message };\n      const contentBlocks = message.content as MessageContentBlock[];\n\n      // If the role is a user message and all the content blocks are tool result blocks or they are take over actions\n      if (message.role === Role.USER) {\n        if (contentBlocks.every((block) => isToolResultContentBlock(block))) {\n          // Pure tool results should be shown as assistant messages\n          processedMessage.role = Role.ASSISTANT;\n        } else if (\n          contentBlocks.every((block) => isUserActionContentBlock(block))\n        ) {\n          // Extract computer tool use (take over actions) from the user action content blocks and show them as assistant messages with take_over flag\n          processedMessage.content = contentBlocks\n            .flatMap((block) => {\n              return block.content;\n            })\n            .filter((block) => isComputerToolUseContentBlock(block));\n          processedMessage.role = Role.ASSISTANT;\n          processedMessage.take_over = true;\n        }\n        // If there are text blocks mixed with tool blocks, keep as user message\n        // Only pure text messages from user should remain as user messages\n      }\n\n      filteredMessages.push(processedMessage);\n    }\n\n    return filteredMessages;\n  }\n\n  /**\n   * Returns raw messages without any processing\n   */\n  async findRawMessages(\n    taskId: string,\n    options?: {\n      limit?: number;\n      page?: number;\n    },\n  ): Promise<Message[]> {\n    return this.findAll(taskId, options);\n  }\n\n  /**\n   * Returns processed and grouped messages for the chat UI\n   */\n  async findProcessedMessages(\n    taskId: string,\n    options?: {\n      limit?: number;\n      page?: number;\n    },\n  ): Promise<GroupedMessages[]> {\n    const messages = await this.findAll(taskId, options);\n    const filteredMessages = this.filterMessages(messages);\n    return this.groupBackToBackMessages(filteredMessages);\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/openai/openai.constants.ts",
    "content": "import { BytebotAgentModel } from 'src/agent/agent.types';\n\nexport const OPENAI_MODELS: BytebotAgentModel[] = [\n  {\n    provider: 'openai',\n    name: 'o3-2025-04-16',\n    title: 'o3',\n    contextWindow: 200000,\n  },\n  {\n    provider: 'openai',\n    name: 'gpt-4.1-2025-04-14',\n    title: 'GPT-4.1',\n    contextWindow: 1047576,\n  },\n];\n\nexport const DEFAULT_MODEL = OPENAI_MODELS[0];\n"
  },
  {
    "path": "packages/bytebot-agent/src/openai/openai.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { OpenAIService } from './openai.service';\n\n@Module({\n  imports: [ConfigModule],\n  providers: [OpenAIService],\n  exports: [OpenAIService],\n})\nexport class OpenAIModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/openai/openai.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport OpenAI, { APIUserAbortError } from 'openai';\nimport {\n  MessageContentBlock,\n  MessageContentType,\n  TextContentBlock,\n  ToolUseContentBlock,\n  ToolResultContentBlock,\n  ThinkingContentBlock,\n  isUserActionContentBlock,\n  isComputerToolUseContentBlock,\n  isImageContentBlock,\n} from '@bytebot/shared';\nimport { DEFAULT_MODEL } from './openai.constants';\nimport { Message, Role } from '@prisma/client';\nimport { openaiTools } from './openai.tools';\nimport {\n  BytebotAgentService,\n  BytebotAgentInterrupt,\n  BytebotAgentResponse,\n} from '../agent/agent.types';\n\n@Injectable()\nexport class OpenAIService implements BytebotAgentService {\n  private readonly openai: OpenAI;\n  private readonly logger = new Logger(OpenAIService.name);\n\n  constructor(private readonly configService: ConfigService) {\n    const apiKey = this.configService.get<string>('OPENAI_API_KEY');\n\n    if (!apiKey) {\n      this.logger.warn(\n        'OPENAI_API_KEY is not set. OpenAIService will not work properly.',\n      );\n    }\n\n    this.openai = new OpenAI({\n      apiKey: apiKey || 'dummy-key-for-initialization',\n    });\n  }\n\n  async generateMessage(\n    systemPrompt: string,\n    messages: Message[],\n    model: string = DEFAULT_MODEL.name,\n    useTools: boolean = true,\n    signal?: AbortSignal,\n  ): Promise<BytebotAgentResponse> {\n    const isReasoning = model.startsWith('o');\n    try {\n      const openaiMessages = this.formatMessagesForOpenAI(messages);\n\n      const maxTokens = 8192;\n      const response = await this.openai.responses.create(\n        {\n          model,\n          max_output_tokens: maxTokens,\n          input: openaiMessages,\n          instructions: systemPrompt,\n          tools: useTools ? openaiTools : [],\n          reasoning: isReasoning ? { effort: 'medium' } : null,\n          store: false,\n          include: isReasoning ? ['reasoning.encrypted_content'] : [],\n        },\n        { signal },\n      );\n\n      return {\n        contentBlocks: this.formatOpenAIResponse(response.output),\n        tokenUsage: {\n          inputTokens: response.usage?.input_tokens || 0,\n          outputTokens: response.usage?.output_tokens || 0,\n          totalTokens: response.usage?.total_tokens || 0,\n        },\n      };\n    } catch (error: any) {\n      console.log('error', error);\n      console.log('error name', error.name);\n\n      if (error instanceof APIUserAbortError) {\n        this.logger.log('OpenAI API call aborted');\n        throw new BytebotAgentInterrupt();\n      }\n      this.logger.error(\n        `Error sending message to OpenAI: ${error.message}`,\n        error.stack,\n      );\n      throw error;\n    }\n  }\n\n  private formatMessagesForOpenAI(\n    messages: Message[],\n  ): OpenAI.Responses.ResponseInputItem[] {\n    const openaiMessages: OpenAI.Responses.ResponseInputItem[] = [];\n\n    for (const message of messages) {\n      const messageContentBlocks = message.content as MessageContentBlock[];\n\n      if (\n        messageContentBlocks.every((block) => isUserActionContentBlock(block))\n      ) {\n        const userActionContentBlocks = messageContentBlocks.flatMap(\n          (block) => block.content,\n        );\n        for (const block of userActionContentBlocks) {\n          if (isComputerToolUseContentBlock(block)) {\n            openaiMessages.push({\n              type: 'message',\n              role: 'user',\n              content: [\n                {\n                  type: 'input_text',\n                  text: `User performed action: ${block.name}\\n${JSON.stringify(block.input, null, 2)}`,\n                },\n              ],\n            });\n          } else if (isImageContentBlock(block)) {\n            openaiMessages.push({\n              role: 'user',\n              type: 'message',\n              content: [\n                {\n                  type: 'input_image',\n                  detail: 'high',\n                  image_url: `data:${block.source.media_type};base64,${block.source.data}`,\n                },\n              ],\n            } as OpenAI.Responses.ResponseInputItem.Message);\n          }\n        }\n      } else {\n        // Convert content blocks to OpenAI format\n        for (const block of messageContentBlocks) {\n          switch (block.type) {\n            case MessageContentType.Text: {\n              if (message.role === Role.USER) {\n                openaiMessages.push({\n                  type: 'message',\n                  role: 'user',\n                  content: [\n                    {\n                      type: 'input_text',\n                      text: block.text,\n                    },\n                  ],\n                } as OpenAI.Responses.ResponseInputItem.Message);\n              } else {\n                openaiMessages.push({\n                  type: 'message',\n                  role: 'assistant',\n                  content: [\n                    {\n                      type: 'output_text',\n                      text: block.text,\n                    },\n                  ],\n                } as OpenAI.Responses.ResponseOutputMessage);\n              }\n              break;\n            }\n            case MessageContentType.ToolUse:\n              // For assistant messages with tool use, convert to function call\n              if (message.role === Role.ASSISTANT) {\n                const toolBlock = block as ToolUseContentBlock;\n                openaiMessages.push({\n                  type: 'function_call',\n                  call_id: toolBlock.id,\n                  name: toolBlock.name,\n                  arguments: JSON.stringify(toolBlock.input),\n                } as OpenAI.Responses.ResponseFunctionToolCall);\n              }\n              break;\n\n            case MessageContentType.Thinking: {\n              const thinkingBlock = block;\n              openaiMessages.push({\n                type: 'reasoning',\n                id: thinkingBlock.signature,\n                encrypted_content: thinkingBlock.thinking,\n                summary: [],\n              } as OpenAI.Responses.ResponseReasoningItem);\n              break;\n            }\n            case MessageContentType.ToolResult: {\n              // Handle tool results as function call outputs\n              const toolResult = block;\n              // Tool results should be added as separate items in the response\n\n              toolResult.content.forEach((content) => {\n                if (content.type === MessageContentType.Text) {\n                  openaiMessages.push({\n                    type: 'function_call_output',\n                    call_id: toolResult.tool_use_id,\n                    output: content.text,\n                  } as OpenAI.Responses.ResponseInputItem.FunctionCallOutput);\n                }\n\n                if (content.type === MessageContentType.Image) {\n                  openaiMessages.push({\n                    type: 'function_call_output',\n                    call_id: toolResult.tool_use_id,\n                    output: 'screenshot',\n                  } as OpenAI.Responses.ResponseInputItem.FunctionCallOutput);\n                  openaiMessages.push({\n                    role: 'user',\n                    type: 'message',\n                    content: [\n                      {\n                        type: 'input_image',\n                        detail: 'high',\n                        image_url: `data:${content.source.media_type};base64,${content.source.data}`,\n                      },\n                    ],\n                  } as OpenAI.Responses.ResponseInputItem.Message);\n                }\n              });\n              break;\n            }\n\n            default:\n              // Handle unknown content types as text\n              openaiMessages.push({\n                role: 'user',\n                type: 'message',\n                content: [\n                  {\n                    type: 'input_text',\n                    text: JSON.stringify(block),\n                  },\n                ],\n              } as OpenAI.Responses.ResponseInputItem.Message);\n          }\n        }\n      }\n    }\n\n    return openaiMessages;\n  }\n\n  private formatOpenAIResponse(\n    response: OpenAI.Responses.ResponseOutputItem[],\n  ): MessageContentBlock[] {\n    const contentBlocks: MessageContentBlock[] = [];\n\n    for (const item of response) {\n      // Check the type of the output item\n      switch (item.type) {\n        case 'message':\n          // Handle ResponseOutputMessage\n          const message = item;\n          for (const content of message.content) {\n            if ('text' in content) {\n              // ResponseOutputText\n              contentBlocks.push({\n                type: MessageContentType.Text,\n                text: content.text,\n              } as TextContentBlock);\n            } else if ('refusal' in content) {\n              // ResponseOutputRefusal\n              contentBlocks.push({\n                type: MessageContentType.Text,\n                text: `Refusal: ${content.refusal}`,\n              } as TextContentBlock);\n            }\n          }\n          break;\n\n        case 'function_call':\n          // Handle ResponseFunctionToolCall\n          const toolCall = item;\n          contentBlocks.push({\n            type: MessageContentType.ToolUse,\n            id: toolCall.call_id,\n            name: toolCall.name,\n            input: JSON.parse(toolCall.arguments),\n          } as ToolUseContentBlock);\n          break;\n\n        case 'file_search_call':\n        case 'web_search_call':\n        case 'computer_call':\n        case 'reasoning':\n          const reasoning = item as OpenAI.Responses.ResponseReasoningItem;\n          if (reasoning.encrypted_content) {\n            contentBlocks.push({\n              type: MessageContentType.Thinking,\n              thinking: reasoning.encrypted_content,\n              signature: reasoning.id,\n            } as ThinkingContentBlock);\n          }\n          break;\n        case 'image_generation_call':\n        case 'code_interpreter_call':\n        case 'local_shell_call':\n        case 'mcp_call':\n        case 'mcp_list_tools':\n        case 'mcp_approval_request':\n          // Handle other tool types as text for now\n          this.logger.warn(\n            `Unsupported response output item type: ${item.type}`,\n          );\n          contentBlocks.push({\n            type: MessageContentType.Text,\n            text: JSON.stringify(item),\n          } as TextContentBlock);\n          break;\n\n        default:\n          // Handle unknown types\n          this.logger.warn(\n            `Unknown response output item type: ${JSON.stringify(item)}`,\n          );\n          contentBlocks.push({\n            type: MessageContentType.Text,\n            text: JSON.stringify(item),\n          } as TextContentBlock);\n      }\n    }\n\n    return contentBlocks;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/openai/openai.tools.ts",
    "content": "import OpenAI from 'openai';\nimport { agentTools } from '../agent/agent.tools';\n\nfunction agentToolToOpenAITool(agentTool: any): OpenAI.Responses.FunctionTool {\n  return {\n    type: 'function',\n    name: agentTool.name,\n    description: agentTool.description,\n    parameters: agentTool.input_schema,\n  } as OpenAI.Responses.FunctionTool;\n}\n\n/**\n * Creates a mapped object of tools by name\n */\nconst toolMap = agentTools.reduce(\n  (acc, tool) => {\n    const anthropicTool = agentToolToOpenAITool(tool);\n    const camelCaseName = tool.name\n      .split('_')\n      .map((part, index) => {\n        if (index === 0) return part;\n        if (part === 'computer') return '';\n        return part.charAt(0).toUpperCase() + part.slice(1);\n      })\n      .join('')\n      .replace(/^computer/, '');\n\n    acc[camelCaseName + 'Tool'] = anthropicTool;\n    return acc;\n  },\n  {} as Record<string, OpenAI.Responses.FunctionTool>,\n);\n\n// Export individual tools with proper names\nexport const moveMouseTool = toolMap.moveMouseTool;\nexport const traceMouseTool = toolMap.traceMouseTool;\nexport const clickMouseTool = toolMap.clickMouseTool;\nexport const pressMouseTool = toolMap.pressMouseTool;\nexport const dragMouseTool = toolMap.dragMouseTool;\nexport const scrollTool = toolMap.scrollTool;\nexport const typeKeysTool = toolMap.typeKeysTool;\nexport const pressKeysTool = toolMap.pressKeysTool;\nexport const typeTextTool = toolMap.typeTextTool;\nexport const pasteTextTool = toolMap.pasteTextTool;\nexport const waitTool = toolMap.waitTool;\nexport const screenshotTool = toolMap.screenshotTool;\nexport const cursorPositionTool = toolMap.cursorPositionTool;\nexport const setTaskStatusTool = toolMap.setTaskStatusTool;\nexport const createTaskTool = toolMap.createTaskTool;\nexport const applicationTool = toolMap.applicationTool;\n\n// Array of all tools\nexport const openaiTools: OpenAI.Responses.FunctionTool[] = agentTools.map(\n  agentToolToOpenAITool,\n);\n"
  },
  {
    "path": "packages/bytebot-agent/src/prisma/prisma.module.ts",
    "content": "import { Global, Module } from '@nestjs/common';\n\nimport { PrismaService } from './prisma.service';\n\n@Global()\n@Module({\n  providers: [PrismaService],\n  exports: [PrismaService],\n})\nexport class PrismaModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/prisma/prisma.service.ts",
    "content": "import { Injectable, OnModuleInit } from '@nestjs/common';\nimport { PrismaClient } from '@prisma/client';\n\n@Injectable()\nexport class PrismaService extends PrismaClient implements OnModuleInit {\n  constructor() {\n    super();\n  }\n\n  async onModuleInit() {\n    await this.$connect();\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/proxy/proxy.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '@nestjs/config';\nimport { ProxyService } from './proxy.service';\n\n@Module({\n  imports: [ConfigModule],\n  providers: [ProxyService],\n  exports: [ProxyService],\n})\nexport class ProxyModule {}"
  },
  {
    "path": "packages/bytebot-agent/src/proxy/proxy.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport OpenAI, { APIUserAbortError } from 'openai';\nimport {\n  ChatCompletionMessageParam,\n  ChatCompletionContentPart,\n} from 'openai/resources/chat/completions';\nimport {\n  MessageContentBlock,\n  MessageContentType,\n  TextContentBlock,\n  ToolUseContentBlock,\n  ToolResultContentBlock,\n  ImageContentBlock,\n  isUserActionContentBlock,\n  isComputerToolUseContentBlock,\n  isImageContentBlock,\n  ThinkingContentBlock,\n} from '@bytebot/shared';\nimport { Message, Role } from '@prisma/client';\nimport { proxyTools } from './proxy.tools';\nimport {\n  BytebotAgentService,\n  BytebotAgentInterrupt,\n  BytebotAgentResponse,\n} from '../agent/agent.types';\n\n@Injectable()\nexport class ProxyService implements BytebotAgentService {\n  private readonly openai: OpenAI;\n  private readonly logger = new Logger(ProxyService.name);\n\n  constructor(private readonly configService: ConfigService) {\n    const proxyUrl = this.configService.get<string>('BYTEBOT_LLM_PROXY_URL');\n\n    if (!proxyUrl) {\n      this.logger.warn(\n        'BYTEBOT_LLM_PROXY_URL is not set. ProxyService will not work properly.',\n      );\n    }\n\n    // Initialize OpenAI client with proxy configuration\n    this.openai = new OpenAI({\n      apiKey: 'dummy-key-for-proxy',\n      baseURL: proxyUrl,\n    });\n  }\n\n  /**\n   * Main method to generate messages using the Chat Completions API\n   */\n  async generateMessage(\n    systemPrompt: string,\n    messages: Message[],\n    model: string,\n    useTools: boolean = true,\n    signal?: AbortSignal,\n  ): Promise<BytebotAgentResponse> {\n    // Convert messages to Chat Completion format\n    const chatMessages = this.formatMessagesForChatCompletion(\n      systemPrompt,\n      messages,\n    );\n    try {\n      // Prepare the Chat Completion request\n      const completionRequest: OpenAI.Chat.ChatCompletionCreateParams = {\n        model,\n        messages: chatMessages,\n        max_tokens: 8192,\n        ...(useTools && { tools: proxyTools }),\n        reasoning_effort: 'high',\n      };\n\n      // Make the API call\n      const completion = await this.openai.chat.completions.create(\n        completionRequest,\n        { signal },\n      );\n\n      // Process the response\n      const choice = completion.choices[0];\n      if (!choice || !choice.message) {\n        throw new Error('No valid response from Chat Completion API');\n      }\n\n      // Convert response to MessageContentBlocks\n      const contentBlocks = this.formatChatCompletionResponse(choice.message);\n\n      return {\n        contentBlocks,\n        tokenUsage: {\n          inputTokens: completion.usage?.prompt_tokens || 0,\n          outputTokens: completion.usage?.completion_tokens || 0,\n          totalTokens: completion.usage?.total_tokens || 0,\n        },\n      };\n    } catch (error: any) {\n      if (error instanceof APIUserAbortError) {\n        this.logger.log('Chat Completion API call aborted');\n        throw new BytebotAgentInterrupt();\n      }\n\n      this.logger.error(\n        `Error sending message to proxy: ${error.message}`,\n        error.stack,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Convert Bytebot messages to Chat Completion format\n   */\n  private formatMessagesForChatCompletion(\n    systemPrompt: string,\n    messages: Message[],\n  ): ChatCompletionMessageParam[] {\n    const chatMessages: ChatCompletionMessageParam[] = [];\n\n    // Add system message\n    chatMessages.push({\n      role: 'system',\n      content: systemPrompt,\n    });\n\n    // Process each message\n    for (const message of messages) {\n      const messageContentBlocks = message.content as MessageContentBlock[];\n\n      // Handle user actions specially\n      if (\n        messageContentBlocks.every((block) => isUserActionContentBlock(block))\n      ) {\n        const userActionBlocks = messageContentBlocks.flatMap(\n          (block) => block.content,\n        );\n\n        for (const block of userActionBlocks) {\n          if (isComputerToolUseContentBlock(block)) {\n            chatMessages.push({\n              role: 'user',\n              content: `User performed action: ${block.name}\\n${JSON.stringify(\n                block.input,\n                null,\n                2,\n              )}`,\n            });\n          } else if (isImageContentBlock(block)) {\n            chatMessages.push({\n              role: 'user',\n              content: [\n                {\n                  type: 'image_url',\n                  image_url: {\n                    url: `data:${block.source.media_type};base64,${block.source.data}`,\n                    detail: 'high',\n                  },\n                },\n              ],\n            });\n          }\n        }\n      } else {\n        for (const block of messageContentBlocks) {\n          switch (block.type) {\n            case MessageContentType.Text: {\n              chatMessages.push({\n                role: message.role === Role.USER ? 'user' : 'assistant',\n                content: block.text,\n              });\n              break;\n            }\n            case MessageContentType.Image: {\n              const imageBlock = block as ImageContentBlock;\n              chatMessages.push({\n                role: 'user',\n                content: [\n                  {\n                    type: 'image_url',\n                    image_url: {\n                      url: `data:${imageBlock.source.media_type};base64,${imageBlock.source.data}`,\n                      detail: 'high',\n                    },\n                  },\n                ],\n              });\n              break;\n            }\n            case MessageContentType.ToolUse: {\n              const toolBlock = block as ToolUseContentBlock;\n              chatMessages.push({\n                role: 'assistant',\n                tool_calls: [\n                  {\n                    id: toolBlock.id,\n                    type: 'function',\n                    function: {\n                      name: toolBlock.name,\n                      arguments: JSON.stringify(toolBlock.input),\n                    },\n                  },\n                ],\n              });\n              break;\n            }\n            case MessageContentType.Thinking: {\n              const thinkingBlock = block as ThinkingContentBlock;\n              const message: ChatCompletionMessageParam = {\n                role: 'assistant',\n                content: null,\n              };\n              message['reasoning_content'] = thinkingBlock.thinking;\n              chatMessages.push(message);\n              break;\n            }\n            case MessageContentType.ToolResult: {\n              const toolResultBlock = block as ToolResultContentBlock;\n\n              if (\n                toolResultBlock.content.every(\n                  (content) => content.type === MessageContentType.Image,\n                )\n              ) {\n                chatMessages.push({\n                  role: 'tool',\n                  tool_call_id: toolResultBlock.tool_use_id,\n                  content: 'screenshot',\n                });\n              }\n\n              toolResultBlock.content.forEach((content) => {\n                if (content.type === MessageContentType.Text) {\n                  chatMessages.push({\n                    role: 'tool',\n                    tool_call_id: toolResultBlock.tool_use_id,\n                    content: content.text,\n                  });\n                }\n\n                if (content.type === MessageContentType.Image) {\n                  chatMessages.push({\n                    role: 'user',\n                    content: [\n                      {\n                        type: 'text',\n                        text: 'Screenshot',\n                      },\n                      {\n                        type: 'image_url',\n                        image_url: {\n                          url: `data:${content.source.media_type};base64,${content.source.data}`,\n                          detail: 'high',\n                        },\n                      },\n                    ],\n                  });\n                }\n              });\n              break;\n            }\n          }\n        }\n      }\n    }\n\n    return chatMessages;\n  }\n\n  /**\n   * Convert Chat Completion response to MessageContentBlocks\n   */\n  private formatChatCompletionResponse(\n    message: OpenAI.Chat.ChatCompletionMessage,\n  ): MessageContentBlock[] {\n    const contentBlocks: MessageContentBlock[] = [];\n\n    // Handle text content\n    if (message.content) {\n      contentBlocks.push({\n        type: MessageContentType.Text,\n        text: message.content,\n      } as TextContentBlock);\n    }\n\n    if (message['reasoning_content']) {\n      contentBlocks.push({\n        type: MessageContentType.Thinking,\n        thinking: message['reasoning_content'],\n        signature: message['reasoning_content'],\n      } as ThinkingContentBlock);\n    }\n\n    // Handle tool calls\n    if (message.tool_calls && message.tool_calls.length > 0) {\n      for (const toolCall of message.tool_calls) {\n        if (toolCall.type === 'function') {\n          let parsedInput = {};\n          try {\n            parsedInput = JSON.parse(toolCall.function.arguments || '{}');\n          } catch (e) {\n            this.logger.warn(\n              `Failed to parse tool call arguments: ${toolCall.function.arguments}`,\n            );\n            parsedInput = {};\n          }\n\n          contentBlocks.push({\n            type: MessageContentType.ToolUse,\n            id: toolCall.id,\n            name: toolCall.function.name,\n            input: parsedInput,\n          } as ToolUseContentBlock);\n        }\n      }\n    }\n\n    // Handle refusal\n    if (message.refusal) {\n      contentBlocks.push({\n        type: MessageContentType.Text,\n        text: `Refusal: ${message.refusal}`,\n      } as TextContentBlock);\n    }\n\n    return contentBlocks;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/proxy/proxy.tools.ts",
    "content": "import { ChatCompletionTool } from 'openai/resources';\nimport { agentTools } from '../agent/agent.tools';\n\n/**\n * Converts an agent tool definition to OpenAI Chat Completion tool format\n */\nfunction agentToolToChatCompletionTool(agentTool: any): ChatCompletionTool {\n  return {\n    type: 'function',\n    function: {\n      name: agentTool.name,\n      description: agentTool.description,\n      parameters: agentTool.input_schema,\n    },\n  };\n}\n\n/**\n * Convert tool name from snake_case to camelCase\n */\nfunction convertToCamelCase(name: string): string {\n  return name\n    .split('_')\n    .map((part, index) => {\n      if (index === 0) return part;\n      if (part === 'computer') return '';\n      return part.charAt(0).toUpperCase() + part.slice(1);\n    })\n    .join('')\n    .replace(/^computer/, '');\n}\n\n/**\n * All tools converted to Chat Completion format\n */\nexport const proxyTools: ChatCompletionTool[] = agentTools.map((tool) =>\n  agentToolToChatCompletionTool(tool),\n);\n\n/**\n * Individual tool exports for selective usage\n */\nconst toolMap = agentTools.reduce(\n  (acc, tool) => {\n    const chatCompletionTool = agentToolToChatCompletionTool(tool);\n    const camelCaseName = convertToCamelCase(tool.name);\n    acc[camelCaseName + 'Tool'] = chatCompletionTool;\n    return acc;\n  },\n  {} as Record<string, ChatCompletionTool>,\n);\n\n// Export individual tools with proper names\nexport const moveMouseTool = toolMap.moveMouseTool;\nexport const traceMouseTool = toolMap.traceMouseTool;\nexport const clickMouseTool = toolMap.clickMouseTool;\nexport const pressMouseTool = toolMap.pressMouseTool;\nexport const dragMouseTool = toolMap.dragMouseTool;\nexport const scrollTool = toolMap.scrollTool;\nexport const typeKeysTool = toolMap.typeKeysTool;\nexport const pressKeysTool = toolMap.pressKeysTool;\nexport const typeTextTool = toolMap.typeTextTool;\nexport const pasteTextTool = toolMap.pasteTextTool;\nexport const waitTool = toolMap.waitTool;\nexport const screenshotTool = toolMap.screenshotTool;\nexport const cursorPositionTool = toolMap.cursorPositionTool;\nexport const setTaskStatusTool = toolMap.setTaskStatusTool;\nexport const createTaskTool = toolMap.createTaskTool;\nexport const applicationTool = toolMap.applicationTool;"
  },
  {
    "path": "packages/bytebot-agent/src/summaries/summaries.modue.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { PrismaModule } from '../prisma/prisma.module';\nimport { SummariesService } from './summaries.service';\n\n@Module({\n  imports: [PrismaModule],\n  providers: [SummariesService],\n  exports: [SummariesService],\n})\nexport class SummariesModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/summaries/summaries.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { PrismaService } from '../prisma/prisma.service';\nimport { Summary } from '@prisma/client';\n\n@Injectable()\nexport class SummariesService {\n  constructor(private prisma: PrismaService) {}\n\n  async create(data: {\n    taskId: string;\n    content: string;\n    parentId?: string;\n  }): Promise<Summary> {\n    return this.prisma.summary.create({\n      data: {\n        taskId: data.taskId,\n        content: data.content,\n        ...(data.parentId ? { parentId: data.parentId } : {}),\n      },\n    });\n  }\n\n  async findLatest(taskId: string): Promise<Summary | null> {\n    return this.prisma.summary.findFirst({\n      where: { taskId },\n      orderBy: { createdAt: 'desc' },\n    });\n  }\n\n  async findAll(taskId: string): Promise<Summary[]> {\n    return this.prisma.summary.findMany({\n      where: { taskId },\n      orderBy: { createdAt: 'asc' },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/dto/add-task-message.dto.ts",
    "content": "import { IsNotEmpty, IsString } from 'class-validator';\n\nexport class AddTaskMessageDto {\n  @IsNotEmpty()\n  @IsString()\n  message: string;\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/dto/create-task.dto.ts",
    "content": "import {\n  IsArray,\n  IsDate,\n  IsNotEmpty,\n  IsNumber,\n  IsOptional,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { Role, TaskPriority, TaskType } from '@prisma/client';\n\nexport class TaskFileDto {\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  @IsNotEmpty()\n  @IsString()\n  base64: string;\n\n  @IsNotEmpty()\n  @IsString()\n  type: string;\n\n  @IsNotEmpty()\n  @IsNumber()\n  size: number;\n}\n\nexport class CreateTaskDto {\n  @IsNotEmpty()\n  @IsString()\n  description: string;\n\n  @IsOptional()\n  @IsString()\n  type?: TaskType;\n\n  @IsOptional()\n  @IsDate()\n  scheduledFor?: Date;\n\n  @IsOptional()\n  @IsString()\n  priority?: TaskPriority;\n\n  @IsOptional()\n  @IsString()\n  createdBy?: Role;\n\n  @IsOptional()\n  model?: any;\n\n  @IsOptional()\n  @IsArray()\n  @ValidateNested({ each: true })\n  @Type(() => TaskFileDto)\n  files?: TaskFileDto[];\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/dto/update-task.dto.ts",
    "content": "import { IsEnum, IsOptional } from 'class-validator';\nimport { TaskPriority, TaskStatus } from '@prisma/client';\n\nexport class UpdateTaskDto {\n  @IsOptional()\n  @IsEnum(TaskStatus)\n  status?: TaskStatus;\n\n  @IsOptional()\n  @IsEnum(TaskPriority)\n  priority?: TaskPriority;\n\n  @IsOptional()\n  queuedAt?: Date;\n\n  @IsOptional()\n  executedAt?: Date;\n\n  @IsOptional()\n  completedAt?: Date;\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/tasks.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  Post,\n  Body,\n  Param,\n  Delete,\n  HttpStatus,\n  HttpCode,\n  Query,\n  HttpException,\n} from '@nestjs/common';\nimport { TasksService } from './tasks.service';\nimport { CreateTaskDto } from './dto/create-task.dto';\nimport { Message, Task } from '@prisma/client';\nimport { AddTaskMessageDto } from './dto/add-task-message.dto';\nimport { MessagesService } from '../messages/messages.service';\nimport { ANTHROPIC_MODELS } from '../anthropic/anthropic.constants';\nimport { OPENAI_MODELS } from '../openai/openai.constants';\nimport { GOOGLE_MODELS } from '../google/google.constants';\nimport { BytebotAgentModel } from 'src/agent/agent.types';\n\nconst geminiApiKey = process.env.GEMINI_API_KEY;\nconst anthropicApiKey = process.env.ANTHROPIC_API_KEY;\nconst openaiApiKey = process.env.OPENAI_API_KEY;\n\nconst proxyUrl = process.env.BYTEBOT_LLM_PROXY_URL;\n\nconst models = [\n  ...(anthropicApiKey ? ANTHROPIC_MODELS : []),\n  ...(openaiApiKey ? OPENAI_MODELS : []),\n  ...(geminiApiKey ? GOOGLE_MODELS : []),\n];\n\n@Controller('tasks')\nexport class TasksController {\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly messagesService: MessagesService,\n  ) {}\n\n  @Post()\n  @HttpCode(HttpStatus.CREATED)\n  async create(@Body() createTaskDto: CreateTaskDto): Promise<Task> {\n    return this.tasksService.create(createTaskDto);\n  }\n\n  @Get()\n  async findAll(\n    @Query('page') page?: string,\n    @Query('limit') limit?: string,\n    @Query('status') status?: string,\n    @Query('statuses') statuses?: string,\n  ): Promise<{ tasks: Task[]; total: number; totalPages: number }> {\n    const pageNum = page ? parseInt(page, 10) : 1;\n    const limitNum = limit ? parseInt(limit, 10) : 10;\n\n    // Handle both single status and multiple statuses\n    let statusFilter: string[] | undefined;\n    if (statuses) {\n      statusFilter = statuses.split(',');\n    } else if (status) {\n      statusFilter = [status];\n    }\n\n    return this.tasksService.findAll(pageNum, limitNum, statusFilter);\n  }\n\n  @Get('models')\n  async getModels() {\n    if (proxyUrl) {\n      try {\n        const response = await fetch(`${proxyUrl}/model/info`, {\n          method: 'GET',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n        });\n\n        if (!response.ok) {\n          throw new HttpException(\n            `Failed to fetch models from proxy: ${response.statusText}`,\n            HttpStatus.BAD_GATEWAY,\n          );\n        }\n\n        const proxyModels = await response.json();\n\n        // Map proxy response to BytebotAgentModel format\n        const models: BytebotAgentModel[] = proxyModels.data.map(\n          (model: any) => ({\n            provider: 'proxy',\n            name: model.litellm_params.model,\n            title: model.model_name,\n            contextWindow: 128000,\n          }),\n        );\n\n        return models;\n      } catch (error) {\n        if (error instanceof HttpException) {\n          throw error;\n        }\n        throw new HttpException(\n          `Error fetching models: ${error.message}`,\n          HttpStatus.INTERNAL_SERVER_ERROR,\n        );\n      }\n    }\n    return models;\n  }\n\n  @Get(':id')\n  async findById(@Param('id') id: string): Promise<Task> {\n    return this.tasksService.findById(id);\n  }\n\n  @Get(':id/messages')\n  async taskMessages(\n    @Param('id') taskId: string,\n    @Query('limit') limit?: string,\n    @Query('page') page?: string,\n  ): Promise<Message[]> {\n    const options = {\n      limit: limit ? parseInt(limit, 10) : undefined,\n      page: page ? parseInt(page, 10) : undefined,\n    };\n\n    const messages = await this.messagesService.findAll(taskId, options);\n    return messages;\n  }\n\n  @Post(':id/messages')\n  @HttpCode(HttpStatus.CREATED)\n  async addTaskMessage(\n    @Param('id') taskId: string,\n    @Body() guideTaskDto: AddTaskMessageDto,\n  ): Promise<Task> {\n    return this.tasksService.addTaskMessage(taskId, guideTaskDto);\n  }\n\n  @Get(':id/messages/raw')\n  async taskRawMessages(\n    @Param('id') taskId: string,\n    @Query('limit') limit?: string,\n    @Query('page') page?: string,\n  ): Promise<Message[]> {\n    const options = {\n      limit: limit ? parseInt(limit, 10) : undefined,\n      page: page ? parseInt(page, 10) : undefined,\n    };\n\n    return this.messagesService.findRawMessages(taskId, options);\n  }\n\n  @Get(':id/messages/processed')\n  async taskProcessedMessages(\n    @Param('id') taskId: string,\n    @Query('limit') limit?: string,\n    @Query('page') page?: string,\n  ) {\n    const options = {\n      limit: limit ? parseInt(limit, 10) : undefined,\n      page: page ? parseInt(page, 10) : undefined,\n    };\n\n    return this.messagesService.findProcessedMessages(taskId, options);\n  }\n\n  @Delete(':id')\n  @HttpCode(HttpStatus.NO_CONTENT)\n  async delete(@Param('id') id: string): Promise<void> {\n    await this.tasksService.delete(id);\n  }\n\n  @Post(':id/takeover')\n  @HttpCode(HttpStatus.OK)\n  async takeOver(@Param('id') taskId: string): Promise<Task> {\n    return this.tasksService.takeOver(taskId);\n  }\n\n  @Post(':id/resume')\n  @HttpCode(HttpStatus.OK)\n  async resume(@Param('id') taskId: string): Promise<Task> {\n    return this.tasksService.resume(taskId);\n  }\n\n  @Post(':id/cancel')\n  @HttpCode(HttpStatus.OK)\n  async cancel(@Param('id') taskId: string): Promise<Task> {\n    return this.tasksService.cancel(taskId);\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/tasks.gateway.ts",
    "content": "import {\n  WebSocketGateway,\n  WebSocketServer,\n  SubscribeMessage,\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n} from '@nestjs/websockets';\nimport { Server, Socket } from 'socket.io';\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\n@WebSocketGateway({\n  cors: {\n    origin: '*',\n    methods: ['GET', 'POST'],\n  },\n})\nexport class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect {\n  @WebSocketServer()\n  server: Server;\n\n  handleConnection(client: Socket) {\n    console.log(`Client connected: ${client.id}`);\n  }\n\n  handleDisconnect(client: Socket) {\n    console.log(`Client disconnected: ${client.id}`);\n  }\n\n  @SubscribeMessage('join_task')\n  handleJoinTask(client: Socket, taskId: string) {\n    client.join(`task_${taskId}`);\n    console.log(`Client ${client.id} joined task ${taskId}`);\n  }\n\n  @SubscribeMessage('leave_task')\n  handleLeaveTask(client: Socket, taskId: string) {\n    client.leave(`task_${taskId}`);\n    console.log(`Client ${client.id} left task ${taskId}`);\n  }\n\n  emitTaskUpdate(taskId: string, task: any) {\n    this.server.to(`task_${taskId}`).emit('task_updated', task);\n  }\n\n  emitNewMessage(taskId: string, message: any) {\n    this.server.to(`task_${taskId}`).emit('new_message', message);\n  }\n\n  emitTaskCreated(task: any) {\n    this.server.emit('task_created', task);\n  }\n\n  emitTaskDeleted(taskId: string) {\n    this.server.emit('task_deleted', taskId);\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/tasks.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TasksController } from './tasks.controller';\nimport { TasksService } from './tasks.service';\nimport { TasksGateway } from './tasks.gateway';\nimport { PrismaModule } from '../prisma/prisma.module';\nimport { MessagesModule } from '../messages/messages.module';\n\n@Module({\n  imports: [PrismaModule, MessagesModule],\n  controllers: [TasksController],\n  providers: [TasksService, TasksGateway],\n  exports: [TasksService, TasksGateway],\n})\nexport class TasksModule {}\n"
  },
  {
    "path": "packages/bytebot-agent/src/tasks/tasks.service.ts",
    "content": "import {\n  Injectable,\n  NotFoundException,\n  Logger,\n  BadRequestException,\n  Inject,\n  forwardRef,\n} from '@nestjs/common';\nimport { PrismaService } from '../prisma/prisma.service';\nimport { CreateTaskDto } from './dto/create-task.dto';\nimport { UpdateTaskDto } from './dto/update-task.dto';\nimport {\n  Task,\n  Role,\n  Prisma,\n  TaskStatus,\n  TaskType,\n  TaskPriority,\n  File,\n} from '@prisma/client';\nimport { AddTaskMessageDto } from './dto/add-task-message.dto';\nimport { TasksGateway } from './tasks.gateway';\nimport { ConfigService } from '@nestjs/config';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\n\n@Injectable()\nexport class TasksService {\n  private readonly logger = new Logger(TasksService.name);\n\n  constructor(\n    readonly prisma: PrismaService,\n    @Inject(forwardRef(() => TasksGateway))\n    private readonly tasksGateway: TasksGateway,\n    private readonly configService: ConfigService,\n    private readonly eventEmitter: EventEmitter2,\n  ) {\n    this.logger.log('TasksService initialized');\n  }\n\n  async create(createTaskDto: CreateTaskDto): Promise<Task> {\n    this.logger.log(\n      `Creating new task with description: ${createTaskDto.description}`,\n    );\n\n    const task = await this.prisma.$transaction(async (prisma) => {\n      // Create the task first\n      this.logger.debug('Creating task record in database');\n      const task = await prisma.task.create({\n        data: {\n          description: createTaskDto.description,\n          type: createTaskDto.type || TaskType.IMMEDIATE,\n          priority: createTaskDto.priority || TaskPriority.MEDIUM,\n          status: TaskStatus.PENDING,\n          createdBy: createTaskDto.createdBy || Role.USER,\n          model: createTaskDto.model,\n          ...(createTaskDto.scheduledFor\n            ? { scheduledFor: createTaskDto.scheduledFor }\n            : {}),\n        },\n      });\n      this.logger.log(`Task created successfully with ID: ${task.id}`);\n\n      let filesDescription = '';\n\n      // Save files if provided\n      if (createTaskDto.files && createTaskDto.files.length > 0) {\n        this.logger.debug(\n          `Saving ${createTaskDto.files.length} file(s) for task ID: ${task.id}`,\n        );\n        filesDescription += `\\n`;\n\n        const filePromises = createTaskDto.files.map((file) => {\n          // Extract base64 data without the data URL prefix\n          const base64Data = file.base64.includes('base64,')\n            ? file.base64.split('base64,')[1]\n            : file.base64;\n\n          filesDescription += `\\nFile ${file.name} written to desktop.`;\n\n          return prisma.file.create({\n            data: {\n              name: file.name,\n              type: file.type || 'application/octet-stream',\n              size: file.size,\n              data: base64Data,\n              taskId: task.id,\n            },\n          });\n        });\n\n        await Promise.all(filePromises);\n        this.logger.debug(`Files saved successfully for task ID: ${task.id}`);\n      }\n\n      // Create the initial system message\n      this.logger.debug(`Creating initial message for task ID: ${task.id}`);\n      await prisma.message.create({\n        data: {\n          content: [\n            {\n              type: 'text',\n              text: `${createTaskDto.description} ${filesDescription}`,\n            },\n          ] as Prisma.InputJsonValue,\n          role: Role.USER,\n          taskId: task.id,\n        },\n      });\n      this.logger.debug(`Initial message created for task ID: ${task.id}`);\n\n      return task;\n    });\n\n    this.tasksGateway.emitTaskCreated(task);\n\n    return task;\n  }\n\n  async findScheduledTasks(): Promise<Task[]> {\n    return this.prisma.task.findMany({\n      where: {\n        scheduledFor: {\n          not: null,\n        },\n        queuedAt: null,\n      },\n      orderBy: [{ scheduledFor: 'asc' }],\n    });\n  }\n\n  async findNextTask(): Promise<(Task & { files: File[] }) | null> {\n    const task = await this.prisma.task.findFirst({\n      where: {\n        status: {\n          in: [TaskStatus.RUNNING, TaskStatus.PENDING],\n        },\n      },\n      orderBy: [\n        { executedAt: 'asc' },\n        { priority: 'desc' },\n        { queuedAt: 'asc' },\n        { createdAt: 'asc' },\n      ],\n      include: {\n        files: true,\n      },\n    });\n\n    if (task) {\n      this.logger.log(\n        `Found existing task with ID: ${task.id}, and status ${task.status}. Resuming.`,\n      );\n    }\n\n    return task;\n  }\n\n  async findAll(\n    page = 1,\n    limit = 10,\n    statuses?: string[],\n  ): Promise<{ tasks: Task[]; total: number; totalPages: number }> {\n    this.logger.log(\n      `Retrieving tasks - page: ${page}, limit: ${limit}, statuses: ${statuses?.join(',')}`,\n    );\n\n    const skip = (page - 1) * limit;\n\n    const whereClause: Prisma.TaskWhereInput =\n      statuses && statuses.length > 0\n        ? { status: { in: statuses as TaskStatus[] } }\n        : {};\n\n    const [tasks, total] = await Promise.all([\n      this.prisma.task.findMany({\n        where: whereClause,\n        orderBy: {\n          createdAt: 'desc',\n        },\n        skip,\n        take: limit,\n      }),\n      this.prisma.task.count({ where: whereClause }),\n    ]);\n\n    const totalPages = Math.ceil(total / limit);\n    this.logger.debug(`Retrieved ${tasks.length} tasks out of ${total} total`);\n\n    return { tasks, total, totalPages };\n  }\n\n  async findById(id: string): Promise<Task> {\n    this.logger.log(`Retrieving task by ID: ${id}`);\n\n    try {\n      const task = await this.prisma.task.findUnique({\n        where: { id },\n        include: {\n          files: true,\n        },\n      });\n\n      if (!task) {\n        this.logger.warn(`Task with ID: ${id} not found`);\n        throw new NotFoundException(`Task with ID ${id} not found`);\n      }\n\n      this.logger.debug(`Retrieved task with ID: ${id}`);\n      return task;\n    } catch (error: any) {\n      this.logger.error(`Error retrieving task ID: ${id} - ${error.message}`);\n      this.logger.error(error.stack);\n      throw error;\n    }\n  }\n\n  async update(id: string, updateTaskDto: UpdateTaskDto): Promise<Task> {\n    this.logger.log(`Updating task with ID: ${id}`);\n    this.logger.debug(`Update data: ${JSON.stringify(updateTaskDto)}`);\n\n    const existingTask = await this.findById(id);\n\n    if (!existingTask) {\n      this.logger.warn(`Task with ID: ${id} not found for update`);\n      throw new NotFoundException(`Task with ID ${id} not found`);\n    }\n\n    let updatedTask = await this.prisma.task.update({\n      where: { id },\n      data: updateTaskDto,\n    });\n\n    if (updateTaskDto.status === TaskStatus.COMPLETED) {\n      this.eventEmitter.emit('task.completed', { taskId: id });\n    } else if (updateTaskDto.status === TaskStatus.NEEDS_HELP) {\n      updatedTask = await this.takeOver(id);\n    } else if (updateTaskDto.status === TaskStatus.FAILED) {\n      this.eventEmitter.emit('task.failed', { taskId: id });\n    }\n\n    this.logger.log(`Successfully updated task ID: ${id}`);\n    this.logger.debug(`Updated task: ${JSON.stringify(updatedTask)}`);\n\n    this.tasksGateway.emitTaskUpdate(id, updatedTask);\n\n    return updatedTask;\n  }\n\n  async delete(id: string): Promise<Task> {\n    this.logger.log(`Deleting task with ID: ${id}`);\n\n    const deletedTask = await this.prisma.task.delete({\n      where: { id },\n    });\n\n    this.logger.log(`Successfully deleted task ID: ${id}`);\n\n    this.tasksGateway.emitTaskDeleted(id);\n\n    return deletedTask;\n  }\n\n  async addTaskMessage(taskId: string, addTaskMessageDto: AddTaskMessageDto) {\n    const task = await this.findById(taskId);\n    if (!task) {\n      this.logger.warn(`Task with ID: ${taskId} not found for guiding`);\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    const message = await this.prisma.message.create({\n      data: {\n        content: [{ type: 'text', text: addTaskMessageDto.message }],\n        role: Role.USER,\n        taskId,\n      },\n    });\n\n    this.tasksGateway.emitNewMessage(taskId, message);\n    return task;\n  }\n\n  async resume(taskId: string): Promise<Task> {\n    this.logger.log(`Resuming task ID: ${taskId}`);\n\n    const task = await this.findById(taskId);\n    if (!task) {\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    if (task.control !== Role.USER) {\n      throw new BadRequestException(`Task ${taskId} is not under user control`);\n    }\n\n    const updatedTask = await this.prisma.task.update({\n      where: { id: taskId },\n      data: {\n        control: Role.ASSISTANT,\n        status: TaskStatus.RUNNING,\n      },\n    });\n\n    try {\n      await fetch(\n        `${this.configService.get<string>('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/stop`,\n        { method: 'POST' },\n      );\n    } catch (error) {\n      this.logger.error('Failed to stop input tracking', error);\n    }\n\n    // Broadcast resume event so AgentProcessor can react\n    this.eventEmitter.emit('task.resume', { taskId });\n\n    this.logger.log(`Task ${taskId} resumed`);\n    this.tasksGateway.emitTaskUpdate(taskId, updatedTask);\n\n    return updatedTask;\n  }\n\n  async takeOver(taskId: string): Promise<Task> {\n    this.logger.log(`Taking over control for task ID: ${taskId}`);\n\n    const task = await this.findById(taskId);\n    if (!task) {\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    if (task.control !== Role.ASSISTANT) {\n      throw new BadRequestException(\n        `Task ${taskId} is not under agent control`,\n      );\n    }\n\n    const updatedTask = await this.prisma.task.update({\n      where: { id: taskId },\n      data: {\n        control: Role.USER,\n      },\n    });\n\n    try {\n      await fetch(\n        `${this.configService.get<string>('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/start`,\n        { method: 'POST' },\n      );\n    } catch (error) {\n      this.logger.error('Failed to start input tracking', error);\n    }\n\n    // Broadcast takeover event so AgentProcessor can react\n    this.eventEmitter.emit('task.takeover', { taskId });\n\n    this.logger.log(`Task ${taskId} takeover initiated`);\n    this.tasksGateway.emitTaskUpdate(taskId, updatedTask);\n\n    return updatedTask;\n  }\n\n  async cancel(taskId: string): Promise<Task> {\n    this.logger.log(`Cancelling task ID: ${taskId}`);\n\n    const task = await this.findById(taskId);\n    if (!task) {\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    if (\n      task.status === TaskStatus.COMPLETED ||\n      task.status === TaskStatus.FAILED ||\n      task.status === TaskStatus.CANCELLED\n    ) {\n      throw new BadRequestException(\n        `Task ${taskId} is already completed, failed, or cancelled`,\n      );\n    }\n\n    const updatedTask = await this.prisma.task.update({\n      where: { id: taskId },\n      data: {\n        status: TaskStatus.CANCELLED,\n      },\n    });\n\n    // Broadcast cancel event so AgentProcessor can cancel processing\n    this.eventEmitter.emit('task.cancel', { taskId });\n\n    this.logger.log(`Task ${taskId} cancelled and marked as failed`);\n    this.tasksGateway.emitTaskUpdate(taskId, updatedTask);\n\n    return updatedTask;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\"]\n}\n"
  },
  {
    "path": "packages/bytebot-agent/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true,\n    \"skipLibCheck\": true,\n    \"strictNullChecks\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitAny\": false,\n    \"strictBindCallApply\": false,\n    \"noFallthroughCasesInSwitch\": false\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/.dockerignore",
    "content": "**/node_modules\n**/dist\n**/.git\n**/.vscode\n**/.env*\n**/npm-debug.log\n**/yarn-debug.log\n**/yarn-error.log\n**/package-lock.json"
  },
  {
    "path": "packages/bytebot-agent-cc/.gitignore",
    "content": "# compiled output\n/dist\n/node_modules\n/build\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\npnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# temp directory\n.temp\n.tmp\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n"
  },
  {
    "path": "packages/bytebot-agent-cc/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}"
  },
  {
    "path": "packages/bytebot-agent-cc/Dockerfile",
    "content": "# Base image\nFROM node:20-alpine\n\n# Create app directory\nWORKDIR /app\n\n# Copy app source\nCOPY ./shared ./shared\nCOPY ./bytebot-agent-cc/ ./bytebot-agent-cc/\n\nWORKDIR /app/bytebot-agent-cc\n\n# Install dependencies\nRUN npm install\n\n\nRUN npm run build\n\n# Run the application\nCMD [\"npm\", \"run\", \"start:prod\"] \n\n\n"
  },
  {
    "path": "packages/bytebot-agent-cc/eslint.config.mjs",
    "content": "// @ts-check\nimport eslint from '@eslint/js';\nimport eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';\nimport globals from 'globals';\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n  {\n    ignores: ['eslint.config.mjs'],\n  },\n  eslint.configs.recommended,\n  ...tseslint.configs.recommendedTypeChecked,\n  eslintPluginPrettierRecommended,\n  {\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest,\n      },\n      ecmaVersion: 5,\n      sourceType: 'module',\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n  },\n  {\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-floating-promises': 'warn',\n      '@typescript-eslint/no-unsafe-argument': 'warn'\n    },\n  },\n);"
  },
  {
    "path": "packages/bytebot-agent-cc/nest-cli.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/nest-cli\",\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\",\n  \"compilerOptions\": {\n    \"deleteOutDir\": true\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/package.json",
    "content": "{\n  \"name\": \"bytebot-agent\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"author\": \"\",\n  \"private\": true,\n  \"license\": \"UNLICENSED\",\n  \"scripts\": {\n    \"prisma:dev\": \"npx prisma migrate dev && npx prisma generate\",\n    \"prisma:prod\": \"npx prisma migrate deploy && npx prisma generate\",\n    \"build\": \"npm run build --prefix ../shared && npx prisma generate && nest build\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"start\": \"npm run build --prefix ../shared && nest start\",\n    \"start:dev\": \"npm run build --prefix ../shared && nest start --watch\",\n    \"start:debug\": \"npm run build --prefix ../shared && nest start --debug --watch\",\n    \"start:prod\": \"npm run build --prefix ../shared && npx prisma migrate deploy && npx prisma generate && node dist/main\",\n    \"lint\": \"eslint \\\"{src,apps,libs,test}/**/*.ts\\\" --fix\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:cov\": \"jest --coverage\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/claude-code\": \"^1.0.105\",\n    \"@anthropic-ai/sdk\": \"^0.39.0\",\n    \"@bytebot/shared\": \"../shared\",\n    \"@google/genai\": \"^1.8.0\",\n    \"@nestjs/common\": \"^11.0.1\",\n    \"@nestjs/config\": \"^4.0.2\",\n    \"@nestjs/core\": \"^11.0.1\",\n    \"@nestjs/event-emitter\": \"^3.0.1\",\n    \"@nestjs/platform-express\": \"^11.1.5\",\n    \"@nestjs/platform-socket.io\": \"^11.1.1\",\n    \"@nestjs/schedule\": \"^6.0.0\",\n    \"@nestjs/websockets\": \"^11.1.1\",\n    \"@prisma/client\": \"^6.16.1\",\n    \"@thallesp/nestjs-better-auth\": \"^1.0.0\",\n    \"class-transformer\": \"^0.5.1\",\n    \"class-validator\": \"^0.14.2\",\n    \"openai\": \"^5.8.2\",\n    \"reflect-metadata\": \"^0.2.2\",\n    \"rxjs\": \"^7.8.1\",\n    \"socket.io\": \"^4.8.1\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"zod\": \"^4.0.5\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.2.0\",\n    \"@eslint/js\": \"^9.18.0\",\n    \"@nestjs/cli\": \"^11.0.0\",\n    \"@nestjs/schematics\": \"^11.0.0\",\n    \"@nestjs/testing\": \"^11.0.1\",\n    \"@swc/cli\": \"^0.6.0\",\n    \"@swc/core\": \"^1.10.7\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.10.7\",\n    \"@types/supertest\": \"^6.0.2\",\n    \"eslint\": \"^9.18.0\",\n    \"eslint-config-prettier\": \"^10.0.1\",\n    \"eslint-plugin-prettier\": \"^5.2.2\",\n    \"globals\": \"^15.14.0\",\n    \"jest\": \"^29.7.0\",\n    \"prettier\": \"^3.4.2\",\n    \"prisma\": \"^6.16.1\",\n    \"source-map-support\": \"^0.5.21\",\n    \"supertest\": \"^7.0.0\",\n    \"ts-jest\": \"^29.2.5\",\n    \"ts-loader\": \"^9.5.2\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^4.2.0\",\n    \"typescript\": \"^5.7.3\",\n    \"typescript-eslint\": \"^8.20.0\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\n      \"js\",\n      \"json\",\n      \"ts\"\n    ],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".*\\\\.spec\\\\.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"collectCoverageFrom\": [\n      \"**/*.(t|j)s\"\n    ],\n    \"coverageDirectory\": \"../coverage\",\n    \"testEnvironment\": \"node\"\n  },\n  \"overrides\": {\n    \"openai\": {\n      \"zod\": \"^4.0.5\"\n    }\n  },\n  \"engines\": {\n    \"node\": \"20\"\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250328022708_initial_migration/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"TaskStatus\" AS ENUM ('PENDING', 'IN_PROGRESS', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED');\n\n-- CreateEnum\nCREATE TYPE \"TaskPriority\" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');\n\n-- CreateEnum\nCREATE TYPE \"MessageType\" AS ENUM ('USER', 'ASSISTANT');\n\n-- CreateTable\nCREATE TABLE \"Task\" (\n    \"id\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"status\" \"TaskStatus\" NOT NULL DEFAULT 'PENDING',\n    \"priority\" \"TaskPriority\" NOT NULL DEFAULT 'MEDIUM',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Task_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Summary\" (\n    \"id\" TEXT NOT NULL,\n    \"content\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"taskId\" TEXT NOT NULL,\n    \"parentId\" TEXT,\n\n    CONSTRAINT \"Summary_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Message\" (\n    \"id\" TEXT NOT NULL,\n    \"content\" JSONB NOT NULL,\n    \"type\" \"MessageType\" NOT NULL DEFAULT 'ASSISTANT',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"taskId\" TEXT NOT NULL,\n    \"summaryId\" TEXT,\n\n    CONSTRAINT \"Message_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"Summary\" ADD CONSTRAINT \"Summary_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Summary\" ADD CONSTRAINT \"Summary_parentId_fkey\" FOREIGN KEY (\"parentId\") REFERENCES \"Summary\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_summaryId_fkey\" FOREIGN KEY (\"summaryId\") REFERENCES \"Summary\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250413053912_message_role/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `type` on the `Message` table. All the data in the column will be lost.\n\n*/\n-- CreateEnum\nCREATE TYPE \"MessageRole\" AS ENUM ('USER', 'ASSISTANT');\n\n-- AlterTable\nALTER TABLE \"Message\" DROP COLUMN \"type\",\nADD COLUMN     \"role\" \"MessageRole\" NOT NULL DEFAULT 'ASSISTANT';\n\n-- DropEnum\nDROP TYPE \"MessageType\";\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250522200556_updated_task_structure/migration.sql",
    "content": "\n-- CreateEnum\nCREATE TYPE \"Role\" AS ENUM ('USER', 'ASSISTANT');\n\n-- CreateEnum\nCREATE TYPE \"TaskType\" AS ENUM ('IMMEDIATE', 'SCHEDULED');\n\n-- AlterEnum\nBEGIN;\nCREATE TYPE \"TaskStatus_new\" AS ENUM ('PENDING', 'RUNNING', 'NEEDS_HELP', 'NEEDS_REVIEW', 'COMPLETED', 'CANCELLED', 'FAILED');\nALTER TABLE \"Task\" ALTER COLUMN \"status\" DROP DEFAULT;\nALTER TABLE \"Task\" ALTER COLUMN \"status\" TYPE \"TaskStatus_new\" USING (CASE \"status\"::text WHEN 'IN_PROGRESS' THEN 'RUNNING' ELSE \"status\"::text END::\"TaskStatus_new\");\nALTER TYPE \"TaskStatus\" RENAME TO \"TaskStatus_old\";\nALTER TYPE \"TaskStatus_new\" RENAME TO \"TaskStatus\";\nDROP TYPE \"TaskStatus_old\";\nALTER TABLE \"Task\" ALTER COLUMN \"status\" SET DEFAULT 'PENDING';\nCOMMIT;\n\n-- DropForeignKey\nALTER TABLE \"Message\" DROP CONSTRAINT \"Message_taskId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"Summary\" DROP CONSTRAINT \"Summary_taskId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Message\" ADD COLUMN \"new_role\" \"Role\" NOT NULL DEFAULT 'ASSISTANT';\nUPDATE \"Message\"\nSET \"new_role\" = CASE\n    WHEN lower(\"role\"::text) = 'user' THEN 'USER'::\"Role\"\n    WHEN lower(\"role\"::text) = 'assistant' THEN 'ASSISTANT'::\"Role\"\n    ELSE 'ASSISTANT'::\"Role\"\nEND;\n\n-- Step 3: Drop the old 'role' column.\nALTER TABLE \"Message\" DROP COLUMN \"role\";\n\n-- Step 4: Rename 'new_role' to 'role'.\nALTER TABLE \"Message\" RENAME COLUMN \"new_role\" TO \"role\";\n\n-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"completedAt\" TIMESTAMP(3),\nADD COLUMN     \"createdBy\" \"Role\" NOT NULL DEFAULT 'USER',\nADD COLUMN     \"error\" TEXT,\nADD COLUMN     \"executedAt\" TIMESTAMP(3),\nADD COLUMN     \"result\" JSONB,\nADD COLUMN     \"type\" \"TaskType\" NOT NULL DEFAULT 'IMMEDIATE';\n\n-- DropEnum\nDROP TYPE \"MessageRole\";\n\n-- AddForeignKey\nALTER TABLE \"Summary\" ADD CONSTRAINT \"Summary_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250523162632_add_scheduling/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"queuedAt\" TIMESTAMP(3),\nADD COLUMN     \"scheduledFor\" TIMESTAMP(3);\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250529003255_tasks_control/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"control\" \"Role\" NOT NULL DEFAULT 'USER';\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250530012753_tasks_control/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ALTER COLUMN \"control\" SET DEFAULT 'ASSISTANT';\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250619013027_add_better_auth_schema/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Message\" ADD COLUMN     \"userId\" TEXT;\n\n-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT,\n    \"email\" TEXT NOT NULL,\n    \"emailVerified\" BOOLEAN NOT NULL DEFAULT false,\n    \"image\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"User_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Session\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"ipAddress\" TEXT,\n    \"userAgent\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Session_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Account\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"accountId\" TEXT NOT NULL,\n    \"providerId\" TEXT NOT NULL,\n    \"accessToken\" TEXT,\n    \"refreshToken\" TEXT,\n    \"accessTokenExpiresAt\" TIMESTAMP(3),\n    \"refreshTokenExpiresAt\" TIMESTAMP(3),\n    \"scope\" TEXT,\n    \"idToken\" TEXT,\n    \"password\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Account_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Verification\" (\n    \"id\" TEXT NOT NULL,\n    \"identifier\" TEXT NOT NULL,\n    \"value\" TEXT NOT NULL,\n    \"expiresAt\" TIMESTAMP(3) NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n\n    CONSTRAINT \"Verification_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Session_token_key\" ON \"Session\"(\"token\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Account_providerId_accountId_key\" ON \"Account\"(\"providerId\", \"accountId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Verification_identifier_value_key\" ON \"Verification\"(\"identifier\", \"value\");\n\n-- AddForeignKey\nALTER TABLE \"Message\" ADD CONSTRAINT \"Message_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Session\" ADD CONSTRAINT \"Session_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Account\" ADD CONSTRAINT \"Account_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250622195148_add_user_to_task/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Task\" ADD COLUMN     \"userId\" TEXT;\n\n-- AddForeignKey\nALTER TABLE \"Task\" ADD CONSTRAINT \"Task_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250706223912_model_picker/migration.sql",
    "content": "-- AlterTable: add `model` column as JSONB (nullable initially)\nALTER TABLE \"Task\" ADD COLUMN \"model\" JSONB;\n\n-- Backfill existing tasks with the default Anthropic Claude Opus 4 model\nUPDATE \"Task\"\nSET \"model\" = jsonb_build_object(\n  'provider', 'anthropic',\n  'name', 'claude-opus-4-20250514',\n  'title', 'Claude Opus 4'\n)\nWHERE \"model\" IS NULL;\n\n-- Enforce NOT NULL constraint now that data is populated\nALTER TABLE \"Task\" ALTER COLUMN \"model\" SET NOT NULL;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250722041608_files/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"File\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"type\" TEXT NOT NULL,\n    \"size\" INTEGER NOT NULL,\n    \"data\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL,\n    \"taskId\" TEXT NOT NULL,\n\n    CONSTRAINT \"File_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"File\" ADD CONSTRAINT \"File_taskId_fkey\" FOREIGN KEY (\"taskId\") REFERENCES \"Task\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/20250820172813_remove_auth/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `userId` on the `Message` table. All the data in the column will be lost.\n  - You are about to drop the column `userId` on the `Task` table. All the data in the column will be lost.\n  - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.\n  - You are about to drop the `Verification` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"public\".\"Account\" DROP CONSTRAINT \"Account_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"public\".\"Message\" DROP CONSTRAINT \"Message_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"public\".\"Session\" DROP CONSTRAINT \"Session_userId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"public\".\"Task\" DROP CONSTRAINT \"Task_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"public\".\"Message\" DROP COLUMN \"userId\";\n\n-- AlterTable\nALTER TABLE \"public\".\"Task\" DROP COLUMN \"userId\";\n\n-- DropTable\nDROP TABLE \"public\".\"Account\";\n\n-- DropTable\nDROP TABLE \"public\".\"Session\";\n\n-- DropTable\nDROP TABLE \"public\".\"User\";\n\n-- DropTable\nDROP TABLE \"public\".\"Verification\";\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (e.g., Git)\nprovider = \"postgresql\"\n"
  },
  {
    "path": "packages/bytebot-agent-cc/prisma/schema.prisma",
    "content": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\nenum TaskStatus {\n  PENDING\n  RUNNING\n  NEEDS_HELP\n  NEEDS_REVIEW\n  COMPLETED\n  CANCELLED\n  FAILED\n}\n\nenum TaskPriority {\n  LOW\n  MEDIUM\n  HIGH\n  URGENT\n}\n\nenum Role {\n  USER\n  ASSISTANT\n}\n\nenum TaskType {\n  IMMEDIATE\n  SCHEDULED\n}\n\nmodel Task {\n  id            String        @id @default(uuid())\n  description   String\n  type          TaskType      @default(IMMEDIATE)\n  status        TaskStatus    @default(PENDING)\n  priority      TaskPriority  @default(MEDIUM)\n  control       Role          @default(ASSISTANT)\n  createdAt     DateTime      @default(now())\n  createdBy     Role          @default(USER)\n  scheduledFor  DateTime?\n  updatedAt     DateTime      @updatedAt\n  executedAt    DateTime?\n  completedAt   DateTime?\n  queuedAt      DateTime?\n  error         String?\n  result        Json?\n  // Example: \n  // { \"provider\": \"anthropic\", \"name\": \"claude-opus-4-20250514\", \"title\": \"Claude Opus 4\" }\n  model         Json\n  messages      Message[]\n  summaries     Summary[]\n  files         File[]\n}\n\nmodel Summary {\n  id             String     @id @default(uuid())\n  content        String\n  createdAt      DateTime   @default(now())\n  updatedAt      DateTime   @updatedAt\n  messages       Message[]  // One-to-many relationship: one Summary has many Messages\n\n  task      Task        @relation(fields: [taskId], references: [id], onDelete: Cascade)\n  taskId    String\n  \n  // Self-referential relationship\n  parentSummary  Summary?   @relation(\"SummaryHierarchy\", fields: [parentId], references: [id])\n  parentId       String?\n  childSummaries Summary[]  @relation(\"SummaryHierarchy\")\n}\n\nmodel Message {\n  id        String      @id @default(uuid())\n  // Content field follows Anthropic's content blocks structure\n  // Example: \n  // [\n  //   {\"type\": \"text\", \"text\": \"Hello world\"},\n  //   {\"type\": \"image\", \"source\": {\"type\": \"base64\", \"media_type\": \"image/jpeg\", \"data\": \"...\"}}\n  // ]\n  content   Json\n  role      Role @default(ASSISTANT)\n  createdAt DateTime    @default(now())\n  updatedAt DateTime    @updatedAt\n  task      Task        @relation(fields: [taskId], references: [id], onDelete: Cascade)\n  taskId    String\n  summary   Summary?    @relation(fields: [summaryId], references: [id])\n  summaryId String?     // Optional foreign key to Summary\n}\n\nmodel File {\n  id            String      @id @default(uuid())\n  name          String\n  type          String      // MIME type\n  size          Int         // Size in bytes\n  data          String      // Base64 encoded file data\n  createdAt     DateTime    @default(now())\n  updatedAt     DateTime    @updatedAt\n  \n  // Relations\n  task          Task        @relation(fields: [taskId], references: [id], onDelete: Cascade)\n  taskId        String\n}\n\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.analytics.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { ConfigService } from '@nestjs/config';\nimport { TasksService } from '../tasks/tasks.service';\nimport { MessagesService } from '../messages/messages.service';\n\n@Injectable()\nexport class AgentAnalyticsService {\n  private readonly logger = new Logger(AgentAnalyticsService.name);\n  private readonly endpoint?: string;\n\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly messagesService: MessagesService,\n    configService: ConfigService,\n  ) {\n    this.endpoint = configService.get<string>('BYTEBOT_ANALYTICS_ENDPOINT');\n    if (!this.endpoint) {\n      this.logger.warn(\n        'BYTEBOT_ANALYTICS_ENDPOINT is not set. Analytics service disabled.',\n      );\n    }\n  }\n\n  @OnEvent('task.cancel')\n  @OnEvent('task.failed')\n  @OnEvent('task.completed')\n  async handleTaskEvent(payload: { taskId: string }) {\n    if (!this.endpoint) return;\n\n    try {\n      const task = await this.tasksService.findById(payload.taskId);\n      const messages = await this.messagesService.findEvery(payload.taskId);\n\n      await fetch(this.endpoint, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ ...task, messages }),\n      });\n    } catch (error: any) {\n      this.logger.error(\n        `Failed to send analytics for task ${payload.taskId}: ${error.message}`,\n        error.stack,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.computer-use.ts",
    "content": "import {\n  Button,\n  Coordinates,\n  Press,\n  ComputerToolUseContentBlock,\n  ToolResultContentBlock,\n  MessageContentType,\n  isScreenshotToolUseBlock,\n  isCursorPositionToolUseBlock,\n  isMoveMouseToolUseBlock,\n  isTraceMouseToolUseBlock,\n  isClickMouseToolUseBlock,\n  isPressMouseToolUseBlock,\n  isDragMouseToolUseBlock,\n  isScrollToolUseBlock,\n  isTypeKeysToolUseBlock,\n  isPressKeysToolUseBlock,\n  isTypeTextToolUseBlock,\n  isWaitToolUseBlock,\n  isApplicationToolUseBlock,\n  isPasteTextToolUseBlock,\n  isReadFileToolUseBlock,\n} from '@bytebot/shared';\nimport { Logger } from '@nestjs/common';\n\nconst BYTEBOT_DESKTOP_BASE_URL = process.env.BYTEBOT_DESKTOP_BASE_URL as string;\n\nexport async function handleComputerToolUse(\n  block: ComputerToolUseContentBlock,\n  logger: Logger,\n): Promise<ToolResultContentBlock> {\n  logger.debug(\n    `Handling computer tool use: ${block.name}, tool_use_id: ${block.id}`,\n  );\n\n  if (isScreenshotToolUseBlock(block)) {\n    logger.debug('Processing screenshot request');\n    try {\n      logger.debug('Taking screenshot');\n      const image = await screenshot();\n      logger.debug('Screenshot captured successfully');\n\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Image,\n            source: {\n              data: image,\n              media_type: 'image/png',\n              type: 'base64',\n            },\n          },\n        ],\n      };\n    } catch (error) {\n      logger.error(`Screenshot failed: ${error.message}`, error.stack);\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Text,\n            text: 'ERROR: Failed to take screenshot',\n          },\n        ],\n        is_error: true,\n      };\n    }\n  }\n\n  if (isCursorPositionToolUseBlock(block)) {\n    logger.debug('Processing cursor position request');\n    try {\n      logger.debug('Getting cursor position');\n      const position = await cursorPosition();\n      logger.debug(`Cursor position obtained: ${position.x}, ${position.y}`);\n\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Text,\n            text: `Cursor position: ${position.x}, ${position.y}`,\n          },\n        ],\n      };\n    } catch (error) {\n      logger.error(\n        `Getting cursor position failed: ${error.message}`,\n        error.stack,\n      );\n      return {\n        type: MessageContentType.ToolResult,\n        tool_use_id: block.id,\n        content: [\n          {\n            type: MessageContentType.Text,\n            text: 'ERROR: Failed to get cursor position',\n          },\n        ],\n        is_error: true,\n      };\n    }\n  }\n\n  try {\n    if (isMoveMouseToolUseBlock(block)) {\n      await moveMouse(block.input);\n    }\n    if (isTraceMouseToolUseBlock(block)) {\n      await traceMouse(block.input);\n    }\n    if (isClickMouseToolUseBlock(block)) {\n      await clickMouse(block.input);\n    }\n    if (isPressMouseToolUseBlock(block)) {\n      await pressMouse(block.input);\n    }\n    if (isDragMouseToolUseBlock(block)) {\n      await dragMouse(block.input);\n    }\n    if (isScrollToolUseBlock(block)) {\n      await scroll(block.input);\n    }\n    if (isTypeKeysToolUseBlock(block)) {\n      await typeKeys(block.input);\n    }\n    if (isPressKeysToolUseBlock(block)) {\n      await pressKeys(block.input);\n    }\n    if (isTypeTextToolUseBlock(block)) {\n      await typeText(block.input);\n    }\n    if (isPasteTextToolUseBlock(block)) {\n      await pasteText(block.input);\n    }\n    if (isWaitToolUseBlock(block)) {\n      await wait(block.input);\n    }\n    if (isApplicationToolUseBlock(block)) {\n      await application(block.input);\n    }\n    if (isReadFileToolUseBlock(block)) {\n      logger.debug(`Reading file: ${block.input.path}`);\n      const result = await readFile(block.input);\n\n      if (result.success && result.data) {\n        // Return document content block\n        return {\n          type: MessageContentType.ToolResult,\n          tool_use_id: block.id,\n          content: [\n            {\n              type: MessageContentType.Document,\n              source: {\n                type: 'base64',\n                media_type: result.mediaType || 'application/octet-stream',\n                data: result.data,\n              },\n              name: result.name || 'file',\n              size: result.size,\n            },\n          ],\n        };\n      } else {\n        // Return error message\n        return {\n          type: MessageContentType.ToolResult,\n          tool_use_id: block.id,\n          content: [\n            {\n              type: MessageContentType.Text,\n              text: result.message || 'Error reading file',\n            },\n          ],\n          is_error: true,\n        };\n      }\n    }\n\n    let image: string | null = null;\n    try {\n      logger.debug('Taking screenshot');\n      image = await screenshot();\n      logger.debug('Screenshot captured successfully');\n    } catch (error) {\n      logger.error('Failed to take screenshot', error);\n    }\n\n    logger.debug(`Tool execution successful for tool_use_id: ${block.id}`);\n    const toolResult: ToolResultContentBlock = {\n      type: MessageContentType.ToolResult,\n      tool_use_id: block.id,\n      content: [\n        {\n          type: MessageContentType.Text,\n          text: 'Tool executed successfully',\n        },\n      ],\n    };\n\n    if (image) {\n      toolResult.content.push({\n        type: MessageContentType.Image,\n        source: {\n          data: image,\n          media_type: 'image/png',\n          type: 'base64',\n        },\n      });\n    }\n\n    return toolResult;\n  } catch (error) {\n    logger.error(\n      `Error executing ${block.name} tool: ${error.message}`,\n      error.stack,\n    );\n    return {\n      type: MessageContentType.ToolResult,\n      tool_use_id: block.id,\n      content: [\n        {\n          type: MessageContentType.Text,\n          text: `Error executing ${block.name} tool: ${error.message}`,\n        },\n      ],\n      is_error: true,\n    };\n  }\n}\n\nasync function moveMouse(input: { coordinates: Coordinates }): Promise<void> {\n  const { coordinates } = input;\n  console.log(\n    `Moving mouse to coordinates: [${coordinates.x}, ${coordinates.y}]`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'move_mouse',\n        coordinates,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in move_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function traceMouse(input: {\n  path: Coordinates[];\n  holdKeys?: string[];\n}): Promise<void> {\n  const { path, holdKeys } = input;\n  console.log(\n    `Tracing mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'trace_mouse',\n        path,\n        holdKeys,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in trace_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function clickMouse(input: {\n  coordinates?: Coordinates;\n  button: Button;\n  holdKeys?: string[];\n  clickCount: number;\n}): Promise<void> {\n  const { coordinates, button, holdKeys, clickCount } = input;\n  console.log(\n    `Clicking mouse ${button} ${clickCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}] ` : ''} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'click_mouse',\n        coordinates,\n        button,\n        holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,\n        clickCount,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in click_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function pressMouse(input: {\n  coordinates?: Coordinates;\n  button: Button;\n  press: Press;\n}): Promise<void> {\n  const { coordinates, button, press } = input;\n  console.log(\n    `Pressing mouse ${button} ${press} ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'press_mouse',\n        coordinates,\n        button,\n        press,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in press_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function dragMouse(input: {\n  path: Coordinates[];\n  button: Button;\n  holdKeys?: string[];\n}): Promise<void> {\n  const { path, button, holdKeys } = input;\n  console.log(\n    `Dragging mouse to path: ${path} ${holdKeys ? `with holdKeys: ${holdKeys}` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'drag_mouse',\n        path,\n        button,\n        holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in drag_mouse action:', error);\n    throw error;\n  }\n}\n\nasync function scroll(input: {\n  coordinates?: Coordinates;\n  direction: 'up' | 'down' | 'left' | 'right';\n  scrollCount: number;\n  holdKeys?: string[];\n}): Promise<void> {\n  const { coordinates, direction, scrollCount, holdKeys } = input;\n  console.log(\n    `Scrolling ${direction} ${scrollCount} times ${coordinates ? `at coordinates: [${coordinates.x}, ${coordinates.y}]` : ''}`,\n  );\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'scroll',\n        coordinates,\n        direction,\n        scrollCount,\n        holdKeys: holdKeys && holdKeys.length > 0 ? holdKeys : undefined,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in scroll action:', error);\n    throw error;\n  }\n}\n\nasync function typeKeys(input: {\n  keys: string[];\n  delay?: number;\n}): Promise<void> {\n  const { keys, delay } = input;\n  console.log(`Typing keys: ${keys}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'type_keys',\n        keys,\n        delay,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in type_keys action:', error);\n    throw error;\n  }\n}\n\nasync function pressKeys(input: {\n  keys: string[];\n  press: Press;\n}): Promise<void> {\n  const { keys, press } = input;\n  console.log(`Pressing keys: ${keys}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'press_keys',\n        keys,\n        press,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in press_keys action:', error);\n    throw error;\n  }\n}\n\nasync function typeText(input: {\n  text: string;\n  delay?: number;\n}): Promise<void> {\n  const { text, delay } = input;\n  console.log(`Typing text: ${text}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'type_text',\n        text,\n        delay,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in type_text action:', error);\n    throw error;\n  }\n}\n\nasync function pasteText(input: { text: string }): Promise<void> {\n  const { text } = input;\n  console.log(`Pasting text: ${text}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'paste_text',\n        text,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in paste_text action:', error);\n    throw error;\n  }\n}\n\nasync function wait(input: { duration: number }): Promise<void> {\n  const { duration } = input;\n  console.log(`Waiting for ${duration}ms`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'wait',\n        duration,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in wait action:', error);\n    throw error;\n  }\n}\n\nasync function cursorPosition(): Promise<Coordinates> {\n  console.log('Getting cursor position');\n\n  try {\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'cursor_position',\n      }),\n    });\n\n    const data = await response.json();\n    return { x: data.x, y: data.y };\n  } catch (error) {\n    console.error('Error in cursor_position action:', error);\n    throw error;\n  }\n}\n\nasync function screenshot(): Promise<string> {\n  console.log('Taking screenshot');\n\n  try {\n    const requestBody = {\n      action: 'screenshot',\n    };\n\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(requestBody),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to take screenshot: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n\n    if (!data.image) {\n      throw new Error('Failed to take screenshot: No image data received');\n    }\n\n    return data.image; // Base64 encoded image\n  } catch (error) {\n    console.error('Error in screenshot action:', error);\n    throw error;\n  }\n}\n\nasync function application(input: { application: string }): Promise<void> {\n  const { application } = input;\n  console.log(`Opening application: ${application}`);\n\n  try {\n    await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'application',\n        application,\n      }),\n    });\n  } catch (error) {\n    console.error('Error in application action:', error);\n    throw error;\n  }\n}\n\nasync function readFile(input: { path: string }): Promise<{\n  success: boolean;\n  data?: string;\n  name?: string;\n  size?: number;\n  mediaType?: string;\n  message?: string;\n}> {\n  const { path } = input;\n  console.log(`Reading file: ${path}`);\n\n  try {\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'read_file',\n        path,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to read file: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n    return data;\n  } catch (error) {\n    console.error('Error in read_file action:', error);\n    return {\n      success: false,\n      message: `Error reading file: ${error.message}`,\n    };\n  }\n}\n\nexport async function writeFile(input: {\n  path: string;\n  content: string;\n}): Promise<{ success: boolean; message?: string }> {\n  const { path, content } = input;\n  console.log(`Writing file: ${path}`);\n\n  try {\n    // Content is always base64 encoded\n    const base64Data = content;\n\n    const response = await fetch(`${BYTEBOT_DESKTOP_BASE_URL}/computer-use`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        action: 'write_file',\n        path,\n        data: base64Data,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to write file: ${response.statusText}`);\n    }\n\n    const data = await response.json();\n    return data;\n  } catch (error) {\n    console.error('Error in write_file action:', error);\n    return {\n      success: false,\n      message: `Error writing file: ${error.message}`,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.constants.ts",
    "content": "export const DEFAULT_DISPLAY_SIZE = {\n  width: 1280,\n  height: 960,\n};\n\nexport const SUMMARIZATION_SYSTEM_PROMPT = `You are a helpful assistant that summarizes conversations for long-running tasks.\nYour job is to create concise summaries that preserve all important information, tool usage, and key decisions.\nFocus on:\n- Task progress and completed actions\n- Important tool calls and their results\n- Key decisions made\n- Any errors or issues encountered\n- Current state and what remains to be done\n\nProvide a structured summary that can be used as context for continuing the task.`;\n\nexport const AGENT_SYSTEM_PROMPT = `\nYou are **Bytebot**, a highly-reliable AI engineer operating a virtual computer whose display measures ${DEFAULT_DISPLAY_SIZE.width} x ${DEFAULT_DISPLAY_SIZE.height} pixels.\n\nThe current date is ${new Date().toLocaleDateString()}. The current time is ${new Date().toLocaleTimeString()}. The current timezone is ${Intl.DateTimeFormat().resolvedOptions().timeZone}.\n\n────────────────────────\nAVAILABLE APPLICATIONS\n────────────────────────\n\nOn the desktop, the following applications are available:\n\nFirefox Browser -- The default web browser, use it to navigate to websites.\nThunderbird -- The default email client, use it to send and receive emails (if you have an account).\n1Password -- The password manager, use it to store and retrieve your passwords (if you have an account).\nVisual Studio Code -- The default code editor, use it to create and edit files.\nTerminal -- The default terminal, use it to run commands.\nFile Manager -- The default file manager, use it to navigate and manage files.\nTrash -- The default trash\n\nALL APPLICATIONS ARE GUI BASED, USE THE COMPUTER TOOLS TO INTERACT WITH THEM. ONLY ACCESS THE APPLICATIONS VIA THEIR DESKTOP ICONS.\n\n*Never* use keyboard shortcuts to switch between applications, only use \\`computer_application\\` to switch between the default applications. \n\n────────────────────────\nCORE WORKING PRINCIPLES\n────────────────────────\n1. **Observe First** - *Always* invoke \\`computer_screenshot\\` before your first action **and** whenever the UI may have changed. Screenshot before every action when filling out forms. Never act blindly. When opening documents or PDFs, scroll through at least the first page to confirm it is the correct document. \n2. **Navigate applications**  = *Always* invoke \\`computer_application\\` to switch between the default applications.\n3. **Human-Like Interaction**\n   • Move in smooth, purposeful paths; click near the visual centre of targets.  \n   • Double-click desktop icons to open them.  \n   • Type realistic, context-appropriate text with \\`computer_type_text\\` (for short strings) or \\`computer_paste_text\\` (for long strings), or shortcuts with \\`computer_type_keys\\`.\n4. **Valid Keys Only** - \n   Use **exactly** the identifiers listed in **VALID KEYS** below when supplying \\`keys\\` to \\`computer_type_keys\\` or \\`computer_press_keys\\`. All identifiers come from nut-tree's \\`Key\\` enum; they are case-sensitive and contain *no spaces*.\n5. **Verify Every Step** - After each action:  \n   a. Take another screenshot.  \n   b. Confirm the expected state before continuing. If it failed, retry sensibly (try again, and then try 2 different methods).\n6. **Efficiency & Clarity** - Combine related key presses; prefer scrolling or dragging over many small moves; minimise unnecessary waits.\n7. **Stay Within Scope** - Do nothing the user didn't request; don't suggest unrelated tasks. For form and login fields, don't fill in random data, unless explicitly told to do so.\n8. **Security** - If you see a password, secret key, or other sensitive information (or the user shares it with you), do not repeat it in conversation. When typing sensitive information, use \\`computer_type_text\\` with \\`isSensitive\\` set to \\`true\\`.\n9. **Consistency & Persistence** - Even if the task is repetitive, do not end the task until the user's goal is completely met. For bulk operations, maintain focus and continue until all items are processed.\n\n────────────────────────\nREPETITIVE TASK HANDLING\n────────────────────────\nWhen performing repetitive tasks (e.g., \"visit each profile\", \"process all items\"):\n\n1. **Track Progress** - Maintain a mental count of:\n   • Total items to process (if known)\n   • Items completed so far\n   • Current item being processed\n   • Any errors encountered\n\n2. **Batch Processing** - For large sets:\n   • Process in groups of 10-20 items\n   • Take brief pauses between batches to prevent system overload\n   • Continue until ALL items are processed\n\n3. **Error Recovery** - If an item fails:\n   • Note the error but continue with the next item\n   • Keep a list of failed items to report at the end\n   • Don't let one failure stop the entire operation\n\n4. **Progress Updates** - Every 10-20 items:\n   • Brief status: \"Processed 20/100 profiles, continuing...\"\n   • No need for detailed reports unless requested\n\n5. **Completion Criteria** - The task is NOT complete until:\n   • All items in the set are processed, OR\n   • You reach a clear endpoint (e.g., \"No more profiles to load\"), OR\n   • The user explicitly tells you to stop\n\n6. **State Management** - If the task might span multiple tabs/pages:\n   • Save progress to a file periodically\n   • Include timestamps and item identifiers\n\n────────────────────────\nTASK LIFECYCLE TEMPLATE\n────────────────────────\n1. **Prepare** - Initial screenshot → plan → estimate scope if possible.  \n2. **Execute Loop** - For each sub-goal: Screenshot → Think → Act → Verify.\n3. **Batch Loop** - For repetitive tasks:\n   • While items remain:\n     - Process batch of 10-20 items\n     - Update progress counter\n     - Check for stop conditions\n     - Brief status update\n   • Continue until ALL done\n\n4. **Switch Applications** - If you need to switch between the default applications, reach the home directory, or return to the desktop, invoke          \n   \\`\\`\\`json\n   { \"name\": \"computer_application\", \"input\": { \"application\": \"application name\" } }\n   \\`\\`\\` \n   It will open (or focus if it is already open) the application, in fullscreen.\n   The application name must be one of the following: firefox, thunderbird, 1password, vscode, terminal, directory, desktop.\n5. **Create other tasks** - If you need to create additional separate tasks, invoke          \n   \\`\\`\\`json\n   { \"name\": \"create_task\", \"input\": { \"description\": \"Subtask description\", \"type\": \"IMMEDIATE\", \"priority\": \"MEDIUM\" } }\n   \\`\\`\\` \n   The other tasks will be executed in the order they are created, after the current task is completed. Only create separate tasks if they are not related to the current task.\n6. **Schedule future tasks** - If you need to schedule a task to run in the future, invoke          \n   \\`\\`\\`json\n{ \"name\": \"create_task\", \"input\": { \"description\": \"Subtask description\", \"type\": \"SCHEDULED\", \"scheduledFor\": <ISO Date>, \"priority\": \"MEDIUM\" } }\n   \\`\\`\\` \n   Only schedule tasks if they must be run in the future. Do not schedule tasks that can be run immediately.\n7. **Read Files** - If you need to read file contents, invoke\n   \\`\\`\\`json\n   { \"name\": \"computer_read_file\", \"input\": { \"path\": \"/path/to/file\" } }\n   \\`\\`\\`\n   This tool reads files and returns them as document content blocks with base64 data, supporting various file types including documents (PDF, DOCX, TXT, etc.) and images (PNG, JPG, etc.).\n8. **Cleanup** - When the user's goal is met:  \n   • Close every window, file, or app you opened so the desktop is tidy.  \n   • Return to an idle desktop/background.  \n\n**IMPORTANT**: For bulk operations like \"visit each profile in the directory\":\n- Do NOT mark as completed after just a few profiles\n- Continue until you've processed ALL profiles or reached a clear end\n- If there are 100+ profiles, process them ALL\n- Only stop when explicitly told or when there are genuinely no more items\n\n────────────────────────\nVALID KEYS\n────────────────────────\nA, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp,  \nB, Backslash, Backspace,  \nC, CapsLock, Clear, Comma,  \nD, Decimal, Delete, Divide, Down,  \nE, End, Enter, Equal, Escape, F,  \nF1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,  \nFn,  \nG, Grave,  \nH, Home,  \nI, Insert,  \nJ, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin,  \nM, Menu, Minus, Multiply,  \nN, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock,  \nNumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9,  \nO, P, PageDown, PageUp, Pause, Period, Print,  \nQ, Quote,  \nR, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin,  \nS, ScrollLock, Semicolon, Slash, Space, Subtract,  \nT, Tab,  \nU, Up,  \nV, W, X, Y, Z\n\nRemember: **accuracy over speed, clarity and consistency over cleverness**.  \n\n**For repetitive tasks**: Persistence is key. Continue until ALL items are processed, not just the first few.\n`;\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TasksModule } from '../tasks/tasks.module';\nimport { MessagesModule } from '../messages/messages.module';\nimport { AgentProcessor } from './agent.processor';\nimport { ConfigModule } from '@nestjs/config';\nimport { AgentScheduler } from './agent.scheduler';\nimport { InputCaptureService } from './input-capture.service';\nimport { AgentAnalyticsService } from './agent.analytics';\n\n@Module({\n  imports: [ConfigModule, TasksModule, MessagesModule],\n  providers: [\n    AgentProcessor,\n    AgentScheduler,\n    InputCaptureService,\n    AgentAnalyticsService,\n  ],\n  exports: [AgentProcessor],\n})\nexport class AgentModule {}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.processor.ts",
    "content": "import { TasksService } from '../tasks/tasks.service';\nimport { MessagesService } from '../messages/messages.service';\nimport { Injectable, Logger } from '@nestjs/common';\nimport {\n  Message,\n  Role,\n  Task,\n  TaskPriority,\n  TaskStatus,\n  TaskType,\n} from '@prisma/client';\nimport {\n  isComputerToolUseContentBlock,\n  isSetTaskStatusToolUseBlock,\n  isCreateTaskToolUseBlock,\n  SetTaskStatusToolUseBlock,\n  RedactedThinkingContentBlock,\n  ThinkingContentBlock,\n  ToolUseContentBlock,\n} from '@bytebot/shared';\n\nimport {\n  MessageContentBlock,\n  MessageContentType,\n  ToolResultContentBlock,\n  TextContentBlock,\n} from '@bytebot/shared';\nimport { InputCaptureService } from './input-capture.service';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport {\n  BytebotAgentModel,\n  BytebotAgentService,\n  BytebotAgentResponse,\n} from './agent.types';\nimport {\n  AGENT_SYSTEM_PROMPT,\n  SUMMARIZATION_SYSTEM_PROMPT,\n} from './agent.constants';\nimport { query } from '@anthropic-ai/claude-code';\nimport Anthropic from '@anthropic-ai/sdk';\n\n@Injectable()\nexport class AgentProcessor {\n  private readonly logger = new Logger(AgentProcessor.name);\n  private currentTaskId: string | null = null;\n  private isProcessing = false;\n  private abortController: AbortController | null = null;\n\n  private readonly BYTEBOT_DESKTOP_BASE_URL = process.env\n    .BYTEBOT_DESKTOP_BASE_URL as string;\n\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly messagesService: MessagesService,\n    private readonly inputCaptureService: InputCaptureService,\n  ) {\n    this.logger.log('AgentProcessor initialized');\n  }\n\n  /**\n   * Check if the processor is currently processing a task\n   */\n  isRunning(): boolean {\n    return this.isProcessing;\n  }\n\n  /**\n   * Get the current task ID being processed\n   */\n  getCurrentTaskId(): string | null {\n    return this.currentTaskId;\n  }\n\n  @OnEvent('task.takeover')\n  handleTaskTakeover({ taskId }: { taskId: string }) {\n    this.logger.log(`Task takeover event received for task ID: ${taskId}`);\n\n    // If the agent is still processing this task, abort any in-flight operations\n    if (this.currentTaskId === taskId && this.isProcessing) {\n      this.abortController?.abort();\n    }\n\n    // Always start capturing user input so that emitted actions are received\n    this.inputCaptureService.start(taskId);\n  }\n\n  @OnEvent('task.resume')\n  handleTaskResume({ taskId }: { taskId: string }) {\n    if (this.currentTaskId === taskId && this.isProcessing) {\n      this.logger.log(`Task resume event received for task ID: ${taskId}`);\n      this.abortController = new AbortController();\n\n      void this.runIteration(taskId);\n    }\n  }\n\n  @OnEvent('task.cancel')\n  async handleTaskCancel({ taskId }: { taskId: string }) {\n    this.logger.log(`Task cancel event received for task ID: ${taskId}`);\n\n    await this.stopProcessing();\n  }\n\n  processTask(taskId: string) {\n    this.logger.log(`Starting processing for task ID: ${taskId}`);\n\n    if (this.isProcessing) {\n      this.logger.warn('AgentProcessor is already processing another task');\n      return;\n    }\n\n    this.isProcessing = true;\n    this.currentTaskId = taskId;\n    this.abortController = new AbortController();\n\n    // Kick off the first iteration without blocking the caller\n    void this.runIteration(taskId);\n  }\n\n  /**\n   * Convert Anthropic's response content to our MessageContentBlock format\n   */\n  private formatAnthropicResponse(\n    content: Anthropic.ContentBlock[],\n  ): MessageContentBlock[] {\n    // filter out tool_use blocks that aren't computer tool uses\n    content = content.filter(\n      (block) =>\n        block.type !== 'tool_use' || block.name.startsWith('mcp__desktop__'),\n    );\n    return content.map((block) => {\n      switch (block.type) {\n        case 'text':\n          return {\n            type: MessageContentType.Text,\n            text: block.text,\n          } as TextContentBlock;\n        case 'tool_use':\n          return {\n            type: MessageContentType.ToolUse,\n            id: block.id,\n            name: block.name.replace('mcp__desktop__', ''),\n            input: block.input,\n          } as ToolUseContentBlock;\n        case 'thinking':\n          return {\n            type: MessageContentType.Thinking,\n            thinking: block.thinking,\n            signature: block.signature,\n          } as ThinkingContentBlock;\n        case 'redacted_thinking':\n          return {\n            type: MessageContentType.RedactedThinking,\n            data: block.data,\n          } as RedactedThinkingContentBlock;\n      }\n    });\n  }\n\n  /**\n   * Runs a single iteration of task processing and schedules the next\n   * iteration via setImmediate while the task remains RUNNING.\n   */\n  private async runIteration(taskId: string): Promise<void> {\n    if (!this.isProcessing) {\n      return;\n    }\n\n    try {\n      const task: Task = await this.tasksService.findById(taskId);\n\n      if (task.status !== TaskStatus.RUNNING) {\n        this.logger.log(\n          `Task processing completed for task ID: ${taskId} with status: ${task.status}`,\n        );\n        this.isProcessing = false;\n        this.currentTaskId = null;\n        return;\n      }\n\n      this.logger.log(`Processing iteration for task ID: ${taskId}`);\n\n      // Refresh abort controller for this iteration to avoid accumulating\n      // \"abort\" listeners on a single AbortSignal across iterations.\n      this.abortController = new AbortController();\n      for await (const message of query({\n        prompt: task.description,\n        options: {\n          abortController: this.abortController,\n          appendSystemPrompt: AGENT_SYSTEM_PROMPT,\n          permissionMode: 'bypassPermissions',\n          mcpServers: {\n            desktop: {\n              type: 'sse',\n              url: `${this.BYTEBOT_DESKTOP_BASE_URL}/mcp`,\n            },\n          },\n        },\n      })) {\n        let messageContentBlocks: MessageContentBlock[] = [];\n        let role: Role = Role.ASSISTANT;\n        switch (message.type) {\n          case 'user': {\n            if (Array.isArray(message.message.content)) {\n              messageContentBlocks = message.message\n                .content as MessageContentBlock[];\n            } else if (typeof message.message.content === 'string') {\n              messageContentBlocks = [\n                {\n                  type: MessageContentType.Text,\n                  text: message.message.content,\n                } as TextContentBlock,\n              ];\n            }\n\n            role = Role.USER;\n            break;\n          }\n          case 'assistant': {\n            messageContentBlocks = this.formatAnthropicResponse(\n              message.message.content,\n            );\n            break;\n          }\n          case 'system':\n            break;\n          case 'result': {\n            switch (message.subtype) {\n              case 'success':\n                await this.tasksService.update(taskId, {\n                  status: TaskStatus.COMPLETED,\n                  completedAt: new Date(),\n                });\n                break;\n              case 'error_max_turns':\n              case 'error_during_execution':\n                await this.tasksService.update(taskId, {\n                  status: TaskStatus.NEEDS_HELP,\n                });\n                break;\n            }\n            break;\n          }\n        }\n\n        this.logger.debug(\n          `Received ${messageContentBlocks.length} content blocks from LLM`,\n        );\n\n        if (messageContentBlocks.length > 0) {\n          await this.messagesService.create({\n            content: messageContentBlocks,\n            role,\n            taskId,\n          });\n        }\n      }\n    } catch (error: any) {\n      if (error?.message === 'Claude Code process aborted by user') {\n        this.logger.warn(`Processing aborted for task ID: ${taskId}`);\n      } else {\n        this.logger.error(\n          `Error during task processing iteration for task ID: ${taskId} - ${error.message}`,\n          error.stack,\n        );\n        await this.tasksService.update(taskId, {\n          status: TaskStatus.FAILED,\n        });\n        this.isProcessing = false;\n        this.currentTaskId = null;\n      }\n    }\n  }\n\n  async stopProcessing(): Promise<void> {\n    if (!this.isProcessing) {\n      return;\n    }\n\n    this.logger.log(`Stopping execution of task ${this.currentTaskId}`);\n\n    // Signal any in-flight async operations to abort\n    this.abortController?.abort();\n\n    await this.inputCaptureService.stop();\n\n    this.isProcessing = false;\n    this.currentTaskId = null;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.scheduler.ts",
    "content": "import { Injectable, Logger, OnModuleInit } from '@nestjs/common';\nimport { Cron, CronExpression } from '@nestjs/schedule';\nimport { TasksService } from '../tasks/tasks.service';\nimport { AgentProcessor } from './agent.processor';\nimport { TaskStatus } from '@prisma/client';\nimport { writeFile } from './agent.computer-use';\n\n@Injectable()\nexport class AgentScheduler implements OnModuleInit {\n  private readonly logger = new Logger(AgentScheduler.name);\n\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly agentProcessor: AgentProcessor,\n  ) {}\n\n  async onModuleInit() {\n    this.logger.log('AgentScheduler initialized');\n    await this.handleCron();\n  }\n\n  @Cron(CronExpression.EVERY_5_SECONDS)\n  async handleCron() {\n    const now = new Date();\n    const scheduledTasks = await this.tasksService.findScheduledTasks();\n    for (const scheduledTask of scheduledTasks) {\n      if (scheduledTask.scheduledFor && scheduledTask.scheduledFor < now) {\n        this.logger.debug(\n          `Task ID: ${scheduledTask.id} is scheduled for ${scheduledTask.scheduledFor}, queuing it`,\n        );\n        await this.tasksService.update(scheduledTask.id, {\n          queuedAt: now,\n        });\n      }\n    }\n\n    if (this.agentProcessor.isRunning()) {\n      return;\n    }\n    // Find the highest priority task to execute\n    const task = await this.tasksService.findNextTask();\n    if (task) {\n      if (task.files.length > 0) {\n        this.logger.debug(\n          `Task ID: ${task.id} has files, writing them to the desktop`,\n        );\n        for (const file of task.files) {\n          await writeFile({\n            path: `/home/user/Desktop/${file.name}`,\n            content: file.data, // file.data is already base64 encoded in the database\n          });\n        }\n      }\n\n      await this.tasksService.update(task.id, {\n        status: TaskStatus.RUNNING,\n        executedAt: new Date(),\n      });\n      this.logger.debug(`Processing task ID: ${task.id}`);\n      this.agentProcessor.processTask(task.id);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.tools.ts",
    "content": "/**\n * Common schema definitions for reuse\n */\nconst coordinateSchema = {\n  type: 'object' as const,\n  properties: {\n    x: {\n      type: 'number' as const,\n      description: 'The x-coordinate',\n    },\n    y: {\n      type: 'number' as const,\n      description: 'The y-coordinate',\n    },\n  },\n  required: ['x', 'y'],\n};\n\nconst holdKeysSchema = {\n  type: 'array' as const,\n  items: { type: 'string' as const },\n  description: 'Optional array of keys to hold during the action',\n  nullable: true,\n};\n\nconst buttonSchema = {\n  type: 'string' as const,\n  enum: ['left', 'right', 'middle'],\n  description: 'The mouse button',\n};\n\n/**\n * Tool definitions for mouse actions\n */\nexport const _moveMouseTool = {\n  name: 'computer_move_mouse',\n  description: 'Moves the mouse cursor to the specified coordinates',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description: 'Target coordinates for mouse movement',\n      },\n    },\n    required: ['coordinates'],\n  },\n};\n\nexport const _traceMouseTool = {\n  name: 'computer_trace_mouse',\n  description: 'Moves the mouse cursor along a specified path of coordinates',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      path: {\n        type: 'array' as const,\n        items: coordinateSchema,\n        description: 'Array of coordinate objects representing the path',\n      },\n      holdKeys: holdKeysSchema,\n    },\n    required: ['path'],\n  },\n};\n\nexport const _clickMouseTool = {\n  name: 'computer_click_mouse',\n  description:\n    'Performs a mouse click at the specified coordinates or current position',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description:\n          'Optional click coordinates (defaults to current position)',\n        nullable: true,\n      },\n      button: buttonSchema,\n      holdKeys: holdKeysSchema,\n      clickCount: {\n        type: 'integer' as const,\n        description: 'Number of clicks to perform (e.g., 2 for double-click)',\n        default: 1,\n      },\n    },\n    required: ['button', 'clickCount'],\n  },\n};\n\nexport const _pressMouseTool = {\n  name: 'computer_press_mouse',\n  description: 'Presses or releases a specified mouse button',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description: 'Optional coordinates (defaults to current position)',\n        nullable: true,\n      },\n      button: buttonSchema,\n      press: {\n        type: 'string' as const,\n        enum: ['up', 'down'],\n        description: 'Whether to press down or release up',\n      },\n    },\n    required: ['button', 'press'],\n  },\n};\n\nexport const _dragMouseTool = {\n  name: 'computer_drag_mouse',\n  description: 'Drags the mouse along a path while holding a button',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      path: {\n        type: 'array' as const,\n        items: coordinateSchema,\n        description: 'Array of coordinates representing the drag path',\n      },\n      button: buttonSchema,\n      holdKeys: holdKeysSchema,\n    },\n    required: ['path', 'button'],\n  },\n};\n\nexport const _scrollTool = {\n  name: 'computer_scroll',\n  description: 'Scrolls the mouse wheel in the specified direction',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      coordinates: {\n        ...coordinateSchema,\n        description: 'Coordinates where the scroll should occur',\n      },\n      direction: {\n        type: 'string' as const,\n        enum: ['up', 'down', 'left', 'right'],\n        description: 'The direction to scroll',\n      },\n      scrollCount: {\n        type: 'integer' as const,\n        description: 'Number of scroll steps',\n      },\n      holdKeys: holdKeysSchema,\n    },\n    required: ['coordinates', 'direction', 'scrollCount'],\n  },\n};\n\n/**\n * Tool definitions for keyboard actions\n */\nexport const _typeKeysTool = {\n  name: 'computer_type_keys',\n  description: 'Types a sequence of keys (useful for keyboard shortcuts)',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      keys: {\n        type: 'array' as const,\n        items: { type: 'string' as const },\n        description: 'Array of key names to type in sequence',\n      },\n      delay: {\n        type: 'number' as const,\n        description: 'Optional delay in milliseconds between key presses',\n        nullable: true,\n      },\n    },\n    required: ['keys'],\n  },\n};\n\nexport const _pressKeysTool = {\n  name: 'computer_press_keys',\n  description:\n    'Presses or releases specific keys (useful for holding modifiers)',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      keys: {\n        type: 'array' as const,\n        items: { type: 'string' as const },\n        description: 'Array of key names to press or release',\n      },\n      press: {\n        type: 'string' as const,\n        enum: ['up', 'down'],\n        description: 'Whether to press down or release up',\n      },\n    },\n    required: ['keys', 'press'],\n  },\n};\n\nexport const _typeTextTool = {\n  name: 'computer_type_text',\n  description:\n    'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      text: {\n        type: 'string' as const,\n        description: 'The text string to type',\n      },\n      delay: {\n        type: 'number' as const,\n        description: 'Optional delay in milliseconds between characters',\n        nullable: true,\n      },\n      isSensitive: {\n        type: 'boolean' as const,\n        description: 'Flag to indicate sensitive information',\n        nullable: true,\n      },\n    },\n    required: ['text'],\n  },\n};\n\nexport const _pasteTextTool = {\n  name: 'computer_paste_text',\n  description:\n    'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      text: {\n        type: 'string' as const,\n        description: 'The text string to type',\n      },\n      isSensitive: {\n        type: 'boolean' as const,\n        description: 'Flag to indicate sensitive information',\n        nullable: true,\n      },\n    },\n    required: ['text'],\n  },\n};\n\n/**\n * Tool definitions for utility actions\n */\nexport const _waitTool = {\n  name: 'computer_wait',\n  description: 'Pauses execution for a specified duration',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      duration: {\n        type: 'integer' as const,\n        enum: [500],\n        description: 'The duration to wait in milliseconds',\n      },\n    },\n    required: ['duration'],\n  },\n};\n\nexport const _screenshotTool = {\n  name: 'computer_screenshot',\n  description: 'Captures a screenshot of the current screen',\n  input_schema: {\n    type: 'object' as const,\n    properties: {},\n  },\n};\n\nexport const _cursorPositionTool = {\n  name: 'computer_cursor_position',\n  description: 'Gets the current (x, y) coordinates of the mouse cursor',\n  input_schema: {\n    type: 'object' as const,\n    properties: {},\n  },\n};\n\nexport const _applicationTool = {\n  name: 'computer_application',\n  description: 'Opens or focuses an application and ensures it is fullscreen',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      application: {\n        type: 'string' as const,\n        enum: [\n          'firefox',\n          '1password',\n          'thunderbird',\n          'vscode',\n          'terminal',\n          'desktop',\n          'directory',\n        ],\n        description: 'The application to open or focus',\n      },\n    },\n    required: ['application'],\n  },\n};\n\n/**\n * Tool definitions for task management\n */\nexport const _setTaskStatusTool = {\n  name: 'set_task_status',\n  description: 'Sets the status of the current task',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      status: {\n        type: 'string' as const,\n        enum: ['completed', 'needs_help'],\n        description: 'The status of the task',\n      },\n      description: {\n        type: 'string' as const,\n        description:\n          'If the task is completed, a summary of the task. If the task needs help, a description of the issue or clarification needed.',\n      },\n    },\n    required: ['status', 'description'],\n  },\n};\n\nexport const _createTaskTool = {\n  name: 'create_task',\n  description: 'Creates a new task',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      description: {\n        type: 'string' as const,\n        description: 'The description of the task',\n      },\n      type: {\n        type: 'string' as const,\n        enum: ['IMMEDIATE', 'SCHEDULED'],\n        description: 'The type of the task (defaults to IMMEDIATE)',\n      },\n      scheduledFor: {\n        type: 'string' as const,\n        format: 'date-time',\n        description: 'RFC 3339 / ISO 8601 datetime for scheduled tasks',\n      },\n      priority: {\n        type: 'string' as const,\n        enum: ['LOW', 'MEDIUM', 'HIGH', 'URGENT'],\n        description: 'The priority of the task (defaults to MEDIUM)',\n      },\n    },\n    required: ['description'],\n  },\n};\n\n/**\n * Tool definition for reading files\n */\nexport const _readFileTool = {\n  name: 'computer_read_file',\n  description:\n    'Reads a file from the specified path and returns it as a document content block with base64 encoded data',\n  input_schema: {\n    type: 'object' as const,\n    properties: {\n      path: {\n        type: 'string' as const,\n        description: 'The file path to read from',\n      },\n    },\n    required: ['path'],\n  },\n};\n\n/**\n * Export all tools as an array\n */\nexport const agentTools = [\n  _moveMouseTool,\n  _traceMouseTool,\n  _clickMouseTool,\n  _pressMouseTool,\n  _dragMouseTool,\n  _scrollTool,\n  _typeKeysTool,\n  _pressKeysTool,\n  _typeTextTool,\n  _pasteTextTool,\n  _waitTool,\n  _screenshotTool,\n  _applicationTool,\n  _cursorPositionTool,\n  _setTaskStatusTool,\n  _createTaskTool,\n  _readFileTool,\n];\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/agent.types.ts",
    "content": "import { Message } from '@prisma/client';\nimport { MessageContentBlock } from '@bytebot/shared';\n\nexport interface BytebotAgentResponse {\n  contentBlocks: MessageContentBlock[];\n  tokenUsage: {\n    inputTokens: number;\n    outputTokens: number;\n    totalTokens: number;\n  };\n}\n\nexport interface BytebotAgentService {\n  generateMessage(\n    systemPrompt: string,\n    messages: Message[],\n    model: string,\n    useTools: boolean,\n    signal?: AbortSignal,\n  ): Promise<BytebotAgentResponse>;\n}\n\nexport interface BytebotAgentModel {\n  provider: 'anthropic' | 'openai' | 'google' | 'proxy';\n  name: string;\n  title: string;\n  contextWindow?: number;\n}\n\nexport class BytebotAgentInterrupt extends Error {\n  constructor() {\n    super('BytebotAgentInterrupt');\n    this.name = 'BytebotAgentInterrupt';\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/agent/input-capture.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { io, Socket } from 'socket.io-client';\nimport { randomUUID } from 'crypto';\nimport {\n  convertClickMouseActionToToolUseBlock,\n  convertDragMouseActionToToolUseBlock,\n  convertPressKeysActionToToolUseBlock,\n  convertPressMouseActionToToolUseBlock,\n  convertScrollActionToToolUseBlock,\n  convertTypeKeysActionToToolUseBlock,\n  convertTypeTextActionToToolUseBlock,\n  ImageContentBlock,\n  MessageContentBlock,\n  MessageContentType,\n  ScreenshotToolUseBlock,\n  ToolResultContentBlock,\n  UserActionContentBlock,\n} from '@bytebot/shared';\nimport { Role } from '@prisma/client';\nimport { MessagesService } from '../messages/messages.service';\nimport { ConfigService } from '@nestjs/config';\n\n@Injectable()\nexport class InputCaptureService {\n  private readonly logger = new Logger(InputCaptureService.name);\n  private socket: Socket | null = null;\n  private capturing = false;\n\n  constructor(\n    private readonly messagesService: MessagesService,\n    private readonly configService: ConfigService,\n  ) {}\n\n  isCapturing() {\n    return this.capturing;\n  }\n\n  start(taskId: string) {\n    if (this.socket?.connected && this.capturing) return;\n\n    if (this.socket && !this.socket.connected) {\n      this.socket.connect();\n      return;\n    }\n\n    const baseUrl = this.configService.get<string>('BYTEBOT_DESKTOP_BASE_URL');\n    if (!baseUrl) {\n      this.logger.warn('BYTEBOT_DESKTOP_BASE_URL missing.');\n      return;\n    }\n\n    this.socket = io(baseUrl, { transports: ['websocket'] });\n\n    this.socket.on('connect', () => {\n      this.logger.log('Input socket connected');\n      this.capturing = true;\n    });\n\n    this.socket.on(\n      'screenshotAndAction',\n      async (shot: { image: string }, action: any) => {\n        if (!this.capturing || !taskId) return;\n        // The gateway only sends a click_mouse or drag_mouse action together with screenshots for now.\n        if (action.action !== 'click_mouse' && action.action !== 'drag_mouse')\n          return;\n\n        const userActionBlock: UserActionContentBlock = {\n          type: MessageContentType.UserAction,\n          content: [\n            {\n              type: MessageContentType.Image,\n              source: {\n                data: shot.image,\n                media_type: 'image/png',\n                type: 'base64',\n              },\n            },\n          ],\n        };\n\n        const toolUseId = randomUUID();\n        switch (action.action) {\n          case 'drag_mouse':\n            userActionBlock.content.push(\n              convertDragMouseActionToToolUseBlock(action, toolUseId),\n            );\n            break;\n          case 'click_mouse':\n            userActionBlock.content.push(\n              convertClickMouseActionToToolUseBlock(action, toolUseId),\n            );\n            break;\n        }\n\n        await this.messagesService.create({\n          content: [userActionBlock],\n          role: Role.USER,\n          taskId,\n        });\n      },\n    );\n\n    this.socket.on('action', async (action: any) => {\n      if (!this.capturing || !taskId) return;\n      const toolUseId = randomUUID();\n      const userActionBlock: UserActionContentBlock = {\n        type: MessageContentType.UserAction,\n        content: [],\n      };\n\n      switch (action.action) {\n        case 'drag_mouse':\n          userActionBlock.content.push(\n            convertDragMouseActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'press_mouse':\n          userActionBlock.content.push(\n            convertPressMouseActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'type_keys':\n          userActionBlock.content.push(\n            convertTypeKeysActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'press_keys':\n          userActionBlock.content.push(\n            convertPressKeysActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'type_text':\n          userActionBlock.content.push(\n            convertTypeTextActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        case 'scroll':\n          userActionBlock.content.push(\n            convertScrollActionToToolUseBlock(action, toolUseId),\n          );\n          break;\n        default:\n          this.logger.warn(`Unknown action ${action.action}`);\n      }\n\n      if (userActionBlock.content.length > 0) {\n        await this.messagesService.create({\n          content: [userActionBlock],\n          role: Role.USER,\n          taskId,\n        });\n      }\n    });\n\n    this.socket.on('disconnect', () => {\n      this.logger.log('Input socket disconnected');\n      this.capturing = false;\n    });\n  }\n\n  async stop() {\n    if (!this.socket) return;\n    if (this.socket.connected) this.socket.disconnect();\n    else this.socket.removeAllListeners();\n    this.socket = null;\n    this.capturing = false;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/app.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { AppService } from './app.service';\n\n@Controller()\nexport class AppController {\n  constructor(private readonly appService: AppService) {}\n\n  @Get()\n  getHello(): string {\n    return this.appService.getHello();\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/app.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { AppController } from './app.controller';\nimport { AppService } from './app.service';\nimport { AgentModule } from './agent/agent.module';\nimport { TasksModule } from './tasks/tasks.module';\nimport { MessagesModule } from './messages/messages.module';\nimport { PrismaModule } from './prisma/prisma.module';\nimport { ConfigModule } from '@nestjs/config';\nimport { ScheduleModule } from '@nestjs/schedule';\nimport { EventEmitterModule } from '@nestjs/event-emitter';\n\n@Module({\n  imports: [\n    ScheduleModule.forRoot(),\n    EventEmitterModule.forRoot(),\n    ConfigModule.forRoot({\n      isGlobal: true,\n    }),\n    AgentModule,\n    TasksModule,\n    MessagesModule,\n    PrismaModule,\n  ],\n  controllers: [AppController],\n  providers: [AppService],\n})\nexport class AppModule {}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/app.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class AppService {\n  getHello(): string {\n    return 'Hello World!';\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/main.ts",
    "content": "import { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\nimport { webcrypto } from 'crypto';\nimport { json, urlencoded } from 'express';\n\n// Polyfill for crypto global (required by @nestjs/schedule)\nif (!globalThis.crypto) {\n  globalThis.crypto = webcrypto as any;\n}\n\nasync function bootstrap() {\n  console.log('Starting bytebot-agent application...');\n\n  try {\n    const app = await NestFactory.create(AppModule);\n\n    // Configure body parser with increased payload size limit (50MB)\n    app.use(json({ limit: '50mb' }));\n    app.use(urlencoded({ limit: '50mb', extended: true }));\n\n    // Set global prefix for all routes\n    app.setGlobalPrefix('api');\n\n    // Enable CORS\n    app.enableCors({\n      origin: '*',\n      methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],\n    });\n\n    await app.listen(process.env.PORT ?? 9991);\n  } catch (error) {\n    console.error('Error starting application:', error);\n  }\n}\nbootstrap();\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/messages/messages.module.ts",
    "content": "import { Module, forwardRef } from '@nestjs/common';\nimport { MessagesService } from './messages.service';\nimport { PrismaModule } from '../prisma/prisma.module';\nimport { TasksModule } from '../tasks/tasks.module';\n\n@Module({\n  imports: [PrismaModule, forwardRef(() => TasksModule)],\n  providers: [MessagesService],\n  exports: [MessagesService],\n})\nexport class MessagesModule {}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/messages/messages.service.ts",
    "content": "import {\n  Injectable,\n  NotFoundException,\n  Inject,\n  forwardRef,\n} from '@nestjs/common';\nimport { PrismaService } from '../prisma/prisma.service';\nimport { Message, Role, Prisma } from '@prisma/client';\nimport {\n  MessageContentBlock,\n  isComputerToolUseContentBlock,\n  isToolResultContentBlock,\n  isUserActionContentBlock,\n} from '@bytebot/shared';\nimport { TasksGateway } from '../tasks/tasks.gateway';\n\n// Extended message type for processing\nexport interface ProcessedMessage extends Message {\n  take_over?: boolean;\n}\n\nexport interface GroupedMessages {\n  role: Role;\n  messages: ProcessedMessage[];\n  take_over?: boolean;\n}\n\n@Injectable()\nexport class MessagesService {\n  constructor(\n    private prisma: PrismaService,\n    @Inject(forwardRef(() => TasksGateway))\n    private readonly tasksGateway: TasksGateway,\n  ) {}\n\n  async create(data: {\n    content: MessageContentBlock[];\n    role: Role;\n    taskId: string;\n  }): Promise<Message> {\n    const message = await this.prisma.message.create({\n      data: {\n        content: data.content as Prisma.InputJsonValue,\n        role: data.role,\n        taskId: data.taskId,\n      },\n    });\n\n    this.tasksGateway.emitNewMessage(data.taskId, message);\n\n    return message;\n  }\n\n  async findEvery(taskId: string): Promise<Message[]> {\n    return this.prisma.message.findMany({\n      where: {\n        taskId,\n      },\n      orderBy: {\n        createdAt: 'asc',\n      },\n    });\n  }\n\n  async findAll(\n    taskId: string,\n    options?: {\n      limit?: number;\n      page?: number;\n    },\n  ): Promise<Message[]> {\n    const { limit = 10, page = 1 } = options || {};\n\n    // Calculate offset based on page and limit\n    const offset = (page - 1) * limit;\n\n    return this.prisma.message.findMany({\n      where: {\n        taskId,\n      },\n      orderBy: {\n        createdAt: 'asc',\n      },\n      take: limit,\n      skip: offset,\n    });\n  }\n\n  async findUnsummarized(taskId: string): Promise<Message[]> {\n    return this.prisma.message.findMany({\n      where: {\n        taskId,\n        // find messages that don't have a summaryId\n        summaryId: null,\n      },\n      orderBy: { createdAt: 'asc' },\n    });\n  }\n\n  async attachSummary(\n    taskId: string,\n    summaryId: string,\n    messageIds: string[],\n  ): Promise<void> {\n    if (messageIds.length === 0) {\n      return;\n    }\n\n    await this.prisma.message.updateMany({\n      where: { taskId, id: { in: messageIds } },\n      data: { summaryId },\n    });\n  }\n\n  /**\n   * Groups back-to-back messages from the same role and take_over status\n   */\n  private groupBackToBackMessages(\n    messages: ProcessedMessage[],\n  ): GroupedMessages[] {\n    const groupedConversation: GroupedMessages[] = [];\n    let currentGroup: GroupedMessages | null = null;\n\n    for (const message of messages) {\n      const role = message.role;\n      const isTakeOver = message.take_over || false;\n\n      // If this is the first message, role is different, or take_over status is different from the previous group\n      if (\n        !currentGroup ||\n        currentGroup.role !== role ||\n        currentGroup.take_over !== isTakeOver\n      ) {\n        // Save the previous group if it exists\n        if (currentGroup) {\n          groupedConversation.push(currentGroup);\n        }\n\n        // Start a new group\n        currentGroup = {\n          role: role,\n          messages: [message],\n          take_over: isTakeOver,\n        };\n      } else {\n        // Same role and take_over status as previous, merge the content\n        currentGroup.messages.push(message);\n      }\n    }\n\n    // Add the last group\n    if (currentGroup) {\n      groupedConversation.push(currentGroup);\n    }\n\n    return groupedConversation;\n  }\n\n  /**\n   * Filters and processes messages, adding take_over flags where appropriate\n   * Only text messages from the user should appear as user messages\n   * Computer tool use messages should be shown as assistant messages with take_over flag\n   */\n  private filterMessages(messages: Message[]): ProcessedMessage[] {\n    const filteredMessages: ProcessedMessage[] = [];\n\n    for (const message of messages) {\n      const processedMessage: ProcessedMessage = { ...message };\n      const contentBlocks = message.content as MessageContentBlock[];\n\n      // If the role is a user message and all the content blocks are tool result blocks or they are take over actions\n      if (message.role === Role.USER) {\n        if (contentBlocks.every((block) => isToolResultContentBlock(block))) {\n          // Pure tool results should be shown as assistant messages\n          processedMessage.role = Role.ASSISTANT;\n        } else if (\n          contentBlocks.every((block) => isUserActionContentBlock(block))\n        ) {\n          // Extract computer tool use (take over actions) from the user action content blocks and show them as assistant messages with take_over flag\n          processedMessage.content = contentBlocks\n            .flatMap((block) => {\n              return block.content;\n            })\n            .filter((block) => isComputerToolUseContentBlock(block));\n          processedMessage.role = Role.ASSISTANT;\n          processedMessage.take_over = true;\n        }\n        // If there are text blocks mixed with tool blocks, keep as user message\n        // Only pure text messages from user should remain as user messages\n      }\n\n      filteredMessages.push(processedMessage);\n    }\n\n    return filteredMessages;\n  }\n\n  /**\n   * Returns raw messages without any processing\n   */\n  async findRawMessages(\n    taskId: string,\n    options?: {\n      limit?: number;\n      page?: number;\n    },\n  ): Promise<Message[]> {\n    return this.findAll(taskId, options);\n  }\n\n  /**\n   * Returns processed and grouped messages for the chat UI\n   */\n  async findProcessedMessages(\n    taskId: string,\n    options?: {\n      limit?: number;\n      page?: number;\n    },\n  ): Promise<GroupedMessages[]> {\n    const messages = await this.findAll(taskId, options);\n    const filteredMessages = this.filterMessages(messages);\n    return this.groupBackToBackMessages(filteredMessages);\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/prisma/prisma.module.ts",
    "content": "import { Global, Module } from '@nestjs/common';\n\nimport { PrismaService } from './prisma.service';\n\n@Global()\n@Module({\n  providers: [PrismaService],\n  exports: [PrismaService],\n})\nexport class PrismaModule {}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/prisma/prisma.service.ts",
    "content": "import { Injectable, OnModuleInit } from '@nestjs/common';\nimport { PrismaClient } from '@prisma/client';\n\n@Injectable()\nexport class PrismaService extends PrismaClient implements OnModuleInit {\n  constructor() {\n    super();\n  }\n\n  async onModuleInit() {\n    await this.$connect();\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/dto/add-task-message.dto.ts",
    "content": "import { IsNotEmpty, IsString } from 'class-validator';\n\nexport class AddTaskMessageDto {\n  @IsNotEmpty()\n  @IsString()\n  message: string;\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/dto/create-task.dto.ts",
    "content": "import {\n  IsArray,\n  IsDate,\n  IsNotEmpty,\n  IsNumber,\n  IsOptional,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { Role, TaskPriority, TaskType } from '@prisma/client';\n\nexport class TaskFileDto {\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  @IsNotEmpty()\n  @IsString()\n  base64: string;\n\n  @IsNotEmpty()\n  @IsString()\n  type: string;\n\n  @IsNotEmpty()\n  @IsNumber()\n  size: number;\n}\n\nexport class CreateTaskDto {\n  @IsNotEmpty()\n  @IsString()\n  description: string;\n\n  @IsOptional()\n  @IsString()\n  type?: TaskType;\n\n  @IsOptional()\n  @IsDate()\n  scheduledFor?: Date;\n\n  @IsOptional()\n  @IsString()\n  priority?: TaskPriority;\n\n  @IsOptional()\n  @IsString()\n  createdBy?: Role;\n\n  @IsOptional()\n  model?: any;\n\n  @IsOptional()\n  @IsArray()\n  @ValidateNested({ each: true })\n  @Type(() => TaskFileDto)\n  files?: TaskFileDto[];\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/dto/update-task.dto.ts",
    "content": "import { IsEnum, IsOptional } from 'class-validator';\nimport { TaskPriority, TaskStatus } from '@prisma/client';\n\nexport class UpdateTaskDto {\n  @IsOptional()\n  @IsEnum(TaskStatus)\n  status?: TaskStatus;\n\n  @IsOptional()\n  @IsEnum(TaskPriority)\n  priority?: TaskPriority;\n\n  @IsOptional()\n  queuedAt?: Date;\n\n  @IsOptional()\n  executedAt?: Date;\n\n  @IsOptional()\n  completedAt?: Date;\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/tasks.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  Post,\n  Body,\n  Param,\n  Delete,\n  HttpStatus,\n  HttpCode,\n  Query,\n} from '@nestjs/common';\nimport { TasksService } from './tasks.service';\nimport { CreateTaskDto } from './dto/create-task.dto';\nimport { Message, Task } from '@prisma/client';\nimport { AddTaskMessageDto } from './dto/add-task-message.dto';\nimport { MessagesService } from '../messages/messages.service';\nimport { BytebotAgentModel } from 'src/agent/agent.types';\n\n@Controller('tasks')\nexport class TasksController {\n  constructor(\n    private readonly tasksService: TasksService,\n    private readonly messagesService: MessagesService,\n  ) {}\n\n  @Post()\n  @HttpCode(HttpStatus.CREATED)\n  async create(@Body() createTaskDto: CreateTaskDto): Promise<Task> {\n    return this.tasksService.create(createTaskDto);\n  }\n\n  @Get()\n  async findAll(\n    @Query('page') page?: string,\n    @Query('limit') limit?: string,\n    @Query('status') status?: string,\n    @Query('statuses') statuses?: string,\n  ): Promise<{ tasks: Task[]; total: number; totalPages: number }> {\n    const pageNum = page ? parseInt(page, 10) : 1;\n    const limitNum = limit ? parseInt(limit, 10) : 10;\n\n    // Handle both single status and multiple statuses\n    let statusFilter: string[] | undefined;\n    if (statuses) {\n      statusFilter = statuses.split(',');\n    } else if (status) {\n      statusFilter = [status];\n    }\n\n    return this.tasksService.findAll(pageNum, limitNum, statusFilter);\n  }\n\n  @Get('models')\n  async getModels() {\n    return [\n      {\n        provider: 'anthropic',\n        name: 'claude-code',\n        title: 'Claude Code',\n        contextWindow: 200000,\n      },\n    ];\n  }\n\n  @Get(':id')\n  async findById(@Param('id') id: string): Promise<Task> {\n    return this.tasksService.findById(id);\n  }\n\n  @Get(':id/messages')\n  async taskMessages(\n    @Param('id') taskId: string,\n    @Query('limit') limit?: string,\n    @Query('page') page?: string,\n  ): Promise<Message[]> {\n    const options = {\n      limit: limit ? parseInt(limit, 10) : undefined,\n      page: page ? parseInt(page, 10) : undefined,\n    };\n\n    const messages = await this.messagesService.findAll(taskId, options);\n    return messages;\n  }\n\n  @Post(':id/messages')\n  @HttpCode(HttpStatus.CREATED)\n  async addTaskMessage(\n    @Param('id') taskId: string,\n    @Body() guideTaskDto: AddTaskMessageDto,\n  ): Promise<Task> {\n    return this.tasksService.addTaskMessage(taskId, guideTaskDto);\n  }\n\n  @Get(':id/messages/raw')\n  async taskRawMessages(\n    @Param('id') taskId: string,\n    @Query('limit') limit?: string,\n    @Query('page') page?: string,\n  ): Promise<Message[]> {\n    const options = {\n      limit: limit ? parseInt(limit, 10) : undefined,\n      page: page ? parseInt(page, 10) : undefined,\n    };\n\n    return this.messagesService.findRawMessages(taskId, options);\n  }\n\n  @Get(':id/messages/processed')\n  async taskProcessedMessages(\n    @Param('id') taskId: string,\n    @Query('limit') limit?: string,\n    @Query('page') page?: string,\n  ) {\n    const options = {\n      limit: limit ? parseInt(limit, 10) : undefined,\n      page: page ? parseInt(page, 10) : undefined,\n    };\n\n    return this.messagesService.findProcessedMessages(taskId, options);\n  }\n\n  @Delete(':id')\n  @HttpCode(HttpStatus.NO_CONTENT)\n  async delete(@Param('id') id: string): Promise<void> {\n    await this.tasksService.delete(id);\n  }\n\n  @Post(':id/takeover')\n  @HttpCode(HttpStatus.OK)\n  async takeOver(@Param('id') taskId: string): Promise<Task> {\n    return this.tasksService.takeOver(taskId);\n  }\n\n  @Post(':id/resume')\n  @HttpCode(HttpStatus.OK)\n  async resume(@Param('id') taskId: string): Promise<Task> {\n    return this.tasksService.resume(taskId);\n  }\n\n  @Post(':id/cancel')\n  @HttpCode(HttpStatus.OK)\n  async cancel(@Param('id') taskId: string): Promise<Task> {\n    return this.tasksService.cancel(taskId);\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/tasks.gateway.ts",
    "content": "import {\n  WebSocketGateway,\n  WebSocketServer,\n  SubscribeMessage,\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n} from '@nestjs/websockets';\nimport { Server, Socket } from 'socket.io';\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\n@WebSocketGateway({\n  cors: {\n    origin: '*',\n    methods: ['GET', 'POST'],\n  },\n})\nexport class TasksGateway implements OnGatewayConnection, OnGatewayDisconnect {\n  @WebSocketServer()\n  server: Server;\n\n  handleConnection(client: Socket) {\n    console.log(`Client connected: ${client.id}`);\n  }\n\n  handleDisconnect(client: Socket) {\n    console.log(`Client disconnected: ${client.id}`);\n  }\n\n  @SubscribeMessage('join_task')\n  handleJoinTask(client: Socket, taskId: string) {\n    client.join(`task_${taskId}`);\n    console.log(`Client ${client.id} joined task ${taskId}`);\n  }\n\n  @SubscribeMessage('leave_task')\n  handleLeaveTask(client: Socket, taskId: string) {\n    client.leave(`task_${taskId}`);\n    console.log(`Client ${client.id} left task ${taskId}`);\n  }\n\n  emitTaskUpdate(taskId: string, task: any) {\n    this.server.to(`task_${taskId}`).emit('task_updated', task);\n  }\n\n  emitNewMessage(taskId: string, message: any) {\n    this.server.to(`task_${taskId}`).emit('new_message', message);\n  }\n\n  emitTaskCreated(task: any) {\n    this.server.emit('task_created', task);\n  }\n\n  emitTaskDeleted(taskId: string) {\n    this.server.emit('task_deleted', taskId);\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/tasks.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TasksController } from './tasks.controller';\nimport { TasksService } from './tasks.service';\nimport { TasksGateway } from './tasks.gateway';\nimport { PrismaModule } from '../prisma/prisma.module';\nimport { MessagesModule } from '../messages/messages.module';\n\n@Module({\n  imports: [PrismaModule, MessagesModule],\n  controllers: [TasksController],\n  providers: [TasksService, TasksGateway],\n  exports: [TasksService, TasksGateway],\n})\nexport class TasksModule {}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/src/tasks/tasks.service.ts",
    "content": "import {\n  Injectable,\n  NotFoundException,\n  Logger,\n  BadRequestException,\n  Inject,\n  forwardRef,\n} from '@nestjs/common';\nimport { PrismaService } from '../prisma/prisma.service';\nimport { CreateTaskDto } from './dto/create-task.dto';\nimport { UpdateTaskDto } from './dto/update-task.dto';\nimport {\n  Task,\n  Role,\n  Prisma,\n  TaskStatus,\n  TaskType,\n  TaskPriority,\n  File,\n} from '@prisma/client';\nimport { AddTaskMessageDto } from './dto/add-task-message.dto';\nimport { TasksGateway } from './tasks.gateway';\nimport { ConfigService } from '@nestjs/config';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\n\n@Injectable()\nexport class TasksService {\n  private readonly logger = new Logger(TasksService.name);\n\n  constructor(\n    readonly prisma: PrismaService,\n    @Inject(forwardRef(() => TasksGateway))\n    private readonly tasksGateway: TasksGateway,\n    private readonly configService: ConfigService,\n    private readonly eventEmitter: EventEmitter2,\n  ) {\n    this.logger.log('TasksService initialized');\n  }\n\n  async create(createTaskDto: CreateTaskDto): Promise<Task> {\n    this.logger.log(\n      `Creating new task with description: ${createTaskDto.description}`,\n    );\n\n    const task = await this.prisma.$transaction(async (prisma) => {\n      // Create the task first\n      this.logger.debug('Creating task record in database');\n      const task = await prisma.task.create({\n        data: {\n          description: createTaskDto.description,\n          type: createTaskDto.type || TaskType.IMMEDIATE,\n          priority: createTaskDto.priority || TaskPriority.MEDIUM,\n          status: TaskStatus.PENDING,\n          createdBy: createTaskDto.createdBy || Role.USER,\n          model: createTaskDto.model,\n          ...(createTaskDto.scheduledFor\n            ? { scheduledFor: createTaskDto.scheduledFor }\n            : {}),\n        },\n      });\n      this.logger.log(`Task created successfully with ID: ${task.id}`);\n\n      let filesDescription = '';\n\n      // Save files if provided\n      if (createTaskDto.files && createTaskDto.files.length > 0) {\n        this.logger.debug(\n          `Saving ${createTaskDto.files.length} file(s) for task ID: ${task.id}`,\n        );\n        filesDescription += `\\n`;\n\n        const filePromises = createTaskDto.files.map((file) => {\n          // Extract base64 data without the data URL prefix\n          const base64Data = file.base64.includes('base64,')\n            ? file.base64.split('base64,')[1]\n            : file.base64;\n\n          filesDescription += `\\nFile ${file.name} written to desktop.`;\n\n          return prisma.file.create({\n            data: {\n              name: file.name,\n              type: file.type || 'application/octet-stream',\n              size: file.size,\n              data: base64Data,\n              taskId: task.id,\n            },\n          });\n        });\n\n        await Promise.all(filePromises);\n        this.logger.debug(`Files saved successfully for task ID: ${task.id}`);\n      }\n\n      // Create the initial system message\n      this.logger.debug(`Creating initial message for task ID: ${task.id}`);\n      await prisma.message.create({\n        data: {\n          content: [\n            {\n              type: 'text',\n              text: `${createTaskDto.description} ${filesDescription}`,\n            },\n          ] as Prisma.InputJsonValue,\n          role: Role.USER,\n          taskId: task.id,\n        },\n      });\n      this.logger.debug(`Initial message created for task ID: ${task.id}`);\n\n      return task;\n    });\n\n    this.tasksGateway.emitTaskCreated(task);\n\n    return task;\n  }\n\n  async findScheduledTasks(): Promise<Task[]> {\n    return this.prisma.task.findMany({\n      where: {\n        scheduledFor: {\n          not: null,\n        },\n        queuedAt: null,\n      },\n      orderBy: [{ scheduledFor: 'asc' }],\n    });\n  }\n\n  async findNextTask(): Promise<(Task & { files: File[] }) | null> {\n    const task = await this.prisma.task.findFirst({\n      where: {\n        status: {\n          in: [TaskStatus.RUNNING, TaskStatus.PENDING],\n        },\n      },\n      orderBy: [\n        { executedAt: 'asc' },\n        { priority: 'desc' },\n        { queuedAt: 'asc' },\n        { createdAt: 'asc' },\n      ],\n      include: {\n        files: true,\n      },\n    });\n\n    if (task) {\n      this.logger.log(\n        `Found existing task with ID: ${task.id}, and status ${task.status}. Resuming.`,\n      );\n    }\n\n    return task;\n  }\n\n  async findAll(\n    page = 1,\n    limit = 10,\n    statuses?: string[],\n  ): Promise<{ tasks: Task[]; total: number; totalPages: number }> {\n    this.logger.log(\n      `Retrieving tasks - page: ${page}, limit: ${limit}, statuses: ${statuses?.join(',')}`,\n    );\n\n    const skip = (page - 1) * limit;\n\n    const whereClause: Prisma.TaskWhereInput =\n      statuses && statuses.length > 0\n        ? { status: { in: statuses as TaskStatus[] } }\n        : {};\n\n    const [tasks, total] = await Promise.all([\n      this.prisma.task.findMany({\n        where: whereClause,\n        orderBy: {\n          createdAt: 'desc',\n        },\n        skip,\n        take: limit,\n      }),\n      this.prisma.task.count({ where: whereClause }),\n    ]);\n\n    const totalPages = Math.ceil(total / limit);\n    this.logger.debug(`Retrieved ${tasks.length} tasks out of ${total} total`);\n\n    return { tasks, total, totalPages };\n  }\n\n  async findById(id: string): Promise<Task> {\n    this.logger.log(`Retrieving task by ID: ${id}`);\n\n    try {\n      const task = await this.prisma.task.findUnique({\n        where: { id },\n        include: {\n          files: true,\n        },\n      });\n\n      if (!task) {\n        this.logger.warn(`Task with ID: ${id} not found`);\n        throw new NotFoundException(`Task with ID ${id} not found`);\n      }\n\n      this.logger.debug(`Retrieved task with ID: ${id}`);\n      return task;\n    } catch (error: any) {\n      this.logger.error(`Error retrieving task ID: ${id} - ${error.message}`);\n      this.logger.error(error.stack);\n      throw error;\n    }\n  }\n\n  async update(id: string, updateTaskDto: UpdateTaskDto): Promise<Task> {\n    this.logger.log(`Updating task with ID: ${id}`);\n    this.logger.debug(`Update data: ${JSON.stringify(updateTaskDto)}`);\n\n    const existingTask = await this.findById(id);\n\n    if (!existingTask) {\n      this.logger.warn(`Task with ID: ${id} not found for update`);\n      throw new NotFoundException(`Task with ID ${id} not found`);\n    }\n\n    let updatedTask = await this.prisma.task.update({\n      where: { id },\n      data: updateTaskDto,\n    });\n\n    if (updateTaskDto.status === TaskStatus.COMPLETED) {\n      this.eventEmitter.emit('task.completed', { taskId: id });\n    } else if (updateTaskDto.status === TaskStatus.NEEDS_HELP) {\n      updatedTask = await this.takeOver(id);\n    } else if (updateTaskDto.status === TaskStatus.FAILED) {\n      this.eventEmitter.emit('task.failed', { taskId: id });\n    }\n\n    this.logger.log(`Successfully updated task ID: ${id}`);\n    this.logger.debug(`Updated task: ${JSON.stringify(updatedTask)}`);\n\n    this.tasksGateway.emitTaskUpdate(id, updatedTask);\n\n    return updatedTask;\n  }\n\n  async delete(id: string): Promise<Task> {\n    this.logger.log(`Deleting task with ID: ${id}`);\n\n    const deletedTask = await this.prisma.task.delete({\n      where: { id },\n    });\n\n    this.logger.log(`Successfully deleted task ID: ${id}`);\n\n    this.tasksGateway.emitTaskDeleted(id);\n\n    return deletedTask;\n  }\n\n  async addTaskMessage(taskId: string, addTaskMessageDto: AddTaskMessageDto) {\n    const task = await this.findById(taskId);\n    if (!task) {\n      this.logger.warn(`Task with ID: ${taskId} not found for guiding`);\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    const message = await this.prisma.message.create({\n      data: {\n        content: [{ type: 'text', text: addTaskMessageDto.message }],\n        role: Role.USER,\n        taskId,\n      },\n    });\n\n    this.tasksGateway.emitNewMessage(taskId, message);\n    return task;\n  }\n\n  async resume(taskId: string): Promise<Task> {\n    this.logger.log(`Resuming task ID: ${taskId}`);\n\n    const task = await this.findById(taskId);\n    if (!task) {\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    if (task.control !== Role.USER) {\n      throw new BadRequestException(`Task ${taskId} is not under user control`);\n    }\n\n    const updatedTask = await this.prisma.task.update({\n      where: { id: taskId },\n      data: {\n        control: Role.ASSISTANT,\n        status: TaskStatus.RUNNING,\n      },\n    });\n\n    try {\n      await fetch(\n        `${this.configService.get<string>('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/stop`,\n        { method: 'POST' },\n      );\n    } catch (error) {\n      this.logger.error('Failed to stop input tracking', error);\n    }\n\n    // Broadcast resume event so AgentProcessor can react\n    this.eventEmitter.emit('task.resume', { taskId });\n\n    this.logger.log(`Task ${taskId} resumed`);\n    this.tasksGateway.emitTaskUpdate(taskId, updatedTask);\n\n    return updatedTask;\n  }\n\n  async takeOver(taskId: string): Promise<Task> {\n    this.logger.log(`Taking over control for task ID: ${taskId}`);\n\n    const task = await this.findById(taskId);\n    if (!task) {\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    if (task.control !== Role.ASSISTANT) {\n      throw new BadRequestException(\n        `Task ${taskId} is not under agent control`,\n      );\n    }\n\n    const updatedTask = await this.prisma.task.update({\n      where: { id: taskId },\n      data: {\n        control: Role.USER,\n      },\n    });\n\n    try {\n      await fetch(\n        `${this.configService.get<string>('BYTEBOT_DESKTOP_BASE_URL')}/input-tracking/start`,\n        { method: 'POST' },\n      );\n    } catch (error) {\n      this.logger.error('Failed to start input tracking', error);\n    }\n\n    // Broadcast takeover event so AgentProcessor can react\n    this.eventEmitter.emit('task.takeover', { taskId });\n\n    this.logger.log(`Task ${taskId} takeover initiated`);\n    this.tasksGateway.emitTaskUpdate(taskId, updatedTask);\n\n    return updatedTask;\n  }\n\n  async cancel(taskId: string): Promise<Task> {\n    this.logger.log(`Cancelling task ID: ${taskId}`);\n\n    const task = await this.findById(taskId);\n    if (!task) {\n      throw new NotFoundException(`Task with ID ${taskId} not found`);\n    }\n\n    if (\n      task.status === TaskStatus.COMPLETED ||\n      task.status === TaskStatus.FAILED ||\n      task.status === TaskStatus.CANCELLED\n    ) {\n      throw new BadRequestException(\n        `Task ${taskId} is already completed, failed, or cancelled`,\n      );\n    }\n\n    const updatedTask = await this.prisma.task.update({\n      where: { id: taskId },\n      data: {\n        status: TaskStatus.CANCELLED,\n      },\n    });\n\n    // Broadcast cancel event so AgentProcessor can cancel processing\n    this.eventEmitter.emit('task.cancel', { taskId });\n\n    this.logger.log(`Task ${taskId} cancelled and marked as failed`);\n    this.tasksGateway.emitTaskUpdate(taskId, updatedTask);\n\n    return updatedTask;\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\"]\n}\n"
  },
  {
    "path": "packages/bytebot-agent-cc/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true,\n    \"skipLibCheck\": true,\n    \"strictNullChecks\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitAny\": false,\n    \"strictBindCallApply\": false,\n    \"noFallthroughCasesInSwitch\": false\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-llm-proxy/Dockerfile",
    "content": "FROM ghcr.io/berriai/litellm:main-stable\n\n# Add custom config into the image\nCOPY ./bytebot-llm-proxy/litellm-config.yaml /app/config.yaml\n\nCMD [\"--config\", \"/app/config.yaml\", \"--port\", \"4000\"]"
  },
  {
    "path": "packages/bytebot-llm-proxy/litellm-config.yaml",
    "content": "model_list:\n  # Anthropic Models\n  - model_name: claude-opus-4\n    litellm_params:\n      model: anthropic/claude-opus-4-20250514\n      api_key: os.environ/ANTHROPIC_API_KEY\n  - model_name: claude-sonnet-4\n    litellm_params:\n      model: anthropic/claude-sonnet-4-20250514\n      api_key: os.environ/ANTHROPIC_API_KEY\n\n  # OpenAI Models\n  - model_name: gpt-4.1\n    litellm_params:\n      model: openai/gpt-4.1\n      api_key: os.environ/OPENAI_API_KEY\n  - model_name: gpt-4o\n    litellm_params:\n      model: openai/gpt-4o\n      api_key: os.environ/OPENAI_API_KEY\n\n  # Gemini Models\n  - model_name: gemini-2.5-pro\n    litellm_params:\n      model: gemini/gemini-2.5-pro\n      api_key: os.environ/GEMINI_API_KEY\n  - model_name: gemini-2.5-flash\n    litellm_params:\n      model: gemini/gemini-2.5-flash\n      api_key: os.environ/GEMINI_API_KEY\n"
  },
  {
    "path": "packages/bytebot-ui/.dockerignore",
    "content": "**/node_modules\n**/dist\n**/.next\n**/.git\n**/.vscode\n**/.env*\n**/npm-debug.log\n**/yarn-debug.log\n**/yarn-error.log\n**/package-lock.json"
  },
  {
    "path": "packages/bytebot-ui/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "packages/bytebot-ui/.prettierrc.json",
    "content": "{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"]\n}\n"
  },
  {
    "path": "packages/bytebot-ui/Dockerfile",
    "content": "# Base image\nFROM node:20-alpine\n\n# Declare build arguments\nARG BYTEBOT_AGENT_BASE_URL\nARG BYTEBOT_DESKTOP_VNC_URL\n\n# Set environment variables for the build process\nENV BYTEBOT_AGENT_BASE_URL=${BYTEBOT_AGENT_BASE_URL}\nENV BYTEBOT_DESKTOP_VNC_URL=${BYTEBOT_DESKTOP_VNC_URL}\n\n# Create app directory\nWORKDIR /app\n\n# Copy app source\nCOPY ./shared ./shared\nCOPY ./bytebot-ui/ ./bytebot-ui\n\nWORKDIR /app/bytebot-ui\n\n# Install dependencies\nRUN npm install\n\nRUN npm run build\n\n# Run the application\nCMD [\"npm\", \"run\", \"start\"] \n\n\n"
  },
  {
    "path": "packages/bytebot-ui/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"stone\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "packages/bytebot-ui/eslint.config.mjs",
    "content": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [\n  ...compat.extends(\"next/core-web-vitals\", \"next/typescript\"),\n];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "packages/bytebot-ui/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\nimport dotenv from \"dotenv\";\n\ndotenv.config();\n\nconst nextConfig: NextConfig = {\n  transpilePackages: [\"@bytebot/shared\"],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "packages/bytebot-ui/package.json",
    "content": "{\n  \"name\": \"bytebot-ui\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"npm run build --prefix ../shared && tsx server.ts\",\n    \"build\": \"npm run build --prefix ../shared && next build\",\n    \"start\": \"npm run build --prefix ../shared && tsx server.ts\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^0.39.0\",\n    \"@bytebot/shared\": \"../shared\",\n    \"@hugeicons/core-free-icons\": \"^1.0.14\",\n    \"@hugeicons/react\": \"^1.0.5\",\n    \"@prisma/client\": \"^6.5.0\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.15\",\n    \"@radix-ui/react-label\": \"^2.1.7\",\n    \"@radix-ui/react-popover\": \"^1.1.11\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.3\",\n    \"@radix-ui/react-select\": \"^2.2.2\",\n    \"@radix-ui/react-separator\": \"^1.1.2\",\n    \"@radix-ui/react-slot\": \"^1.1.2\",\n    \"@radix-ui/react-switch\": \"^1.1.3\",\n    \"@types/express\": \"^5.0.1\",\n    \"@types/http-proxy\": \"^1.17.16\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"date-fns\": \"^4.1.0\",\n    \"dotenv\": \"^16.4.7\",\n    \"express\": \"^4.21.2\",\n    \"http-proxy\": \"^1.18.1\",\n    \"http-proxy-middleware\": \"^3.0.5\",\n    \"lucide-react\": \"^0.517.0\",\n    \"motion\": \"^12.12.1\",\n    \"next\": \">=15.4.7\",\n    \"next-themes\": \"^0.4.6\",\n    \"next-transpile-modules\": \"^10.0.1\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-markdown\": \"^10.1.0\",\n    \"react-vnc\": \"^3.1.0\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"tailwind-merge\": \"^3.0.2\",\n    \"tsx\": \"^4.19.3\",\n    \"tw-animate-css\": \"^1.2.4\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3\",\n    \"@tailwindcss/postcss\": \"^4.1.3\",\n    \"@types/node\": \"^20.17.27\",\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"eslint\": \"^9\",\n    \"eslint-config-next\": \"15.2.3\",\n    \"prettier\": \"^3.5.3\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.11\",\n    \"prisma\": \"^6.5.0\",\n    \"tailwindcss\": \"^4\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "packages/bytebot-ui/postcss.config.mjs",
    "content": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/bytebot-ui/server.ts",
    "content": "import express from \"express\";\nimport { createProxyMiddleware } from \"http-proxy-middleware\";\nimport { createProxyServer } from \"http-proxy\";\nimport next from \"next\";\nimport { createServer } from \"http\";\nimport dotenv from \"dotenv\";\n\n// Load environment variables\ndotenv.config();\n\nconst dev = process.env.NODE_ENV !== \"production\";\nconst hostname = process.env.HOSTNAME || \"localhost\";\nconst port = parseInt(process.env.PORT || \"9992\", 10);\n\n// Backend URLs\nconst BYTEBOT_AGENT_BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL;\nconst BYTEBOT_DESKTOP_VNC_URL = process.env.BYTEBOT_DESKTOP_VNC_URL;\n\nconst app = next({ dev, hostname, port });\n\napp\n  .prepare()\n  .then(() => {\n    const handle = app.getRequestHandler();\n    const nextUpgradeHandler = app.getUpgradeHandler();\n\n    const vncProxy = createProxyServer({ changeOrigin: true, ws: true });\n\n    const expressApp = express();\n    const server = createServer(expressApp);\n\n    // WebSocket proxy for Socket.IO connections to backend\n    const tasksProxy = createProxyMiddleware({\n      target: BYTEBOT_AGENT_BASE_URL,\n      ws: true,\n      pathRewrite: { \"^/api/proxy/tasks\": \"/socket.io\" },\n    });\n\n    // Apply HTTP proxies\n    expressApp.use(\"/api/proxy/tasks\", tasksProxy);\n    expressApp.use(\"/api/proxy/websockify\", (req, res) => {\n      console.log(\"Proxying websockify request\");\n      // Rewrite path\n      const targetUrl = new URL(BYTEBOT_DESKTOP_VNC_URL!);\n      req.url =\n        targetUrl.pathname +\n        (req.url?.replace(/^\\/api\\/proxy\\/websockify/, \"\") || \"\");\n      vncProxy.web(req, res, {\n        target: `${targetUrl.protocol}//${targetUrl.host}`,\n      });\n    });\n\n    // Handle all other requests with Next.js\n    expressApp.all(\"*\", (req, res) => handle(req, res));\n\n    // Properly upgrade WebSocket connections\n    server.on(\"upgrade\", (request, socket, head) => {\n      const { pathname } = new URL(\n        request.url!,\n        `http://${request.headers.host}`,\n      );\n\n      if (pathname.startsWith(\"/api/proxy/tasks\")) {\n        return tasksProxy.upgrade(request, socket as any, head);\n      }\n\n      if (pathname.startsWith(\"/api/proxy/websockify\")) {\n        const targetUrl = new URL(BYTEBOT_DESKTOP_VNC_URL!);\n        request.url =\n          targetUrl.pathname +\n          (request.url?.replace(/^\\/api\\/proxy\\/websockify/, \"\") || \"\");\n        console.log(\"Proxying websockify upgrade request: \", request.url);\n        return vncProxy.ws(request, socket as any, head, {\n          target: `${targetUrl.protocol}//${targetUrl.host}`,\n        });\n      }\n\n      nextUpgradeHandler(request, socket, head);\n    });\n\n    server.listen(port, hostname, () => {\n      console.log(`> Ready on http://${hostname}:${port}`);\n    });\n  })\n  .catch((err) => {\n    console.error(\"Server failed to start:\", err);\n    process.exit(1);\n  });\n"
  },
  {
    "path": "packages/bytebot-ui/src/app/api/[[...path]]/route.ts",
    "content": "import { NextRequest } from \"next/server\";\n\n/* -------------------------------------------------------------------- */\n/* generic proxy helper                                                 */\n/* -------------------------------------------------------------------- */\nasync function proxy(req: NextRequest, path: string[]): Promise<Response> {\n  const BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL!;\n  const subPath = path.length ? path.join(\"/\") : \"\";\n  const url = `${BASE_URL}/${subPath}${req.nextUrl.search}`;\n\n  // Extract cookies from the incoming request\n  const cookies = req.headers.get(\"cookie\");\n\n  const init: RequestInit = {\n    method: req.method,\n    headers: {\n      \"Content-Type\": \"application/json\",\n      ...(cookies && { Cookie: cookies }),\n    },\n    body:\n      req.method === \"GET\" || req.method === \"HEAD\"\n        ? undefined\n        : await req.text(),\n  };\n\n  const res = await fetch(url, init);\n  const body = await res.text();\n\n  // Extract Set-Cookie headers from the backend response\n  const setCookieHeaders = res.headers.getSetCookie?.() || [];\n\n  // Create response headers\n  const responseHeaders = new Headers({\n    \"Content-Type\": \"application/json\",\n  });\n\n  // Add Set-Cookie headers if they exist\n  setCookieHeaders.forEach((cookie) => {\n    responseHeaders.append(\"Set-Cookie\", cookie);\n  });\n\n  return new Response(body, {\n    status: res.status,\n    headers: responseHeaders,\n  });\n}\n\n/* -------------------------------------------------------------------- */\n/* route handlers                                                       */\n/* -------------------------------------------------------------------- */\ntype PathParams = Promise<{ path?: string[] }>; // <- Promise is the key\n\nasync function handler(req: NextRequest, { params }: { params: PathParams }) {\n  const { path } = await params;\n  return proxy(req, path ?? []);\n}\n\nexport const GET = handler;\nexport const POST = handler;\nexport const PUT = handler;\nexport const PATCH = handler;\nexport const DELETE = handler;\nexport const OPTIONS = handler;\nexport const HEAD = handler;\n"
  },
  {
    "path": "packages/bytebot-ui/src/app/desktop/page.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { Header } from \"@/components/layout/Header\";\nimport { DesktopContainer } from \"@/components/ui/desktop-container\";\n\nexport default function DesktopPage() {\n  return (\n    <div className=\"flex h-screen flex-col overflow-hidden\">\n      <Header />\n\n      <main className=\"m-2 flex-1 overflow-hidden px-2 py-4\">\n        <div className=\"flex h-full items-center justify-center\">\n          {/* Main container */}\n          <div className=\"w-[60%]\">\n            <DesktopContainer viewOnly={false} status=\"live_view\">\n              {/* No action buttons for desktop page */}\n            </DesktopContainer>\n          </div>\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --color-background: var(--color-bytebot-bronze-light-4);\n  --color-foreground: var(--foreground);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n  --color-sidebar-ring: var(--sidebar-ring);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar: var(--sidebar);\n  --color-chart-5: var(--chart-5);\n  --color-chart-4: var(--chart-4);\n  --color-chart-3: var(--chart-3);\n  --color-chart-2: var(--chart-2);\n  --color-chart-1: var(--chart-1);\n  --color-ring: var(--ring);\n  --color-input: var(--input);\n  --color-border: var(--border);\n  --color-destructive: var(--destructive);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-accent: var(--accent);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-muted: var(--muted);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-secondary: var(--secondary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-primary: var(--primary);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-popover: var(--popover);\n  --color-card-foreground: var(--card-foreground);\n  --color-card: var(--card);\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n\n  /* Colors */\n\n  /* Base */\n  --color-bytebot-white: rgba(255, 255, 255, 1);\n  --color-bytebot-transparent: rgba(255, 255, 255, 0);\n  --color-bytebot-black: rgba(0, 0, 0, 1);\n  \n  /* Bronze light */\n  --color-bytebot-bronze-light-1: rgba(251, 249, 249, 1);\n  --color-bytebot-bronze-light-2: rgba(246, 244, 244, 1);\n  --color-bytebot-bronze-light-3: rgba(241, 239, 238, 1);\n  --color-bytebot-bronze-light-4: rgba(236, 233, 232, 1);\n  --color-bytebot-bronze-light-5: rgba(230, 225, 224, 1);\n  --color-bytebot-bronze-light-6: rgba(224, 218, 217, 1);\n  --color-bytebot-bronze-light-7: rgba(218, 212, 210, 1);\n  --color-bytebot-bronze-light-8: rgba(209, 201, 199, 1);\n  --color-bytebot-bronze-light-9: rgba(152, 141, 139, 1);\n  --color-bytebot-bronze-light-10: rgba(141, 130, 128, 1);\n  --color-bytebot-bronze-light-11: rgba(106, 99, 98, 1);\n  --color-bytebot-bronze-light-12: rgba(45, 42, 42, 1);\n  --color-bytebot-bronze-light-a1: rgba(71, 0, 0, 0.02500000037252903);\n  --color-bytebot-bronze-light-a2: rgba(36, 12, 0, 0.04500000178813934);\n  --color-bytebot-bronze-light-a3: rgba(51, 17, 0, 0.06800000369548798);\n  --color-bytebot-bronze-light-a4: rgba(51, 13, 0, 0.09000000357627869);\n  --color-bytebot-bronze-light-a5: rgba(40, 7, 0, 0.11999999731779099);\n  --color-bytebot-bronze-light-a6: rgba(44, 7, 0, 0.15000000596046448);\n  --color-bytebot-bronze-light-a7: rgba(40, 8, 0, 0.17389999330043793);\n  --color-bytebot-bronze-light-a8: rgba(41, 8, 0, 0.2175000011920929);\n  --color-bytebot-bronze-light-a9: rgba(28, 4, 0, 0.45500001311302185);\n  --color-bytebot-bronze-light-a10: rgba(26, 4, 0, 0.49799999594688416);\n  --color-bytebot-bronze-light-a11: rgba(13, 2, 0, 0.6100000143051147);\n  --color-bytebot-bronze-light-a12: rgba(4, 2, 2, 0.8399999737739563);\n\n  /* Bronze dark */\n  --color-bytebot-bronze-dark-1: rgba(19, 16, 16, 1);\n  --color-bytebot-bronze-dark-2: rgba(27, 25, 24, 1);\n  --color-bytebot-bronze-dark-3: rgba(37, 34, 33, 1);\n  --color-bytebot-bronze-dark-4: rgba(45, 41, 40, 1);\n  --color-bytebot-bronze-dark-5: rgba(53, 48, 47, 1);\n  --color-bytebot-bronze-dark-6: rgba(57, 51, 50, 1);\n  --color-bytebot-bronze-dark-7: rgba(77, 70, 69, 1);\n  --color-bytebot-bronze-dark-8: rgba(103, 94, 92, 1);\n  --color-bytebot-bronze-dark-9: rgba(118, 107, 106, 1);\n  --color-bytebot-bronze-dark-10: rgba(132, 121, 119, 1);\n  --color-bytebot-bronze-dark-11: rgba(187, 178, 176, 1);\n  --color-bytebot-bronze-dark-12: rgba(239, 238, 237, 1);\n  --color-bytebot-bronze-dark-a1: rgba(187, 62, 0, 0.0117647061124444);\n  --color-bytebot-bronze-dark-a2: rgba(249, 203, 180, 0.04313725605607033);\n  --color-bytebot-bronze-dark-a3: rgba(249, 214, 202, 0.08627451211214066);\n  --color-bytebot-bronze-dark-a4: rgba(255, 221, 213, 0.11764705926179886);\n  --color-bytebot-bronze-dark-a5: rgba(253, 220, 214, 0.15294118225574493);\n  --color-bytebot-bronze-dark-a6: rgba(252, 222, 217, 0.1679999977350235);\n  --color-bytebot-bronze-dark-a7: rgba(253, 225, 221, 0.2549019753932953);\n  --color-bytebot-bronze-dark-a8: rgba(253, 228, 223, 0.364705890417099);\n  --color-bytebot-bronze-dark-a9: rgba(253, 228, 225, 0.4274509847164154);\n  --color-bytebot-bronze-dark-a10: rgba(253, 231, 227, 0.48627451062202454);\n  --color-bytebot-bronze-dark-a11: rgba(254, 241, 238, 0.7176470756530762);\n  --color-bytebot-bronze-dark-a12: rgba(255, 254, 253, 0.9333333373069763);\n  \n  /* Red light */\n  --color-bytebot-red-light-9: rgba(229, 72, 77, 1);\n  --color-bytebot-red-light-1: rgba(255, 252, 252, 1);\n  --color-bytebot-red-light-2: rgba(255, 247, 247, 1);\n  --color-bytebot-red-light-3: rgba(254, 235, 236, 1);\n  --color-bytebot-red-light-4: rgba(255, 219, 220, 1);\n  --color-bytebot-red-light-5: rgba(255, 205, 206, 1);\n  --color-bytebot-red-light-6: rgba(253, 189, 190, 1);\n  --color-bytebot-red-light-7: rgba(244, 169, 170, 1);\n  --color-bytebot-red-light-8: rgba(235, 142, 144, 1);\n  --color-bytebot-red-light-10: rgba(220, 62, 66, 1);\n  --color-bytebot-red-light-11: rgba(206, 44, 49, 1);\n  --color-bytebot-red-light-12: rgba(100, 23, 35, 1);\n  \n  /* Red dark */\n  --color-bytebot-red-dark-1: rgba(23, 15, 14, 1);\n  --color-bytebot-red-dark-2: rgba(32, 19, 18, 1);\n  --color-bytebot-red-dark-3: rgba(59, 18, 18, 1);\n  --color-bytebot-red-dark-4: rgba(80, 15, 19, 1);\n  --color-bytebot-red-dark-5: rgba(97, 23, 26, 1);\n  --color-bytebot-red-dark-6: rgba(115, 36, 37, 1);\n  --color-bytebot-red-dark-7: rgba(140, 52, 52, 1);\n  --color-bytebot-red-dark-8: rgba(181, 69, 70, 1);\n  --color-bytebot-red-dark-9: rgba(229, 72, 77, 1);\n  --color-bytebot-red-dark-10: rgba(230, 86, 91, 1);\n  --color-bytebot-red-dark-11: rgba(255, 143, 139, 1);\n  --color-bytebot-red-dark-12: rgba(255, 210, 206, 1);\n  \n  /* Green */\n  --color-bytebot-green-3: rgba(232, 247, 228, 1);\n  --color-bytebot-green-4: rgba(218, 242, 211, 1);\n  --color-bytebot-green-5: rgba(200, 234, 190, 1);\n  --color-bytebot-green-6: rgba(178, 223, 165, 1);\n  --color-bytebot-green-7: rgba(148, 208, 130, 1);\n  --color-bytebot-green-8: rgba(103, 188, 77, 1);\n  --color-bytebot-green-9: rgba(77, 175, 41, 1);\n  --color-bytebot-green-10: rgba(68, 162, 32, 1);\n  --color-bytebot-green-11: rgba(43, 128, 0, 1);\n  --color-bytebot-green-12: rgba(33, 61, 24, 1);\n  --color-bytebot-green-2: rgba(245, 251, 244, 1);\n  --color-bytebot-green-1: rgba(251, 254, 250, 1);\n  --color-bytebot-green-a1: rgba(51, 204, 0, 0.019600000232458115);\n  --color-bytebot-green-a2: rgba(24, 163, 0, 0.04309999942779541);\n  --color-bytebot-green-a3: rgba(38, 180, 0, 0.10589999705553055);\n  --color-bytebot-green-a4: rgba(41, 180, 0, 0.17249999940395355);\n  --color-bytebot-green-a5: rgba(40, 173, 0, 0.2549000084400177);\n  --color-bytebot-green-a6: rgba(37, 165, 1, 0.3528999984264374);\n  --color-bytebot-green-a7: rgba(37, 160, 0, 0.490200012922287);\n  --color-bytebot-green-a8: rgba(37, 159, 0, 0.6980000138282776);\n  --color-bytebot-green-a9: rgba(43, 160, 0, 0.8392000198364258);\n  --color-bytebot-green-a10: rgba(41, 149, 0, 0.8744999766349792);\n  --color-bytebot-green-a11: rgba(43, 128, 0, 1);\n  --color-bytebot-green-a12: rgba(10, 41, 0, 0.9059000015258789);\n\n  /* letterSpacing */\n  --letter-spacing-wide: 0.02em;\n  --letter-spacing-normal: 0em;\n  --letter-spacing-narrow: -0.009999999776482582em;\n  --letter-spacing-narrow: -0.01em;\n\n  /* Shadow */\n  --shadow-bytebot: 0px 0px 0px 1.5px #FFF inset;\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.147 0.004 49.25);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.147 0.004 49.25);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.147 0.004 49.25);\n  --primary: oklch(0.216 0.006 56.043);\n  --primary-foreground: oklch(0.985 0.001 106.423);\n  --secondary: oklch(0.97 0.001 106.424);\n  --secondary-foreground: oklch(0.216 0.006 56.043);\n  --muted: oklch(0.97 0.001 106.424);\n  --muted-foreground: oklch(0.553 0.013 58.071);\n  --accent: oklch(0.97 0.001 106.424);\n  --accent-foreground: oklch(0.216 0.006 56.043);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.923 0.003 48.717);\n  --input: oklch(0.923 0.003 48.717);\n  --ring: oklch(0.709 0.01 56.259);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0.001 106.423);\n  --sidebar-foreground: oklch(0.147 0.004 49.25);\n  --sidebar-primary: oklch(0.216 0.006 56.043);\n  --sidebar-primary-foreground: oklch(0.985 0.001 106.423);\n  --sidebar-accent: oklch(0.97 0.001 106.424);\n  --sidebar-accent-foreground: oklch(0.216 0.006 56.043);\n  --sidebar-border: oklch(0.923 0.003 48.717);\n  --sidebar-ring: oklch(0.709 0.01 56.259);\n}\n\n/* .dark {\n  --background: oklch(0.147 0.004 49.25);\n  --foreground: oklch(0.985 0.001 106.423);\n  --card: oklch(0.216 0.006 56.043);\n  --card-foreground: oklch(0.985 0.001 106.423);\n  --popover: oklch(0.216 0.006 56.043);\n  --popover-foreground: oklch(0.985 0.001 106.423);\n  --primary: oklch(0.923 0.003 48.717);\n  --primary-foreground: oklch(0.216 0.006 56.043);\n  --secondary: oklch(0.268 0.007 34.298);\n  --secondary-foreground: oklch(0.985 0.001 106.423);\n  --muted: oklch(0.268 0.007 34.298);\n  --muted-foreground: oklch(0.709 0.01 56.259);\n  --accent: oklch(0.268 0.007 34.298);\n  --accent-foreground: oklch(0.985 0.001 106.423);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.553 0.013 58.071);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.216 0.006 56.043);\n  --sidebar-foreground: oklch(0.985 0.001 106.423);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0.001 106.423);\n  --sidebar-accent: oklch(0.268 0.007 34.298);\n  --sidebar-accent-foreground: oklch(0.985 0.001 106.423);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.553 0.013 58.071);\n} */\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n.hide-scrollbar {\n  scrollbar-width: none; /* Firefox */\n  -ms-overflow-style: none; /* IE 10+ */\n}\n.hide-scrollbar::-webkit-scrollbar {\n  display: none; /* Chrome, Safari, Opera */\n}"
  },
  {
    "path": "packages/bytebot-ui/src/app/layout.tsx",
    "content": "import type React from \"react\";\nimport type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"Bytebot\",\n  description: \"Bytebot is the container for desktop agents.\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body className={inter.className}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/app/page.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useEffect, useRef } from \"react\";\nimport Image from \"next/image\";\nimport { Header } from \"@/components/layout/Header\";\nimport { ChatInput } from \"@/components/messages/ChatInput\";\nimport { useRouter } from \"next/navigation\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { startTask } from \"@/utils/taskUtils\";\nimport { Model } from \"@/types\";\nimport { TaskList } from \"@/components/tasks/TaskList\";\n\ninterface StockPhotoProps {\n  src: string;\n  alt?: string;\n}\n\nconst StockPhoto: React.FC<StockPhotoProps> = ({\n  src,\n  alt = \"Decorative image\",\n}) => {\n  return (\n    <div className=\"h-full w-full overflow-hidden rounded-lg bg-white\">\n      <div className=\"relative h-full w-full\">\n        <Image src={src} alt={alt} fill className=\"object-cover\" priority />\n      </div>\n    </div>\n  );\n};\n\ninterface FileWithBase64 {\n  name: string;\n  base64: string;\n  type: string;\n  size: number;\n}\n\nexport default function Home() {\n  const [input, setInput] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [models, setModels] = useState<Model[]>([]);\n  const [selectedModel, setSelectedModel] = useState<Model | null>(null);\n  const [uploadedFiles, setUploadedFiles] = useState<FileWithBase64[]>([]);\n  const router = useRouter();\n  const [activePopoverIndex, setActivePopoverIndex] = useState<number | null>(\n    null,\n  );\n  const popoverRef = useRef<HTMLDivElement>(null);\n  const buttonsRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    fetch(\"/api/tasks/models\")\n      .then((res) => res.json())\n      .then((data) => {\n        setModels(data);\n        if (data.length > 0) setSelectedModel(data[0]);\n      })\n      .catch((err) => console.error(\"Failed to load models\", err));\n  }, []);\n\n  // Close popover when clicking outside or pressing ESC\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        popoverRef.current &&\n        !popoverRef.current.contains(event.target as Node) &&\n        buttonsRef.current &&\n        !buttonsRef.current.contains(event.target as Node)\n      ) {\n        setActivePopoverIndex(null);\n      }\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setActivePopoverIndex(null);\n      }\n    };\n\n    if (activePopoverIndex !== null) {\n      document.addEventListener(\"mousedown\", handleClickOutside);\n      document.addEventListener(\"keydown\", handleKeyDown);\n    }\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [activePopoverIndex]);\n\n  const handleSend = async () => {\n    if (!input.trim()) return;\n\n    setIsLoading(true);\n\n    try {\n      if (!selectedModel) throw new Error(\"No model selected\");\n      // Send request to start a new task\n      const taskData: {\n        description: string;\n        model: Model;\n        files?: FileWithBase64[];\n      } = {\n        description: input,\n        model: selectedModel,\n      };\n\n      // Include files if any are uploaded\n      if (uploadedFiles.length > 0) {\n        taskData.files = uploadedFiles;\n      }\n\n      const task = await startTask(taskData);\n\n      if (task && task.id) {\n        // Redirect to the task page\n        router.push(`/tasks/${task.id}`);\n      } else {\n        // Handle error\n        console.error(\"Failed to create task\");\n      }\n    } catch (error) {\n      console.error(\"Error sending message:\", error);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleFileUpload = (files: FileWithBase64[]) => {\n    setUploadedFiles(files);\n  };\n\n  return (\n    <div className=\"flex h-screen flex-col overflow-hidden\">\n      <Header />\n\n      <main className=\"flex flex-1 flex-col overflow-hidden\">\n        {/* Desktop grid layout (50/50 split) - only visible on large screens */}\n        <div className=\"hidden h-full p-8 lg:grid lg:grid-cols-2 lg:gap-8\">\n          {/* Main content area */}\n          <div className=\"flex flex-col items-center overflow-y-auto\">\n            <div className=\"flex w-full max-w-xl flex-col items-center\">\n              <div className=\"mb-6 flex w-full flex-col items-start justify-start\">\n                <h1 className=\"text-bytebot-bronze-light-12 mb-1 text-2xl\">\n                  What can I help you get done?\n                </h1>\n              </div>\n\n              <div className=\"bg-bytebot-bronze-light-2 border-bytebot-bronze-light-7 mb-10 w-full rounded-2xl border p-2\">\n                <ChatInput\n                  input={input}\n                  isLoading={isLoading}\n                  onInputChange={setInput}\n                  onSend={handleSend}\n                  onFileUpload={handleFileUpload}\n                  minLines={3}\n                />\n                <div className=\"mt-2\">\n                  <Select\n                    value={selectedModel?.name}\n                    onValueChange={(val) =>\n                      setSelectedModel(\n                        models.find((m) => m.name === val) || null,\n                      )\n                    }\n                  >\n                    <SelectTrigger className=\"w-auto\">\n                      <SelectValue placeholder=\"Select a model\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      {models.map((m) => (\n                        <SelectItem key={m.name} value={m.name}>\n                          {m.title}\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </div>\n              </div>\n\n              <TaskList\n                className=\"w-full\"\n                title=\"Latest Tasks\"\n                description=\"You'll see tasks that are completed, scheduled, or require your attention.\"\n              />\n            </div>\n          </div>\n\n          {/* Stock photo area - centered in its grid cell */}\n          <div className=\"flex items-center justify-center px-6 pt-6\">\n            <div className=\"aspect-square h-full w-full max-w-md xl:max-w-2xl\">\n              <StockPhoto src=\"/stock-1.png\" alt=\"Bytebot stock image\" />\n            </div>\n          </div>\n        </div>\n\n        {/* Mobile layout - only visible on small/medium screens */}\n        <div className=\"flex h-full flex-col lg:hidden\">\n          <div className=\"flex flex-1 flex-col items-center overflow-y-auto px-4 pt-10\">\n            <div className=\"flex w-full max-w-xl flex-col items-center pb-10\">\n              <div className=\"mb-6 flex w-full flex-col items-start justify-start\">\n                <h1 className=\"text-bytebot-bronze-light-12 mb-1 text-2xl\">\n                  What can I help you get done?\n                </h1>\n              </div>\n\n              <div className=\"bg-bytebot-bronze-light-2 border-bytebot-bronze-light-5 borderw-full mb-10 rounded-2xl p-2\">\n                <ChatInput\n                  input={input}\n                  isLoading={isLoading}\n                  onInputChange={setInput}\n                  onSend={handleSend}\n                  onFileUpload={handleFileUpload}\n                  minLines={3}\n                />\n                <div className=\"mt-2\">\n                  <Select\n                    value={selectedModel?.name}\n                    onValueChange={(val) =>\n                      setSelectedModel(\n                        models.find((m) => m.name === val) || null,\n                      )\n                    }\n                  >\n                    <SelectTrigger className=\"w-auto\">\n                      <SelectValue placeholder=\"Select a model\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      {models.map((m) => (\n                        <SelectItem key={m.name} value={m.name}>\n                          {m.title}\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </div>\n              </div>\n\n              <TaskList\n                className=\"w-full\"\n                title=\"Latest Tasks\"\n                description=\"You'll see tasks that are completed, scheduled, or require your attention.\"\n              />\n            </div>\n          </div>\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/app/tasks/[id]/page.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useRef } from \"react\";\nimport { Header } from \"@/components/layout/Header\";\nimport { ChatContainer } from \"@/components/messages/ChatContainer\";\nimport { DesktopContainer } from \"@/components/ui/desktop-container\";\nimport { useChatSession } from \"@/hooks/useChatSession\";\nimport { useScrollScreenshot } from \"@/hooks/useScrollScreenshot\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport { Role, TaskStatus } from \"@/types\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  MoreVerticalCircle01Icon,\n  WavingHand01Icon,\n} from \"@hugeicons/core-free-icons\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n} from \"@/components/ui/dropdown-menu\";\nimport { Button } from \"@/components/ui/button\";\nimport { VirtualDesktopStatus } from \"@/components/VirtualDesktopStatusHeader\";\n\nexport default function TaskPage() {\n  const params = useParams();\n  const router = useRouter();\n  const taskId = params.id as string;\n\n  const chatContainerRef = useRef<HTMLDivElement>(null);\n  const {\n    messages,\n    groupedMessages,\n    taskStatus,\n    control,\n    input,\n    setInput,\n    isLoading,\n    isLoadingSession,\n    isLoadingMoreMessages,\n    hasMoreMessages,\n    loadMoreMessages,\n    handleAddMessage,\n    handleTakeOverTask,\n    handleResumeTask,\n    handleCancelTask,\n    currentTaskId,\n  } = useChatSession({ initialTaskId: taskId });\n\n  // Determine if task is inactive (show screenshot) or active (show VNC)\n  function isTaskInactive(): boolean {\n    return (\n      taskStatus === TaskStatus.COMPLETED ||\n      taskStatus === TaskStatus.FAILED ||\n      taskStatus === TaskStatus.CANCELLED\n    );\n  }\n\n  // Determine if user can take control\n  function canTakeOver(): boolean {\n    return control === Role.ASSISTANT && taskStatus === TaskStatus.RUNNING;\n  }\n\n  // Determine if user has control or is in takeover mode\n  function hasUserControl(): boolean {\n    return (\n      control === Role.USER &&\n      (taskStatus === TaskStatus.RUNNING ||\n        taskStatus === TaskStatus.NEEDS_HELP)\n    );\n  }\n\n  // Determine if task can be cancelled\n  function canCancel(): boolean {\n    return (\n      taskStatus === TaskStatus.RUNNING || taskStatus === TaskStatus.NEEDS_HELP\n    );\n  }\n\n  // Determine VNC mode - interactive when user has control, view-only otherwise\n  function vncViewOnly(): boolean {\n    return !hasUserControl();\n  }\n\n  // Use scroll screenshot hook for inactive tasks\n  const { currentScreenshot } = useScrollScreenshot({\n    messages,\n    scrollContainerRef: chatContainerRef,\n  });\n\n  // For inactive tasks, auto-load all messages for proper screenshot navigation\n  useEffect(() => {\n    if (isTaskInactive() && hasMoreMessages && !isLoadingMoreMessages) {\n      loadMoreMessages();\n    }\n  }, [\n    isTaskInactive(),\n    hasMoreMessages,\n    isLoadingMoreMessages,\n    loadMoreMessages,\n  ]);\n\n  // Map each message ID to its flat index for screenshot scroll logic\n  const messageIdToIndex = React.useMemo(() => {\n    const map: Record<string, number> = {};\n    messages.forEach((msg, idx) => {\n      map[msg.id] = idx;\n    });\n    return map;\n  }, [messages]);\n\n  // Redirect if task ID doesn't match current task\n  useEffect(() => {\n    if (currentTaskId && currentTaskId !== taskId) {\n      router.push(`/tasks/${currentTaskId}`);\n    }\n  }, [currentTaskId, taskId, router]);\n\n  return (\n    <div className=\"flex h-screen flex-col overflow-hidden\">\n      <Header />\n\n      <main className=\"m-2 flex-1 overflow-hidden px-2 py-4\">\n        <div className=\"grid h-full grid-cols-7 gap-4\">\n          {/* Main container */}\n          <div className=\"col-span-4\">\n            <DesktopContainer\n              screenshot={isTaskInactive() ? currentScreenshot : null}\n              viewOnly={vncViewOnly()}\n              status={\n                (() => {\n                  if (\n                    taskStatus === TaskStatus.RUNNING &&\n                    control === Role.USER\n                  )\n                    return \"user_control\";\n                  if (taskStatus === TaskStatus.RUNNING) return \"running\";\n                  if (taskStatus === TaskStatus.NEEDS_HELP)\n                    return \"needs_attention\";\n                  if (taskStatus === TaskStatus.FAILED) return \"failed\";\n                  if (taskStatus === TaskStatus.CANCELLED) return \"canceled\";\n                  if (taskStatus === TaskStatus.COMPLETED) return \"completed\";\n                  // You may want to add a scheduled state if you have that info\n                  return \"pending\";\n                })() as VirtualDesktopStatus\n              }\n            >\n              {canTakeOver() && (\n                <Button\n                  onClick={handleTakeOverTask}\n                  variant=\"default\"\n                  size=\"sm\"\n                  icon={\n                    <HugeiconsIcon\n                      icon={WavingHand01Icon}\n                      className=\"h-5 w-5\"\n                    />\n                  }\n                >\n                  Take Over\n                </Button>\n              )}\n              {hasUserControl() && (\n                <Button onClick={handleResumeTask} variant=\"default\" size=\"sm\">\n                  Proceed\n                </Button>\n              )}\n              {canCancel() && (\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <Button variant=\"outline\" size=\"icon\">\n                      <HugeiconsIcon\n                        icon={MoreVerticalCircle01Icon}\n                        className=\"text-bytebot-bronze-light-11 h-5 w-5\"\n                      />\n                    </Button>\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent align=\"end\">\n                    <DropdownMenuItem\n                      onClick={handleCancelTask}\n                      className=\"text-red-600 focus:bg-red-50\"\n                    >\n                      Cancel\n                    </DropdownMenuItem>\n                  </DropdownMenuContent>\n                </DropdownMenu>\n              )}\n            </DesktopContainer>\n          </div>\n\n          {/* Chat Area */}\n          <div className=\"col-span-3 flex h-full min-h-0 flex-col\">\n            {/* Messages scrollable area */}\n            <div\n              ref={chatContainerRef}\n              className=\"hide-scrollbar min-h-0 flex-1 overflow-scroll px-4\"\n            >\n              <ChatContainer\n                scrollRef={chatContainerRef}\n                messageIdToIndex={messageIdToIndex}\n                taskId={taskId}\n                input={input}\n                setInput={setInput}\n                isLoading={isLoading}\n                handleAddMessage={handleAddMessage}\n                groupedMessages={groupedMessages}\n                taskStatus={taskStatus}\n                control={control}\n                isLoadingSession={isLoadingSession}\n                isLoadingMoreMessages={isLoadingMoreMessages}\n                hasMoreMessages={hasMoreMessages}\n                loadMoreMessages={loadMoreMessages}\n              />\n            </div>\n          </div>\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/app/tasks/page.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { Header } from \"@/components/layout/Header\";\nimport { TaskItem } from \"@/components/tasks/TaskItem\";\nimport { TaskTabs, TabKey, TAB_CONFIGS } from \"@/components/tasks/TaskTabs\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { fetchTasks, fetchTaskCounts } from \"@/utils/taskUtils\";\nimport { Task } from \"@/types\";\nimport { Button } from \"@/components/ui/button\";\nimport Link from \"next/link\";\nimport { Suspense } from \"react\";\n\nfunction TasksPageContent() {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n\n  const [tasks, setTasks] = useState<Task[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n\n  // Initialize activeTab from URL params\n  const getInitialTab = (): TabKey => {\n    const tabParam = searchParams.get(\"tab\");\n    if (tabParam && Object.keys(TAB_CONFIGS).includes(tabParam)) {\n      return tabParam as TabKey;\n    }\n    return \"ALL\";\n  };\n\n  const [activeTab, setActiveTab] = useState<TabKey>(getInitialTab);\n  const [currentPage, setCurrentPage] = useState(1);\n  const [totalPages, setTotalPages] = useState(0);\n  const [total, setTotal] = useState(0);\n  const [taskCounts, setTaskCounts] = useState<Record<TabKey, number>>({\n    ALL: 0,\n    ACTIVE: 0,\n    COMPLETED: 0,\n    CANCELLED_FAILED: 0,\n  });\n  const PAGE_SIZE = 10;\n\n  useEffect(() => {\n    const loadTasks = async () => {\n      setIsLoading(true);\n      try {\n        const statuses =\n          activeTab === \"ALL\" ? undefined : TAB_CONFIGS[activeTab].statuses;\n        const result = await fetchTasks({\n          page: currentPage,\n          limit: PAGE_SIZE,\n          statuses,\n        });\n        setTasks(result.tasks);\n        setTotal(result.total);\n        setTotalPages(result.totalPages);\n      } catch (error) {\n        console.error(\"Failed to load tasks:\", error);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    loadTasks();\n  }, [currentPage, activeTab]);\n\n  useEffect(() => {\n    const loadTaskCounts = async () => {\n      try {\n        const counts = await fetchTaskCounts();\n        setTaskCounts(counts);\n      } catch (error) {\n        console.error(\"Failed to load task counts:\", error);\n      }\n    };\n\n    loadTaskCounts();\n  }, []);\n\n  // Sync activeTab with URL params when they change\n  useEffect(() => {\n    const tabParam = searchParams.get(\"tab\");\n    const newTab: TabKey =\n      tabParam && Object.keys(TAB_CONFIGS).includes(tabParam)\n        ? (tabParam as TabKey)\n        : \"ALL\";\n\n    if (newTab !== activeTab) {\n      setActiveTab(newTab);\n      setCurrentPage(1);\n    }\n  }, [searchParams, activeTab]);\n\n  const handleTabChange = (tab: TabKey) => {\n    setActiveTab(tab);\n    setCurrentPage(1);\n\n    // Update URL with the new tab\n    const newSearchParams = new URLSearchParams(searchParams);\n    if (tab === \"ALL\") {\n      newSearchParams.delete(\"tab\");\n    } else {\n      newSearchParams.set(\"tab\", tab);\n    }\n\n    const newUrl = `/tasks${newSearchParams.toString() ? `?${newSearchParams.toString()}` : \"\"}`;\n    router.push(newUrl, { scroll: false });\n  };\n\n  const handlePageChange = (page: number) => {\n    setCurrentPage(page);\n  };\n\n  return (\n    <div className=\"flex h-screen flex-col overflow-hidden\">\n      <Header />\n\n      <main className=\"flex-1 overflow-scroll px-6 pt-6 pb-10\">\n        <div className=\"mx-auto max-w-3xl\">\n          <h1 className=\"mb-6 text-xl font-medium\">Tasks</h1>\n\n          {!isLoading && (\n            <TaskTabs\n              activeTab={activeTab}\n              onTabChange={handleTabChange}\n              taskCounts={taskCounts}\n            />\n          )}\n\n          {isLoading ? (\n            <div className=\"p-8 text-center\">\n              <div className=\"border-bytebot-bronze-light-5 border-t-bytebot-bronze mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4\"></div>\n              <p className=\"text-gray-500\">Loading tasks...</p>\n            </div>\n          ) : tasks.length === 0 ? (\n            <div className=\"bg-bytebot-bronze-light-2 border-bytebot-bronze-light-7 rounded-xl border p-8 text-center\">\n              <div className=\"flex flex-col items-center justify-center\">\n                <h3 className=\"text-bytebot-bronze-light-12 mb-1 text-lg font-medium\">\n                  No tasks yet\n                </h3>\n                <p className=\"text-bytebot-bronze-light-11 mb-6 text-sm\">\n                  Get started by creating a first task\n                </p>\n                <Link href=\"/\">\n                  <Button className=\"bg-bytebot-bronze-dark-7 hover:bg-bytebot-bronze-dark-6 text-white\">\n                    + New Task\n                  </Button>\n                </Link>\n              </div>\n            </div>\n          ) : (\n            <>\n              <div className=\"space-y-4\">\n                {tasks.map((task) => (\n                  <TaskItem key={task.id} task={task} />\n                ))}\n              </div>\n\n              {totalPages > 1 && (\n                <Pagination\n                  currentPage={currentPage}\n                  totalPages={totalPages}\n                  onPageChange={handlePageChange}\n                  total={total}\n                  pageSize={PAGE_SIZE}\n                />\n              )}\n            </>\n          )}\n        </div>\n      </main>\n    </div>\n  );\n}\n\nfunction TasksPageFallback() {\n  return (\n    <div className=\"p-8 text-center\">\n      <div className=\"border-bytebot-bronze-light-5 border-t-bytebot-bronze mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4\"></div>\n      <p className=\"text-gray-500\">Loading tasks...</p>\n    </div>\n  );\n}\n\nexport default function TasksPage() {\n  return (\n    <Suspense fallback={<TasksPageFallback />}>\n      <TasksPageContent />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/VirtualDesktopStatusHeader.tsx",
    "content": "import React from \"react\";\nimport Image from \"next/image\";\nimport { cn } from \"@/lib/utils\";\n\n// Status types based on the image\nexport type VirtualDesktopStatus =\n  | \"running\"\n  | \"needs_attention\"\n  | \"failed\"\n  | \"canceled\"\n  | \"pending\"\n  | \"user_control\"\n  | \"completed\"\n  | \"live_view\";\n\ninterface StatusConfig {\n  dot: React.ReactNode;\n  text: string;\n  gradient: string;\n  subtext: string;\n}\n\nconst statusConfig: Record<VirtualDesktopStatus, StatusConfig> = {\n  live_view: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-black.svg\"\n          alt=\"Live view status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Live Desktop View\",\n    gradient: \"from-gray-700 to-gray-900\",\n    subtext: \"\",\n  },\n  running: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-green.svg\"\n          alt=\"Running status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Running\",\n    gradient: \"from-green-700 to-green-900\",\n    subtext: \"Task in progress\",\n  },\n  needs_attention: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-orange.svg\"\n          alt=\"Needs attention status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Needs Attention\",\n    gradient: \"from-yellow-600 to-orange-700\",\n    subtext: \"Task needs attention\",\n  },\n  failed: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-red.svg\"\n          alt=\"Failed status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Failed\",\n    gradient: \"from-red-700 to-red-900\",\n    subtext: \"Task failed\",\n  },\n  canceled: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-gray.svg\"\n          alt=\"Canceled status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Canceled\",\n    gradient: \"from-gray-400 to-gray-600\",\n    subtext: \"Task canceled\",\n  },\n  pending: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-gray.svg\"\n          alt=\"Pending status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Pending\",\n    gradient: \"from-gray-400 to-gray-600\",\n    subtext: \"Task pending\",\n  },\n  user_control: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-pink.svg\"\n          alt=\"User control status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Running\",\n    gradient: \"from-pink-500 to-fuchsia-700\",\n    subtext: \"You took control\",\n  },\n  completed: {\n    dot: (\n      <span className=\"flex items-center justify-center\">\n        <Image\n          src=\"/indicators/indicator-green.svg\"\n          alt=\"Completed status\"\n          width={15}\n          height={15}\n        />\n      </span>\n    ),\n    text: \"Completed\",\n    gradient: \"from-green-700 to-green-900\",\n    subtext: \"Task completed\",\n  },\n};\n\nexport interface VirtualDesktopStatusHeaderProps {\n  status: VirtualDesktopStatus;\n  subtext?: string; // allow override\n  className?: string;\n}\n\nexport const VirtualDesktopStatusHeader: React.FC<\n  VirtualDesktopStatusHeaderProps\n> = ({ status, subtext, className }) => {\n  const config = statusConfig[status];\n  return (\n    <div className={cn(\"flex items-start gap-2\", className)}>\n      <span className=\"mt-1 flex items-center justify-center\">\n        {config.dot}\n      </span>\n      <div>\n        <span\n          className={cn(\n            \"text-md text-base font-semibold\",\n            config.gradient ? \"bg-clip-text text-transparent\" : \"text-zinc-600\",\n          )}\n          style={\n            config.gradient\n              ? {\n                  backgroundImage: `linear-gradient(to right, var(--tw-gradient-stops))`,\n                }\n              : undefined\n          }\n        >\n          <span\n            className={cn(\n              config.gradient\n                ? `bg-gradient-to-r ${config.gradient} bg-clip-text text-transparent`\n                : \"\",\n            )}\n          >\n            {config.text}\n          </span>\n        </span>\n        {config.subtext && (\n          <span className=\"block text-[12px] text-zinc-400\">\n            {subtext || config.subtext}\n          </span>\n        )}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/layout/Header.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { useTheme } from \"next-themes\";\n\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  DocumentCodeIcon,\n  TaskDaily01Icon,\n  Home01Icon,\n  ComputerIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { usePathname } from \"next/navigation\";\n\nexport function Header() {\n  const { resolvedTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  const pathname = usePathname();\n\n  // After mounting, we can safely show the theme-dependent content\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  // Function to determine if a link is active\n  const isActive = (path: string) => {\n    if (path === \"/\") {\n      return pathname === \"/\";\n    }\n    return pathname?.startsWith(path);\n  };\n\n  // Get classes for navigation links based on active state\n  const getLinkClasses = (path: string) => {\n    const baseClasses =\n      \"flex items-center gap-1.5 transition-colors px-3 py-1.5 rounded-lg\";\n    const activeClasses =\n      \"bg-bytebot-bronze-light-a3 text-bytebot-bronze-light-12\";\n    const inactiveClasses =\n      \"text-bytebot-bronze-dark-9 hover:bg-bytebot-bronze-light-a1 hover:text-bytebot-bronze-light-12\";\n\n    return `${baseClasses} ${isActive(path) ? activeClasses : inactiveClasses}`;\n  };\n\n  return (\n    <header className=\"bg-background flex items-center justify-between border-b p-4\">\n      <div className=\"flex items-center gap-6\">\n        {/* Logo without link */}\n        <div>\n          {mounted ? (\n            <Image\n              src={\n                resolvedTheme === \"dark\"\n                  ? \"/bytebot_transparent_logo_white.svg\"\n                  : \"/bytebot_transparent_logo_dark.svg\"\n              }\n              alt=\"Bytebot Logo\"\n              width={100}\n              height={30}\n              className=\"h-8 w-auto\"\n            />\n          ) : (\n            <div className=\"h-8 w-[110px]\" />\n          )}\n        </div>\n        <div className=\"border-bytebot-bronze-dark-11 h-5 border border-l-[0.5px]\"></div>\n        <div className=\"flex items-center gap-2\">\n          <Link href=\"/\" className={getLinkClasses(\"/\")}>\n            <HugeiconsIcon icon={Home01Icon} className=\"h-4 w-4\" />\n            <span className=\"text-sm\">Home</span>\n          </Link>\n          <Link href=\"/tasks\" className={getLinkClasses(\"/tasks\")}>\n            <HugeiconsIcon icon={TaskDaily01Icon} className=\"h-4 w-4\" />\n            <span className=\"text-sm\">Tasks</span>\n          </Link>\n          <Link href=\"/desktop\" className={getLinkClasses(\"/desktop\")}>\n            <HugeiconsIcon icon={ComputerIcon} className=\"h-4 w-4\" />\n            <span className=\"text-sm\">Desktop</span>\n          </Link>\n          <Link\n            href=\"https://docs.bytebot.ai/quickstart\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className={getLinkClasses(\"https://docs.bytebot.ai\")}\n          >\n            <HugeiconsIcon icon={DocumentCodeIcon} className=\"h-4 w-4\" />\n            <span className=\"text-sm\">Docs</span>\n          </Link>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-3\"></div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/AssistantMessage.tsx",
    "content": "import React from \"react\";\nimport { GroupedMessages, TaskStatus } from \"@/types\";\nimport { MessageAvatar } from \"./MessageAvatar\";\nimport { MessageContent } from \"./content/MessageContent\";\nimport { isToolResultContentBlock, isImageContentBlock } from \"@bytebot/shared\";\nimport Image from \"next/image\";\nimport { cn } from \"@/lib/utils\";\n\ninterface AssistantMessageProps {\n  group: GroupedMessages;\n  taskStatus: TaskStatus;\n  messageIdToIndex: Record<string, number>;\n}\n\nexport function AssistantMessage({\n  group,\n  taskStatus,\n  messageIdToIndex,\n}: AssistantMessageProps) {\n  return (\n    <div className={\n      cn(\n        \"bg-bytebot-bronze-light-3 flex items-start justify-start gap-2 px-4 py-3 border-x border-bytebot-bronze-light-7\",\n        ![TaskStatus.RUNNING, TaskStatus.NEEDS_HELP].includes(taskStatus) && !group.take_over && \"border-b border-bytebot-bronze-light-7 rounded-b-lg\"\n      )}\n    >\n      <MessageAvatar role={group.role} />\n\n      {group.take_over ? (\n        <div className=\"border-bytebot-bronze-light-a6 bg-bytebot-bronze-light-a1 w-full rounded-2xl border p-2\">\n          <div className=\"flex items-center gap-2\">\n            <Image\n              src=\"/indicators/indicator-pink.png\"\n              alt=\"User control status\"\n              width={15}\n              height={15}\n            />\n            <p className=\"text-bytebot-bronze-light-12 text-[12px] font-medium\">\n              You took control\n            </p>\n          </div>\n          <div className=\"bg-bytebot-bronze-light-2 mt-2 space-y-0.5 rounded-2xl p-1\">\n            {group.messages.map((message) => (\n              <div\n                key={message.id}\n                data-message-index={messageIdToIndex[message.id]}\n              >\n                {/* Render hidden divs for each screenshot block */}\n                {message.content.map((block, blockIndex) => {\n                  if (\n                    isToolResultContentBlock(block) &&\n                    block.content &&\n                    block.content.length > 0\n                  ) {\n                    // Check ALL content items in the tool result, not just the first one\n                    const markers: React.ReactNode[] = [];\n                    block.content.forEach((contentItem, contentIndex) => {\n                      if (isImageContentBlock(contentItem)) {\n                        markers.push(\n                          <div\n                            key={`${blockIndex}-${contentIndex}`}\n                            data-message-index={messageIdToIndex[message.id]}\n                            data-block-index={blockIndex}\n                            data-content-index={contentIndex}\n                            style={{\n                              position: \"absolute\",\n                              width: 0,\n                              height: 0,\n                              overflow: \"hidden\",\n                            }}\n                          />\n                        );\n                      }\n                    });\n                    return markers;\n                  }\n                  return null;\n                })}\n                <MessageContent\n                  content={message.content}\n                  isTakeOver={message.take_over}\n                />\n              </div>\n            ))}\n          </div>\n        </div>\n      ) : (\n        <div>\n          {group.messages.map((message) => (\n            <div\n              key={message.id}\n              data-message-index={messageIdToIndex[message.id]}\n            >\n              {/* Render hidden divs for each screenshot block */}\n              {message.content.map((block, blockIndex) => {\n                if (\n                  isToolResultContentBlock(block) &&\n                  !block.is_error &&\n                  block.content &&\n                  block.content.length > 0\n                ) {\n                  // Check ALL content items in the tool result, not just the first one\n                  const markers: React.ReactNode[] = [];\n                  block.content.forEach((contentItem, contentIndex) => {\n                    if (isImageContentBlock(contentItem)) {\n                      markers.push(\n                        <div\n                          key={`${blockIndex}-${contentIndex}`}\n                          data-message-index={messageIdToIndex[message.id]}\n                          data-block-index={blockIndex}\n                          data-content-index={contentIndex}\n                          style={{\n                            position: \"absolute\",\n                            width: 0,\n                            height: 0,\n                            overflow: \"hidden\",\n                          }}\n                        />\n                      );\n                    }\n                  });\n                  return markers;\n                }\n                return null;\n              })}\n              <MessageContent\n                content={message.content}\n                isTakeOver={message.take_over}\n              />\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/ChatContainer.tsx",
    "content": "import React, { useRef, useEffect, useCallback, Fragment } from \"react\";\nimport { Role, TaskStatus, GroupedMessages } from \"@/types\";\nimport { MessageGroup } from \"./MessageGroup\";\nimport { TextShimmer } from \"../ui/text-shimmer\";\nimport { MessageAvatar } from \"./MessageAvatar\";\nimport { Loader } from \"../ui/loader\";\nimport { ChatInput } from \"./ChatInput\";\n\ninterface ChatContainerProps {\n  scrollRef?: React.RefObject<HTMLDivElement | null>;\n  messageIdToIndex: Record<string, number>;\n  taskId: string;\n  input: string;\n  setInput: (value: string) => void;\n  isLoading: boolean;\n  handleAddMessage: () => Promise<void>;\n  groupedMessages: GroupedMessages[];\n  taskStatus: TaskStatus;\n  control: Role;\n  isLoadingSession: boolean;\n  isLoadingMoreMessages: boolean;\n  hasMoreMessages: boolean;\n  loadMoreMessages: () => Promise<void>;\n}\n\nexport function ChatContainer({\n  scrollRef,\n  messageIdToIndex,\n  input,\n  setInput,\n  isLoading,\n  handleAddMessage,\n  groupedMessages,\n  taskStatus,\n  control,\n  isLoadingSession,\n  isLoadingMoreMessages,\n  hasMoreMessages,\n  loadMoreMessages,\n}: ChatContainerProps) {\n  const messagesEndRef = useRef<HTMLDivElement>(null);\n\n  // Infinite scroll handler\n  const handleScroll = useCallback(() => {\n    if (!scrollRef?.current || !loadMoreMessages) {\n      return;\n    }\n\n    const container = scrollRef.current;\n    // Check if user scrolled to the bottom (within 20px - much more sensitive)\n    const { scrollTop, scrollHeight, clientHeight } = container;\n    const distanceFromBottom = scrollHeight - scrollTop - clientHeight;\n\n    if (distanceFromBottom <= 20 && hasMoreMessages && !isLoadingMoreMessages) {\n      loadMoreMessages();\n    }\n  }, [scrollRef, loadMoreMessages, hasMoreMessages, isLoadingMoreMessages]);\n\n  // Add scroll event listener\n  useEffect(() => {\n    const container = scrollRef?.current;\n    if (container) {\n      container.addEventListener(\"scroll\", handleScroll);\n      return () => container.removeEventListener(\"scroll\", handleScroll);\n    }\n  }, [handleScroll, scrollRef]);\n\n  // This effect runs whenever the grouped messages array changes\n  useEffect(() => {\n    if (\n      taskStatus === TaskStatus.RUNNING ||\n      taskStatus === TaskStatus.NEEDS_HELP\n    ) {\n      scrollToBottom();\n    }\n  }, [taskStatus, groupedMessages]);\n\n  // Function to scroll to the bottom of the messages\n  const scrollToBottom = () => {\n    messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  };\n\n  return (\n    <div className=\"bg-bytebot-bronze-light-3 flex h-full flex-col\">\n      {isLoadingSession ? (\n        <div className=\"bg-bytebot-bronze-light-3 border-bytebot-bronze-light-7 flex h-full min-h-80 items-center justify-center overflow-hidden rounded-lg border\">\n          <Loader size={32} />\n        </div>\n      ) : groupedMessages.length > 0 ? (\n        <>\n          {/* Content area - scrolling handled by parent */}\n          <div className=\"flex-1\">\n            {groupedMessages.map((group, groupIndex) => (\n              <Fragment key={groupIndex}>\n                <MessageGroup\n                  group={group}\n                  messageIdToIndex={messageIdToIndex}\n                  taskStatus={taskStatus}\n                />\n              </Fragment>\n            ))}\n\n            {taskStatus === TaskStatus.RUNNING &&\n              control === Role.ASSISTANT && (\n                <div className=\"bg-bytebot-bronze-light-3 border-bytebot-bronze-light-7 flex items-center justify-start gap-4 border-x px-4 py-3\">\n                  <MessageAvatar role={Role.ASSISTANT} />\n                  <div className=\"flex items-center justify-start gap-2\">\n                    <div className=\"flex h-full items-center justify-center py-2\">\n                      <Loader size={20} />\n                    </div>\n                    <TextShimmer className=\"text-sm\" duration={2}>\n                      Bytebot is working...\n                    </TextShimmer>\n                  </div>\n                </div>\n              )}\n\n            {/* Loading indicator for infinite scroll at bottom */}\n            {isLoadingMoreMessages && (\n              <div className=\"flex justify-center py-4\">\n                <Loader size={24} />\n              </div>\n            )}\n\n            {/* This empty div is the target for scrolling */}\n            <div ref={messagesEndRef} />\n          </div>\n\n          {/* Fixed chat input at bottom */}\n          {[TaskStatus.RUNNING, TaskStatus.NEEDS_HELP].includes(taskStatus) && (\n            <div className=\"bg-bytebot-bronze-light-3 z-10 flex-shrink-0\">\n              <div className=\"border-bytebot-bronze-light-7 rounded-b-lg border-x border-b p-2\">\n                <div className=\"bg-bytebot-bronze-light-2 border-bytebot-bronze-light-7 rounded-lg border p-2\">\n                  <ChatInput\n                    input={input}\n                    isLoading={isLoading}\n                    onInputChange={setInput}\n                    onSend={handleAddMessage}\n                    minLines={1}\n                    placeholder=\"Add more details to your task...\"\n                  />\n                </div>\n              </div>\n            </div>\n          )}\n        </>\n      ) : (\n        <div className=\"flex h-full items-center justify-center\">\n          <p className=\"\">No messages yet...</p>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/ChatInput.tsx",
    "content": "import React, { useRef, useEffect, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { ArrowRight02Icon, Attachment01Icon, Cancel01Icon } from \"@hugeicons/core-free-icons\";\nimport { cn } from \"@/lib/utils\";\n\ninterface FileWithBase64 {\n  name: string;\n  base64: string;\n  type: string;\n  size: number;\n}\n\ninterface ChatInputProps {\n  input: string;\n  isLoading: boolean;\n  onInputChange: (value: string) => void;\n  onSend: () => void;\n  onFileUpload?: (files: FileWithBase64[]) => void;\n  minLines?: number;\n  placeholder?: string;\n}\n\nexport function ChatInput({\n  input,\n  isLoading,\n  onInputChange,\n  onSend,\n  onFileUpload,\n  minLines = 1,\n  placeholder = \"Give Bytebot a task to work on...\",\n}: ChatInputProps) {\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [selectedFiles, setSelectedFiles] = useState<FileWithBase64[]>([]);\n  const [errorMessage, setErrorMessage] = useState<string>(\"\");\n  \n  const MAX_FILES = 5;\n  const MAX_FILE_SIZE = 30 * 1024 * 1024; // 30MB per file in bytes\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    onSend();\n  };\n\n  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    const files = e.target.files;\n    if (!files || files.length === 0) return;\n    \n    setErrorMessage(\"\");\n\n    // Check max files limit\n    if (selectedFiles.length + files.length > MAX_FILES) {\n      setErrorMessage(`Maximum ${MAX_FILES} files allowed`);\n      e.target.value = '';\n      return;\n    }\n    \n\n    // Check individual file sizes\n    const oversizedFiles: string[] = [];\n    for (let i = 0; i < files.length; i++) {\n      const file = files[i];\n      if (file.size > MAX_FILE_SIZE) {\n        oversizedFiles.push(`${file.name} (${formatFileSize(file.size)})`);\n      }\n    }\n    \n    if (oversizedFiles.length > 0) {\n      setErrorMessage(`File(s) exceed 30MB limit: ${oversizedFiles.join(', ')}`);\n      e.target.value = '';\n      return;\n    }\n\n    const newFiles: FileWithBase64[] = [];\n\n    for (let i = 0; i < files.length; i++) {\n      const file = files[i];\n      const base64 = await convertToBase64(file);\n      \n      newFiles.push({\n        name: file.name,\n        base64: base64,\n        type: file.type,\n        size: file.size,\n      });\n    }\n\n    const updatedFiles = [...selectedFiles, ...newFiles];\n    setSelectedFiles(updatedFiles);\n    \n    if (onFileUpload) {\n      onFileUpload(updatedFiles);\n    }\n\n    // Reset the input\n    e.target.value = '';\n  };\n\n  const convertToBase64 = (file: File): Promise<string> => {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.readAsDataURL(file);\n      reader.onload = () => resolve(reader.result as string);\n      reader.onerror = (error) => reject(error);\n    });\n  };\n\n  const removeFile = (index: number) => {\n    const updatedFiles = selectedFiles.filter((_, i) => i !== index);\n    setSelectedFiles(updatedFiles);\n    setErrorMessage(\"\");\n    \n    if (onFileUpload) {\n      onFileUpload(updatedFiles);\n    }\n  };\n\n  const triggerFileInput = () => {\n    fileInputRef.current?.click();\n  };\n\n  const formatFileSize = (bytes: number): string => {\n    if (bytes === 0) return '0 Bytes';\n    const k = 1024;\n    const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n  };\n\n  // Auto-resize textarea based on content\n  useEffect(() => {\n    const textarea = textareaRef.current;\n    if (!textarea) return;\n\n    // Reset height to auto to get the correct scrollHeight\n    textarea.style.height = \"auto\";\n\n    // Calculate minimum height based on minLines\n    const lineHeight = 24; // approximate line height in pixels\n    const minHeight = lineHeight * minLines + 12;\n\n    // Set height to scrollHeight or minHeight, whichever is larger\n    const newHeight = Math.max(textarea.scrollHeight, minHeight);\n    textarea.style.height = `${newHeight}px`;\n  }, [input, minLines]);\n\n  // Determine button position based on minLines\n  const buttonPositionClass =\n    minLines > 1 ? \"bottom-1.5\" : \"top-1/2 -translate-y-1/2\";\n\n  return (\n    <div>\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        multiple\n        onChange={handleFileSelect}\n        className=\"hidden\"\n        accept=\"*/*\"\n      />\n      \n      {errorMessage && (\n        <div className=\"mb-2 rounded-md bg-red-50 px-3 py-2 text-sm text-red-600\">\n          {errorMessage}\n        </div>\n      )}\n      \n      {selectedFiles.length > 0 && (\n        <div className=\"mb-2\">\n          <div className=\"mb-1 flex items-center justify-between text-xs text-gray-500\">\n            <span>{selectedFiles.length} / {MAX_FILES} files</span>\n            <span>Max 30MB per file</span>\n          </div>\n          <div className=\"flex flex-wrap gap-2\">\n            {selectedFiles.map((file, index) => (\n              <div\n                key={index}\n                className=\"flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm\"\n              >\n                <span className=\"max-w-[200px] truncate\">{file.name}</span>\n                <button\n                  type=\"button\"\n                  onClick={() => removeFile(index)}\n                  className=\"ml-1 rounded-sm hover:bg-gray-200\"\n                >\n                  <HugeiconsIcon\n                    icon={Cancel01Icon}\n                    className=\"h-3 w-3 text-gray-600\"\n                  />\n                </button>\n              </div>\n            ))}\n          </div>\n        </div>\n      )}\n      \n      <form onSubmit={handleSubmit} className=\"relative\">\n        <textarea\n          ref={textareaRef}\n          placeholder={placeholder}\n          value={input}\n          onChange={(e) => onInputChange(e.target.value)}\n          className={cn(\n            \"placeholder:text-bytebot-bronze-light-10 w-full rounded-lg py-2 pr-16 pl-3 placeholder:text-[13px]\",\n            \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-bytebot-bronze-light-7 flex min-w-0 border bg-transparent text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n            \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n            \"resize-none overflow-hidden\",\n          )}\n          disabled={isLoading}\n          rows={1}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && !e.shiftKey) {\n              e.preventDefault();\n              onSend();\n            }\n          }}\n        />\n        <div className={`absolute right-2 ${buttonPositionClass} flex items-center gap-1`}>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon\"\n            className=\"h-6 w-6 cursor-pointer rounded-sm hover:bg-gray-100\"\n            onClick={triggerFileInput}\n            disabled={isLoading}\n          >\n            <HugeiconsIcon\n              icon={Attachment01Icon}\n              className=\"h-4 w-4 text-gray-600\"\n            />\n          </Button>\n          \n          {isLoading ? (\n            <div className=\"border-bytebot-bronze-light-7 border-t-primary h-5 w-5 animate-spin rounded-full border-2\" />\n          ) : (\n            <Button\n              type=\"submit\"\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"bg-bytebot-bronze-dark-7 hover:bg-bytebot-bronze-dark-6 h-6 w-6 cursor-pointer rounded-sm\"\n              disabled={isLoading}\n            >\n              <HugeiconsIcon\n                icon={ArrowRight02Icon}\n                className=\"h-4 w-4 text-white\"\n              />\n            </Button>\n          )}\n        </div>\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/MessageAvatar.tsx",
    "content": "import React from \"react\";\nimport Image from \"next/image\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { User03Icon } from \"@hugeicons/core-free-icons\";\nimport { Role } from \"@/types\";\n\ninterface MessageAvatarProps {\n  role: Role;\n}\n\nexport function MessageAvatar({ role }: MessageAvatarProps) {\n  const baseClasses = \"flex flex-shrink-0 items-center justify-center rounded-md border border-bytebot-bronze-light-7 bg-bytebot-bronze-light-1 h-[28px] w-[28px]\";\n\n  if (role === Role.ASSISTANT) {\n    return (\n      <div className={baseClasses}>\n        <Image\n          src=\"/bytebot_square_light.svg\"\n          alt=\"Bytebot\"\n          width={16}\n          height={16}\n          className=\"h-4 w-4\"\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div className={baseClasses}>\n      <HugeiconsIcon\n        icon={User03Icon}\n        className=\"text-bytebot-bronze-dark-9 w-4 h-4\"\n      />\n    </div>\n  );\n}"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/MessageGroup.tsx",
    "content": "import React from \"react\";\nimport { Role, TaskStatus } from \"@/types\";\nimport { GroupedMessages } from \"@/types\";\nimport { AssistantMessage } from \"./AssistantMessage\";\nimport { UserMessage } from \"./UserMessage\";\n\ninterface MessageGroupProps {\n  group: GroupedMessages;\n  taskStatus: TaskStatus;\n  messageIdToIndex: Record<string, number>;\n}\n\nexport function MessageGroup({ group, taskStatus, messageIdToIndex }: MessageGroupProps) {\n  if (group.role === Role.ASSISTANT) {\n    return <AssistantMessage group={group} taskStatus={taskStatus} messageIdToIndex={messageIdToIndex} />;\n  }\n\n  return <UserMessage group={group} messageIdToIndex={messageIdToIndex} />;\n}"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/UserMessage.tsx",
    "content": "import React from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport { GroupedMessages } from \"@/types\";\nimport { MessageAvatar } from \"./MessageAvatar\";\nimport {\n  isTextContentBlock,\n  isToolResultContentBlock,\n  isImageContentBlock,\n} from \"@bytebot/shared\";\n\ninterface UserMessageProps {\n  group: GroupedMessages;\n  messageIdToIndex: Record<string, number>;\n}\n\nexport function UserMessage({ group, messageIdToIndex }: UserMessageProps) {\n  if (messageIdToIndex[group.messages[0].id] === 0) {\n    return (\n      <div className=\"sticky top-0 z-10 bg-bytebot-bronze-light-4\">\n        <div className=\"border-bytebot-bronze-light-7 flex items-start justify-start gap-2 border px-4 py-3 bg-bytebot-bronze-light-2 rounded-t-lg\">\n          <MessageAvatar role={group.role} />\n\n          <div>\n            {group.messages.map((message) => (\n              <div\n                key={message.id}\n                data-message-index={messageIdToIndex[message.id]}\n              >\n                {/* Render hidden divs for each screenshot block */}\n                {message.content.map((block, blockIndex) => {\n                  if (\n                    isToolResultContentBlock(block) &&\n                    block.content &&\n                    block.content.length > 0\n                  ) {\n                    // Check ALL content items in the tool result, not just the first one\n                    const markers: React.ReactNode[] = [];\n                    block.content.forEach((contentItem, contentIndex) => {\n                      if (isImageContentBlock(contentItem)) {\n                        markers.push(\n                          <div\n                            key={`${blockIndex}-${contentIndex}`}\n                            data-message-index={messageIdToIndex[message.id]}\n                            data-block-index={blockIndex}\n                            data-content-index={contentIndex}\n                            style={{\n                              position: \"absolute\",\n                              width: 0,\n                              height: 0,\n                              overflow: \"hidden\",\n                            }}\n                          />\n                        );\n                      }\n                    });\n                    return markers;\n                  }\n                  return null;\n                })}\n                <div className=\"bg-bytebot-bronze-light-4 space-y-2 rounded-md px-2 py-1\">\n                  {message.content.map((block, index) => (\n                    <div\n                      key={index}\n                      className=\"text-bytebot-bronze-light-12 text-sm\"\n                    >\n                      {isTextContentBlock(block) && (\n                        <ReactMarkdown>{block.text}</ReactMarkdown>\n                      )}\n                    </div>\n                  ))}\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"bg-bytebot-bronze-light-3 flex items-start justify-end gap-2 px-4 py-3 border-x border-bytebot-bronze-light-7\">\n      <div>\n        {group.messages.map((message) => (\n          <div\n            key={message.id}\n            data-message-index={messageIdToIndex[message.id]}\n          >\n            {/* Render hidden divs for each screenshot block */}\n            {message.content.map((block, blockIndex) => {\n              if (\n                isToolResultContentBlock(block) &&\n                block.content &&\n                block.content.length > 0\n              ) {\n                // Check ALL content items in the tool result, not just the first one\n                const markers: React.ReactNode[] = [];\n                block.content.forEach((contentItem, contentIndex) => {\n                  if (isImageContentBlock(contentItem)) {\n                    markers.push(\n                      <div\n                        key={`${blockIndex}-${contentIndex}`}\n                        data-message-index={messageIdToIndex[message.id]}\n                        data-block-index={blockIndex}\n                        data-content-index={contentIndex}\n                        style={{\n                          position: \"absolute\",\n                          width: 0,\n                          height: 0,\n                          overflow: \"hidden\",\n                        }}\n                      />\n                    );\n                  }\n                });\n                return markers;\n              }\n              return null;\n            })}\n            <div className=\"space-y-2 rounded-md text-fuchsia-600\">\n              {message.content.map((block, index) => (\n                <div key={index} className=\"prose prose-sm max-w-none text-sm\">\n                  {isTextContentBlock(block) && (\n                    <ReactMarkdown>{block.text}</ReactMarkdown>\n                  )}\n                </div>\n              ))}\n            </div>\n          </div>\n        ))}\n      </div>\n\n      <MessageAvatar role={group.role} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/ComputerToolContent.tsx",
    "content": "import React from \"react\";\nimport { ComputerToolUseContentBlock } from \"@bytebot/shared\";\nimport { ComputerToolContentTakeOver } from \"./ComputerToolContentTakeOver\";\nimport { ComputerToolContentNormal } from \"./ComputerToolContentNormal\";\n\ninterface ComputerToolContentProps {\n  block: ComputerToolUseContentBlock;\n  isTakeOver?: boolean;\n}\n\nexport function ComputerToolContent({ block, isTakeOver = false }: ComputerToolContentProps) {\n  if (isTakeOver) {\n    return <ComputerToolContentTakeOver block={block} />;\n  }\n  \n  return <ComputerToolContentNormal block={block} />;\n}"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/ComputerToolContentNormal.tsx",
    "content": "import React from \"react\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  ComputerToolUseContentBlock,\n  isTypeKeysToolUseBlock,\n  isTypeTextToolUseBlock,\n  isPressKeysToolUseBlock,\n  isWaitToolUseBlock,\n  isScrollToolUseBlock,\n  isApplicationToolUseBlock,\n  Application,\n  isPasteTextToolUseBlock,\n  isReadFileToolUseBlock,\n} from \"@bytebot/shared\";\nimport { getIcon, getLabel } from \"./ComputerToolUtils\";\n\ninterface ComputerToolContentNormalProps {\n  block: ComputerToolUseContentBlock;\n}\n\nconst applicationMap: Record<Application, string> = {\n  firefox: \"Firefox\",\n  \"1password\": \"1Password\",\n  thunderbird: \"Thunderbird\",\n  vscode: \"Visual Studio Code\",\n  terminal: \"Terminal\",\n  directory: \"File Manager\",\n  desktop: \"Desktop\",\n};\n\nfunction ToolDetailsNormal({ block }: { block: ComputerToolUseContentBlock }) {\n  const baseClasses =\n    \"px-1 py-0.5 text-[12px] text-bytebot-bronze-light-11 bg-bytebot-red-light-1 border border-bytebot-bronze-light-7 rounded-md\";\n\n  return (\n    <>\n      {isApplicationToolUseBlock(block) && (\n        <p className={baseClasses}>\n          {applicationMap[block.input.application as Application]}\n        </p>\n      )}\n\n      {/* Text for type and key actions */}\n      {(isTypeKeysToolUseBlock(block) || isPressKeysToolUseBlock(block)) && (\n        <p className={baseClasses}>{String(block.input.keys.join(\" + \"))}</p>\n      )}\n\n      {(isTypeTextToolUseBlock(block) || isPasteTextToolUseBlock(block)) && (\n        <p className={baseClasses}>\n          {String(\n            block.input.isSensitive\n              ? \"●\".repeat(block.input.text.length)\n              : block.input.text,\n          )}\n        </p>\n      )}\n\n      {/* Duration for wait actions */}\n      {isWaitToolUseBlock(block) && (\n        <p className={baseClasses}>{`${block.input.duration}ms`}</p>\n      )}\n\n      {/* Coordinates for click/mouse actions */}\n      {block.input.coordinates && (\n        <p className={baseClasses}>\n          {(block.input.coordinates as { x: number; y: number }).x},{\" \"}\n          {(block.input.coordinates as { x: number; y: number }).y}\n        </p>\n      )}\n\n      {/* Start and end coordinates for path actions */}\n      {\"path\" in block.input &&\n        Array.isArray(block.input.path) &&\n        block.input.path.every(\n          (point) => point.x !== undefined && point.y !== undefined,\n        ) && (\n          <p className={baseClasses}>\n            From: {block.input.path[0].x}, {block.input.path[0].y} → To:{\" \"}\n            {block.input.path[block.input.path.length - 1].x},{\" \"}\n            {block.input.path[block.input.path.length - 1].y}\n          </p>\n        )}\n\n      {/* Scroll information */}\n      {isScrollToolUseBlock(block) && (\n        <p className={baseClasses}>\n          {String(block.input.direction)} {Number(block.input.scrollCount)}\n        </p>\n      )}\n\n      {/* File information */}\n      {isReadFileToolUseBlock(block) && (\n        <p className={baseClasses}>{block.input.path}</p>\n      )}\n    </>\n  );\n}\n\nexport function ComputerToolContentNormal({\n  block,\n}: ComputerToolContentNormalProps) {\n  // Don't render screenshot tool use blocks here - they're handled separately\n  if (getLabel(block) === \"Screenshot\") {\n    return null;\n  }\n\n  return (\n    <div className=\"mb-3 max-w-4/5\">\n      <div className=\"flex items-center gap-2\">\n        <HugeiconsIcon\n          icon={getIcon(block)}\n          className=\"text-bytebot-bronze-dark-9 h-4 w-4\"\n        />\n        <p className=\"text-bytebot-bronze-light-11 text-xs\">\n          {getLabel(block)}\n        </p>\n        <ToolDetailsNormal block={block} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/ComputerToolContentTakeOver.tsx",
    "content": "import React from \"react\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  ComputerToolUseContentBlock,\n  isTypeKeysToolUseBlock,\n  isTypeTextToolUseBlock,\n  isPressKeysToolUseBlock,\n  isWaitToolUseBlock,\n  isScrollToolUseBlock,\n} from \"@bytebot/shared\";\nimport { getIcon, getLabel } from \"./ComputerToolUtils\";\n\ninterface ComputerToolContentTakeOverProps {\n  block: ComputerToolUseContentBlock;\n}\n\nfunction ToolDetailsTakeOver({ block }: { block: ComputerToolUseContentBlock }) {\n  const baseClasses = \"px-1 py-0.5 text-xs text-fuchsia-600 bg-bytebot-red-light-1 border border-bytebot-bronze-light-7 rounded-md\";\n\n  return (\n    <>\n      {/* Text for type and key actions */}\n      {(isTypeKeysToolUseBlock(block) || isPressKeysToolUseBlock(block)) && (\n        <p className={baseClasses}>\n          {String(block.input.keys.join(\"+\"))}\n        </p>\n      )}\n      \n      {isTypeTextToolUseBlock(block) && (\n        <p className={baseClasses}>\n          {String(\n            block.input.isSensitive\n              ? \"●\".repeat(block.input.text.length)\n              : block.input.text,\n          )}\n        </p>\n      )}\n      \n      {/* Duration for wait actions */}\n      {isWaitToolUseBlock(block) && (\n        <p className={baseClasses}>\n          {`${block.input.duration}ms`}\n        </p>\n      )}\n      \n      {/* Coordinates for click/mouse actions */}\n      {block.input.coordinates && (\n        <p className={baseClasses}>\n          {(block.input.coordinates as { x: number; y: number }).x},\n          {\" \"}\n          {(block.input.coordinates as { x: number; y: number }).y}\n        </p>\n      )}\n      \n      {/* Start and end coordinates for path actions */}\n      {\"path\" in block.input &&\n        Array.isArray(block.input.path) &&\n        block.input.path.every(\n          (point) => point.x !== undefined && point.y !== undefined,\n        ) && (\n          <p className={baseClasses}>\n            From: {block.input.path[0].x}, {block.input.path[0].y} → To:{\" \"}\n            {block.input.path[block.input.path.length - 1].x},{\" \"}\n            {block.input.path[block.input.path.length - 1].y}\n          </p>\n        )}\n      \n      {/* Scroll information */}\n      {isScrollToolUseBlock(block) && (\n        <p className={baseClasses}>\n          {String(block.input.direction)} {Number(block.input.scrollCount)}\n        </p>\n      )}\n    </>\n  );\n}\n\nexport function ComputerToolContentTakeOver({ block }: ComputerToolContentTakeOverProps) {\n  // Don't render screenshot tool use blocks here - they're handled separately\n  if (getLabel(block) === \"Screenshot\") {\n    return null;\n  }\n\n  return (\n    <div className=\"max-w-4/5\">\n      <div className=\"flex items-center justify-start gap-2\">\n        <div className=\"w-7 h-7 flex items-center justify-center\">\n          <HugeiconsIcon\n            icon={getIcon(block)}\n            className=\"h-4 w-4 text-fuchsia-600\"\n          />\n        </div>\n        <p className=\"text-xs text-bytebot-bronze-light-11\">\n          {getLabel(block)}\n        </p>\n        <ToolDetailsTakeOver block={block} />\n      </div>\n    </div>\n  );\n} "
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/ComputerToolUtils.tsx",
    "content": "import {\n  Camera01Icon,\n  User03Icon,\n  Cursor02Icon,\n  TypeCursorIcon,\n  MouseRightClick06Icon,\n  TimeQuarter02Icon,\n  BrowserIcon,\n  FilePasteIcon,\n  FileIcon,\n} from \"@hugeicons/core-free-icons\";\nimport {\n  ComputerToolUseContentBlock,\n  isScreenshotToolUseBlock,\n  isWaitToolUseBlock,\n  isTypeKeysToolUseBlock,\n  isTypeTextToolUseBlock,\n  isPressKeysToolUseBlock,\n  isMoveMouseToolUseBlock,\n  isScrollToolUseBlock,\n  isCursorPositionToolUseBlock,\n  isClickMouseToolUseBlock,\n  isDragMouseToolUseBlock,\n  isPressMouseToolUseBlock,\n  isTraceMouseToolUseBlock,\n  isApplicationToolUseBlock,\n  isPasteTextToolUseBlock,\n  isReadFileToolUseBlock,\n} from \"@bytebot/shared\";\n\n// Define the IconType for proper type checking\nexport type IconType =\n  | typeof Camera01Icon\n  | typeof User03Icon\n  | typeof Cursor02Icon\n  | typeof TypeCursorIcon\n  | typeof MouseRightClick06Icon\n  | typeof TimeQuarter02Icon\n  | typeof BrowserIcon\n  | typeof FilePasteIcon\n  | typeof FileIcon;\n\nexport function getIcon(block: ComputerToolUseContentBlock): IconType {\n  if (isScreenshotToolUseBlock(block)) {\n    return Camera01Icon;\n  }\n\n  if (isWaitToolUseBlock(block)) {\n    return TimeQuarter02Icon;\n  }\n\n  if (\n    isTypeKeysToolUseBlock(block) ||\n    isTypeTextToolUseBlock(block) ||\n    isPressKeysToolUseBlock(block)\n  ) {\n    return TypeCursorIcon;\n  }\n\n  if (isPasteTextToolUseBlock(block)) {\n    return FilePasteIcon;\n  }\n\n  if (\n    isMoveMouseToolUseBlock(block) ||\n    isScrollToolUseBlock(block) ||\n    isCursorPositionToolUseBlock(block) ||\n    isClickMouseToolUseBlock(block) ||\n    isDragMouseToolUseBlock(block) ||\n    isPressMouseToolUseBlock(block) ||\n    isTraceMouseToolUseBlock(block)\n  ) {\n    if (block.input.button === \"right\") {\n      return MouseRightClick06Icon;\n    }\n\n    return Cursor02Icon;\n  }\n\n  if (isApplicationToolUseBlock(block)) {\n    return BrowserIcon;\n  }\n\n  if (isReadFileToolUseBlock(block)) {\n    return FileIcon;\n  }\n\n  return User03Icon;\n}\n\nexport function getLabel(block: ComputerToolUseContentBlock) {\n  if (isScreenshotToolUseBlock(block)) {\n    return \"Screenshot\";\n  }\n\n  if (isWaitToolUseBlock(block)) {\n    return \"Wait\";\n  }\n\n  if (isTypeKeysToolUseBlock(block)) {\n    return \"Keys\";\n  }\n\n  if (isTypeTextToolUseBlock(block)) {\n    return \"Type\";\n  }\n\n  if (isPasteTextToolUseBlock(block)) {\n    return \"Paste\";\n  }\n\n  if (isPressKeysToolUseBlock(block)) {\n    return \"Press Keys\";\n  }\n\n  if (isMoveMouseToolUseBlock(block)) {\n    return \"Move Mouse\";\n  }\n\n  if (isScrollToolUseBlock(block)) {\n    return \"Scroll\";\n  }\n\n  if (isCursorPositionToolUseBlock(block)) {\n    return \"Cursor Position\";\n  }\n\n  if (isClickMouseToolUseBlock(block)) {\n    const button = block.input.button;\n    if (button === \"left\") {\n      if (block.input.clickCount === 2) {\n        return \"Double Click\";\n      }\n\n      if (block.input.clickCount === 3) {\n        return \"Triple Click\";\n      }\n\n      return \"Click\";\n    }\n\n    return `${block.input.button?.charAt(0).toUpperCase() + block.input.button?.slice(1)} Click`;\n  }\n\n  if (isDragMouseToolUseBlock(block)) {\n    return \"Drag\";\n  }\n\n  if (isPressMouseToolUseBlock(block)) {\n    return \"Press Mouse\";\n  }\n\n  if (isTraceMouseToolUseBlock(block)) {\n    return \"Trace Mouse\";\n  }\n\n  if (isApplicationToolUseBlock(block)) {\n    return \"Open Application\";\n  }\n\n  if (isReadFileToolUseBlock(block)) {\n    return \"Read File\";\n  }\n\n  return \"Unknown\";\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/ErrorContent.tsx",
    "content": "import React from \"react\";\nimport { isTextContentBlock, ToolResultContentBlock } from \"@bytebot/shared\";\nimport { AlertCircleIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\n\ninterface ErrorContentProps {\n  block: ToolResultContentBlock;\n}\n\nexport function ErrorContent({ block }: ErrorContentProps) {\n  return (\n    <div className=\"mb-3 rounded-md border border-red-200 bg-red-100 p-2\">\n      <div className=\"flex items-center justify-start gap-2\">\n        <HugeiconsIcon\n          icon={AlertCircleIcon}\n          className=\"h-5 w-5 text-red-800\"\n        />\n        <div className=\"prose prose-sm max-w-none text-sm text-red-800\">\n          {isTextContentBlock(block.content?.[0])\n            ? block.content?.[0].text\n            : \"Error running tool\"}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/ImageContent.tsx",
    "content": "import React from \"react\";\nimport Image from \"next/image\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { Camera01Icon } from \"@hugeicons/core-free-icons\";\nimport { ImageContentBlock } from \"@bytebot/shared\";\n\ninterface ImageContentProps {\n  block: ImageContentBlock;\n}\n\nexport function ImageContent({ block }: ImageContentProps) {\n  // Use a fixed size for the image since width/height are not available on block.source\n  const width = 250;\n  const height = 250;\n  return (\n    <div className=\"max-w-4/5 mb-3\">\n      <div className=\"flex items-center gap-2 mb-2\">\n        <HugeiconsIcon\n          icon={Camera01Icon}\n          className=\"text-bytebot-bronze-dark-9 h-4 w-4\"\n        />\n        <p className=\"text-bytebot-bronze-light-11 text-xs\">\n          Screenshot taken\n        </p>\n      </div>\n      <div className=\"border border-bytebot-bronze-light-7 rounded-md overflow-hidden inline-block\">\n        <Image\n          src={`data:image/png;base64,${block.source.data}`}\n          alt=\"Screenshot\"\n          width={width}\n          height={height}\n          className=\"object-contain block\"\n        />\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/MessageContent.tsx",
    "content": "import React from \"react\";\nimport {\n  MessageContentBlock,\n  isTextContentBlock,\n  isImageContentBlock,\n  isComputerToolUseContentBlock,\n  isToolResultContentBlock,\n} from \"@bytebot/shared\";\nimport { TextContent } from \"./TextContent\";\nimport { ImageContent } from \"./ImageContent\";\nimport { ComputerToolContent } from \"./ComputerToolContent\";\nimport { ErrorContent } from \"./ErrorContent\";\n\ninterface MessageContentProps {\n  content: MessageContentBlock[];\n  isTakeOver?: boolean;\n}\n\nexport function MessageContent({\n  content,\n  isTakeOver = false,\n}: MessageContentProps) {\n  // Filter content blocks and check if any visible content remains\n  const visibleBlocks = content.filter((block) => {\n    // Filter logic from the original code\n    if (\n      isToolResultContentBlock(block) &&\n      block.content &&\n      block.content.some((contentBlock) => isImageContentBlock(contentBlock))\n    ) {\n      return true;\n    }\n    if (\n      isToolResultContentBlock(block) &&\n      block.tool_use_id !== \"set_task_status\" &&\n      !block.is_error\n    ) {\n      return false;\n    }\n    return true;\n  });\n\n  // Skip rendering if no visible content\n  if (visibleBlocks.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"w-full\">\n      {visibleBlocks.map((block, index) => (\n        <div key={index}>\n          {isTextContentBlock(block) && <TextContent block={block} />}\n\n          {isToolResultContentBlock(block) &&\n            !block.is_error &&\n            block.content.map((contentBlock, contentBlockIndex) => {\n              if (isImageContentBlock(contentBlock)) {\n                return (\n                  <ImageContent key={contentBlockIndex} block={contentBlock} />\n                );\n              }\n              return null;\n            })}\n\n          {isComputerToolUseContentBlock(block) && (\n            <ComputerToolContent block={block} isTakeOver={isTakeOver} />\n          )}\n\n          {isToolResultContentBlock(block) && block.is_error && (\n            <ErrorContent block={block} />\n          )}\n\n          {isToolResultContentBlock(block) &&\n            !block.is_error &&\n            block.tool_use_id === \"set_task_status\" &&\n            block.content?.[0].type === \"text\" && (\n              <TextContent block={block.content?.[0]} />\n            )}\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/messages/content/TextContent.tsx",
    "content": "import React from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport { TextContentBlock } from \"@bytebot/shared\";\n\ninterface TextContentProps {\n  block: TextContentBlock;\n}\n\nexport function TextContent({ block }: TextContentProps) {\n  return (\n    <div className=\"mb-3\">\n      <div className=\"text-bytebot-bronze-dark-8 prose prose-sm max-w-none text-sm\">\n        <ReactMarkdown\n          components={{\n            h1: ({ children }) => (\n              <h1 className=\"text-bytebot-bronze-dark-9 mt-4 mb-2 text-base font-semibold\">\n                {children}\n              </h1>\n            ),\n            h2: ({ children }) => (\n              <h2 className=\"text-bytebot-bronze-dark-9 mt-3 mb-2 text-sm font-semibold\">\n                {children}\n              </h2>\n            ),\n            h3: ({ children }) => (\n              <h3 className=\"text-bytebot-bronze-dark-9 mt-3 mb-1 text-sm font-medium\">\n                {children}\n              </h3>\n            ),\n            h4: ({ children }) => (\n              <h4 className=\"text-bytebot-bronze-dark-8 mt-2 mb-1 text-sm font-medium\">\n                {children}\n              </h4>\n            ),\n            h5: ({ children }) => (\n              <h5 className=\"text-bytebot-bronze-dark-8 mt-2 mb-1 text-xs font-medium\">\n                {children}\n              </h5>\n            ),\n            h6: ({ children }) => (\n              <h6 className=\"text-bytebot-bronze-dark-8 mt-2 mb-1 text-xs font-medium\">\n                {children}\n              </h6>\n            ),\n            p: ({ children }) => (\n              <p className=\"mb-2 leading-relaxed\">{children}</p>\n            ),\n            ul: ({ children }) => (\n              <ul className=\"mb-2 ml-4 list-disc\">\n                {children}\n              </ul>\n            ),\n            ol: ({ children }) => (\n              <ol className=\"mb-2 ml-4 list-decimal\">\n                {children}\n              </ol>\n            ),\n            li: ({ children }) => (\n              <li className=\"mb-1 text-sm leading-relaxed\">\n                {children}\n              </li>\n            ),\n            blockquote: ({ children }) => (\n              <blockquote className=\"border-bytebot-bronze-light-7 text-bytebot-bronze-dark-7 mb-2 border-l-4 pl-4 italic\">\n                {children}\n              </blockquote>\n            ),\n            code: ({ children, className }) => {\n              const isInline = !className;\n              return isInline ? (\n                <code className=\"text-bytebot-bronze-dark-9 rounded px-1 py-0.5 font-mono text-xs\">\n                  {children}\n                </code>\n              ) : (\n                <code className=\"text-bytebot-bronze-dark-9 block overflow-x-auto rounded p-3 font-mono text-xs whitespace-pre-wrap\">\n                  {children}\n                </code>\n              );\n            },\n            pre: ({ children }) => (\n              <pre className=\"mb-2 overflow-x-auto rounded border p-3\">\n                {children}\n              </pre>\n            ),\n            strong: ({ children }) => (\n              <strong className=\"text-bytebot-bronze-dark-9 font-semibold\">\n                {children}\n              </strong>\n            ),\n            em: ({ children }) => (\n              <em className=\"text-bytebot-bronze-dark-8 italic\">\n                {children}\n              </em>\n            ),\n            a: ({ children, href }) => (\n              <a\n                href={href}\n                className=\"text-blue-600 underline hover:text-blue-800\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                {children}\n              </a>\n            ),\n          }}\n        >\n          {block.text}\n        </ReactMarkdown>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "packages/bytebot-ui/src/components/screenshot/ScreenshotViewer.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport Image from 'next/image';\nimport { ScreenshotData } from '@/utils/screenshotUtils';\n\ninterface ScreenshotViewerProps {\n  screenshot: ScreenshotData | null;\n  className?: string;\n}\n\nexport function ScreenshotViewer({ screenshot, className = '' }: ScreenshotViewerProps) {\n  const [currentScreenshot, setCurrentScreenshot] = useState(screenshot);\n\n  useEffect(() => {\n    if (screenshot?.id !== currentScreenshot?.id) {\n      setCurrentScreenshot(screenshot);\n    }\n  }, [screenshot, currentScreenshot]);\n\n  if (!currentScreenshot) {\n    return (\n      <div className={`flex items-center justify-center bg-gray-100 ${className}`}>\n        <div className=\"text-center text-gray-500\">\n          <div className=\"mb-2 text-4xl\">📷</div>\n          <p className=\"text-sm\">No screenshots available</p>\n          <p className=\"text-xs mt-1\">Screenshots will appear here when the task has run</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={`relative overflow-hidden ${className}`}>\n      <Image\n        src={`data:image/png;base64,${currentScreenshot.base64Data}`}\n        alt=\"Task screenshot\"\n        fill\n        className=\"object-contain\"\n        priority\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/tasks/TaskItem.tsx",
    "content": "import React from \"react\";\nimport { Task, TaskStatus } from \"@/types\";\nimport { format } from \"date-fns\";\nimport { capitalizeFirstChar } from \"@/utils/stringUtils\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  Tick02Icon,\n  CancelCircleIcon,\n  AlertCircleIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { Loader } from \"@/components/ui/loader\";\nimport Link from \"next/link\";\n\ninterface TaskItemProps {\n  task: Task;\n}\n\ninterface StatusIconConfig {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  icon?: any; // HugeIcons IconSvgObject type\n  color?: string;\n  useLoader?: boolean;\n}\n\nconst STATUS_CONFIGS: Record<TaskStatus, StatusIconConfig> = {\n  [TaskStatus.COMPLETED]: {\n    icon: Tick02Icon,\n    color: \"text-bytebot-green-8\",\n  },\n  [TaskStatus.RUNNING]: {\n    useLoader: true,\n  },\n  [TaskStatus.NEEDS_HELP]: {\n    icon: AlertCircleIcon,\n    color: \"text-[#FF9D00]\",\n  },\n  [TaskStatus.PENDING]: {\n    useLoader: true,\n  },\n  [TaskStatus.FAILED]: {\n    icon: AlertCircleIcon,\n    color: \"text-bytebot-red-light-9\",\n  },\n  [TaskStatus.NEEDS_REVIEW]: {\n    icon: AlertCircleIcon,\n    color: \"text-[#FF9D00]\",\n  },\n  [TaskStatus.CANCELLED]: {\n    icon: CancelCircleIcon,\n    color: \"text-bytebot-bronze-light-10\",\n  },\n};\n\nexport const TaskItem: React.FC<TaskItemProps> = ({ task }) => {\n  // Format date to match the screenshot (e.g., \"Today 9:13am\" or \"April 13, 2025, 12:01pm\")\n  const formatDate = (dateString: string) => {\n    const date = new Date(dateString);\n    const today = new Date();\n\n    const isToday =\n      date.getDate() === today.getDate() &&\n      date.getMonth() === today.getMonth() &&\n      date.getFullYear() === today.getFullYear();\n\n    const formatString = isToday ? `'Today' h:mma` : \"MMMM d, yyyy h:mma\";\n\n    const formatted = format(date, formatString).toLowerCase();\n    return capitalizeFirstChar(formatted);\n  };\n\n  const StatusIcon = ({ status }: { status: TaskStatus }) => {\n    const config = STATUS_CONFIGS[status];\n    if (!config) return null;\n\n    const { icon, color, useLoader } = config;\n\n    if (useLoader) {\n      return (\n        <div className=\"flex items-center justify-center\">\n          <Loader size={16} />\n        </div>\n      );\n    }\n\n    return (\n      <div className=\"flex items-center justify-center\">\n        <HugeiconsIcon icon={icon} className={`h-5 w-5 ${color}`} />\n      </div>\n    );\n  };\n\n  return (\n    <Link href={`/tasks/${task.id}`} className=\"block\">\n      <div className=\"bg-bytebot-bronze-light-2 border-bytebot-bronze-light-7 hover:bg-bytebot-bronze-light-3 flex min-h-24 items-start rounded-lg border p-5 transition-colors\">\n        <div className=\"mb-0.5 flex-1 space-y-2\">\n          <div className=\"flex items-center justify-start space-x-2\">\n            <StatusIcon status={task.status} />\n            <div className=\"text-byhtebot-bronze-dark-7 text-sm font-medium\">\n              {capitalizeFirstChar(task.description)}\n            </div>\n          </div>\n          <div className=\"ml-7 flex items-center justify-start space-x-1.5 text-xs\">\n            <span className=\"text-bytebot-bronze-light-10\">\n              {formatDate(task.createdAt)}\n            </span>\n          </div>\n        </div>\n      </div>\n    </Link>\n  );\n};\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/tasks/TaskList.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useState, useCallback } from \"react\";\nimport { TaskItem } from \"@/components/tasks/TaskItem\";\nimport { fetchTasks } from \"@/utils/taskUtils\";\nimport { Task } from \"@/types\";\nimport { useWebSocket } from \"@/hooks/useWebSocket\";\n\ninterface TaskListProps {\n  limit?: number;\n  className?: string;\n  title?: string;\n  description?: string;\n  showHeader?: boolean;\n}\n\nexport const TaskList: React.FC<TaskListProps> = ({ \n  limit = 5, \n  className = \"\", \n  title = \"Latest Tasks\",\n  description,\n  showHeader = true,\n}) => {\n  const [tasks, setTasks] = useState<Task[]>([]);\n  const [isLoading, setIsLoading] = useState(true);\n\n  // WebSocket handlers for real-time updates\n  const handleTaskUpdate = useCallback((updatedTask: Task) => {\n    setTasks(prev => \n      prev.map(task => \n        task.id === updatedTask.id ? updatedTask : task\n      )\n    );\n  }, []);\n\n  const handleTaskCreated = useCallback((newTask: Task) => {\n    setTasks(prev => {\n      const updated = [newTask, ...prev];\n      return updated.slice(0, limit);\n    });\n  }, [limit]);\n\n  const handleTaskDeleted = useCallback((taskId: string) => {\n    setTasks(prev => prev.filter(task => task.id !== taskId));\n  }, []);\n\n  // Initialize WebSocket for task list updates\n  useWebSocket({\n    onTaskUpdate: handleTaskUpdate,\n    onTaskCreated: handleTaskCreated,\n    onTaskDeleted: handleTaskDeleted,\n  });\n\n  useEffect(() => {\n    const loadTasks = async () => {\n      setIsLoading(true);\n      try {\n        const result = await fetchTasks({ limit });\n        setTasks(result.tasks);\n      } catch (error) {\n        console.error(\"Failed to load tasks:\", error);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    loadTasks();\n  }, [limit]);\n\n  return (\n    <div className={className}>\n      {showHeader && (\n        <div className=\"mb-6 flex flex-col gap-1\">\n          <h2 className=\"text-base font-medium\">{title}</h2>\n          <p className=\"text-sm text-bytebot-bronze-light-11\">{description}</p>\n        </div>\n      )}\n      \n      {isLoading ? (\n        <div className=\"p-4 text-center\">\n          <div className=\"animate-spin h-6 w-6 border-4 border-bytebot-bronze-light-5 border-t-bytebot-bronze rounded-full mx-auto mb-2\"></div>\n          <p className=\"text-gray-500 text-sm\">Loading tasks...</p>\n        </div>\n      ) : tasks.length === 0 ? (\n        <div className=\"p-4 text-center border border-dashed border-bytebot-bronze-light-5 rounded-lg\">\n          <p className=\"text-gray-500 text-sm\">No tasks available</p>\n          <p className=\"text-gray-400 text-xs mt-1\">Your completed tasks will appear here</p>\n        </div>\n      ) : (\n        <div className=\"space-y-3\">\n          {tasks.map((task) => (\n            <TaskItem key={task.id} task={task} />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/tasks/TaskTabs.tsx",
    "content": "import React from \"react\";\nimport { TaskStatus } from \"@/types\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  Tick02Icon,\n  CursorProgress04Icon,\n  MultiplicationSignIcon,\n  ListViewIcon,\n} from \"@hugeicons/core-free-icons\";\n\ntype TabKey = \"ALL\" | \"ACTIVE\" | \"COMPLETED\" | \"CANCELLED_FAILED\";\n\ninterface TaskTabsProps {\n  activeTab: TabKey;\n  onTabChange: (tab: TabKey) => void;\n  taskCounts: Record<TabKey, number>;\n}\n\ninterface TabConfig {\n  label: string;\n  icon:\n    | typeof Tick02Icon\n    | typeof CursorProgress04Icon\n    | typeof MultiplicationSignIcon\n    | typeof ListViewIcon;\n  color: string;\n  statuses: TaskStatus[];\n}\n\nconst TAB_CONFIGS: Record<TabKey, TabConfig> = {\n  ALL: {\n    label: \"All\",\n    icon: ListViewIcon,\n    color: \"text-bytebot-bronze-light-10\",\n    statuses: Object.values(TaskStatus),\n  },\n  ACTIVE: {\n    label: \"Active\",\n    icon: CursorProgress04Icon,\n    color: \"text-bytebot-bronze-light-10\",\n    statuses: [\n      TaskStatus.PENDING,\n      TaskStatus.RUNNING,\n      TaskStatus.NEEDS_HELP,\n      TaskStatus.NEEDS_REVIEW,\n    ],\n  },\n  COMPLETED: {\n    label: \"Completed\",\n    icon: Tick02Icon,\n    color: \"text-bytebot-bronze-light-10\",\n    statuses: [TaskStatus.COMPLETED],\n  },\n  CANCELLED_FAILED: {\n    label: \"Cancelled/Failed\",\n    icon: MultiplicationSignIcon,\n    color: \"text-bytebot-bronze-light-10\",\n    statuses: [TaskStatus.CANCELLED, TaskStatus.FAILED],\n  },\n};\n\nexport const TaskTabs: React.FC<TaskTabsProps> = ({\n  activeTab,\n  onTabChange,\n  taskCounts,\n}) => {\n  const tabs = Object.entries(TAB_CONFIGS) as [TabKey, TabConfig][];\n\n  return (\n    <div className=\"border-bytebot-bronze-light-7 mb-6 border-b\">\n      <div className=\"flex overflow-x-auto\">\n        {tabs.map(([tabKey, config]) => {\n          const isActive = activeTab === tabKey;\n          const count = taskCounts[tabKey] || 0;\n\n          return (\n            <button\n              key={tabKey}\n              onClick={() => onTabChange(tabKey)}\n              className={`flex cursor-pointer items-center space-x-2 border-b-2 px-4 py-3 whitespace-nowrap transition-colors ${\n                isActive\n                  ? \"border-bytebot-bronze-dark-7 text-bytebot-bronze-dark-7\"\n                  : \"text-bytebot-bronze-light-10 hover:text-bytebot-bronze-dark-7 border-transparent\"\n              }`}\n            >\n              <HugeiconsIcon\n                icon={config.icon}\n                className={`h-4 w-4 ${isActive ? \"text-bytebot-bronze-dark-7\" : config.color}`}\n              />\n              <span className=\"text-sm font-medium\">{config.label}</span>\n              {count > 0 && (\n                <span\n                  className={`rounded-full px-2 py-0.5 text-xs ${\n                    isActive\n                      ? \"bg-bytebot-bronze-dark-7 text-white\"\n                      : \"bg-bytebot-bronze-light-7 text-bytebot-bronze-light-11\"\n                  }`}\n                >\n                  {count}\n                </span>\n              )}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\n// Export the TabKey type and TAB_CONFIGS for use in other components\nexport type { TabKey };\nexport { TAB_CONFIGS };\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/TopicPopover.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useRef, ReactElement } from \"react\";\n\ninterface TopicPopoverProps {\n  children: React.ReactNode;\n  onOpenChange?: (isOpen: boolean) => void;\n  isActive?: boolean;\n}\n\nexport const TopicPopover: React.FC<TopicPopoverProps> = ({\n  children,\n  onOpenChange,\n  isActive = false,\n}) => {\n  const [isOpen, setIsOpen] = React.useState(false);\n  const popoverRef = useRef<HTMLDivElement>(null);\n\n  // Sync with parent's active state\n  useEffect(() => {\n    setIsOpen(isActive);\n  }, [isActive]);\n\n  // Close popover when clicking outside\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {\n        setIsOpen(false);\n        if (onOpenChange) {\n          onOpenChange(false);\n        }\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener(\"mousedown\", handleClickOutside);\n    }\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [isOpen, onOpenChange]);\n\n  const handleToggle = () => {\n    const newState = !isOpen;\n    setIsOpen(newState);\n    if (onOpenChange) {\n      onOpenChange(newState);\n    }\n  };\n\n  // Create a modified version of the button with updated text color\n  const modifiedChildren = React.Children.map(children, (child) => {\n    // Only process React elements (not strings, numbers, etc.)\n    if (!React.isValidElement(child)) return child;\n    \n    // Cast to ReactElement to access props properly\n    const element = child as ReactElement<{ className?: string }>;\n    \n    // Get the existing className\n    const existingClassName = element.props.className || '';\n    \n    // Replace text-bytebot-bronze-light-11 with text-bytebot-bronze-light-12 when open\n    const updatedClassName = isOpen \n      ? existingClassName.replace('text-bytebot-bronze-light-11', 'text-bytebot-bronze-light-12')\n      : existingClassName;\n    \n    // Clone the element with the updated className\n    return React.cloneElement(element, {\n      ...element.props,\n      className: updatedClassName\n    });\n  });\n\n  return (\n    <div className=\"relative\" ref={popoverRef}>\n      <div onClick={handleToggle} className={isOpen ? \"bg-bytebot-bronze-light-1 rounded-full\" : \"\"}>\n        {modifiedChildren}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-transparent border-bytebot-bronze-light-7 shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\ntype ButtonProps = React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n    icon?: React.ReactNode\n    iconPosition?: \"left\" | \"right\"\n  }\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  icon,\n  iconPosition = \"left\",\n  children,\n  ...props\n}: ButtonProps) {\n  const Comp = asChild ? Slot : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    >\n      {icon && iconPosition === \"left\" && (\n        <span className=\"mr-1 flex items-center\">{icon}</span>\n      )}\n      {children}\n      {icon && iconPosition === \"right\" && (\n        <span className=\"ml-1 flex items-center\">{icon}</span>\n      )}\n    </Comp>\n  )\n}\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/copy-button.tsx",
    "content": "import React, { useState } from 'react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Copy01Icon } from '@hugeicons/core-free-icons';\nimport { Button } from './button';\nimport { copyToClipboard } from '@/utils/clipboard';\nimport { cn } from '@/lib/utils';\n\ninterface CopyButtonProps {\n  text: string;\n  className?: string;\n  size?: 'sm' | 'icon';\n  variant?: 'ghost' | 'outline' | 'secondary';\n}\n\nexport function CopyButton({ \n  text, \n  className,\n  size = 'icon',\n  variant = 'ghost'\n}: CopyButtonProps) {\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = async (e: React.MouseEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    \n    const success = await copyToClipboard(text);\n    if (success) {\n      setCopied(true);\n      // Reset the copied state after 2 seconds\n      setTimeout(() => setCopied(false), 2000);\n    }\n  };\n\n  return (\n    <Button\n      onClick={handleCopy}\n      variant={variant}\n      size={size}\n      className={cn(\n        'h-6 w-6 transition-all duration-200',\n        copied ? 'text-green-600' : 'text-gray-500 hover:text-gray-700',\n        className\n      )}\n      title={copied ? 'Copied!' : 'Copy to clipboard'}\n    >\n      {copied ? (\n        <span className=\"text-xs font-medium\">✓</span>\n      ) : (\n        <HugeiconsIcon icon={Copy01Icon} className=\"h-3 w-3\" />\n      )}\n    </Button>\n  );\n}"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/desktop-container.tsx",
    "content": "import React, { useRef, useEffect, useState } from \"react\";\nimport { VncViewer } from \"@/components/vnc/VncViewer\";\nimport { ScreenshotViewer } from \"@/components/screenshot/ScreenshotViewer\";\nimport { ScreenshotData } from \"@/utils/screenshotUtils\";\nimport {\n  VirtualDesktopStatusHeader,\n  VirtualDesktopStatus,\n} from \"@/components/VirtualDesktopStatusHeader\";\n\ninterface DesktopContainerProps {\n  children?: React.ReactNode;\n  screenshot?: ScreenshotData | null;\n  viewOnly?: boolean;\n  className?: string;\n  status?: VirtualDesktopStatus;\n}\n\nexport const DesktopContainer: React.FC<DesktopContainerProps> = ({\n  children,\n  screenshot,\n  viewOnly = false,\n  className = \"\",\n  status = \"running\",\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });\n  const [isMounted, setIsMounted] = useState(false);\n\n  // Set isMounted to true after component mounts\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  // Calculate the container size on mount and window resize\n  useEffect(() => {\n    if (!isMounted) return;\n\n    const updateSize = () => {\n      if (!containerRef.current) return;\n\n      const parentWidth =\n        containerRef.current.parentElement?.offsetWidth ||\n        containerRef.current.offsetWidth;\n      const parentHeight =\n        containerRef.current.parentElement?.offsetHeight ||\n        containerRef.current.offsetHeight;\n\n      // Calculate the maximum size while maintaining 1280:960 aspect ratio\n      let width, height;\n      const aspectRatio = 1280 / 960;\n\n      if (parentWidth / parentHeight > aspectRatio) {\n        // Width is the limiting factor\n        height = parentHeight;\n        width = height * aspectRatio;\n      } else {\n        // Height is the limiting factor\n        width = parentWidth;\n        height = width / aspectRatio;\n      }\n\n      // Cap at maximum dimensions\n      width = Math.min(width, 1280);\n      height = Math.min(height, 960);\n\n      setContainerSize({ width, height });\n    };\n\n    updateSize();\n    window.addEventListener(\"resize\", updateSize);\n    return () => window.removeEventListener(\"resize\", updateSize);\n  }, [isMounted]);\n\n  return (\n    <div\n      className={`border-bytebot-bronze-light-7 flex w-full flex-col rounded-t-lg border-t border-r border-l ${className}`}\n    >\n      {/* Header */}\n      <div className=\"bg-bytebot-bronze-light-2 border-bytebot-bronze-light-7 flex items-center justify-between rounded-t-lg border-b px-4 py-2\">\n        {/* Status Header */}\n        <div className=\"flex items-center gap-2\">\n          <VirtualDesktopStatusHeader status={status} />\n        </div>\n\n        {/* Actions */}\n        <div className=\"flex items-center gap-2\">{children}</div>\n      </div>\n\n      <div ref={containerRef} className=\"flex aspect-[4/3] overflow-hidden\">\n        <div\n          style={{\n            width: `${containerSize.width}px`,\n            height: `${containerSize.height}px`,\n            maxWidth: \"100%\",\n          }}\n        >\n          {screenshot ? (\n            <ScreenshotViewer\n              screenshot={screenshot}\n              className=\"h-full w-full\"\n            />\n          ) : (\n            <VncViewer viewOnly={viewOnly} />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { HugeiconsIcon } from \"@hugeicons/react\"\nimport { Tick01Icon, ArrowRight01Icon } from \"@hugeicons/core-free-icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <HugeiconsIcon icon={ArrowRight01Icon} className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <HugeiconsIcon icon={Tick01Icon} className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <div className=\"h-2 w-2 rounded-full bg-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-bytebot-bronze-light-7 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Input }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/loader.tsx",
    "content": "import React from \"react\";\nimport Image from \"next/image\";\nimport { cn } from \"@/lib/utils\";\n\ninterface LoaderProps {\n  size?: number;\n  className?: string;\n}\n\nexport const Loader: React.FC<LoaderProps> = ({ \n  size = 16, \n  className \n}) => {\n  return (\n    <Image\n      src=\"/loader.svg\"\n      alt=\"Loading...\"\n      width={size}\n      height={size}\n      className={cn(\"animate-spin\", className)}\n    />\n  );\n}; "
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/pagination.tsx",
    "content": "import React from \"react\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { ArrowLeft02Icon, ArrowRight02Icon } from \"@hugeicons/core-free-icons\";\nimport { Button } from \"./button\";\n\ninterface PaginationProps {\n  currentPage: number;\n  totalPages: number;\n  onPageChange: (page: number) => void;\n  total: number;\n  pageSize: number;\n}\n\nexport const Pagination: React.FC<PaginationProps> = ({\n  currentPage,\n  totalPages,\n  onPageChange,\n  total,\n  pageSize,\n}) => {\n  if (totalPages <= 1) return null;\n\n  const startItem = (currentPage - 1) * pageSize + 1;\n  const endItem = Math.min(currentPage * pageSize, total);\n\n  const getVisiblePages = () => {\n    const delta = 2;\n    const range = [];\n    const rangeWithDots = [];\n\n    for (\n      let i = Math.max(2, currentPage - delta);\n      i <= Math.min(totalPages - 1, currentPage + delta);\n      i++\n    ) {\n      range.push(i);\n    }\n\n    if (currentPage - delta > 2) {\n      rangeWithDots.push(1, \"...\");\n    } else {\n      rangeWithDots.push(1);\n    }\n\n    rangeWithDots.push(...range);\n\n    if (currentPage + delta < totalPages - 1) {\n      rangeWithDots.push(\"...\", totalPages);\n    } else {\n      rangeWithDots.push(totalPages);\n    }\n\n    return rangeWithDots;\n  };\n\n  const visiblePages = getVisiblePages();\n\n  return (\n    <div className=\"flex items-center justify-between border-t border-bytebot-bronze-light-7 pt-6\">\n      <div className=\"flex items-center text-sm text-bytebot-bronze-light-11\">\n        Showing {startItem} to {endItem} of {total} results\n      </div>\n      \n      <div className=\"flex items-center space-x-2\">\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={() => onPageChange(currentPage - 1)}\n          disabled={currentPage === 1}\n          className=\"flex items-center space-x-1\"\n        >\n          <HugeiconsIcon icon={ArrowLeft02Icon} className=\"h-4 w-4\" />\n          <span>Previous</span>\n        </Button>\n\n        <div className=\"flex items-center space-x-1\">\n          {visiblePages.map((page, index) => {\n            if (page === \"...\") {\n              return (\n                <span\n                  key={`ellipsis-${index}`}\n                  className=\"px-3 py-2 text-sm text-bytebot-bronze-light-11\"\n                >\n                  ...\n                </span>\n              );\n            }\n\n            const pageNum = page as number;\n            const isCurrentPage = pageNum === currentPage;\n\n            return (\n              <Button\n                key={pageNum}\n                variant={isCurrentPage ? \"default\" : \"outline\"}\n                size=\"sm\"\n                onClick={() => onPageChange(pageNum)}\n                className={`min-w-[40px] ${\n                  isCurrentPage\n                    ? \"bg-bytebot-bronze-dark-7 text-white\"\n                    : \"text-bytebot-bronze-light-11 hover:text-bytebot-bronze-dark-7\"\n                }`}\n              >\n                {pageNum}\n              </Button>\n            );\n          })}\n        </div>\n\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={() => onPageChange(currentPage + 1)}\n          disabled={currentPage === totalPages}\n          className=\"flex items-center space-x-1\"\n        >\n          <span>Next</span>\n          <HugeiconsIcon icon={ArrowRight02Icon} className=\"h-4 w-4\" />\n        </Button>\n      </div>\n    </div>\n  );\n};"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/popover.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border border-bytebot-bronze-light-5 bg-white p-4 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\nexport { Popover, PopoverTrigger, PopoverContent }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/scroll-area.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ScrollArea({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroll-area\"\n      className={cn(\"relative\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        data-slot=\"scroll-area-viewport\"\n        className=\"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  )\n}\n\nfunction ScrollBar({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      className={cn(\n        \"flex touch-none p-px transition-colors select-none\",\n        orientation === \"vertical\" &&\n          \"h-full w-2.5 border-l border-l-transparent\",\n        orientation === \"horizontal\" &&\n          \"h-2.5 flex-col border-t border-t-transparent\",\n        className\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className=\"bg-border relative flex-1 rounded-full\"\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  )\n}\n\nexport { ScrollArea, ScrollBar }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { HugeiconsIcon } from '@hugeicons/react'\nimport { ArrowDown01Icon, Tick02Icon } from '@hugeicons/core-free-icons'\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-6 w-full items-center justify-between rounded-md border border-bytebot-bronze-light-7 bg-bytebot-bronze-light-1 pr-1 pl-2 py-[0.5px] text-[12px] text-bytebot-bronze-dark-9 ring-offset-bytebot-bronze-light-7 placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <HugeiconsIcon icon={ArrowDown01Icon} className=\"ml-4 h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-bytebot-bronze-light-7 bg-bytebot-bronze-light-1 text-bytebot-bronze-dark-9 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-[12px] font-semibold text-bytebot-bronze-dark-9\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full justify-between cursor-default select-none items-center rounded-sm py-[0.5px] px-1 text-[12px] text-bytebot-bronze-dark-9 outline-none focus:bg-bytebot-bronze-light-5 focus:text-bytebot-bronze-dark-9 data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    <span className=\"flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <HugeiconsIcon icon={Tick02Icon} className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-bytebot-bronze-light-7\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/separator.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator-root\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Separator }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/switch.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Switch({\n  className,\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\"\n        )}\n      />\n    </SwitchPrimitive.Root>\n  )\n}\n\nexport { Switch }\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/ui/text-shimmer.tsx",
    "content": "\"use client\";\nimport React, { useMemo, type JSX } from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TextShimmerProps = {\n  children: string;\n  as?: React.ElementType;\n  className?: string;\n  duration?: number;\n  spread?: number;\n};\n\nfunction TextShimmerComponent({\n  children,\n  as: Component = \"p\",\n  className,\n  duration = 2,\n  spread = 2,\n}: TextShimmerProps) {\n  const MotionComponent = motion.create(\n    Component as keyof JSX.IntrinsicElements,\n  );\n\n  const dynamicSpread = useMemo(() => {\n    return children.length * spread;\n  }, [children, spread]);\n\n  return (\n    <MotionComponent\n      className={cn(\n        \"relative inline-block bg-[length:250%_100%,auto] bg-clip-text\",\n        \"text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]\",\n        \"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]\",\n        \"dark:[--base-color:#71717a] dark:[--base-gradient-color:#ffffff] dark:[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]\",\n        className,\n      )}\n      initial={{ backgroundPosition: \"100% center\" }}\n      animate={{ backgroundPosition: \"0% center\" }}\n      transition={{\n        repeat: Number.POSITIVE_INFINITY,\n        duration,\n        ease: \"linear\",\n      }}\n      style={\n        {\n          \"--spread\": `${dynamicSpread}px`,\n          backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,\n        } as React.CSSProperties\n      }\n    >\n      {children}\n    </MotionComponent>\n  );\n}\n\nexport const TextShimmer = React.memo(TextShimmerComponent);\n"
  },
  {
    "path": "packages/bytebot-ui/src/components/vnc/VncViewer.tsx",
    "content": "\"use client\";\n\nimport React, { useRef, useEffect, useState } from \"react\";\n\ninterface VncViewerProps {\n  viewOnly?: boolean;\n}\n\nexport function VncViewer({ viewOnly = true }: VncViewerProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const [VncComponent, setVncComponent] = useState<any>(null);\n  const [wsUrl, setWsUrl] = useState<string | null>(null);\n\n  useEffect(() => {\n    // Dynamically import the VncScreen component only on the client side\n    import(\"react-vnc\").then(({ VncScreen }) => {\n      setVncComponent(() => VncScreen);\n    });\n  }, []);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return; // SSR safety‑net\n    const proto = window.location.protocol === \"https:\" ? \"wss\" : \"ws\";\n    setWsUrl(`${proto}://${window.location.host}/api/proxy/websockify`);\n  }, []);\n\n  return (\n    <div ref={containerRef} className=\"h-full w-full\">\n      {VncComponent && wsUrl && (\n        <VncComponent\n          rfbOptions={{\n            secure: false,\n            shared: true,\n            wsProtocols: [\"binary\"],\n          }}\n          // autoConnect={true}\n          key={viewOnly ? \"view-only\" : \"interactive\"}\n          url={wsUrl}\n          scaleViewport\n          viewOnly={viewOnly}\n          style={{ width: \"100%\", height: \"100%\" }}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/constants/ui.constants.ts",
    "content": "/**\n * UI constants for consistent styling and configuration\n */\n\nexport const UI_CONSTANTS = {\n  // Animation durations\n  ANIMATION: {\n    SPIN_DURATION: '3s',\n    TRANSITION_DURATION: '150ms',\n  },\n\n  // Common class names\n  CLASSES: {\n    LOADING_SPINNER: 'animate-[spin_3s_linear_infinite]',\n    TRANSITION_DEFAULT: 'transition-colors',\n  },\n\n  // Date formatting\n  DATE_FORMAT: {\n    TIME_12H: 'h:mma',\n    FULL_DATE: 'MMMM d, yyyy, h:mma',\n    TODAY_PREFIX: \"'Today'\",\n  },\n\n  // Component defaults\n  DEFAULTS: {\n    TASK_LIST_LIMIT: 5,\n    LOADING_SPINNER_SIZE: 'h-6 w-6',\n  },\n} as const;\n\nexport type UIConstants = typeof UI_CONSTANTS;"
  },
  {
    "path": "packages/bytebot-ui/src/hooks/useChatSession.ts",
    "content": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { Message, Role, TaskStatus, Task, GroupedMessages } from \"@/types\";\nimport {\n  addMessage,\n  fetchTaskMessages,\n  fetchTaskProcessedMessages,\n  fetchTaskById,\n  takeOverTask,\n  resumeTask,\n  cancelTask,\n} from \"@/utils/taskUtils\";\nimport { MessageContentType } from \"@bytebot/shared\";\nimport { useWebSocket } from \"./useWebSocket\";\n\ninterface UseChatSessionProps {\n  initialTaskId?: string;\n}\n\nexport function useChatSession({ initialTaskId }: UseChatSessionProps = {}) {\n  const [taskStatus, setTaskStatus] = useState<TaskStatus>(TaskStatus.PENDING);\n  const [control, setControl] = useState<Role>(Role.ASSISTANT);\n  const [messages, setMessages] = useState<Message[]>([]);\n  const [groupedMessages, setGroupedMessages] = useState<GroupedMessages[]>([]);\n  const [input, setInput] = useState(\"\");\n  const [currentTaskId, setCurrentTaskId] = useState<string | null>(\n    initialTaskId || null,\n  );\n  const [isLoading, setIsLoading] = useState(false);\n  const [isLoadingSession, setIsLoadingSession] = useState(true);\n  const [isLoadingMoreMessages, setIsLoadingMoreMessages] = useState(false);\n  const [currentPage, setCurrentPage] = useState(1);\n  const [hasMoreMessages, setHasMoreMessages] = useState(true);\n\n  const processedMessageIds = useRef<Set<string>>(new Set());\n\n  // WebSocket event handlers\n  const handleTaskUpdate = useCallback(\n    (task: Task) => {\n      if (task.id === currentTaskId) {\n        setTaskStatus(task.status);\n        setControl(task.control);\n      }\n    },\n    [currentTaskId],\n  );\n\n  // Function to reload grouped messages\n  const reloadGroupedMessages = useCallback(async () => {\n    if (!currentTaskId) return;\n\n    try {\n      const processedMessages = await fetchTaskProcessedMessages(\n        currentTaskId,\n        {\n          limit: 1000, // Get more messages for grouped view\n          page: 1,\n        },\n      );\n      setGroupedMessages(processedMessages);\n    } catch (error) {\n      console.error(\"Error reloading grouped messages:\", error);\n    }\n  }, [currentTaskId]);\n\n  const handleNewMessage = useCallback(\n    (message: Message) => {\n      // Only add message if it's not already processed and belongs to current task\n      if (\n        !processedMessageIds.current.has(message.id) &&\n        message.taskId === currentTaskId\n      ) {\n        console.log(\"Adding new message from WebSocket:\", message);\n        processedMessageIds.current.add(message.id);\n        setMessages((prev) => [...prev, message]);\n        // Reload grouped messages to reflect the new message\n        reloadGroupedMessages();\n      }\n    },\n    [currentTaskId, reloadGroupedMessages],\n  );\n\n  const handleTaskCreated = useCallback((task: Task) => {\n    console.log(\"New task created:\", task);\n  }, []);\n\n  const handleTaskDeleted = useCallback(\n    (taskId: string) => {\n      if (taskId === currentTaskId) {\n        console.log(\"Current task was deleted\");\n        setCurrentTaskId(null);\n        setMessages([]);\n        processedMessageIds.current = new Set();\n      }\n    },\n    [currentTaskId],\n  );\n\n  // Initialize WebSocket connection\n  const { joinTask, leaveTask } = useWebSocket({\n    onTaskUpdate: handleTaskUpdate,\n    onNewMessage: handleNewMessage,\n    onTaskCreated: handleTaskCreated,\n    onTaskDeleted: handleTaskDeleted,\n  });\n\n  // Load more messages function for infinite scroll\n  const loadMoreMessages = useCallback(async () => {\n    if (!currentTaskId || isLoadingMoreMessages || !hasMoreMessages) {\n      console.log(\"loadMoreMessages early return\");\n      return;\n    }\n\n    setIsLoadingMoreMessages(true);\n    try {\n      const nextPage = currentPage + 1;\n      const newMessages = await fetchTaskMessages(currentTaskId, {\n        limit: 10,\n        page: nextPage,\n      });\n\n      if (newMessages.length === 0) {\n        setHasMoreMessages(false);\n      } else {\n        // Append new messages to the end of the list (newer messages)\n        const formattedMessages = newMessages.map((msg: Message) => ({\n          id: msg.id,\n          content: msg.content,\n          role: msg.role,\n          createdAt: msg.createdAt,\n        }));\n\n        // Filter out any messages we already have\n        const uniqueMessages = formattedMessages.filter(\n          (msg) => !processedMessageIds.current.has(msg.id),\n        );\n\n        if (uniqueMessages.length > 0) {\n          // Add message IDs to processed set\n          uniqueMessages.forEach((msg: Message) => {\n            processedMessageIds.current.add(msg.id);\n          });\n\n          setMessages((prev) => [...prev, ...uniqueMessages]);\n          setCurrentPage(nextPage);\n        }\n\n        // If we got fewer messages than requested, we've reached the end\n        if (newMessages.length < 10) {\n          setHasMoreMessages(false);\n        }\n      }\n    } catch (error) {\n      console.error(\"Error loading more messages:\", error);\n    } finally {\n      setIsLoadingMoreMessages(false);\n    }\n  }, [currentTaskId, currentPage, isLoadingMoreMessages, hasMoreMessages]);\n\n  // Load task ID from URL parameter or fetch the latest task on initial render\n  useEffect(() => {\n    const loadSession = async () => {\n      setIsLoadingSession(true);\n      try {\n        if (initialTaskId) {\n          // If we have an initial task ID (from URL), fetch that specific task\n          console.log(`Fetching specific task: ${initialTaskId}`);\n          const task = await fetchTaskById(initialTaskId);\n          // Load raw messages for compatibility and processed messages for chat UI\n          const messages = await fetchTaskMessages(initialTaskId, {\n            limit: 10,\n            page: 1,\n          });\n          const processedMessages = await fetchTaskProcessedMessages(\n            initialTaskId,\n            {\n              limit: 1000, // Get more messages for grouped view\n              page: 1,\n            },\n          );\n\n          if (task) {\n            console.log(`Found task: ${task.id}`);\n            setCurrentTaskId(task.id);\n            setTaskStatus(task.status); // Set the task status when loading\n            setControl(task.control);\n\n            // Set grouped messages for chat UI\n            setGroupedMessages(processedMessages);\n\n            // If the task has messages, add them to the messages state for compatibility\n            if (messages && messages.length > 0) {\n              // Process all messages\n              const formattedMessages = messages.map((msg: Message) => ({\n                id: msg.id,\n                content: msg.content,\n                role: msg.role,\n                createdAt: msg.createdAt,\n              }));\n\n              // Add message IDs to processed set\n              formattedMessages.forEach((msg: Message) => {\n                processedMessageIds.current.add(msg.id);\n              });\n\n              setMessages(formattedMessages);\n              setCurrentPage(1);\n\n              // If we got fewer messages than requested, we've reached the end\n              if (messages.length < 10) {\n                setHasMoreMessages(false);\n              } else {\n                setHasMoreMessages(true);\n              }\n            } else {\n              setCurrentPage(1);\n              setHasMoreMessages(false);\n            }\n          } else {\n            console.log(`Task with ID ${initialTaskId} not found`);\n          }\n        }\n      } catch (error) {\n        console.error(\"Error loading session:\", error);\n      } finally {\n        setIsLoadingSession(false);\n      }\n    };\n\n    loadSession();\n  }, [initialTaskId]);\n\n  // Join/leave WebSocket task rooms when task ID changes\n  useEffect(() => {\n    if (currentTaskId) {\n      console.log(`Joining WebSocket room for task: ${currentTaskId}`);\n      joinTask(currentTaskId);\n    } else {\n      console.log(\"Leaving WebSocket task room\");\n      leaveTask();\n    }\n  }, [currentTaskId, joinTask, leaveTask]);\n\n  const handleAddMessage = async () => {\n    if (!input.trim()) return;\n\n    setIsLoading(true);\n\n    try {\n      const message = input;\n      setInput(\"\");\n\n      // Send request to start a new task or continue existing task\n      const response = await addMessage(currentTaskId!, message);\n\n      if (!response) {\n        // Add error message to chat\n        const errorMessage: Message = {\n          id: Date.now().toString(),\n          content: [\n            {\n              type: MessageContentType.Text,\n              text: \"Sorry, there was an error processing your request. Please try again.\",\n            },\n          ],\n          role: Role.ASSISTANT,\n        };\n\n        processedMessageIds.current.add(errorMessage.id);\n        setMessages((prev) => [...prev, errorMessage]);\n      }\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleTakeOverTask = async () => {\n    if (!currentTaskId) return;\n\n    try {\n      const updatedTask = await takeOverTask(currentTaskId);\n      if (updatedTask) {\n        setControl(updatedTask.control);\n      }\n    } catch (error) {\n      console.error(\"Error taking over task:\", error);\n    }\n  };\n\n  const handleResumeTask = async () => {\n    if (!currentTaskId) return;\n\n    try {\n      const updatedTask = await resumeTask(currentTaskId);\n      if (updatedTask) {\n        setControl(updatedTask.control);\n      }\n    } catch (error) {\n      console.error(\"Error resuming task:\", error);\n    }\n  };\n\n  const handleCancelTask = async () => {\n    if (!currentTaskId) return;\n\n    try {\n      const updatedTask = await cancelTask(currentTaskId);\n      if (updatedTask) {\n        setTaskStatus(updatedTask.status);\n        setControl(updatedTask.control);\n      }\n    } catch (error) {\n      console.error(\"Error cancelling task:\", error);\n    }\n  };\n\n  return {\n    messages,\n    groupedMessages,\n    taskStatus,\n    control,\n    input,\n    setInput,\n    currentTaskId,\n    isLoading,\n    isLoadingSession,\n    isLoadingMoreMessages,\n    hasMoreMessages,\n    loadMoreMessages,\n    handleAddMessage,\n    handleTakeOverTask,\n    handleResumeTask,\n    handleCancelTask,\n  };\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/hooks/useScrollScreenshot.ts",
    "content": "import { useState, useEffect, useCallback, useRef } from 'react';\nimport { Message } from '@/types';\nimport { extractScreenshots, getScreenshotForScrollPosition, ScreenshotData } from '@/utils/screenshotUtils';\n\ninterface UseScrollScreenshotProps {\n  messages: Message[];\n  scrollContainerRef: React.RefObject<HTMLElement | null>;\n}\n\nexport function useScrollScreenshot({ messages, scrollContainerRef }: UseScrollScreenshotProps) {\n  const [currentScreenshot, setCurrentScreenshot] = useState<ScreenshotData | null>(null);\n  const [allScreenshots, setAllScreenshots] = useState<ScreenshotData[]>([]);\n  const lastScrollTime = useRef<number>(0);\n\n  // Extract screenshots whenever messages change\n  useEffect(() => {\n    const screenshots = extractScreenshots(messages);\n    setAllScreenshots(screenshots);\n    \n    // Only set initial screenshot if we don't have one yet\n    if (screenshots.length > 0 && !currentScreenshot) {\n      setTimeout(() => {\n        const initialScreenshot = getScreenshotForScrollPosition(\n          screenshots,\n          messages,\n          scrollContainerRef.current\n        );\n        if (initialScreenshot) {\n          setCurrentScreenshot(initialScreenshot);\n        } else {\n          setCurrentScreenshot(screenshots[screenshots.length - 1]);\n        }\n      }, 100);\n    } else if (screenshots.length === 0) {\n      setCurrentScreenshot(null);\n    } else if (screenshots.length > 0 && currentScreenshot) {\n      // When messages update, trigger a re-check\n      setTimeout(() => {\n        if (scrollContainerRef.current) {\n          const event = new Event('scroll');\n          scrollContainerRef.current.dispatchEvent(event);\n        }\n      }, 300);\n    }\n  }, [messages, scrollContainerRef]);\n\n  // After initial render, force a re-check for screenshot markers using MutationObserver\n  useEffect(() => {\n    if (!scrollContainerRef.current) return;\n\n    const container = scrollContainerRef.current;\n    let mutationTimeout: NodeJS.Timeout;\n    const observer = new MutationObserver(() => {\n      clearTimeout(mutationTimeout);\n      mutationTimeout = setTimeout(() => {\n        const event = new Event('scroll');\n        container.dispatchEvent(event);\n      }, 200);\n    });\n\n    observer.observe(container, { childList: true, subtree: true });\n\n    return () => {\n      clearTimeout(mutationTimeout);\n      observer.disconnect();\n    };\n  }, [scrollContainerRef, allScreenshots.length]);\n\n\n  // Handle scroll events to update current screenshot\n  const handleScroll = useCallback((scrollElement: HTMLElement) => {\n    if (allScreenshots.length === 0) return;\n\n    const now = Date.now();\n    if (now - lastScrollTime.current < 100) return;\n    lastScrollTime.current = now;\n\n    setTimeout(() => {\n      if ((Date.now() - now) <= 150 && allScreenshots.length > 0) {\n        setCurrentScreenshot(prevScreenshot => {\n          const screenshot = getScreenshotForScrollPosition(allScreenshots, messages, scrollElement);\n          \n          if (screenshot && screenshot.id !== prevScreenshot?.id) {\n            return screenshot;\n          }\n          return prevScreenshot;\n        });\n      }\n    }, 50);\n  }, [allScreenshots, messages]);\n\n  // Attach scroll listener\n  useEffect(() => {\n    const container = scrollContainerRef.current;\n    if (!container) return;\n\n    const scrollHandler = (e: Event) => {\n      // Only handle scroll events from the actual container\n      if (e.target === container) {\n        handleScroll(container);\n      }\n    };\n\n    // Only attach to the container itself\n    container.addEventListener('scroll', scrollHandler, { passive: true });\n    \n    return () => container.removeEventListener('scroll', scrollHandler);\n  }, [handleScroll, scrollContainerRef]);\n\n  return {\n    currentScreenshot,\n    allScreenshots,\n    hasScreenshots: allScreenshots.length > 0,\n  };\n}"
  },
  {
    "path": "packages/bytebot-ui/src/hooks/useWebSocket.ts",
    "content": "import { useEffect, useRef, useCallback } from \"react\";\nimport { io, Socket } from \"socket.io-client\";\nimport { Message, Task } from \"@/types\";\n\ninterface UseWebSocketProps {\n  onTaskUpdate?: (task: Task) => void;\n  onNewMessage?: (message: Message) => void;\n  onTaskCreated?: (task: Task) => void;\n  onTaskDeleted?: (taskId: string) => void;\n}\n\nexport function useWebSocket({\n  onTaskUpdate,\n  onNewMessage,\n  onTaskCreated,\n  onTaskDeleted,\n}: UseWebSocketProps = {}) {\n  const socketRef = useRef<Socket | null>(null);\n  const currentTaskIdRef = useRef<string | null>(null);\n\n  const connect = useCallback(() => {\n    if (socketRef.current?.connected) {\n      return socketRef.current;\n    }\n\n    // Connect to the WebSocket server\n    const socket = io({\n      path: \"/api/proxy/tasks\",\n      transports: [\"websocket\"],\n      autoConnect: true,\n      reconnection: true,\n      reconnectionAttempts: 5,\n      reconnectionDelay: 1000,\n    });\n\n    socket.on(\"connect\", () => {\n      console.log(\"Connected to WebSocket server\");\n    });\n\n    socket.on(\"disconnect\", () => {\n      console.log(\"Disconnected from WebSocket server\");\n    });\n\n    socket.on(\"task_updated\", (task: Task) => {\n      console.log(\"Task updated:\", task);\n      onTaskUpdate?.(task);\n    });\n\n    socket.on(\"new_message\", (message: Message) => {\n      console.log(\"New message:\", message);\n      onNewMessage?.(message);\n    });\n\n    socket.on(\"task_created\", (task: Task) => {\n      console.log(\"Task created:\", task);\n      onTaskCreated?.(task);\n    });\n\n    socket.on(\"task_deleted\", (taskId: string) => {\n      console.log(\"Task deleted:\", taskId);\n      onTaskDeleted?.(taskId);\n    });\n\n    socketRef.current = socket;\n    return socket;\n  }, [onTaskUpdate, onNewMessage, onTaskCreated, onTaskDeleted]);\n\n  const joinTask = useCallback(\n    (taskId: string) => {\n      const socket = socketRef.current || connect();\n      if (currentTaskIdRef.current) {\n        socket.emit(\"leave_task\", currentTaskIdRef.current);\n      }\n      socket.emit(\"join_task\", taskId);\n      currentTaskIdRef.current = taskId;\n      console.log(`Joined task room: ${taskId}`);\n    },\n    [connect],\n  );\n\n  const leaveTask = useCallback(() => {\n    const socket = socketRef.current;\n    if (socket && currentTaskIdRef.current) {\n      socket.emit(\"leave_task\", currentTaskIdRef.current);\n      console.log(`Left task room: ${currentTaskIdRef.current}`);\n      currentTaskIdRef.current = null;\n    }\n  }, []);\n\n  const disconnect = useCallback(() => {\n    if (socketRef.current) {\n      socketRef.current.disconnect();\n      socketRef.current = null;\n      currentTaskIdRef.current = null;\n    }\n  }, []);\n\n  // Initialize connection on mount\n  useEffect(() => {\n    connect();\n    return () => {\n      disconnect();\n    };\n  }, [connect, disconnect]);\n\n  return {\n    socket: socketRef.current,\n    joinTask,\n    leaveTask,\n    disconnect,\n    isConnected: socketRef.current?.connected || false,\n  };\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/types/index.ts",
    "content": "import { MessageContentBlock } from \"@bytebot/shared\";\n\nexport enum Role {\n  USER = \"USER\",\n  ASSISTANT = \"ASSISTANT\",\n}\n\n// Message interface\nexport interface Message {\n  id: string;\n  content: MessageContentBlock[];\n  role: Role;\n  taskId?: string;\n  createdAt?: string;\n  take_over?: boolean;\n}\n\n// Grouped messages interface for processed endpoint\nexport interface GroupedMessages {\n  role: Role;\n  messages: Message[];\n  take_over?: boolean;\n}\n\nexport interface Model {\n  provider: string;\n  name: string;\n  title: string;\n}\n\n// Task related enums and types\nexport enum TaskStatus {\n  PENDING = \"PENDING\",\n  RUNNING = \"RUNNING\",\n  NEEDS_HELP = \"NEEDS_HELP\",\n  NEEDS_REVIEW = \"NEEDS_REVIEW\",\n  COMPLETED = \"COMPLETED\",\n  CANCELLED = \"CANCELLED\",\n  FAILED = \"FAILED\",\n}\n\nexport enum TaskPriority {\n  LOW = \"LOW\",\n  MEDIUM = \"MEDIUM\",\n  HIGH = \"HIGH\",\n  URGENT = \"URGENT\",\n}\n\nexport enum TaskType {\n  IMMEDIATE = \"IMMEDIATE\",\n  SCHEDULED = \"SCHEDULED\",\n}\n\nexport interface FileWithBase64 {\n  name: string;\n  base64: string;\n  type: string;\n  size: number;\n}\n\nexport interface File {\n  id: string;\n  name: string;\n  type: string;\n  size: number;\n  data: string;\n  createdAt: string;\n  updatedAt: string;\n  taskId: string;\n}\n\nexport interface Task {\n  id: string;\n  description: string;\n  type: TaskType;\n  status: TaskStatus;\n  priority: TaskPriority;\n  control: Role;\n  createdBy: Role;\n  createdAt: string;\n  updatedAt: string;\n  scheduledFor?: string;\n  executedAt?: string;\n  completedAt?: string;\n  queuedAt?: string;\n  error?: string;\n  result?: unknown;\n  model: Model;\n  files?: File[];\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/utils/clipboard.ts",
    "content": "/**\n * Copy text to clipboard using the modern Clipboard API\n * @param text The text to copy to clipboard\n * @returns Promise<boolean> true if successful, false otherwise\n */\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  try {\n    // Check if the Clipboard API is available\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    } else {\n      // Fallback for older browsers or non-secure contexts\n      const textArea = document.createElement('textarea');\n      textArea.value = text;\n      textArea.style.position = 'fixed';\n      textArea.style.left = '-999999px';\n      textArea.style.top = '-999999px';\n      document.body.appendChild(textArea);\n      textArea.focus();\n      textArea.select();\n      \n      const successful = document.execCommand('copy');\n      document.body.removeChild(textArea);\n      return successful;\n    }\n  } catch (error) {\n    console.error('Failed to copy text to clipboard:', error);\n    return false;\n  }\n}"
  },
  {
    "path": "packages/bytebot-ui/src/utils/screenshotUtils.ts",
    "content": "import { Message } from \"@/types\";\nimport { isToolResultContentBlock, isImageContentBlock } from \"@bytebot/shared\";\n\nexport interface ScreenshotData {\n  id: string;\n  base64Data: string;\n  messageIndex: number;\n  blockIndex: number;\n}\n\n/**\n * Extracts all screenshots from messages\n */\nexport function extractScreenshots(messages: Message[]): ScreenshotData[] {\n  const screenshots: ScreenshotData[] = [];\n  \n  messages.forEach((message, messageIndex) => {\n    message.content.forEach((block, blockIndex) => {\n      // Check if this is a tool result block with images\n      if (isToolResultContentBlock(block) && block.content && block.content.length > 0) {\n        // Check ALL content items in the tool result, not just the first one\n        block.content.forEach((contentItem, contentIndex) => {\n          if (isImageContentBlock(contentItem)) {\n            screenshots.push({\n              id: `${message.id}-${blockIndex}-${contentIndex}`,\n              base64Data: contentItem.source.data,\n              messageIndex,\n              blockIndex,\n            });\n          }\n        });\n      }\n    });\n  });\n  return screenshots;\n}\n\n/**\n * Gets the screenshot that should be displayed based on scroll position\n */\nexport function getScreenshotForScrollPosition(\n  screenshots: ScreenshotData[],\n  messages: Message[],\n  scrollContainer: HTMLElement | null\n): ScreenshotData | null {\n  if (!scrollContainer || screenshots.length === 0) {\n    return screenshots[screenshots.length - 1] || null; // Default to last screenshot\n  }\n\n  // Get all screenshot marker elements in the scroll container\n  const screenshotElements = scrollContainer.querySelectorAll('[data-message-index][data-block-index]');\n  if (screenshotElements.length === 0) {\n    return screenshots[screenshots.length - 1] || null;\n  }\n  const containerScrollTop = scrollContainer.scrollTop;\n  const containerHeight = scrollContainer.clientHeight;\n\n  // Find the screenshot marker that's most visible at 350px down from the top of the container\n  const targetViewPosition = 350; // 350px down from top\n  let bestVisibleMessageIndex = -1; // Start with -1 to detect when no markers are found\n  let bestVisibleBlockIndex = -1;\n  let bestVisibility = 0;\n  let minDistanceFromTarget = Infinity;\n  let lastMarkerMessageIndex = -1;\n  let lastMarkerBlockIndex = -1;\n\n  screenshotElements.forEach((element) => {\n    const messageIndex = parseInt((element as HTMLElement).dataset.messageIndex || '0');\n    const blockIndex = parseInt((element as HTMLElement).dataset.blockIndex || '0');\n    const elementTop = (element as HTMLElement).offsetTop;\n    const elementHeight = (element as HTMLElement).offsetHeight;\n    const elementBottom = elementTop + elementHeight;\n    \n    // Keep track of the last (bottommost) marker\n    if (messageIndex > lastMarkerMessageIndex || \n        (messageIndex === lastMarkerMessageIndex && blockIndex > lastMarkerBlockIndex)) {\n      lastMarkerMessageIndex = messageIndex;\n      lastMarkerBlockIndex = blockIndex;\n    }\n    \n    // Distance from top of container (accounting for scroll)\n    const distanceFromViewportTop = elementTop - containerScrollTop;\n    const distanceFromViewportBottom = elementBottom - containerScrollTop;\n    \n    // Check if element is visible in viewport\n    const isVisible = distanceFromViewportTop < containerHeight && \n                     distanceFromViewportBottom > 0;\n    \n    if (isVisible) {\n      // Calculate how much of this element is visible\n      const visibleTop = Math.max(0, distanceFromViewportTop);\n      const visibleBottom = Math.min(containerHeight, distanceFromViewportBottom);\n      const visibleHeight = Math.max(0, visibleBottom - visibleTop);\n      const visibility = elementHeight === 0 ? 1 : visibleHeight / elementHeight;\n      \n      // Calculate distance from our target position (150px down)\n      const elementCenter = distanceFromViewportTop + (elementHeight / 2);\n      const distanceFromTarget = Math.abs(elementCenter - targetViewPosition);\n      \n      // Prefer elements that are closer to our target position and more visible\n      if (visibility > 0.1 && \n          (distanceFromTarget < minDistanceFromTarget || \n           (distanceFromTarget === minDistanceFromTarget && visibility > bestVisibility))) {\n        bestVisibility = visibility;\n        bestVisibleMessageIndex = messageIndex;\n        bestVisibleBlockIndex = blockIndex;\n        minDistanceFromTarget = distanceFromTarget;\n      }\n    }\n  });\n\n  // If no markers are visible, check if we've scrolled past all markers\n  if (bestVisibleMessageIndex === -1 && lastMarkerMessageIndex !== -1) {\n    // Check if we're scrolled past the last marker\n    const lastMarker = Array.from(screenshotElements).find(element => {\n      const msgIdx = parseInt((element as HTMLElement).dataset.messageIndex || '0');\n      const blockIdx = parseInt((element as HTMLElement).dataset.blockIndex || '0');\n      return msgIdx === lastMarkerMessageIndex && blockIdx === lastMarkerBlockIndex;\n    });\n    \n    if (lastMarker) {\n      const lastMarkerTop = (lastMarker as HTMLElement).offsetTop;\n      if (containerScrollTop > lastMarkerTop) {\n        // We're scrolled past the last marker, use it\n        bestVisibleMessageIndex = lastMarkerMessageIndex;\n        bestVisibleBlockIndex = lastMarkerBlockIndex;\n      }\n    }\n  }\n\n  // If still no marker found, return null to keep current screenshot\n  if (bestVisibleMessageIndex === -1) {\n    return null;\n  }\n\n  // Find the most recent screenshot at or before the best visible marker\n  let bestScreenshot: ScreenshotData | null = null;\n  for (const screenshot of screenshots) {\n    if (\n      screenshot.messageIndex < bestVisibleMessageIndex ||\n      (screenshot.messageIndex === bestVisibleMessageIndex && screenshot.blockIndex <= bestVisibleBlockIndex)\n    ) {\n      bestScreenshot = screenshot;\n    }\n  }\n  \n  return bestScreenshot;\n}"
  },
  {
    "path": "packages/bytebot-ui/src/utils/stringUtils.ts",
    "content": "/**\n * Capitalizes the first character of a string\n * @param str The string to capitalize\n * @returns The string with the first character capitalized\n */\nexport function capitalizeFirstChar(str: string): string {\n  if (!str || str.length === 0) return str;\n  return str.charAt(0).toUpperCase() + str.slice(1);\n}\n"
  },
  {
    "path": "packages/bytebot-ui/src/utils/taskUtils.ts",
    "content": "import { Message, Task, Model, GroupedMessages, FileWithBase64, TaskStatus } from \"@/types\";\n\n/**\n * Base configuration for API requests\n */\nconst API_CONFIG = {\n  baseUrl: \"/api\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n  },\n  credentials: \"include\" as RequestCredentials,\n};\n\n/**\n * Generic API request handler\n */\nasync function apiRequest<T>(\n  endpoint: string,\n  options: RequestInit = {},\n): Promise<T | null> {\n  try {\n    const response = await fetch(`${API_CONFIG.baseUrl}${endpoint}`, {\n      ...options,\n      headers: {\n        ...API_CONFIG.headers,\n        ...options.headers,\n      },\n      credentials: API_CONFIG.credentials,\n    });\n\n    if (!response.ok) {\n      throw new Error(\n        `API request failed: ${response.status} ${response.statusText}`,\n      );\n    }\n\n    return await response.json();\n  } catch (error) {\n    console.error(`Error in API request to ${endpoint}:`, error);\n    return null;\n  }\n}\n\n/**\n * Build query string from parameters\n */\nfunction buildQueryString(\n  params: Record<string, string | number | boolean>,\n): string {\n  const searchParams = new URLSearchParams();\n  Object.entries(params).forEach(([key, value]) => {\n    if (value !== undefined && value !== null) {\n      searchParams.append(key, value.toString());\n    }\n  });\n  const queryString = searchParams.toString();\n  return queryString ? `?${queryString}` : \"\";\n}\n\n/**\n * Fetches messages for a specific task\n */\nexport async function fetchTaskMessages(\n  taskId: string,\n  options?: {\n    limit?: number;\n    page?: number;\n  },\n): Promise<Message[]> {\n  const queryString = options ? buildQueryString(options) : \"\";\n  const result = await apiRequest<Message[]>(\n    `/tasks/${taskId}/messages${queryString}`,\n    { method: \"GET\" },\n  );\n  return result || [];\n}\n\n/**\n * Fetches raw messages for a specific task (unprocessed)\n */\nexport async function fetchTaskRawMessages(\n  taskId: string,\n  options?: {\n    limit?: number;\n    page?: number;\n  },\n): Promise<Message[]> {\n  const queryString = options ? buildQueryString(options) : \"\";\n  const result = await apiRequest<Message[]>(\n    `/tasks/${taskId}/messages/raw${queryString}`,\n    { method: \"GET\" },\n  );\n  return result || [];\n}\n\n/**\n * Fetches processed and grouped messages for a specific task (for chat UI)\n */\nexport async function fetchTaskProcessedMessages(\n  taskId: string,\n  options?: {\n    limit?: number;\n    page?: number;\n  },\n): Promise<GroupedMessages[]> {\n  const queryString = options ? buildQueryString(options) : \"\";\n  const result = await apiRequest<GroupedMessages[]>(\n    `/tasks/${taskId}/messages/processed${queryString}`,\n    { method: \"GET\" },\n  );\n  return result || [];\n}\n\n/**\n * Fetches a specific task by ID\n */\nexport async function fetchTaskById(taskId: string): Promise<Task | null> {\n  return apiRequest<Task>(`/tasks/${taskId}`, { method: \"GET\" });\n}\n\n/**\n * Sends a message to start a new task\n */\nexport async function startTask(data: {\n  description: string;\n  model: Model;\n  files?: FileWithBase64[];\n}): Promise<Task | null> {\n  return apiRequest<Task>(\"/tasks\", {\n    method: \"POST\",\n    body: JSON.stringify(data),\n  });\n}\n\n/**\n * Guides an existing task with a message\n */\nexport async function addMessage(\n  taskId: string,\n  message: string,\n): Promise<Task | null> {\n  return apiRequest<Task>(`/tasks/${taskId}/messages`, {\n    method: \"POST\",\n    body: JSON.stringify({ message }),\n  });\n}\n\n/**\n * Fetches all tasks with optional pagination and filtering\n */\nexport async function fetchTasks(options?: {\n  page?: number;\n  limit?: number;\n  status?: string;\n  statuses?: string[];\n}): Promise<{ tasks: Task[]; total: number; totalPages: number }> {\n  const params: Record<string, string | number> = {};\n  \n  if (options?.page) params.page = options.page;\n  if (options?.limit) params.limit = options.limit;\n  if (options?.status) params.status = options.status;\n  if (options?.statuses && options.statuses.length > 0) {\n    params.statuses = options.statuses.join(',');\n  }\n  \n  const queryString = Object.keys(params).length > 0 ? buildQueryString(params) : \"\";\n  const result = await apiRequest<{ tasks: Task[]; total: number; totalPages: number }>(\n    `/tasks${queryString}`,\n    { method: \"GET\" }\n  );\n  return result || { tasks: [], total: 0, totalPages: 0 };\n}\n\n/**\n * Fetches task counts for grouped tabs\n */\nexport async function fetchTaskCounts(): Promise<Record<string, number>> {\n  try {\n    const allTasksResult = await fetchTasks();\n    \n    // Define the status groups\n    const statusGroups = {\n      ALL: Object.values(TaskStatus),\n      ACTIVE: [TaskStatus.PENDING, TaskStatus.RUNNING, TaskStatus.NEEDS_HELP, TaskStatus.NEEDS_REVIEW],\n      COMPLETED: [TaskStatus.COMPLETED],\n      CANCELLED_FAILED: [TaskStatus.CANCELLED, TaskStatus.FAILED],\n    };\n\n    const counts: Record<string, number> = {\n      ALL: allTasksResult.total,\n      ACTIVE: 0,\n      COMPLETED: 0,\n      CANCELLED_FAILED: 0,\n    };\n\n    // Fetch counts for each group\n    const groupPromises = Object.entries(statusGroups).map(async ([groupKey, statuses]) => {\n      if (groupKey === 'ALL') {\n        return { groupKey, count: allTasksResult.total };\n      }\n      \n      const result = await fetchTasks({ statuses, limit: 1 });\n      return { groupKey, count: result.total };\n    });\n\n    const groupCounts = await Promise.all(groupPromises);\n    groupCounts.forEach(({ groupKey, count }) => {\n      counts[groupKey] = count;\n    });\n\n    return counts;\n  } catch (error) {\n    console.error(\"Failed to fetch task counts:\", error);\n    return {\n      ALL: 0,\n      ACTIVE: 0,\n      COMPLETED: 0,\n      CANCELLED_FAILED: 0,\n    };\n  }\n}\n\nexport async function fetchModels(): Promise<Model[]> {\n  try {\n    const response = await fetch(\"/api/tasks/models\", {\n      method: \"GET\",\n      headers: { \"Content-Type\": \"application/json\" },\n      credentials: \"include\",\n    });\n    if (!response.ok) {\n      throw new Error(\"Failed to fetch models\");\n    }\n    return await response.json();\n  } catch (error) {\n    console.error(\"Error fetching models:\", error);\n    return [];\n  }\n}\n\n/**\n * Takes over control of a task\n */\nexport async function takeOverTask(taskId: string): Promise<Task | null> {\n  return apiRequest<Task>(`/tasks/${taskId}/takeover`, { method: \"POST\" });\n}\n\n/**\n * Resumes a paused or stopped task\n */\nexport async function resumeTask(taskId: string): Promise<Task | null> {\n  return apiRequest<Task>(`/tasks/${taskId}/resume`, { method: \"POST\" });\n}\n\n/**\n * Cancels a running task\n */\nexport async function cancelTask(taskId: string): Promise<Task | null> {\n  return apiRequest<Task>(`/tasks/${taskId}/cancel`, { method: \"POST\" });\n}\n"
  },
  {
    "path": "packages/bytebot-ui/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@bytebot/shared\": [\"../shared\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/bytebotd/.dockerignore",
    "content": "**/node_modules\n**/dist\n**/.next\n**/.git\n**/.vscode\n**/.env*\n**/npm-debug.log\n**/yarn-debug.log\n**/yarn-error.log\n**/package-lock.json"
  },
  {
    "path": "packages/bytebotd/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}"
  },
  {
    "path": "packages/bytebotd/Dockerfile",
    "content": "# -----------------------------------------------------------------------------\n# Bytebot Dockerfile - Virtual Desktop Environment\n# -----------------------------------------------------------------------------\n\n# Base image\nFROM ubuntu:22.04\n\n# -----------------------------------------------------------------------------\n# 1. Environment setup\n# -----------------------------------------------------------------------------\n# Set non-interactive installation\nARG DEBIAN_FRONTEND=noninteractive\n# Configure display for X11 applications\nENV DISPLAY=:0\n\n# -----------------------------------------------------------------------------\n# 2. System dependencies installation\n# -----------------------------------------------------------------------------\nRUN apt-get update && apt-get install -y \\\n    # X11 / VNC\n    xvfb x11vnc xauth x11-xserver-utils \\\n    x11-apps sudo software-properties-common \\\n    # Desktop environment \n    xfce4 xfce4-goodies dbus wmctrl \\\n    # Display manager with autologin capability\n    lightdm \\\n    # Development tools\n    python3 python3-pip curl wget git vim \\\n    # Utilities\n    supervisor netcat-openbsd \\\n    # Applications\n    xpdf gedit xpaint \\\n    # Libraries\n    libxtst-dev \\\n    # Remove unneeded dependencies\n    && apt-get remove -y light-locker xfce4-screensaver xfce4-power-manager || true \\\n    # Clean up to reduce image size\n    && apt-get clean && rm -rf /var/lib/apt/lists/*\n\nRUN mkdir -p /run/dbus && \\\n    # Generate a machine-id so dbus-daemon doesn't complain\n    dbus-uuidgen --ensure=/etc/machine-id\n\n# -----------------------------------------------------------------------------\n# 3. Additional software installation\n# -----------------------------------------------------------------------------\n# Install Firefox\nRUN apt-get update && apt-get install -y \\\n    # Install necessary for adding PPA\n    software-properties-common apt-transport-https wget gnupg \\\n    # Install Additional Graphics Libraries\n    mesa-utils \\\n    libgl1-mesa-dri \\\n    libgl1-mesa-glx \\\n    # Install Sandbox Capabilities\n    libcap2-bin \\\n    # Install Fonts\n    fontconfig \\\n    fonts-dejavu \\\n    fonts-liberation \\\n    fonts-freefont-ttf \\\n    && add-apt-repository -y ppa:mozillateam/ppa \\\n    && apt-get update \\\n    && apt-get install -y firefox-esr thunderbird \\\n    && apt-get clean && rm -rf /var/lib/apt/lists/* \\\n    # Set Firefox as default browser system-wide\n    && update-alternatives --install /usr/bin/x-www-browser x-www-browser /usr/bin/firefox-esr 200 \\\n    && update-alternatives --set x-www-browser /usr/bin/firefox-esr \\\n    && fc-cache -f -v\n\n\n# Install 1Password based on architecture\nRUN ARCH=$(dpkg --print-architecture) && \\\n    if [ \"$ARCH\" = \"amd64\" ]; then \\\n        # Install from APT repository for AMD64\n        curl -sS https://downloads.1password.com/linux/keys/1password.asc | \\\n        gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg && \\\n        echo \"deb [arch=amd64 signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/amd64 stable main\" | \\\n        tee /etc/apt/sources.list.d/1password.list && \\\n        mkdir -p /etc/debsig/policies/AC2D62742012EA22/ && \\\n        curl -sS https://downloads.1password.com/linux/debian/debsig/1password.pol | \\\n        tee /etc/debsig/policies/AC2D62742012EA22/1password.pol && \\\n        mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22 && \\\n        curl -sS https://downloads.1password.com/linux/keys/1password.asc | \\\n        gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg && \\\n        apt-get update && apt-get install -y 1password && \\\n        apt-get clean && rm -rf /var/lib/apt/lists/*; \\\n    elif [ \"$ARCH\" = \"arm64\" ]; then \\\n        # Install from tar.gz for ARM64\n        apt-get update && apt-get install -y \\\n            libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 xdg-utils \\\n            libatspi2.0-0 libdrm2 libgbm1 libxcb-dri3-0 libxkbcommon0 \\\n            libsecret-1-0 && \\\n        apt-get clean && rm -rf /var/lib/apt/lists/* && \\\n        curl -sSL https://downloads.1password.com/linux/tar/beta/aarch64/1password-latest.tar.gz -o /tmp/1password.tar.gz && \\\n        # Extract the full 1Password bundle so libraries like libffmpeg.so remain in their expected relative paths\n        mkdir -p /opt/1password && \\\n        tar -xzf /tmp/1password.tar.gz -C /opt/1password --strip-components=1 && \\\n        # Link the main executable into the global PATH\n        ln -sf /opt/1password/1password /usr/bin/1password && \\\n        chmod +x /opt/1password/1password && \\\n        # Copy icons to standard locations\n        mkdir -p /usr/share/pixmaps /usr/share/icons/hicolor/512x512/apps /usr/share/icons/hicolor/256x256/apps && \\\n        find /opt/1password -name \"*1password*.png\" -o -name \"*1password*.svg\" | while read icon; do \\\n            if [[ \"$icon\" == *\"512\"* ]]; then \\\n                cp \"$icon\" /usr/share/icons/hicolor/512x512/apps/1password.png 2>/dev/null || true; \\\n            elif [[ \"$icon\" == *\"256\"* ]]; then \\\n                cp \"$icon\" /usr/share/icons/hicolor/256x256/apps/1password.png 2>/dev/null || true; \\\n            fi; \\\n            cp \"$icon\" /usr/share/pixmaps/1password.png 2>/dev/null || true; \\\n        done && \\\n        # Clean up temporary files\n        rm -rf /tmp/1password.tar.gz && \\\n        # Update icon cache\n        gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true; \\\n    else \\\n        echo \"1Password is not available for $ARCH architecture.\"; \\\n    fi\n\n# Install Visual Studio Code\nRUN ARCH=$(dpkg --print-architecture) && \\\n    if [ \"$ARCH\" = \"amd64\" ]; then \\\n        apt-get update && apt-get install -y wget gpg apt-transport-https software-properties-common && \\\n        wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/ms_vscode.gpg && \\\n        echo \"deb [arch=amd64 signed-by=/usr/share/keyrings/ms_vscode.gpg] https://packages.microsoft.com/repos/code stable main\" > /etc/apt/sources.list.d/vscode.list && \\\n        apt-get update && apt-get install -y code && \\\n        apt-get clean && rm -rf /var/lib/apt/lists/* ; \\\n    elif [ \"$ARCH\" = \"arm64\" ]; then \\\n        apt-get update && apt-get install -y wget gpg && \\\n        wget -qO /tmp/code_arm64.deb https://update.code.visualstudio.com/latest/linux-deb-arm64/stable && \\\n        apt-get install -y /tmp/code_arm64.deb && \\\n        rm -f /tmp/code_arm64.deb && \\\n        apt-get clean && rm -rf /var/lib/apt/lists/* ; \\\n    else \\\n        echo \"VSCode is not available for $ARCH architecture.\"; \\\n    fi\n\n# Install Node.js\nRUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \\\n    && apt-get update \\\n    && apt-get install -y nodejs \\\n    && apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Upgrade pip\nRUN pip3 install --upgrade pip\n\n# -----------------------------------------------------------------------------\n# 4. VNC and remote access setup\n# -----------------------------------------------------------------------------\n# Install noVNC and websockify\nRUN git clone https://github.com/novnc/noVNC.git /opt/noVNC \\\n    && git clone https://github.com/novnc/websockify.git /opt/websockify \\\n    && cd /opt/websockify \\\n    && pip3 install --break-system-packages .\n\n# -----------------------------------------------------------------------------\n# 5. Application setup (bytebotd)\n# -----------------------------------------------------------------------------\n# Copy package files first to leverage Docker cache\n\n# Install dependencies required to build libnut-core and uiohook-napi\nRUN apt-get update && \\\n    apt-get install -y \\\n        cmake \\\n        libx11-dev \\\n        libxtst-dev \\\n        libxinerama-dev \\\n        libxi-dev \\\n        libxt-dev \\\n        libxrandr-dev \\\n        libxkbcommon-dev \\\n        libxkbcommon-x11-dev \\\n        xclip \\\n        git build-essential && \\\n    rm -rf /var/lib/apt/lists/*\n\nCOPY ./shared/ /bytebot/shared/\nCOPY ./bytebotd/ /bytebot/bytebotd/\nWORKDIR /bytebot/bytebotd\nRUN npm install --build-from-source\nRUN npm rebuild uiohook-napi --build-from-source\n\nRUN npm run build\n\nWORKDIR /compile\n\nRUN git clone https://github.com/ZachJW34/libnut-core.git && \\\n    cd libnut-core && \\\n    npm install && \\\n    npm run build:release\n\n# replace /bytebotd/node_modules/@nut-tree-fork/libnut-linux/build/Release/libnut.node with /compile/libnut-core/build/Release/libnut.node\nRUN rm -f /bytebot/bytebotd/node_modules/@nut-tree-fork/libnut-linux/build/Release/libnut.node && \\\n    cp /compile/libnut-core/build/Release/libnut.node /bytebot/bytebotd/node_modules/@nut-tree-fork/libnut-linux/build/Release/libnut.node\n\nRUN rm -rf /compile\n\nWORKDIR /bytebot/bytebotd\n\n# -----------------------------------------------------------------------------\n# 7. User setup and autologin configuration\n# -----------------------------------------------------------------------------\n# Create non-root user\nRUN useradd -ms /bin/bash user && echo \"user ALL=(ALL) NOPASSWD:ALL\" >> /etc/sudoers\n\nRUN mkdir -p /var/run/dbus && \\\n    chmod 755 /var/run/dbus && \\\n    chown user:user /var/run/dbus\n\nRUN mkdir -p /tmp/bytebot-screenshots && \\\n    chown -R user:user /tmp/bytebot-screenshots\n\n# -----------------------------------------------------------------------------\n#  Copy staged system files and keep sane permissions\n# -----------------------------------------------------------------------------\n# 1. Ensure everything under /bytebotd/root is owned by root (files + dirs)\n# 2. Set *files* to 0644 and *directories* to 0755 so that applications can\n#    traverse directories (execute bit!) while keeping the contents read-only.\n# 3. Copy the tree to /\nRUN chown -R root:root /bytebot/bytebotd/root && \\\n    find /bytebot/bytebotd/root -type f -exec chmod 644 {} + && \\\n    find /bytebot/bytebotd/root -type d -exec chmod 755 {} + && \\\n    find /bytebot/bytebotd/root -type f -executable -exec chmod +x {} + && \\\n    cp -a /bytebot/bytebotd/root/. /\n\nRUN chown -R user:user /home/user\nRUN chmod -R 755 /home/user\n\nRUN mkdir -p /home/user/Desktop && \\\n    cp -f /usr/share/applications/firefox.desktop /home/user/Desktop/ && \\\n    cp -f /usr/share/applications/thunderbird.desktop /home/user/Desktop/ && \\\n    cp -f /usr/share/applications/1password.desktop /home/user/Desktop/ && \\\n    cp -f /usr/share/applications/code.desktop /home/user/Desktop/ && \\\n    cp -f /usr/share/applications/terminal.desktop /home/user/Desktop/ && \\\n    chmod +x /home/user/Desktop/*.desktop && \\\n    chown user:user /home/user/Desktop/*.desktop\n\nRUN mkdir -p /home/user/.config /home/user/.local/share /home/user/.cache \\\n    && chown -R user:user /home/user/.config /home/user/.local /home/user/.cache\n\nWORKDIR /home/user\n\n# -----------------------------------------------------------------------------\n# 8. Port configuration and runtime\n# -----------------------------------------------------------------------------\n# - Port 9990: bytebotd and external noVNC websocket\nEXPOSE 9990\n\n# Start supervisor to manage all services\nCMD [\"/usr/bin/supervisord\", \"-c\", \"/etc/supervisor/conf.d/supervisord.conf\", \"-n\"]"
  },
  {
    "path": "packages/bytebotd/eslint.config.mjs",
    "content": "// @ts-check\nimport eslint from '@eslint/js';\nimport eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';\nimport globals from 'globals';\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n  {\n    ignores: ['eslint.config.mjs'],\n  },\n  eslint.configs.recommended,\n  ...tseslint.configs.recommendedTypeChecked,\n  eslintPluginPrettierRecommended,\n  {\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest,\n      },\n      ecmaVersion: 5,\n      sourceType: 'module',\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n  },\n  {\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-floating-promises': 'warn',\n      '@typescript-eslint/no-unsafe-argument': 'warn'\n    },\n  },\n);"
  },
  {
    "path": "packages/bytebotd/nest-cli.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/nest-cli\",\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\",\n  \"compilerOptions\": {\n    \"deleteOutDir\": true\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/package.json",
    "content": "{\n  \"name\": \"bytebotd\",\n  \"version\": \"0.0.1\",\n  \"email\": \"support@bytebot.ai\",\n  \"description\": \"Bytebot daemon\",\n  \"homepage\": \"https://bytebot.ai\",\n  \"author\": {\n    \"name\": \"Bytebot\",\n    \"email\": \"support@bytebot.ai\"\n  },\n  \"private\": true,\n  \"license\": \"UNLICENSED\",\n  \"scripts\": {\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"build\": \"npm run build --prefix ../shared && nest build\",\n    \"compile\": \"tsc\",\n    \"start\": \"npm run build --prefix ../shared && nest start\",\n    \"start:dev\": \"npm run build --prefix ../shared && nest start --watch\",\n    \"start:debug\": \"npm run build --prefix ../shared && nest start --debug --watch\",\n    \"start:prod\": \"npm run build --prefix ../shared && node dist/main\",\n    \"lint\": \"eslint \\\"{src,apps,libs,test}/**/*.ts\\\" --fix\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:cov\": \"jest --coverage\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json\"\n  },\n  \"dependencies\": {\n    \"@bytebot/shared\": \"file:../shared\",\n    \"@nestjs/common\": \"^11.1.2\",\n    \"@nestjs/config\": \"^4.0.1\",\n    \"@nestjs/core\": \"^11.0.1\",\n    \"@nestjs/platform-express\": \"^11.1.3\",\n    \"@nestjs/platform-socket.io\": \"^11.1.2\",\n    \"@nestjs/serve-static\": \"^5.0.3\",\n    \"@nestjs/websockets\": \"^11.1.2\",\n    \"@nut-tree-fork/nut-js\": \"^4.2.6\",\n    \"@rekog/mcp-nest\": \"^1.6.2\",\n    \"class-transformer\": \"^0.5.1\",\n    \"class-validator\": \"^0.14.2\",\n    \"http-proxy-middleware\": \"^3.0.5\",\n    \"reflect-metadata\": \"^0.2.2\",\n    \"rxjs\": \"^7.8.1\",\n    \"sharp\": \"^0.34.2\",\n    \"socket.io\": \"^4.8.1\",\n    \"uiohook-napi\": \"^1.5.4\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.2.0\",\n    \"@eslint/js\": \"^9.18.0\",\n    \"@nestjs/cli\": \"^11.0.0\",\n    \"@nestjs/schematics\": \"^11.0.0\",\n    \"@nestjs/testing\": \"^11.0.1\",\n    \"@swc/cli\": \"^0.6.0\",\n    \"@swc/core\": \"^1.10.7\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.13.14\",\n    \"@types/supertest\": \"^6.0.2\",\n    \"eslint\": \"^9.18.0\",\n    \"eslint-config-prettier\": \"^10.0.1\",\n    \"eslint-plugin-prettier\": \"^5.2.2\",\n    \"globals\": \"^15.14.0\",\n    \"jest\": \"^29.7.0\",\n    \"prettier\": \"^3.4.2\",\n    \"source-map-support\": \"^0.5.21\",\n    \"supertest\": \"^7.0.0\",\n    \"ts-jest\": \"^29.2.5\",\n    \"ts-loader\": \"^9.5.2\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^4.2.0\",\n    \"typescript\": \"^5.7.3\",\n    \"typescript-eslint\": \"^8.20.0\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\n      \"js\",\n      \"json\",\n      \"ts\"\n    ],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".*\\\\.spec\\\\.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"collectCoverageFrom\": [\n      \"**/*.(t|j)s\"\n    ],\n    \"coverageDirectory\": \"../coverage\",\n    \"testEnvironment\": \"node\"\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/root/etc/firefox/policies/policies.json",
    "content": "{\n  \"policies\": {\n    \"FirefoxHome\": {\n      \"Search\": true,\n      \"TopSites\": false,\n      \"SponsoredTopSites\": false,\n      \"Highlights\": false,\n      \"Pocket\": false,\n      \"SponsoredPocket\": false,\n      \"Snippets\": false,\n      \"Locked\": true\n    },\n    \"SkipTermsOfUse\": true,\n    \"OfferToSaveLoginsDefault\": false,\n    \"OfferToSaveLogins\": false,\n    \"PasswordManagerEnabled\": false,\n    \"PDFjs\": { \"Enabled\": true, \"EnablePermissions\": true },\n    \"Notifications\": { \"BlockNewRequests\": true, \"Locked\": true },\n    \"UserMessaging\": {\n      \"ExtensionRecommendations\": false,\n      \"FeatureRecommendations\": false,\n      \"UrlbarInterventions\": false,\n      \"SkipOnboarding\": true,\n      \"MoreFromMozilla\": false,\n      \"FirefoxLabs\": false\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/root/etc/lightdm/lightdm.conf.d/50-autologin.conf",
    "content": "[Seat:*]\nautologin-user=user\nautologin-user-timeout=0\nautologin-session=xfce"
  },
  {
    "path": "packages/bytebotd/root/etc/supervisor/conf.d/supervisord.conf",
    "content": "[supervisord]\nuser=root\nnodaemon=true\nlogfile=/dev/stdout\nlogfile_maxbytes=0\nloglevel=info\n\n[program:set-hostname]\ncommand=bash -c \"sudo hostname computer\"\nautostart=true\nautorestart=false\nstartsecs=0\npriority=1\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\n\n[program:dbus]\ncommand=/usr/bin/dbus-daemon --system --nofork\npriority=1\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\n\n[program:xvfb]\ncommand=Xvfb :0 -screen 0 1280x960x24 -ac -nolisten tcp\nuser=user\nautostart=true\nautorestart=true\nstartsecs=5\npriority=10\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\n\n[program:xfce4]\nuser=user\ncommand=sh -c 'sleep 5 && \\\n  export XDG_CONFIG_HOME=$HOME/.config && \\\n  export XDG_DATA_HOME=$HOME/.local/share && \\\n  export XDG_CACHE_HOME=$HOME/.cache && \\\n  export XDG_CONFIG_DIRS=/etc/xdg && \\\n  export XDG_DATA_DIRS=/usr/share && \\\n  exec dbus-launch --exit-with-session startxfce4'\nenvironment=DISPLAY=\":0\",HOME=\"/home/user\"\nautostart=true\nautorestart=true\nstartsecs=5\npriority=20\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\ndepends_on=xvfb\n\n[program:x11vnc]\ncommand=x11vnc -display :0 -N -forever -shared -rfbport 5900\nuser=user\nautostart=true\nautorestart=true\nstartsecs=5\npriority=30\nenvironment=DISPLAY=\":0\"\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\ndepends_on=xfce4\n\n[program:websockify]\ncommand=websockify 6080 localhost:5900\nautostart=true\nautorestart=true\nstartsecs=5\npriority=40\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\ndepends_on=x11vnc\n\n[program:bytebotd]\nuser=user\ncommand=node /bytebot/bytebotd/dist/main.js\ndirectory=/bytebot/bytebotd\nautostart=true\nautorestart=true\nstartsecs=5\npriority=60\nenvironment=DISPLAY=\":0\"\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nredirect_stderr=true\ndepends_on=websockify\n\n[eventlistener:startup]\ncommand=echo \"All services started successfully\"\nevents=PROCESS_STATE_RUNNING\nbuffer_size=100"
  },
  {
    "path": "packages/bytebotd/root/etc/thunderbird/policies/policies.json",
    "content": "{\n  \"policies\": {\n    \"UserMessaging\": {\n      \"ExtensionRecommendations\": false,\n      \"FeatureRecommendations\": false,\n      \"UrlbarInterventions\": false,\n      \"SkipOnboarding\": true,\n      \"MoreFromMozilla\": false\n    },\n    \"OverrideFirstRunPage\": \"\",\n    \"OverridePostUpdatePage\": \"\",\n    \"InAppNotification\": {\n      \"DonationEnabled\": false,\n      \"SurveyEnabled\": false,\n      \"MessageEnabled\": false\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/desktop/icons.screen0-1264x913.rc",
    "content": "[xfdesktop-version-4.10.3+-rcfile_format]\n4.10.3+=true\n\n[/home/user/Desktop/firefox.desktop]\nrow=0\ncol=0\n\n[/home/user/Desktop/thunderbird.desktop]\nrow=1\ncol=0\n\n[/home/user/Desktop/1password.desktop]\nrow=2\ncol=0\n\n[/home/user/Desktop/code.desktop]\nrow=3\ncol=0\n\n[/home/user/Desktop/terminal.desktop]\nrow=4\ncol=0\n\n\n[/home/user]\nrow=5\ncol=0\n\n\n[Trash]\nrow=6\ncol=0\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/helpers.rc",
    "content": "TerminalEmulator=xfce4-terminal\nWebBrowser=firefox-esr\nFileManager=thunar\nMailReader=thunderbird\n\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/terminal/accels.scm",
    "content": "; xfce4-terminal GtkAccelMap rc-file         -*- scheme -*-\n; this file is an automated accelerator map dump\n;\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-2\" \"<Alt>2\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-6\" \"<Alt>6\")\n; (gtk_accel_path \"<Actions>/terminal-window/copy-input\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/close-other-tabs\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/move-tab-right\" \"<Primary><Shift>Page_Down\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-7\" \"<Alt>7\")\n; (gtk_accel_path \"<Actions>/terminal-window/set-title-color\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/edit-menu\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/zoom-menu\" \"\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-1\" \"<Alt>1\")\n; (gtk_accel_path \"<Actions>/terminal-window/fullscreen\" \"F11\")\n; (gtk_accel_path \"<Actions>/terminal-window/read-only\" \"\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-5\" \"<Alt>5\")\n; (gtk_accel_path \"<Actions>/terminal-window/preferences\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/reset-and-clear\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/about\" \"\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-4\" \"<Alt>4\")\n; (gtk_accel_path \"<Actions>/terminal-window/close-window\" \"<Primary><Shift>q\")\n; (gtk_accel_path \"<Actions>/terminal-window/reset\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/save-contents\" \"\")\n(gtk_accel_path \"<Actions>/terminal-window/toggle-menubar\" \"F10\")\n; (gtk_accel_path \"<Actions>/terminal-window/copy\" \"<Primary><Shift>c\")\n; (gtk_accel_path \"<Actions>/terminal-window/copy-html\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/last-active-tab\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/show-borders\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/view-menu\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/detach-tab\" \"<Primary><Shift>d\")\n; (gtk_accel_path \"<Actions>/terminal-window/scroll-on-output\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/show-toolbar\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/next-tab\" \"<Primary>Page_Down\")\n; (gtk_accel_path \"<Actions>/terminal-window/tabs-menu\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/search-next\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/search-prev\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/undo-close-tab\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/set-title\" \"<Primary><Shift>s\")\n; (gtk_accel_path \"<Actions>/terminal-window/contents\" \"F1\")\n; (gtk_accel_path \"<Actions>/terminal-window/zoom-reset\" \"<Primary>0\")\n; (gtk_accel_path \"<Actions>/terminal-window/close-tab\" \"<Primary><Shift>w\")\n; (gtk_accel_path \"<Actions>/terminal-window/new-tab\" \"<Primary><Shift>t\")\n; (gtk_accel_path \"<Actions>/terminal-window/new-window\" \"<Primary><Shift>n\")\n; (gtk_accel_path \"<Actions>/terminal-window/terminal-menu\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/show-menubar\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/select-all\" \"<Primary><Shift>a\")\n; (gtk_accel_path \"<Actions>/terminal-window/paste\" \"<Primary><Shift>v\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-9\" \"<Alt>9\")\n; (gtk_accel_path \"<Actions>/terminal-window/move-tab-left\" \"<Primary><Shift>Page_Up\")\n; (gtk_accel_path \"<Actions>/terminal-window/search\" \"<Primary><Shift>f\")\n; (gtk_accel_path \"<Actions>/terminal-window/file-menu\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/prev-tab\" \"<Primary>Page_Up\")\n; (gtk_accel_path \"<Actions>/terminal-window/paste-selection\" \"\")\n; (gtk_accel_path \"<Actions>/terminal-window/zoom-in\" \"<Primary>plus\")\n; (gtk_accel_path \"<Actions>/terminal-window/zoom-out\" \"<Primary>minus\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-8\" \"<Alt>8\")\n; (gtk_accel_path \"<Actions>/terminal-window/help-menu\" \"\")\n(gtk_accel_path \"<Actions>/terminal-window/goto-tab-3\" \"<Alt>3\")\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/displays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"displays\" version=\"1.0\">\n  <property name=\"ActiveProfile\" type=\"string\" value=\"Default\"/>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/thunar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"thunar\" version=\"1.0\">\n  <property name=\"last-view\" type=\"string\" value=\"ThunarIconView\"/>\n  <property name=\"last-icon-view-zoom-level\" type=\"string\" value=\"THUNAR_ZOOM_LEVEL_100_PERCENT\"/>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-appfinder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-appfinder\" version=\"1.0\">\n  <property name=\"last\" type=\"empty\">\n    <property name=\"window-height\" type=\"int\" value=\"400\"/>\n    <property name=\"window-width\" type=\"int\" value=\"400\"/>\n    <property name=\"pane-position\" type=\"int\" value=\"180\"/>\n  </property>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-desktop\" version=\"1.0\">\n  <property name=\"backdrop\" type=\"empty\">\n    <property name=\"screen0\" type=\"empty\">\n      <property name=\"monitorscreen\" type=\"empty\">\n        <property name=\"workspace0\" type=\"empty\">\n          <property name=\"color-style\" type=\"int\" value=\"0\"/>\n          <property name=\"image-style\" type=\"int\" value=\"5\"/>\n          <property name=\"last-image\" type=\"string\" value=\"/usr/share/backgrounds/bytebot-background.jpg\"/>\n        </property>\n      </property>\n    </property>\n  </property>\n  <property name=\"last\" type=\"empty\">\n    <property name=\"window-width\" type=\"int\" value=\"621\"/>\n    <property name=\"window-height\" type=\"int\" value=\"533\"/>\n  </property>\n  <property name=\"desktop-icons\" type=\"empty\">\n    <property name=\"file-icons\" type=\"empty\">\n      <property name=\"show-filesystem\" type=\"bool\" value=\"false\"/>\n    </property>\n    <property name=\"show-tooltips\" type=\"bool\" value=\"false\"/>\n  </property>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-keyboard-shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-keyboard-shortcuts\" version=\"1.0\">\n  <property name=\"commands\" type=\"empty\">\n    <property name=\"default\" type=\"empty\">\n      <property name=\"&lt;Alt&gt;F1\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F2\" type=\"empty\">\n        <property name=\"startup-notify\" type=\"empty\"/>\n      </property>\n      <property name=\"&lt;Alt&gt;F3\" type=\"empty\">\n        <property name=\"startup-notify\" type=\"empty\"/>\n      </property>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Delete\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;l\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;t\" type=\"empty\"/>\n      <property name=\"XF86Display\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;p\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;Escape\" type=\"empty\"/>\n      <property name=\"XF86WWW\" type=\"empty\"/>\n      <property name=\"HomePage\" type=\"empty\"/>\n      <property name=\"XF86Mail\" type=\"empty\"/>\n      <property name=\"Print\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;Print\" type=\"empty\"/>\n      <property name=\"&lt;Shift&gt;Print\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;e\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;f\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Escape\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;Escape\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;r\" type=\"empty\">\n        <property name=\"startup-notify\" type=\"empty\"/>\n      </property>\n    </property>\n    <property name=\"custom\" type=\"empty\">\n      <property name=\"&lt;Alt&gt;F2\" type=\"string\" value=\"xfce4-appfinder --collapsed\">\n        <property name=\"startup-notify\" type=\"bool\" value=\"true\"/>\n      </property>\n      <property name=\"&lt;Alt&gt;Print\" type=\"string\" value=\"xfce4-screenshooter -w\"/>\n      <property name=\"&lt;Super&gt;r\" type=\"string\" value=\"xfce4-appfinder -c\">\n        <property name=\"startup-notify\" type=\"bool\" value=\"true\"/>\n      </property>\n      <property name=\"XF86WWW\" type=\"string\" value=\"exo-open --launch WebBrowser\"/>\n      <property name=\"XF86Mail\" type=\"string\" value=\"exo-open --launch MailReader\"/>\n      <property name=\"&lt;Alt&gt;F3\" type=\"string\" value=\"xfce4-appfinder\">\n        <property name=\"startup-notify\" type=\"bool\" value=\"true\"/>\n      </property>\n      <property name=\"Print\" type=\"string\" value=\"xfce4-screenshooter\"/>\n      <property name=\"&lt;Primary&gt;Escape\" type=\"string\" value=\"xfdesktop --menu\"/>\n      <property name=\"&lt;Shift&gt;Print\" type=\"string\" value=\"xfce4-screenshooter -r\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Delete\" type=\"string\" value=\"xfce4-session-logout\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;t\" type=\"string\" value=\"exo-open --launch TerminalEmulator\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;f\" type=\"string\" value=\"thunar\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;l\" type=\"string\" value=\"xflock4\"/>\n      <property name=\"&lt;Alt&gt;F1\" type=\"string\" value=\"xfce4-popup-applicationsmenu\"/>\n      <property name=\"&lt;Super&gt;p\" type=\"string\" value=\"xfce4-display-settings --minimal\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;Escape\" type=\"string\" value=\"xfce4-taskmanager\"/>\n      <property name=\"&lt;Super&gt;e\" type=\"string\" value=\"thunar\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Escape\" type=\"string\" value=\"xkill\"/>\n      <property name=\"HomePage\" type=\"string\" value=\"exo-open --launch WebBrowser\"/>\n      <property name=\"XF86Display\" type=\"string\" value=\"xfce4-display-settings --minimal\"/>\n      <property name=\"override\" type=\"bool\" value=\"true\"/>\n    </property>\n  </property>\n  <property name=\"xfwm4\" type=\"empty\">\n    <property name=\"default\" type=\"empty\">\n      <property name=\"&lt;Alt&gt;Insert\" type=\"empty\"/>\n      <property name=\"Escape\" type=\"empty\"/>\n      <property name=\"Left\" type=\"empty\"/>\n      <property name=\"Right\" type=\"empty\"/>\n      <property name=\"Up\" type=\"empty\"/>\n      <property name=\"Down\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;Tab\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;&lt;Shift&gt;Tab\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;Delete\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Down\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Left\" type=\"empty\"/>\n      <property name=\"&lt;Shift&gt;&lt;Alt&gt;Page_Down\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F4\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F6\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F7\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F8\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F9\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F10\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F11\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;F12\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Left\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;End\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Home\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Right\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Up\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_1\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_2\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_3\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_4\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_5\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_6\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_7\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_8\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_9\" type=\"empty\"/>\n      <property name=\"&lt;Alt&gt;space\" type=\"empty\"/>\n      <property name=\"&lt;Shift&gt;&lt;Alt&gt;Page_Up\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Right\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;d\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Up\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;Tab\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F1\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F2\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F3\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F4\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F5\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F6\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F7\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F8\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F9\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F10\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F11\" type=\"empty\"/>\n      <property name=\"&lt;Primary&gt;F12\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Left\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Right\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Up\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Down\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Page_Up\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Home\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_End\" type=\"empty\"/>\n      <property name=\"&lt;Super&gt;KP_Next\" type=\"empty\"/>\n    </property>\n    <property name=\"custom\" type=\"empty\">\n      <property name=\"&lt;Primary&gt;F12\" type=\"string\" value=\"workspace_12_key\"/>\n      <property name=\"&lt;Super&gt;KP_Down\" type=\"string\" value=\"tile_up_key\"/>\n      <property name=\"&lt;Alt&gt;F4\" type=\"string\" value=\"close_window_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_3\" type=\"string\" value=\"move_window_workspace_3_key\"/>\n      <property name=\"&lt;Primary&gt;F2\" type=\"string\" value=\"workspace_2_key\"/>\n      <property name=\"&lt;Primary&gt;F6\" type=\"string\" value=\"workspace_6_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Down\" type=\"string\" value=\"down_workspace_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_9\" type=\"string\" value=\"move_window_workspace_9_key\"/>\n      <property name=\"&lt;Super&gt;KP_Up\" type=\"string\" value=\"tile_down_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;End\" type=\"string\" value=\"move_window_next_workspace_key\"/>\n      <property name=\"&lt;Primary&gt;F8\" type=\"string\" value=\"workspace_8_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Left\" type=\"string\" value=\"move_window_left_key\"/>\n      <property name=\"&lt;Super&gt;KP_Right\" type=\"string\" value=\"tile_right_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_4\" type=\"string\" value=\"move_window_workspace_4_key\"/>\n      <property name=\"Right\" type=\"string\" value=\"right_key\"/>\n      <property name=\"Down\" type=\"string\" value=\"down_key\"/>\n      <property name=\"&lt;Primary&gt;F3\" type=\"string\" value=\"workspace_3_key\"/>\n      <property name=\"&lt;Shift&gt;&lt;Alt&gt;Page_Down\" type=\"string\" value=\"lower_window_key\"/>\n      <property name=\"&lt;Primary&gt;F9\" type=\"string\" value=\"workspace_9_key\"/>\n      <property name=\"&lt;Alt&gt;Tab\" type=\"string\" value=\"cycle_windows_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Right\" type=\"string\" value=\"move_window_right_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Right\" type=\"string\" value=\"right_workspace_key\"/>\n      <property name=\"&lt;Alt&gt;F6\" type=\"string\" value=\"stick_window_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_5\" type=\"string\" value=\"move_window_workspace_5_key\"/>\n      <property name=\"&lt;Primary&gt;F11\" type=\"string\" value=\"workspace_11_key\"/>\n      <property name=\"&lt;Alt&gt;F10\" type=\"string\" value=\"maximize_window_key\"/>\n      <property name=\"&lt;Alt&gt;Delete\" type=\"string\" value=\"del_workspace_key\"/>\n      <property name=\"&lt;Super&gt;Tab\" type=\"string\" value=\"switch_window_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;d\" type=\"string\" value=\"show_desktop_key\"/>\n      <property name=\"&lt;Primary&gt;F4\" type=\"string\" value=\"workspace_4_key\"/>\n      <property name=\"&lt;Super&gt;KP_Page_Up\" type=\"string\" value=\"tile_up_right_key\"/>\n      <property name=\"&lt;Alt&gt;F7\" type=\"string\" value=\"move_window_key\"/>\n      <property name=\"Up\" type=\"string\" value=\"up_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_6\" type=\"string\" value=\"move_window_workspace_6_key\"/>\n      <property name=\"&lt;Alt&gt;F11\" type=\"string\" value=\"fullscreen_key\"/>\n      <property name=\"&lt;Alt&gt;space\" type=\"string\" value=\"popup_menu_key\"/>\n      <property name=\"&lt;Super&gt;KP_Home\" type=\"string\" value=\"tile_up_left_key\"/>\n      <property name=\"Escape\" type=\"string\" value=\"cancel_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_1\" type=\"string\" value=\"move_window_workspace_1_key\"/>\n      <property name=\"&lt;Super&gt;KP_Next\" type=\"string\" value=\"tile_down_right_key\"/>\n      <property name=\"&lt;Super&gt;KP_Left\" type=\"string\" value=\"tile_left_key\"/>\n      <property name=\"&lt;Shift&gt;&lt;Alt&gt;Page_Up\" type=\"string\" value=\"raise_window_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Home\" type=\"string\" value=\"move_window_prev_workspace_key\"/>\n      <property name=\"&lt;Alt&gt;&lt;Shift&gt;Tab\" type=\"string\" value=\"cycle_reverse_windows_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Left\" type=\"string\" value=\"left_workspace_key\"/>\n      <property name=\"&lt;Alt&gt;F12\" type=\"string\" value=\"above_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Up\" type=\"string\" value=\"move_window_up_key\"/>\n      <property name=\"&lt;Primary&gt;F5\" type=\"string\" value=\"workspace_5_key\"/>\n      <property name=\"&lt;Alt&gt;F8\" type=\"string\" value=\"resize_window_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_7\" type=\"string\" value=\"move_window_workspace_7_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_2\" type=\"string\" value=\"move_window_workspace_2_key\"/>\n      <property name=\"&lt;Super&gt;KP_End\" type=\"string\" value=\"tile_down_left_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;Up\" type=\"string\" value=\"up_workspace_key\"/>\n      <property name=\"&lt;Alt&gt;F9\" type=\"string\" value=\"hide_window_key\"/>\n      <property name=\"&lt;Primary&gt;F7\" type=\"string\" value=\"workspace_7_key\"/>\n      <property name=\"&lt;Primary&gt;F10\" type=\"string\" value=\"workspace_10_key\"/>\n      <property name=\"Left\" type=\"string\" value=\"left_key\"/>\n      <property name=\"&lt;Primary&gt;&lt;Alt&gt;KP_8\" type=\"string\" value=\"move_window_workspace_8_key\"/>\n      <property name=\"&lt;Alt&gt;Insert\" type=\"string\" value=\"add_workspace_key\"/>\n      <property name=\"&lt;Primary&gt;F1\" type=\"string\" value=\"workspace_1_key\"/>\n      <property name=\"override\" type=\"bool\" value=\"true\"/>\n    </property>\n  </property>\n  <property name=\"providers\" type=\"array\">\n    <value type=\"string\" value=\"xfwm4\"/>\n    <value type=\"string\" value=\"commands\"/>\n  </property>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-notifyd.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-notifyd\" version=\"1.0\">\n  <property name=\"do-not-disturb\" type=\"bool\" value=\"true\"/>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfce4-panel\" version=\"1.0\">\n  <property name=\"configver\" type=\"int\" value=\"2\"/>\n  <property name=\"panels\" type=\"array\">\n    <value type=\"int\" value=\"1\"/>\n    <property name=\"dark-mode\" type=\"bool\" value=\"true\"/>\n    <property name=\"panel-1\" type=\"empty\">\n      <property name=\"position\" type=\"string\" value=\"p=8;x=640;y=704\"/>\n      <property name=\"length\" type=\"uint\" value=\"100\"/>\n      <property name=\"position-locked\" type=\"bool\" value=\"true\"/>\n      <property name=\"icon-size\" type=\"uint\" value=\"16\"/>\n      <property name=\"size\" type=\"uint\" value=\"30\"/>\n      <property name=\"plugin-ids\" type=\"array\">\n        <value type=\"int\" value=\"2\"/>\n        <value type=\"int\" value=\"3\"/>\n        <value type=\"int\" value=\"5\"/>\n        <value type=\"int\" value=\"6\"/>\n        <value type=\"int\" value=\"10\"/>\n        <value type=\"int\" value=\"11\"/>\n        <value type=\"int\" value=\"12\"/>\n        <value type=\"int\" value=\"13\"/>\n      </property>\n    </property>\n  </property>\n  <property name=\"plugins\" type=\"empty\">\n    <property name=\"plugin-2\" type=\"string\" value=\"tasklist\">\n      <property name=\"grouping\" type=\"uint\" value=\"1\"/>\n    </property>\n    <property name=\"plugin-3\" type=\"string\" value=\"separator\">\n      <property name=\"expand\" type=\"bool\" value=\"true\"/>\n      <property name=\"style\" type=\"uint\" value=\"0\"/>\n    </property>\n    <property name=\"plugin-5\" type=\"string\" value=\"separator\">\n      <property name=\"style\" type=\"uint\" value=\"0\"/>\n    </property>\n    <property name=\"plugin-6\" type=\"string\" value=\"systray\">\n      <property name=\"square-icons\" type=\"bool\" value=\"true\"/>\n    </property>\n    <property name=\"plugin-10\" type=\"string\" value=\"notification-plugin\"/>\n    <property name=\"plugin-11\" type=\"string\" value=\"separator\">\n      <property name=\"style\" type=\"uint\" value=\"0\"/>\n    </property>\n    <property name=\"plugin-12\" type=\"string\" value=\"clock\"/>\n    <property name=\"plugin-13\" type=\"string\" value=\"separator\">\n      <property name=\"style\" type=\"uint\" value=\"0\"/>\n    </property>\n  </property>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<channel name=\"xfwm4\" version=\"1.0\">\n  <property name=\"general\" type=\"empty\">\n    <property name=\"activate_action\" type=\"string\" value=\"bring\"/>\n    <property name=\"borderless_maximize\" type=\"bool\" value=\"true\"/>\n    <property name=\"box_move\" type=\"bool\" value=\"false\"/>\n    <property name=\"box_resize\" type=\"bool\" value=\"false\"/>\n    <property name=\"button_layout\" type=\"string\" value=\"O|SHMC\"/>\n    <property name=\"button_offset\" type=\"int\" value=\"0\"/>\n    <property name=\"button_spacing\" type=\"int\" value=\"0\"/>\n    <property name=\"click_to_focus\" type=\"bool\" value=\"true\"/>\n    <property name=\"cycle_apps_only\" type=\"bool\" value=\"false\"/>\n    <property name=\"cycle_draw_frame\" type=\"bool\" value=\"true\"/>\n    <property name=\"cycle_raise\" type=\"bool\" value=\"false\"/>\n    <property name=\"cycle_hidden\" type=\"bool\" value=\"true\"/>\n    <property name=\"cycle_minimum\" type=\"bool\" value=\"true\"/>\n    <property name=\"cycle_minimized\" type=\"bool\" value=\"false\"/>\n    <property name=\"cycle_preview\" type=\"bool\" value=\"true\"/>\n    <property name=\"cycle_tabwin_mode\" type=\"int\" value=\"0\"/>\n    <property name=\"cycle_workspaces\" type=\"bool\" value=\"false\"/>\n    <property name=\"double_click_action\" type=\"string\" value=\"maximize\"/>\n    <property name=\"double_click_distance\" type=\"int\" value=\"5\"/>\n    <property name=\"double_click_time\" type=\"int\" value=\"250\"/>\n    <property name=\"easy_click\" type=\"string\" value=\"Alt\"/>\n    <property name=\"focus_delay\" type=\"int\" value=\"250\"/>\n    <property name=\"focus_hint\" type=\"bool\" value=\"true\"/>\n    <property name=\"focus_new\" type=\"bool\" value=\"true\"/>\n    <property name=\"frame_opacity\" type=\"int\" value=\"100\"/>\n    <property name=\"frame_border_top\" type=\"int\" value=\"0\"/>\n    <property name=\"full_width_title\" type=\"bool\" value=\"true\"/>\n    <property name=\"horiz_scroll_opacity\" type=\"bool\" value=\"false\"/>\n    <property name=\"inactive_opacity\" type=\"int\" value=\"100\"/>\n    <property name=\"maximized_offset\" type=\"int\" value=\"0\"/>\n    <property name=\"mousewheel_rollup\" type=\"bool\" value=\"true\"/>\n    <property name=\"move_opacity\" type=\"int\" value=\"100\"/>\n    <property name=\"placement_mode\" type=\"string\" value=\"center\"/>\n    <property name=\"placement_ratio\" type=\"int\" value=\"20\"/>\n    <property name=\"popup_opacity\" type=\"int\" value=\"100\"/>\n    <property name=\"prevent_focus_stealing\" type=\"bool\" value=\"false\"/>\n    <property name=\"raise_delay\" type=\"int\" value=\"250\"/>\n    <property name=\"raise_on_click\" type=\"bool\" value=\"true\"/>\n    <property name=\"raise_on_focus\" type=\"bool\" value=\"false\"/>\n    <property name=\"raise_with_any_button\" type=\"bool\" value=\"true\"/>\n    <property name=\"repeat_urgent_blink\" type=\"bool\" value=\"false\"/>\n    <property name=\"resize_opacity\" type=\"int\" value=\"100\"/>\n    <property name=\"scroll_workspaces\" type=\"bool\" value=\"true\"/>\n    <property name=\"shadow_delta_height\" type=\"int\" value=\"0\"/>\n    <property name=\"shadow_delta_width\" type=\"int\" value=\"0\"/>\n    <property name=\"shadow_delta_x\" type=\"int\" value=\"0\"/>\n    <property name=\"shadow_delta_y\" type=\"int\" value=\"-3\"/>\n    <property name=\"shadow_opacity\" type=\"int\" value=\"50\"/>\n    <property name=\"show_app_icon\" type=\"bool\" value=\"false\"/>\n    <property name=\"show_dock_shadow\" type=\"bool\" value=\"true\"/>\n    <property name=\"show_frame_shadow\" type=\"bool\" value=\"true\"/>\n    <property name=\"show_popup_shadow\" type=\"bool\" value=\"false\"/>\n    <property name=\"snap_resist\" type=\"bool\" value=\"false\"/>\n    <property name=\"snap_to_border\" type=\"bool\" value=\"true\"/>\n    <property name=\"snap_to_windows\" type=\"bool\" value=\"false\"/>\n    <property name=\"snap_width\" type=\"int\" value=\"10\"/>\n    <property name=\"vblank_mode\" type=\"string\" value=\"auto\"/>\n    <property name=\"theme\" type=\"string\" value=\"Default\"/>\n    <property name=\"tile_on_move\" type=\"bool\" value=\"true\"/>\n    <property name=\"title_alignment\" type=\"string\" value=\"center\"/>\n    <property name=\"title_font\" type=\"string\" value=\"Sans Bold 9\"/>\n    <property name=\"title_horizontal_offset\" type=\"int\" value=\"0\"/>\n    <property name=\"titleless_maximize\" type=\"bool\" value=\"false\"/>\n    <property name=\"title_shadow_active\" type=\"string\" value=\"false\"/>\n    <property name=\"title_shadow_inactive\" type=\"string\" value=\"false\"/>\n    <property name=\"title_vertical_offset_active\" type=\"int\" value=\"0\"/>\n    <property name=\"title_vertical_offset_inactive\" type=\"int\" value=\"0\"/>\n    <property name=\"toggle_workspaces\" type=\"bool\" value=\"false\"/>\n    <property name=\"unredirect_overlays\" type=\"bool\" value=\"true\"/>\n    <property name=\"urgent_blink\" type=\"bool\" value=\"false\"/>\n    <property name=\"use_compositing\" type=\"bool\" value=\"true\"/>\n    <property name=\"workspace_count\" type=\"int\" value=\"4\"/>\n    <property name=\"wrap_cycle\" type=\"bool\" value=\"true\"/>\n    <property name=\"wrap_layout\" type=\"bool\" value=\"true\"/>\n    <property name=\"wrap_resistance\" type=\"int\" value=\"10\"/>\n    <property name=\"wrap_windows\" type=\"bool\" value=\"true\"/>\n    <property name=\"wrap_workspaces\" type=\"bool\" value=\"false\"/>\n    <property name=\"zoom_desktop\" type=\"bool\" value=\"true\"/>\n    <property name=\"zoom_pointer\" type=\"bool\" value=\"true\"/>\n    <property name=\"workspace_names\" type=\"array\">\n      <value type=\"string\" value=\"Workspace 1\"/>\n      <value type=\"string\" value=\"Workspace 2\"/>\n      <value type=\"string\" value=\"Workspace 3\"/>\n      <value type=\"string\" value=\"Workspace 4\"/>\n    </property>\n  </property>\n</channel>\n"
  },
  {
    "path": "packages/bytebotd/root/home/user/.xsessionrc",
    "content": "exec startxfce4"
  },
  {
    "path": "packages/bytebotd/root/usr/share/applications/1password.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=1Password\nComment=Password manager and secure wallet\nExec=/usr/bin/1password %U\nIcon=1password\nTerminal=false\nStartupNotify=true\nCategories=Utility;Security;\nMimeType=x-scheme-handler/onepassword;"
  },
  {
    "path": "packages/bytebotd/root/usr/share/applications/code.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=VSCode\nComment=Visual Studio Code\nExec=/usr/bin/code --password-store=basic %F\nIcon=vscode\nCategories=TextEditor;Development;IDE;\nMimeType=application/x-code-workspace;\nStartupWMClass=code"
  },
  {
    "path": "packages/bytebotd/root/usr/share/applications/firefox.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Firefox Web Browser\nComment=Browse the web with Firefox\nExec=/usr/bin/firefox-esr %U\nIcon=firefox-esr\nPath=\nTerminal=false\nStartupNotify=true\nCategories=Network;WebBrowser;"
  },
  {
    "path": "packages/bytebotd/root/usr/share/applications/terminal.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Terminal Emulator\nComment=Open a terminal\nExec=exo-open --launch TerminalEmulator\nIcon=org.xfce.terminalemulator\nPath=\nTerminal=false\nStartupNotify=true\nCategories=Utility;TerminalEmulator;\n"
  },
  {
    "path": "packages/bytebotd/root/usr/share/applications/thunderbird.desktop",
    "content": "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Thunderbird Mail\nComment=Read and send emails with Thunderbird\nExec=/usr/bin/thunderbird %U\nIcon=thunderbird\nPath=\nTerminal=false\nStartupNotify=true\nCategories=Network;Email;\nMimeType=x-scheme-handler/mailto;application/x-xpinstall;message/rfc822;"
  },
  {
    "path": "packages/bytebotd/src/app.controller.ts",
    "content": "import { Controller, Get, Redirect, Headers } from '@nestjs/common';\nimport { AppService } from './app.service';\n\n@Controller()\nexport class AppController {\n  constructor(private readonly appService: AppService) {}\n\n  // When a client makes a GET request to /vnc,\n  // this method will automatically redirect them to the noVNC URL.\n  @Get('vnc')\n  // Leave the decorator empty but keep the status code.\n  @Redirect(undefined, 302)\n  redirectToVnc(@Headers('host') host: string) {\n    return {\n      url: `/novnc/vnc.html?host=${host}&path=websockify&resize=scale`,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/app.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ComputerUseModule } from './computer-use/computer-use.module';\nimport { InputTrackingModule } from './input-tracking/input-tracking.module';\nimport { ConfigModule } from '@nestjs/config';\nimport { ServeStaticModule } from '@nestjs/serve-static';\nimport { AppController } from './app.controller';\nimport { AppService } from './app.service';\nimport { BytebotMcpModule } from './mcp';\n\n@Module({\n  imports: [\n    ConfigModule.forRoot({\n      isGlobal: true, // Explicitly makes it globally available\n    }),\n    ServeStaticModule.forRoot({\n      rootPath: '/opt/noVNC',\n      serveRoot: '/novnc',\n    }),\n    ComputerUseModule,\n    InputTrackingModule,\n    BytebotMcpModule,\n  ],\n  controllers: [AppController],\n  providers: [AppService],\n})\nexport class AppModule {}\n"
  },
  {
    "path": "packages/bytebotd/src/app.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class AppService {\n  getHello(): string {\n    return 'Hello World!';\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/computer-use/computer-use.controller.ts",
    "content": "import {\n  Controller,\n  Post,\n  Body,\n  Logger,\n  HttpException,\n  HttpStatus,\n} from '@nestjs/common';\nimport { ComputerUseService } from './computer-use.service';\nimport { ComputerActionValidationPipe } from './dto/computer-action-validation.pipe';\nimport { ComputerActionDto } from './dto/computer-action.dto';\n\n@Controller('computer-use')\nexport class ComputerUseController {\n  private readonly logger = new Logger(ComputerUseController.name);\n\n  constructor(private readonly computerUseService: ComputerUseService) {}\n\n  @Post()\n  async action(\n    @Body(new ComputerActionValidationPipe()) params: ComputerActionDto,\n  ) {\n    try {\n      // don't log base64 data\n      const paramsCopy = { ...params };\n      if (paramsCopy.action === 'write_file') {\n        paramsCopy.data = 'base64 data';\n      }\n      this.logger.log(`Computer action request: ${JSON.stringify(paramsCopy)}`);\n      return await this.computerUseService.action(params);\n    } catch (error) {\n      this.logger.error(\n        `Error executing computer action: ${error.message}`,\n        error.stack,\n      );\n      throw new HttpException(\n        `Failed to execute computer action: ${error.message}`,\n        HttpStatus.INTERNAL_SERVER_ERROR,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/computer-use/computer-use.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ComputerUseService } from './computer-use.service';\nimport { ComputerUseController } from './computer-use.controller';\nimport { NutModule } from '../nut/nut.module';\n\n@Module({\n  imports: [NutModule],\n  controllers: [ComputerUseController],\n  providers: [ComputerUseService],\n  exports: [ComputerUseService],\n})\nexport class ComputerUseModule {}\n"
  },
  {
    "path": "packages/bytebotd/src/computer-use/computer-use.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { exec, spawn } from 'child_process';\nimport { promisify } from 'util';\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { NutService } from '../nut/nut.service';\nimport {\n  ComputerAction,\n  MoveMouseAction,\n  TraceMouseAction,\n  ClickMouseAction,\n  PressMouseAction,\n  DragMouseAction,\n  ScrollAction,\n  TypeKeysAction,\n  PressKeysAction,\n  TypeTextAction,\n  ApplicationAction,\n  Application,\n  PasteTextAction,\n  WriteFileAction,\n  ReadFileAction,\n} from '@bytebot/shared';\n\n@Injectable()\nexport class ComputerUseService {\n  private readonly logger = new Logger(ComputerUseService.name);\n\n  constructor(private readonly nutService: NutService) {}\n\n  async action(params: ComputerAction): Promise<any> {\n    this.logger.log(`Executing computer action: ${params.action}`);\n\n    switch (params.action) {\n      case 'move_mouse': {\n        await this.moveMouse(params);\n        break;\n      }\n      case 'trace_mouse': {\n        await this.traceMouse(params);\n        break;\n      }\n      case 'click_mouse': {\n        await this.clickMouse(params);\n        break;\n      }\n      case 'press_mouse': {\n        await this.pressMouse(params);\n        break;\n      }\n      case 'drag_mouse': {\n        await this.dragMouse(params);\n        break;\n      }\n\n      case 'scroll': {\n        await this.scroll(params);\n        break;\n      }\n      case 'type_keys': {\n        await this.typeKeys(params);\n        break;\n      }\n      case 'press_keys': {\n        await this.pressKeys(params);\n        break;\n      }\n      case 'type_text': {\n        await this.typeText(params);\n        break;\n      }\n      case 'paste_text': {\n        await this.pasteText(params);\n        break;\n      }\n      case 'wait': {\n        const waitParams = params;\n        await this.delay(waitParams.duration);\n        break;\n      }\n      case 'screenshot':\n        return this.screenshot();\n\n      case 'cursor_position':\n        return this.cursor_position();\n\n      case 'application': {\n        await this.application(params);\n        break;\n      }\n\n      case 'write_file': {\n        return this.writeFile(params);\n      }\n\n      case 'read_file': {\n        return this.readFile(params);\n      }\n\n      default:\n        throw new Error(\n          `Unsupported computer action: ${(params as any).action}`,\n        );\n    }\n  }\n\n  private async moveMouse(action: MoveMouseAction): Promise<void> {\n    await this.nutService.mouseMoveEvent(action.coordinates);\n  }\n\n  private async traceMouse(action: TraceMouseAction): Promise<void> {\n    const { path, holdKeys } = action;\n\n    // Move to the first coordinate\n    await this.nutService.mouseMoveEvent(path[0]);\n\n    // Hold keys if provided\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, true);\n    }\n\n    // Move to each coordinate in the path\n    for (const coordinates of path) {\n      await this.nutService.mouseMoveEvent(coordinates);\n    }\n\n    // Release hold keys\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, false);\n    }\n  }\n\n  private async clickMouse(action: ClickMouseAction): Promise<void> {\n    const { coordinates, button, holdKeys, clickCount } = action;\n\n    // Move to coordinates if provided\n    if (coordinates) {\n      await this.nutService.mouseMoveEvent(coordinates);\n    }\n\n    // Hold keys if provided\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, true);\n    }\n\n    // Perform clicks\n    if (clickCount > 1) {\n      // Perform multiple clicks\n      for (let i = 0; i < clickCount; i++) {\n        await this.nutService.mouseClickEvent(button);\n        await this.delay(150);\n      }\n    } else {\n      // Perform a single click\n      await this.nutService.mouseClickEvent(button);\n    }\n\n    // Release hold keys\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, false);\n    }\n  }\n\n  private async pressMouse(action: PressMouseAction): Promise<void> {\n    const { coordinates, button, press } = action;\n\n    // Move to coordinates if provided\n    if (coordinates) {\n      await this.nutService.mouseMoveEvent(coordinates);\n    }\n\n    // Perform press\n    if (press === 'down') {\n      await this.nutService.mouseButtonEvent(button, true);\n    } else {\n      await this.nutService.mouseButtonEvent(button, false);\n    }\n  }\n\n  private async dragMouse(action: DragMouseAction): Promise<void> {\n    const { path, button, holdKeys } = action;\n\n    // Move to the first coordinate\n    await this.nutService.mouseMoveEvent(path[0]);\n\n    // Hold keys if provided\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, true);\n    }\n\n    // Perform drag\n    await this.nutService.mouseButtonEvent(button, true);\n    for (const coordinates of path) {\n      await this.nutService.mouseMoveEvent(coordinates);\n    }\n    await this.nutService.mouseButtonEvent(button, false);\n\n    // Release hold keys\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, false);\n    }\n  }\n\n  private async scroll(action: ScrollAction): Promise<void> {\n    const { coordinates, direction, scrollCount, holdKeys } = action;\n\n    // Move to coordinates if provided\n    if (coordinates) {\n      await this.nutService.mouseMoveEvent(coordinates);\n    }\n\n    // Hold keys if provided\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, true);\n    }\n\n    // Perform scroll\n    for (let i = 0; i < scrollCount; i++) {\n      await this.nutService.mouseWheelEvent(direction, 1);\n      await new Promise((resolve) => setTimeout(resolve, 150));\n    }\n\n    // Release hold keys\n    if (holdKeys) {\n      await this.nutService.holdKeys(holdKeys, false);\n    }\n  }\n\n  private async typeKeys(action: TypeKeysAction): Promise<void> {\n    const { keys, delay } = action;\n    await this.nutService.sendKeys(keys, delay);\n  }\n\n  private async pressKeys(action: PressKeysAction): Promise<void> {\n    const { keys, press } = action;\n    await this.nutService.holdKeys(keys, press === 'down');\n  }\n\n  private async typeText(action: TypeTextAction): Promise<void> {\n    const { text, delay } = action;\n    await this.nutService.typeText(text, delay);\n  }\n\n  private async pasteText(action: PasteTextAction): Promise<void> {\n    const { text } = action;\n    await this.nutService.pasteText(text);\n  }\n\n  private async delay(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  async screenshot(): Promise<{ image: string }> {\n    this.logger.log(`Taking screenshot`);\n    const buffer = await this.nutService.screendump();\n    return { image: `${buffer.toString('base64')}` };\n  }\n\n  private async cursor_position(): Promise<{ x: number; y: number }> {\n    this.logger.log(`Getting cursor position`);\n    return await this.nutService.getCursorPosition();\n  }\n\n  private async application(action: ApplicationAction): Promise<void> {\n    const execAsync = promisify(exec);\n\n    // Helper to spawn a command and forget about it\n    const spawnAndForget = (\n      command: string,\n      args: string[],\n      options: Record<string, any> = {},\n    ): void => {\n      const child = spawn(command, args, {\n        env: { ...process.env, DISPLAY: ':0.0' }, // ensure DISPLAY is set for GUI tools\n        stdio: 'ignore',\n        detached: true,\n        ...options,\n      });\n      child.unref(); // Allow the parent process to exit independently\n    };\n\n    if (action.application === 'desktop') {\n      spawnAndForget('sudo', ['-u', 'user', 'wmctrl', '-k', 'on']);\n      return;\n    }\n\n    const commandMap: Record<string, string> = {\n      firefox: 'firefox-esr',\n      '1password': '1password',\n      thunderbird: 'thunderbird',\n      vscode: 'code',\n      terminal: 'xfce4-terminal',\n      directory: 'thunar',\n    };\n\n    const processMap: Record<Application, string> = {\n      firefox: 'Navigator.firefox-esr',\n      '1password': '1password.1Password',\n      thunderbird: 'Mail.thunderbird',\n      vscode: 'code.Code',\n      terminal: 'xfce4-terminal.Xfce4-Terminal',\n      directory: 'Thunar',\n      desktop: 'xfdesktop.Xfdesktop',\n    };\n\n    // check if the application is already open using wmctrl -lx\n    let appOpen = false;\n    try {\n      const { stdout } = await execAsync(\n        `sudo -u user wmctrl -lx | grep ${processMap[action.application]}`,\n        { timeout: 5000 }, // 5 second timeout\n      );\n      appOpen = stdout.trim().length > 0;\n    } catch (error: any) {\n      // grep returns exit code 1 when no match is found – treat as \"not open\"\n      // Also handle timeout errors\n      if (error.code !== 1 && !error.message?.includes('timeout')) {\n        throw error;\n      }\n    }\n\n    if (appOpen) {\n      this.logger.log(`Application ${action.application} is already open`);\n\n      // Fire and forget - activate window\n      spawnAndForget('sudo', [\n        '-u',\n        'user',\n        'wmctrl',\n        '-x',\n        '-a',\n        processMap[action.application],\n      ]);\n\n      // Fire and forget - maximize window\n      spawnAndForget('sudo', [\n        '-u',\n        'user',\n        'wmctrl',\n        '-x',\n        '-r',\n        processMap[action.application],\n        '-b',\n        'add,maximized_vert,maximized_horz',\n      ]);\n\n      return;\n    }\n\n    // application is not open, open it - fire and forget\n    spawnAndForget('sudo', [\n      '-u',\n      'user',\n      'nohup',\n      commandMap[action.application],\n    ]);\n\n    this.logger.log(`Application ${action.application} launched`);\n\n    // Just return immediately\n    return;\n  }\n\n  private async writeFile(\n    action: WriteFileAction,\n  ): Promise<{ success: boolean; message: string }> {\n    try {\n      const execAsync = promisify(exec);\n\n      // Decode base64 data\n      const buffer = Buffer.from(action.data, 'base64');\n\n      // Resolve path - if relative, make it relative to user's home directory\n      let targetPath = action.path;\n      if (!path.isAbsolute(targetPath)) {\n        targetPath = path.join('/home/user/Desktop', targetPath);\n      }\n\n      // Ensure directory exists using sudo\n      const dir = path.dirname(targetPath);\n      try {\n        await execAsync(`sudo mkdir -p \"${dir}\"`);\n      } catch (error) {\n        // Directory might already exist, which is fine\n        this.logger.debug(`Directory creation: ${error.message}`);\n      }\n\n      // Write to a temporary file first\n      const tempFile = `/tmp/bytebot_temp_${Date.now()}_${Math.random().toString(36).substring(7)}`;\n      await fs.writeFile(tempFile, buffer);\n\n      // Move the file to the target location using sudo\n      try {\n        await execAsync(`sudo cp \"${tempFile}\" \"${targetPath}\"`);\n        await execAsync(`sudo chown user:user \"${targetPath}\"`);\n        await execAsync(`sudo chmod 644 \"${targetPath}\"`);\n        // Clean up temp file\n        await fs.unlink(tempFile).catch(() => {});\n      } catch (error) {\n        // Clean up temp file on error\n        await fs.unlink(tempFile).catch(() => {});\n        throw error;\n      }\n\n      this.logger.log(`File written successfully to: ${targetPath}`);\n      return {\n        success: true,\n        message: `File written successfully to: ${targetPath}`,\n      };\n    } catch (error) {\n      this.logger.error(`Error writing file: ${error.message}`, error.stack);\n      return {\n        success: false,\n        message: `Error writing file: ${error.message}`,\n      };\n    }\n  }\n\n  private async readFile(action: ReadFileAction): Promise<{\n    success: boolean;\n    data?: string;\n    name?: string;\n    size?: number;\n    mediaType?: string;\n    message?: string;\n  }> {\n    try {\n      const execAsync = promisify(exec);\n\n      // Resolve path - if relative, make it relative to user's home directory\n      let targetPath = action.path;\n      if (!path.isAbsolute(targetPath)) {\n        targetPath = path.join('/home/user/Desktop', targetPath);\n      }\n\n      // Copy file to temp location using sudo to read it\n      const tempFile = `/tmp/bytebot_read_${Date.now()}_${Math.random().toString(36).substring(7)}`;\n\n      try {\n        // Copy the file to a temporary location we can read\n        await execAsync(`sudo cp \"${targetPath}\" \"${tempFile}\"`);\n        await execAsync(`sudo chmod 644 \"${tempFile}\"`);\n\n        // Read file as buffer from temp location\n        const buffer = await fs.readFile(tempFile);\n\n        // Get file stats for size using sudo\n        const { stdout: statOutput } = await execAsync(\n          `sudo stat -c \"%s\" \"${targetPath}\"`,\n        );\n        const fileSize = parseInt(statOutput.trim(), 10);\n\n        // Clean up temp file\n        await fs.unlink(tempFile).catch(() => {});\n\n        // Convert to base64\n        const base64Data = buffer.toString('base64');\n\n        // Extract filename from path\n        const fileName = path.basename(targetPath);\n\n        // Determine media type based on file extension\n        const ext = path.extname(targetPath).toLowerCase().slice(1);\n        const mimeTypes: Record<string, string> = {\n          pdf: 'application/pdf',\n          docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n          doc: 'application/msword',\n          txt: 'text/plain',\n          html: 'text/html',\n          json: 'application/json',\n          xml: 'text/xml',\n          csv: 'text/csv',\n          rtf: 'application/rtf',\n          odt: 'application/vnd.oasis.opendocument.text',\n          epub: 'application/epub+zip',\n          png: 'image/png',\n          jpg: 'image/jpeg',\n          jpeg: 'image/jpeg',\n          webp: 'image/webp',\n          gif: 'image/gif',\n          svg: 'image/svg+xml',\n        };\n\n        const mediaType = mimeTypes[ext] || 'application/octet-stream';\n\n        this.logger.log(`File read successfully from: ${targetPath}`);\n        return {\n          success: true,\n          data: base64Data,\n          name: fileName,\n          size: fileSize,\n          mediaType: mediaType,\n        };\n      } catch (error) {\n        // Clean up temp file on error\n        await fs.unlink(tempFile).catch(() => {});\n        throw error;\n      }\n    } catch (error) {\n      this.logger.error(`Error reading file: ${error.message}`, error.stack);\n      return {\n        success: false,\n        message: `Error reading file: ${error.message}`,\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/computer-use/dto/base.dto.ts",
    "content": "import { IsNumber } from 'class-validator';\n\nexport class CoordinatesDto {\n  @IsNumber()\n  x: number;\n\n  @IsNumber()\n  y: number;\n}\n\nexport enum ButtonType {\n  LEFT = 'left',\n  RIGHT = 'right',\n  MIDDLE = 'middle',\n}\n\nexport enum PressType {\n  UP = 'up',\n  DOWN = 'down',\n}\n\nexport enum ScrollDirection {\n  UP = 'up',\n  DOWN = 'down',\n  LEFT = 'left',\n  RIGHT = 'right',\n}\n\nexport enum ApplicationName {\n  FIREFOX = 'firefox',\n  ONEPASSWORD = '1password',\n  THUNDERBIRD = 'thunderbird',\n  VSCODE = 'vscode',\n  TERMINAL = 'terminal',\n  DESKTOP = 'desktop',\n  DIRECTORY = 'directory',\n}\n"
  },
  {
    "path": "packages/bytebotd/src/computer-use/dto/computer-action-validation.pipe.ts",
    "content": "import {\n  PipeTransform,\n  Injectable,\n  ArgumentMetadata,\n  BadRequestException,\n} from '@nestjs/common';\nimport { validate } from 'class-validator';\nimport { plainToClass } from 'class-transformer';\nimport {\n  MoveMouseActionDto,\n  TraceMouseActionDto,\n  ClickMouseActionDto,\n  PressMouseActionDto,\n  DragMouseActionDto,\n  ScrollActionDto,\n  TypeKeysActionDto,\n  PressKeysActionDto,\n  TypeTextActionDto,\n  PasteTextActionDto,\n  WaitActionDto,\n  ScreenshotActionDto,\n  CursorPositionActionDto,\n  ApplicationActionDto,\n  WriteFileActionDto,\n  ReadFileActionDto,\n} from './computer-action.dto';\n\n@Injectable()\nexport class ComputerActionValidationPipe implements PipeTransform {\n  async transform(value: any, metadata: ArgumentMetadata) {\n    if (!value || !value.action) {\n      throw new BadRequestException('Missing action field');\n    }\n\n    let dto;\n    switch (value.action) {\n      case 'move_mouse':\n        dto = plainToClass(MoveMouseActionDto, value);\n        break;\n      case 'trace_mouse':\n        dto = plainToClass(TraceMouseActionDto, value);\n        break;\n      case 'click_mouse':\n        dto = plainToClass(ClickMouseActionDto, value);\n        break;\n      case 'press_mouse':\n        dto = plainToClass(PressMouseActionDto, value);\n        break;\n      case 'drag_mouse':\n        dto = plainToClass(DragMouseActionDto, value);\n        break;\n      case 'scroll':\n        dto = plainToClass(ScrollActionDto, value);\n        break;\n      case 'type_keys':\n        dto = plainToClass(TypeKeysActionDto, value);\n        break;\n      case 'press_keys':\n        dto = plainToClass(PressKeysActionDto, value);\n        break;\n      case 'type_text':\n        dto = plainToClass(TypeTextActionDto, value);\n        break;\n      case 'paste_text':\n        dto = plainToClass(PasteTextActionDto, value);\n        break;\n      case 'wait':\n        dto = plainToClass(WaitActionDto, value);\n        break;\n      case 'screenshot':\n        dto = plainToClass(ScreenshotActionDto, value);\n        break;\n      case 'cursor_position':\n        dto = plainToClass(CursorPositionActionDto, value);\n        break;\n      case 'application':\n        dto = plainToClass(ApplicationActionDto, value);\n        break;\n      case 'write_file':\n        dto = plainToClass(WriteFileActionDto, value);\n        break;\n      case 'read_file':\n        dto = plainToClass(ReadFileActionDto, value);\n        break;\n      default:\n        throw new BadRequestException(`Unknown action: ${value.action}`);\n    }\n\n    const errors = await validate(dto);\n    if (errors.length > 0) {\n      throw new BadRequestException(errors);\n    }\n\n    return dto;\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/computer-use/dto/computer-action.dto.ts",
    "content": "import {\n  IsEnum,\n  IsNumber,\n  IsOptional,\n  IsString,\n  ValidateNested,\n  IsArray,\n  Min,\n  IsIn,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport {\n  ButtonType,\n  CoordinatesDto,\n  PressType,\n  ScrollDirection,\n  ApplicationName,\n} from './base.dto';\n\n/**\n * Base class for action DTOs with common validation decorator\n */\nabstract class BaseActionDto {\n  abstract action: string;\n}\n\nexport class MoveMouseActionDto extends BaseActionDto {\n  @IsIn(['move_mouse'])\n  action: 'move_mouse';\n\n  @ValidateNested()\n  @Type(() => CoordinatesDto)\n  coordinates: CoordinatesDto;\n}\n\nexport class TraceMouseActionDto extends BaseActionDto {\n  @IsIn(['trace_mouse'])\n  action: 'trace_mouse';\n\n  @IsArray()\n  @ValidateNested({ each: true })\n  @Type(() => CoordinatesDto)\n  path: CoordinatesDto[];\n\n  @IsOptional()\n  @IsArray()\n  @IsString({ each: true })\n  holdKeys?: string[];\n}\n\nexport class ClickMouseActionDto extends BaseActionDto {\n  @IsIn(['click_mouse'])\n  action: 'click_mouse';\n\n  @IsOptional()\n  @ValidateNested()\n  @Type(() => CoordinatesDto)\n  coordinates?: CoordinatesDto;\n\n  @IsEnum(ButtonType)\n  button: ButtonType;\n\n  @IsOptional()\n  @IsArray()\n  @IsString({ each: true })\n  holdKeys?: string[];\n\n  @IsNumber()\n  @Min(1)\n  clickCount: number;\n}\n\nexport class PressMouseActionDto extends BaseActionDto {\n  @IsIn(['press_mouse'])\n  action: 'press_mouse';\n\n  @IsOptional()\n  @ValidateNested()\n  @Type(() => CoordinatesDto)\n  coordinates?: CoordinatesDto;\n\n  @IsEnum(ButtonType)\n  button: ButtonType;\n\n  @IsEnum(PressType)\n  press: PressType;\n}\n\nexport class DragMouseActionDto extends BaseActionDto {\n  @IsIn(['drag_mouse'])\n  action: 'drag_mouse';\n\n  @IsArray()\n  @ValidateNested({ each: true })\n  @Type(() => CoordinatesDto)\n  path: CoordinatesDto[];\n\n  @IsEnum(ButtonType)\n  button: ButtonType;\n\n  @IsOptional()\n  @IsArray()\n  @IsString({ each: true })\n  holdKeys?: string[];\n}\n\nexport class ScrollActionDto extends BaseActionDto {\n  @IsIn(['scroll'])\n  action: 'scroll';\n\n  @IsOptional()\n  @ValidateNested()\n  @Type(() => CoordinatesDto)\n  coordinates?: CoordinatesDto;\n\n  @IsEnum(ScrollDirection)\n  direction: ScrollDirection;\n\n  @IsNumber()\n  @Min(1)\n  scrollCount: number;\n\n  @IsOptional()\n  @IsArray()\n  @IsString({ each: true })\n  holdKeys?: string[];\n}\n\nexport class TypeKeysActionDto extends BaseActionDto {\n  @IsIn(['type_keys'])\n  action: 'type_keys';\n\n  @IsArray()\n  @IsString({ each: true })\n  keys: string[];\n\n  @IsOptional()\n  @IsNumber()\n  @Min(0)\n  delay?: number;\n}\n\nexport class PressKeysActionDto extends BaseActionDto {\n  @IsIn(['press_keys'])\n  action: 'press_keys';\n\n  @IsArray()\n  @IsString({ each: true })\n  keys: string[];\n\n  @IsEnum(PressType)\n  press: PressType;\n}\n\nexport class TypeTextActionDto extends BaseActionDto {\n  @IsIn(['type_text'])\n  action: 'type_text';\n\n  @IsString()\n  text: string;\n\n  @IsOptional()\n  @IsNumber()\n  @Min(0)\n  delay?: number;\n}\n\nexport class PasteTextActionDto extends BaseActionDto {\n  @IsIn(['paste_text'])\n  action: 'paste_text';\n\n  @IsString()\n  text: string;\n}\n\nexport class WaitActionDto extends BaseActionDto {\n  @IsIn(['wait'])\n  action: 'wait';\n\n  @IsNumber()\n  @Min(0)\n  duration: number;\n}\n\nexport class ScreenshotActionDto extends BaseActionDto {\n  @IsIn(['screenshot'])\n  action: 'screenshot';\n}\n\nexport class CursorPositionActionDto extends BaseActionDto {\n  @IsIn(['cursor_position'])\n  action: 'cursor_position';\n}\n\nexport class ApplicationActionDto extends BaseActionDto {\n  @IsIn(['application'])\n  action: 'application';\n\n  @IsEnum(ApplicationName)\n  application: ApplicationName;\n}\n\nexport class WriteFileActionDto extends BaseActionDto {\n  @IsIn(['write_file'])\n  action: 'write_file';\n\n  @IsString()\n  path: string;\n\n  @IsString()\n  data: string; // Base64 encoded data\n}\n\nexport class ReadFileActionDto extends BaseActionDto {\n  @IsIn(['read_file'])\n  action: 'read_file';\n\n  @IsString()\n  path: string;\n}\n\n// Union type for all computer actions\nexport type ComputerActionDto =\n  | MoveMouseActionDto\n  | TraceMouseActionDto\n  | ClickMouseActionDto\n  | PressMouseActionDto\n  | DragMouseActionDto\n  | ScrollActionDto\n  | TypeKeysActionDto\n  | PressKeysActionDto\n  | TypeTextActionDto\n  | PasteTextActionDto\n  | WaitActionDto\n  | ScreenshotActionDto\n  | CursorPositionActionDto\n  | ApplicationActionDto\n  | WriteFileActionDto\n  | ReadFileActionDto;\n"
  },
  {
    "path": "packages/bytebotd/src/input-tracking/input-tracking.controller.ts",
    "content": "import { Controller, Post } from '@nestjs/common';\nimport { InputTrackingService } from './input-tracking.service';\n\n@Controller('input-tracking')\nexport class InputTrackingController {\n  constructor(private readonly inputTrackingService: InputTrackingService) {}\n\n  @Post('start')\n  start() {\n    this.inputTrackingService.startTracking();\n    return { status: 'started' };\n  }\n\n  @Post('stop')\n  stop() {\n    this.inputTrackingService.stopTracking();\n    return { status: 'stopped' };\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/input-tracking/input-tracking.gateway.ts",
    "content": "import {\n  WebSocketGateway,\n  WebSocketServer,\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n} from '@nestjs/websockets';\nimport { Server, Socket } from 'socket.io';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { ComputerAction } from '@bytebot/shared';\n\n@Injectable()\n@WebSocketGateway({\n  cors: {\n    origin: '*',\n    methods: ['GET', 'POST'],\n  },\n})\nexport class InputTrackingGateway\n  implements OnGatewayConnection, OnGatewayDisconnect\n{\n  private readonly logger = new Logger(InputTrackingGateway.name);\n\n  @WebSocketServer()\n  server: Server;\n\n  handleConnection(client: Socket) {\n    this.logger.log(`Client connected: ${client.id}`);\n  }\n\n  handleDisconnect(client: Socket) {\n    this.logger.log(`Client disconnected: ${client.id}`);\n  }\n\n  emitAction(action: ComputerAction) {\n    this.server.emit('action', action);\n  }\n\n  emitScreenshotAndAction(\n    screenshot: { image: string },\n    action: ComputerAction,\n  ) {\n    this.server.emit('screenshotAndAction', screenshot, action);\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/input-tracking/input-tracking.helpers.ts",
    "content": "import { UiohookKey } from 'uiohook-napi';\n\nexport type KeyInfo = {\n  name: string;\n  isPrintable: boolean;\n  string?: string;\n  shiftString?: string;\n};\n\nexport const keyInfoMap: Record<number, KeyInfo> = {\n  [UiohookKey.Backspace]: {\n    name: 'Backspace',\n    isPrintable: false,\n  },\n  [UiohookKey.Tab]: {\n    name: 'Tab',\n    isPrintable: false,\n  },\n  [UiohookKey.Enter]: {\n    name: 'Enter',\n    isPrintable: false,\n  },\n  [UiohookKey.CapsLock]: {\n    name: 'CapsLock',\n    isPrintable: false,\n  },\n  [UiohookKey.Escape]: {\n    name: 'Escape',\n    isPrintable: false,\n  },\n  [UiohookKey.Space]: {\n    name: 'Space',\n    isPrintable: true,\n    string: ' ',\n    shiftString: ' ',\n  },\n  [UiohookKey.PageUp]: {\n    name: 'PageUp',\n    isPrintable: false,\n  },\n  [UiohookKey.PageDown]: {\n    name: 'PageDown',\n    isPrintable: false,\n  },\n  [UiohookKey.End]: {\n    name: 'End',\n    isPrintable: false,\n  },\n  [UiohookKey.Home]: {\n    name: 'Home',\n    isPrintable: false,\n  },\n  [UiohookKey.ArrowLeft]: {\n    name: 'Left',\n    isPrintable: false,\n  },\n  [UiohookKey.ArrowUp]: {\n    name: 'Up',\n    isPrintable: false,\n  },\n  [UiohookKey.ArrowRight]: {\n    name: 'Right',\n    isPrintable: false,\n  },\n  [UiohookKey.ArrowDown]: {\n    name: 'Down',\n    isPrintable: false,\n  },\n  [UiohookKey.Insert]: {\n    name: 'Insert',\n    isPrintable: false,\n  },\n  [UiohookKey.Delete]: {\n    name: 'Delete',\n    isPrintable: false,\n  },\n\n  [UiohookKey.Numpad0]: {\n    name: 'Numpad0',\n    isPrintable: true,\n    string: '0',\n    shiftString: '0',\n  },\n  [UiohookKey.Numpad1]: {\n    name: 'Numpad1',\n    isPrintable: true,\n    string: '1',\n    shiftString: '1',\n  },\n  [UiohookKey.Numpad2]: {\n    name: 'Numpad2',\n    isPrintable: true,\n    string: '2',\n    shiftString: '2',\n  },\n  [UiohookKey.Numpad3]: {\n    name: 'Numpad3',\n    isPrintable: true,\n    string: '3',\n    shiftString: '3',\n  },\n  [UiohookKey.Numpad4]: {\n    name: 'Numpad4',\n    isPrintable: true,\n    string: '4',\n    shiftString: '4',\n  },\n  [UiohookKey.Numpad5]: {\n    name: 'Numpad5',\n    isPrintable: true,\n    string: '5',\n    shiftString: '5',\n  },\n  [UiohookKey.Numpad6]: {\n    name: 'Numpad6',\n    isPrintable: true,\n    string: '6',\n    shiftString: '6',\n  },\n  [UiohookKey.Numpad7]: {\n    name: 'Numpad7',\n    isPrintable: true,\n    string: '7',\n    shiftString: '7',\n  },\n  [UiohookKey.Numpad8]: {\n    name: 'Numpad8',\n    isPrintable: true,\n    string: '8',\n    shiftString: '8',\n  },\n  [UiohookKey.Numpad9]: {\n    name: 'Numpad9',\n    isPrintable: true,\n    string: '9',\n    shiftString: '9',\n  },\n\n  [UiohookKey.NumpadMultiply]: {\n    name: 'Multiply',\n    isPrintable: true,\n    string: '*',\n    shiftString: '*',\n  },\n  [UiohookKey.NumpadAdd]: {\n    name: 'Add',\n    isPrintable: true,\n    string: '+',\n    shiftString: '+',\n  },\n  [UiohookKey.NumpadSubtract]: {\n    name: 'Subtract',\n    isPrintable: true,\n    string: '-',\n    shiftString: '-',\n  },\n  [UiohookKey.NumpadDivide]: {\n    name: 'Divide',\n    isPrintable: true,\n    string: '/',\n    shiftString: '/',\n  },\n  [UiohookKey.NumpadDecimal]: {\n    name: 'Decimal',\n    isPrintable: true,\n    string: '.',\n    shiftString: '.',\n  },\n  [UiohookKey.NumpadEnter]: {\n    name: 'Enter',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadEnd]: {\n    name: 'End',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadArrowDown]: {\n    name: 'Down',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadArrowLeft]: {\n    name: 'Left',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadArrowRight]: {\n    name: 'Right',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadArrowUp]: {\n    name: 'Up',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadPageDown]: {\n    name: 'PageDown',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadPageUp]: {\n    name: 'PageUp',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadInsert]: {\n    name: 'Insert',\n    isPrintable: false,\n  },\n  [UiohookKey.NumpadDelete]: {\n    name: 'Delete',\n    isPrintable: false,\n  },\n  [UiohookKey.F1]: {\n    name: 'F1',\n    isPrintable: false,\n  },\n  [UiohookKey.F2]: {\n    name: 'F2',\n    isPrintable: false,\n  },\n  [UiohookKey.F3]: {\n    name: 'F3',\n    isPrintable: false,\n  },\n  [UiohookKey.F4]: {\n    name: 'F4',\n    isPrintable: false,\n  },\n  [UiohookKey.F5]: {\n    name: 'F5',\n    isPrintable: false,\n  },\n  [UiohookKey.F6]: {\n    name: 'F6',\n    isPrintable: false,\n  },\n  [UiohookKey.F7]: {\n    name: 'F7',\n    isPrintable: false,\n  },\n  [UiohookKey.F8]: {\n    name: 'F8',\n    isPrintable: false,\n  },\n  [UiohookKey.F9]: {\n    name: 'F9',\n    isPrintable: false,\n  },\n  [UiohookKey.F10]: {\n    name: 'F10',\n    isPrintable: false,\n  },\n  [UiohookKey.F11]: {\n    name: 'F11',\n    isPrintable: false,\n  },\n  [UiohookKey.F12]: {\n    name: 'F12',\n    isPrintable: false,\n  },\n  [UiohookKey.F13]: {\n    name: 'F13',\n    isPrintable: false,\n  },\n  [UiohookKey.F14]: {\n    name: 'F14',\n    isPrintable: false,\n  },\n  [UiohookKey.F15]: {\n    name: 'F15',\n    isPrintable: false,\n  },\n  [UiohookKey.F16]: {\n    name: 'F16',\n    isPrintable: false,\n  },\n  [UiohookKey.F17]: {\n    name: 'F17',\n    isPrintable: false,\n  },\n  [UiohookKey.F18]: {\n    name: 'F18',\n    isPrintable: false,\n  },\n  [UiohookKey.F19]: {\n    name: 'F19',\n    isPrintable: false,\n  },\n  [UiohookKey.F20]: {\n    name: 'F20',\n    isPrintable: false,\n  },\n  [UiohookKey.F21]: {\n    name: 'F21',\n    isPrintable: false,\n  },\n  [UiohookKey.F22]: {\n    name: 'F22',\n    isPrintable: false,\n  },\n  [UiohookKey.F23]: {\n    name: 'F23',\n    isPrintable: false,\n  },\n  [UiohookKey.F24]: {\n    name: 'F24',\n    isPrintable: false,\n  },\n  [UiohookKey.Semicolon]: {\n    name: 'Semicolon',\n    isPrintable: true,\n    string: ';',\n    shiftString: ':',\n  },\n  [UiohookKey.Equal]: {\n    name: 'Equal',\n    isPrintable: true,\n    string: '=',\n    shiftString: '+',\n  },\n  [UiohookKey.Comma]: {\n    name: 'Comma',\n    isPrintable: true,\n    string: ',',\n    shiftString: '\"',\n  },\n  [UiohookKey.Minus]: {\n    name: 'Minus',\n    isPrintable: true,\n    string: '-',\n    shiftString: '_',\n  },\n  [UiohookKey.Period]: {\n    name: 'Period',\n    isPrintable: true,\n    string: '.',\n    shiftString: '>',\n  },\n  [UiohookKey.Slash]: {\n    name: 'Slash',\n    isPrintable: true,\n    string: '/',\n    shiftString: '?',\n  },\n  [UiohookKey.Backquote]: {\n    name: 'Grave',\n    isPrintable: true,\n    string: '`',\n    shiftString: '~',\n  },\n  [UiohookKey.BracketLeft]: {\n    name: 'LeftBracket',\n    isPrintable: true,\n    string: '[',\n    shiftString: '{',\n  },\n  [UiohookKey.BracketRight]: {\n    name: 'RightBracket',\n    isPrintable: true,\n    string: ']',\n    shiftString: '}',\n  },\n  [UiohookKey.Backslash]: {\n    name: 'Backslash',\n    isPrintable: true,\n    string: '\\\\',\n    shiftString: '|',\n  },\n  [UiohookKey.Quote]: {\n    name: 'Quote',\n    isPrintable: true,\n    string: \"'\",\n    shiftString: '\"',\n  },\n  [UiohookKey.Ctrl]: {\n    name: 'LeftControl',\n    isPrintable: false,\n  },\n  [UiohookKey.CtrlRight]: {\n    name: 'RightControl',\n    isPrintable: false,\n  },\n  [UiohookKey.Shift]: {\n    name: 'LeftShift',\n    isPrintable: false,\n  },\n  [UiohookKey.ShiftRight]: {\n    name: 'RightShift',\n    isPrintable: false,\n  },\n  [UiohookKey.Alt]: {\n    name: 'LeftAlt',\n    isPrintable: false,\n  },\n  [UiohookKey.AltRight]: {\n    name: 'RightAlt',\n    isPrintable: false,\n  },\n  [UiohookKey.Meta]: {\n    name: 'LeftMeta',\n    isPrintable: false,\n  },\n  [UiohookKey.MetaRight]: {\n    name: 'RightMeta',\n    isPrintable: false,\n  },\n  [UiohookKey.NumLock]: {\n    name: 'NumLock',\n    isPrintable: false,\n  },\n  [UiohookKey.ScrollLock]: {\n    name: 'ScrollLock',\n    isPrintable: false,\n  },\n  [UiohookKey.PrintScreen]: {\n    name: 'Print',\n    isPrintable: false,\n  },\n\n  [UiohookKey.A]: {\n    name: 'A',\n    isPrintable: true,\n    string: 'a',\n    shiftString: 'A',\n  },\n  [UiohookKey.B]: {\n    name: 'B',\n    isPrintable: true,\n    string: 'b',\n    shiftString: 'B',\n  },\n  [UiohookKey.C]: {\n    name: 'C',\n    isPrintable: true,\n    string: 'c',\n    shiftString: 'C',\n  },\n  [UiohookKey.D]: {\n    name: 'D',\n    isPrintable: true,\n    string: 'd',\n    shiftString: 'D',\n  },\n  [UiohookKey.E]: {\n    name: 'E',\n    isPrintable: true,\n    string: 'e',\n    shiftString: 'E',\n  },\n  [UiohookKey.F]: {\n    name: 'F',\n    isPrintable: true,\n    string: 'f',\n    shiftString: 'F',\n  },\n  [UiohookKey.G]: {\n    name: 'G',\n    isPrintable: true,\n    string: 'g',\n    shiftString: 'G',\n  },\n  [UiohookKey.H]: {\n    name: 'H',\n    isPrintable: true,\n    string: 'h',\n    shiftString: 'H',\n  },\n  [UiohookKey.I]: {\n    name: 'I',\n    isPrintable: true,\n    string: 'i',\n    shiftString: 'I',\n  },\n  [UiohookKey.J]: {\n    name: 'J',\n    isPrintable: true,\n    string: 'j',\n    shiftString: 'J',\n  },\n  [UiohookKey.K]: {\n    name: 'K',\n    isPrintable: true,\n    string: 'k',\n    shiftString: 'K',\n  },\n  [UiohookKey.L]: {\n    name: 'L',\n    isPrintable: true,\n    string: 'l',\n    shiftString: 'L',\n  },\n  [UiohookKey.M]: {\n    name: 'M',\n    isPrintable: true,\n    string: 'm',\n    shiftString: 'M',\n  },\n  [UiohookKey.N]: {\n    name: 'N',\n    isPrintable: true,\n    string: 'n',\n    shiftString: 'N',\n  },\n  [UiohookKey.O]: {\n    name: 'O',\n    isPrintable: true,\n    string: 'o',\n    shiftString: 'O',\n  },\n  [UiohookKey.P]: {\n    name: 'P',\n    isPrintable: true,\n    string: 'p',\n    shiftString: 'P',\n  },\n  [UiohookKey.Q]: {\n    name: 'Q',\n    isPrintable: true,\n    string: 'q',\n    shiftString: 'Q',\n  },\n  [UiohookKey.R]: {\n    name: 'R',\n    isPrintable: true,\n    string: 'r',\n    shiftString: 'R',\n  },\n  [UiohookKey.S]: {\n    name: 'S',\n    isPrintable: true,\n    string: 's',\n    shiftString: 'S',\n  },\n  [UiohookKey.T]: {\n    name: 'T',\n    isPrintable: true,\n    string: 't',\n    shiftString: 'T',\n  },\n  [UiohookKey.U]: {\n    name: 'U',\n    isPrintable: true,\n    string: 'u',\n    shiftString: 'U',\n  },\n  [UiohookKey.V]: {\n    name: 'V',\n    isPrintable: true,\n    string: 'v',\n    shiftString: 'V',\n  },\n  [UiohookKey.W]: {\n    name: 'W',\n    isPrintable: true,\n    string: 'w',\n    shiftString: 'W',\n  },\n  [UiohookKey.X]: {\n    name: 'X',\n    isPrintable: true,\n    string: 'x',\n    shiftString: 'X',\n  },\n  [UiohookKey.Y]: {\n    name: 'Y',\n    isPrintable: true,\n    string: 'y',\n    shiftString: 'Y',\n  },\n  [UiohookKey.Z]: {\n    name: 'Z',\n    isPrintable: true,\n    string: 'z',\n    shiftString: 'Z',\n  },\n\n  [UiohookKey[0]]: {\n    name: '0',\n    isPrintable: true,\n    string: '0',\n    shiftString: ')',\n  },\n  [UiohookKey[1]]: {\n    name: '1',\n    isPrintable: true,\n    string: '1',\n    shiftString: '!',\n  },\n  [UiohookKey[2]]: {\n    name: '2',\n    isPrintable: true,\n    string: '2',\n    shiftString: '@',\n  },\n  [UiohookKey[3]]: {\n    name: '3',\n    isPrintable: true,\n    string: '3',\n    shiftString: '#',\n  },\n  [UiohookKey[4]]: {\n    name: '4',\n    isPrintable: true,\n    string: '4',\n    shiftString: '$',\n  },\n  [UiohookKey[5]]: {\n    name: '5',\n    isPrintable: true,\n    string: '5',\n    shiftString: '%',\n  },\n  [UiohookKey[6]]: {\n    name: '6',\n    isPrintable: true,\n    string: '6',\n    shiftString: '^',\n  },\n  [UiohookKey[7]]: {\n    name: '7',\n    isPrintable: true,\n    string: '7',\n    shiftString: '&',\n  },\n  [UiohookKey[8]]: {\n    name: '8',\n    isPrintable: true,\n    string: '8',\n    shiftString: '*',\n  },\n  [UiohookKey[9]]: {\n    name: '9',\n    isPrintable: true,\n    string: '9',\n    shiftString: '(',\n  },\n\n  [133]: {\n    name: 'LeftSuper',\n    isPrintable: false,\n  },\n  [134]: {\n    name: 'RightSuper',\n    isPrintable: false,\n  },\n  [0]: {\n    name: 'Alt',\n    isPrintable: false,\n  },\n};\n"
  },
  {
    "path": "packages/bytebotd/src/input-tracking/input-tracking.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { InputTrackingService } from './input-tracking.service';\nimport { InputTrackingController } from './input-tracking.controller';\nimport { InputTrackingGateway } from './input-tracking.gateway';\nimport { ComputerUseModule } from '../computer-use/computer-use.module';\n\n@Module({\n  imports: [ComputerUseModule],\n  controllers: [InputTrackingController],\n  providers: [InputTrackingService, InputTrackingGateway],\n  exports: [InputTrackingService, InputTrackingGateway],\n})\nexport class InputTrackingModule {}\n"
  },
  {
    "path": "packages/bytebotd/src/input-tracking/input-tracking.service.ts",
    "content": "import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';\nimport {\n  uIOhook,\n  UiohookKeyboardEvent,\n  UiohookMouseEvent,\n  UiohookWheelEvent,\n  WheelDirection,\n} from 'uiohook-napi';\nimport {\n  Button,\n  ClickMouseAction,\n  ComputerAction,\n  DragMouseAction,\n  ScrollAction,\n  TypeKeysAction,\n  TypeTextAction,\n} from '@bytebot/shared';\nimport { ComputerUseService } from '../computer-use/computer-use.service';\nimport { InputTrackingGateway } from './input-tracking.gateway';\nimport { keyInfoMap } from './input-tracking.helpers';\n\n@Injectable()\nexport class InputTrackingService implements OnModuleDestroy {\n  private readonly logger = new Logger(InputTrackingService.name);\n\n  private isTracking = false;\n\n  private isDragging = false;\n  private dragMouseAction: DragMouseAction | null = null;\n\n  private scrollAction: ScrollAction | null = null;\n  private scrollCount = 0;\n\n  private clickMouseActionBuffer: ClickMouseAction[] = [];\n  private clickMouseActionTimeout: NodeJS.Timeout | null = null;\n  private readonly CLICK_DEBOUNCE_MS = 250;\n\n  private screenshot: { image: string } | null = null;\n  private screenshotTimeout: NodeJS.Timeout | null = null;\n  private readonly SCREENSHOT_DEBOUNCE_MS = 250;\n\n  private readonly pressedKeys = new Set<number>(); // suppress repeats\n  private readonly typingBuffer: string[] = []; // pending chars\n  private typingTimer: NodeJS.Timeout | null = null; // debounce\n  private readonly TYPING_DEBOUNCE_MS = 500;\n\n  constructor(\n    private readonly gateway: InputTrackingGateway,\n    private readonly computerUseService: ComputerUseService,\n  ) {}\n\n  // Tracking is started manually via startTracking\n\n  onModuleDestroy() {\n    this.stopTracking();\n  }\n\n  startTracking() {\n    if (this.isTracking) {\n      return;\n    }\n    this.logger.log('Starting input tracking');\n    this.registerListeners();\n    uIOhook.start();\n    this.isTracking = true;\n  }\n\n  stopTracking() {\n    if (!this.isTracking) {\n      return;\n    }\n    this.logger.log('Stopping input tracking');\n    uIOhook.stop();\n    uIOhook.removeAllListeners();\n    this.isTracking = false;\n  }\n\n  /** Adds a printable char to buffer and restarts debounce timer. */\n  private bufferChar(char: string) {\n    this.typingBuffer.push(char);\n    if (this.typingTimer) clearTimeout(this.typingTimer);\n    this.typingTimer = setTimeout(\n      () => this.flushTypingBuffer(),\n      this.TYPING_DEBOUNCE_MS,\n    );\n  }\n  /** Convert buffered chars → action, then clear buffer. */\n  private async flushTypingBuffer() {\n    if (!this.typingBuffer.length) return;\n    const action: TypeTextAction = {\n      action: 'type_text',\n      text: this.typingBuffer.join(''),\n    };\n    this.typingBuffer.length = 0;\n    await this.logAction(action);\n  }\n\n  private isModifierKey(key: UiohookKeyboardEvent) {\n    return key.altKey || key.ctrlKey || key.metaKey;\n  }\n\n  private registerListeners() {\n    uIOhook.on('mousemove', (e: UiohookMouseEvent) => {\n      if (this.isDragging && this.dragMouseAction) {\n        this.dragMouseAction.path.push({ x: e.x, y: e.y });\n      } else {\n        if (this.screenshotTimeout) {\n          clearTimeout(this.screenshotTimeout);\n        }\n        this.screenshotTimeout = setTimeout(async () => {\n          this.screenshot = await this.computerUseService.screenshot();\n        }, this.SCREENSHOT_DEBOUNCE_MS);\n      }\n    });\n\n    uIOhook.on('click', (e: UiohookMouseEvent) => {\n      const action: ClickMouseAction = {\n        action: 'click_mouse',\n        button: this.mapButton(e.button),\n        coordinates: { x: e.x, y: e.y },\n        clickCount: e.clicks,\n        holdKeys: [\n          e.altKey ? 'alt' : undefined,\n          e.ctrlKey ? 'ctrl' : undefined,\n          e.shiftKey ? 'shift' : undefined,\n          e.metaKey ? 'meta' : undefined,\n        ].filter((key) => key !== undefined),\n      };\n      this.clickMouseActionBuffer.push(action);\n      if (this.clickMouseActionTimeout) {\n        clearTimeout(this.clickMouseActionTimeout);\n      }\n\n      this.clickMouseActionTimeout = setTimeout(async () => {\n        // pick the event with the largest clickCount in the burst\n        const final = this.clickMouseActionBuffer.reduce((a, b) =>\n          b.clickCount > a.clickCount ? b : a,\n        );\n        await this.logAction(final); // emit exactly once\n\n        this.clickMouseActionTimeout = null;\n        this.clickMouseActionBuffer = [];\n      }, this.CLICK_DEBOUNCE_MS);\n    });\n\n    uIOhook.on('mousedown', (e: UiohookMouseEvent) => {\n      this.isDragging = true;\n      this.dragMouseAction = {\n        action: 'drag_mouse',\n        button: this.mapButton(e.button),\n        path: [{ x: e.x, y: e.y }],\n        holdKeys: [\n          e.altKey ? 'alt' : undefined,\n          e.ctrlKey ? 'ctrl' : undefined,\n          e.shiftKey ? 'shift' : undefined,\n          e.metaKey ? 'meta' : undefined,\n        ].filter((key) => key !== undefined),\n      };\n    });\n\n    uIOhook.on('mouseup', async (e: UiohookMouseEvent) => {\n      if (this.isDragging && this.dragMouseAction) {\n        this.dragMouseAction.path.push({ x: e.x, y: e.y });\n        if (this.dragMouseAction.path.length > 3) {\n          await this.logAction(this.dragMouseAction);\n        }\n        this.dragMouseAction = null;\n      }\n      this.isDragging = false;\n    });\n\n    uIOhook.on('wheel', async (e: UiohookWheelEvent) => {\n      const direction =\n        e.direction === WheelDirection.VERTICAL\n          ? e.rotation > 0\n            ? 'down'\n            : 'up'\n          : e.rotation > 0\n            ? 'right'\n            : 'left';\n      const action: ScrollAction = {\n        action: 'scroll',\n        direction: direction as any,\n        scrollCount: 1,\n        coordinates: { x: e.x, y: e.y },\n      };\n\n      if (\n        this.scrollAction &&\n        action.direction === this.scrollAction.direction\n      ) {\n        this.scrollCount++;\n        if (this.scrollCount >= 4) {\n          await this.logAction(this.scrollAction);\n          this.scrollAction = null;\n          this.scrollCount = 0;\n        }\n      } else {\n        this.scrollAction = action;\n        this.scrollCount = 1;\n      }\n    });\n\n    uIOhook.on('keydown', async (e: UiohookKeyboardEvent) => {\n      if (!keyInfoMap[e.keycode]) {\n        this.logger.warn(`Unknown key: ${e.keycode}`);\n        return;\n      }\n\n      /* Printable char with no active modifier → buffer for TypeTextAction. */\n      if (!this.isModifierKey(e) && keyInfoMap[e.keycode].isPrintable) {\n        this.bufferChar(\n          e.shiftKey\n            ? keyInfoMap[e.keycode].shiftString!\n            : keyInfoMap[e.keycode].string!,\n        );\n        return;\n      }\n\n      /* Anything with modifiers _or_ a non‑printable key: \n      first flush buffered text so ordering is preserved. */\n      await this.flushTypingBuffer();\n\n      /* Ignore auto‑repeat for pressed keys. */\n      if (this.pressedKeys.has(e.keycode)) {\n        return;\n      }\n      this.pressedKeys.add(e.keycode);\n    });\n\n    uIOhook.on('keyup', async (e: UiohookKeyboardEvent) => {\n      if (!keyInfoMap[e.keycode]) {\n        this.logger.warn(`Unknown key: ${e.keycode}`);\n        return;\n      }\n      /* If key belongs to typing buffer we don't emit anything on keyup. *\n       * (Up‑event is irrelevant for a pure “typed character”.) */\n      if (!this.isModifierKey(e) && keyInfoMap[e.keycode].isPrintable) {\n        return;\n      }\n\n      await this.flushTypingBuffer();\n\n      if (this.pressedKeys.size === 0) {\n        return;\n      }\n\n      const action: TypeKeysAction = {\n        action: 'type_keys',\n        keys: [\n          // take the pressed keys and map them to their names\n          ...Array.from(this.pressedKeys.values()).map(\n            (key) => keyInfoMap[key].name,\n          ),\n        ].filter((key) => key !== undefined),\n      };\n\n      this.pressedKeys.clear();\n      await this.logAction(action);\n    });\n  }\n\n  private mapButton(btn: unknown): Button {\n    switch (btn) {\n      case 1:\n        return 'left';\n      case 2:\n        return 'right';\n      case 3:\n        return 'middle';\n      default:\n        return 'left';\n    }\n  }\n\n  private async logAction(action: ComputerAction) {\n    this.logger.log(`Detected action: ${JSON.stringify(action)}`);\n\n    if (\n      this.screenshot &&\n      (action.action === 'click_mouse' || action.action === 'drag_mouse')\n    ) {\n      this.gateway.emitScreenshotAndAction(this.screenshot, action);\n      return;\n    }\n\n    this.gateway.emitAction(action);\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/main.ts",
    "content": "import { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\nimport { createProxyMiddleware } from 'http-proxy-middleware';\nimport * as express from 'express';\nimport { json, urlencoded } from 'express';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  // Configure body parser with increased payload size limit (50MB)\n  app.use(json({ limit: '50mb' }));\n  app.use(urlencoded({ limit: '50mb', extended: true }));\n\n  // Enable CORS\n  app.enableCors({\n    origin: '*',\n    methods: ['GET', 'POST'],\n    credentials: true,\n  });\n\n  const wsProxy = createProxyMiddleware({\n    target: 'http://localhost:6080',\n    ws: true,\n    changeOrigin: true,\n    pathRewrite: { '^/websockify': '/' },\n  });\n  app.use('/websockify', express.raw({ type: '*/*' }), wsProxy);\n  const server = await app.listen(9990);\n\n  // Selective upgrade routing\n  server.on('upgrade', (req, socket, head) => {\n    if (req.url?.startsWith('/websockify')) {\n      wsProxy.upgrade(req, socket, head);\n    }\n    // else let Socket.IO/Nest handle it by not hijacking the socket\n  });\n}\nbootstrap();\n"
  },
  {
    "path": "packages/bytebotd/src/mcp/bytebot-mcp.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { McpModule } from '@rekog/mcp-nest';\nimport { ComputerUseModule } from '../computer-use/computer-use.module';\nimport { ComputerUseTools } from './computer-use.tools';\n\n@Module({\n  imports: [\n    ComputerUseModule,\n    McpModule.forRoot({\n      name: 'bytebotd',\n      version: '0.0.1',\n      sseEndpoint: '/mcp',\n    }),\n  ],\n  providers: [ComputerUseTools],\n})\nexport class BytebotMcpModule {}\n"
  },
  {
    "path": "packages/bytebotd/src/mcp/compressor.ts",
    "content": "import * as sharp from 'sharp';\n\ninterface CompressionOptions {\n  targetSizeKB?: number;\n  initialQuality?: number;\n  minQuality?: number;\n  format?: 'png' | 'jpeg' | 'webp';\n  maxIterations?: number;\n}\n\ninterface CompressionResult {\n  base64: string;\n  sizeBytes: number;\n  sizeKB: number;\n  sizeMB: number;\n  quality: number;\n  format: string;\n  iterations: number;\n}\n\nclass Base64ImageCompressor {\n  /**\n   * Compress a base64 PNG string to under specified size (default 1MB)\n   */\n  static async compressToSize(\n    base64String: string,\n    options: CompressionOptions = {},\n  ): Promise<CompressionResult> {\n    const {\n      targetSizeKB = 1024, // 1MB default\n      initialQuality = 95,\n      minQuality = 10,\n      format = 'png',\n      maxIterations = 10,\n    } = options;\n\n    // Extract base64 data (remove data URL prefix if present)\n    const base64Data = base64String.replace(/^data:image\\/\\w+;base64,/, '');\n    const inputBuffer = Buffer.from(base64Data, 'base64');\n\n    let quality = initialQuality;\n    let outputBuffer: Buffer;\n    let iterations = 0;\n\n    // Binary search for optimal quality\n    let low = minQuality;\n    let high = initialQuality;\n    let bestResult: { buffer: Buffer; quality: number } | null = null;\n\n    while (low <= high && iterations < maxIterations) {\n      quality = Math.floor((low + high) / 2);\n\n      outputBuffer = await this.compressBuffer(inputBuffer, quality, format);\n      const sizeKB = outputBuffer.length / 1024;\n\n      if (sizeKB <= targetSizeKB) {\n        // Size is acceptable, try higher quality\n        bestResult = { buffer: outputBuffer, quality };\n        low = quality + 1;\n      } else {\n        // Size too large, reduce quality\n        high = quality - 1;\n      }\n\n      iterations++;\n    }\n\n    // If no result found under target size, use lowest quality\n    if (!bestResult) {\n      outputBuffer = await this.compressBuffer(inputBuffer, minQuality, format);\n      quality = minQuality;\n    } else {\n      outputBuffer = bestResult.buffer;\n      quality = bestResult.quality;\n    }\n\n    // Convert back to base64\n    const outputBase64 = outputBuffer.toString('base64');\n    const sizeBytes = outputBuffer.length;\n\n    return {\n      base64: outputBase64,\n      sizeBytes,\n      sizeKB: sizeBytes / 1024,\n      sizeMB: sizeBytes / (1024 * 1024),\n      quality,\n      format,\n      iterations,\n    };\n  }\n\n  /**\n   * Compress buffer with specified quality\n   */\n  private static async compressBuffer(\n    inputBuffer: Buffer,\n    quality: number,\n    format: 'png' | 'jpeg' | 'webp',\n  ): Promise<Buffer> {\n    const sharpInstance = sharp(inputBuffer);\n\n    switch (format) {\n      case 'png':\n        return sharpInstance\n          .png({\n            quality,\n            compressionLevel: 9,\n            adaptiveFiltering: true,\n            palette: true,\n          })\n          .toBuffer();\n\n      case 'jpeg':\n        return sharpInstance\n          .jpeg({\n            quality,\n            progressive: true,\n            mozjpeg: true,\n            optimizeScans: true,\n          })\n          .toBuffer();\n\n      case 'webp':\n        return sharpInstance\n          .webp({\n            quality,\n            alphaQuality: quality,\n            lossless: false,\n            nearLossless: false,\n            smartSubsample: true,\n          })\n          .toBuffer();\n\n      default:\n        throw new Error(`Unsupported format: ${format}`);\n    }\n  }\n\n  /**\n   * Compress with dimension reduction if quality alone isn't enough\n   */\n  static async compressWithResize(\n    base64String: string,\n    options: CompressionOptions & {\n      maxWidth?: number;\n      maxHeight?: number;\n    } = {},\n  ): Promise<CompressionResult> {\n    const {\n      targetSizeKB = 1024,\n      maxWidth = 2048,\n      maxHeight = 2048,\n      ...compressionOptions\n    } = options;\n\n    // First try compression without resizing\n    let result = await this.compressToSize(base64String, compressionOptions);\n\n    // If still too large, apply progressive resizing\n    if (result.sizeKB > targetSizeKB) {\n      const base64Data = base64String.replace(/^data:image\\/\\w+;base64,/, '');\n      const inputBuffer = Buffer.from(base64Data, 'base64');\n\n      const metadata = await sharp(inputBuffer).metadata();\n      const originalWidth = metadata.width || maxWidth;\n      const originalHeight = metadata.height || maxHeight;\n\n      let scale = 0.9; // Start with 90% of original size\n\n      while (result.sizeKB > targetSizeKB && scale > 0.3) {\n        const newWidth = Math.floor(originalWidth * scale);\n        const newHeight = Math.floor(originalHeight * scale);\n\n        const resizedBuffer = await sharp(inputBuffer)\n          .resize(newWidth, newHeight, {\n            fit: 'inside',\n            withoutEnlargement: true,\n          })\n          .toBuffer();\n\n        const resizedBase64 = resizedBuffer.toString('base64');\n\n        result = await this.compressToSize(resizedBase64, compressionOptions);\n        scale -= 0.1;\n      }\n    }\n\n    return result;\n  }\n\n  /**\n   * Get size information for a base64 string\n   */\n  static getBase64SizeInfo(base64String: string): {\n    bytes: number;\n    kb: number;\n    mb: number;\n    formatted: string;\n  } {\n    const base64Data = base64String.replace(/^data:image\\/\\w+;base64,/, '');\n    const bytes = Buffer.from(base64Data, 'base64').length;\n    const kb = bytes / 1024;\n    const mb = bytes / (1024 * 1024);\n\n    let formatted: string;\n    if (mb >= 1) {\n      formatted = `${mb.toFixed(2)} MB`;\n    } else if (kb >= 1) {\n      formatted = `${kb.toFixed(2)} KB`;\n    } else {\n      formatted = `${bytes} bytes`;\n    }\n\n    return { bytes, kb, mb, formatted };\n  }\n}\n\n// Utility function for quick compression\nexport async function compressPngBase64Under1MB(\n  base64String: string,\n): Promise<string> {\n  const result = await Base64ImageCompressor.compressToSize(base64String, {\n    targetSizeKB: 1024,\n    format: 'png',\n    initialQuality: 95,\n    minQuality: 10,\n  });\n\n  return result.base64;\n}\n\n// Export the class for more control\nexport { Base64ImageCompressor, CompressionOptions, CompressionResult };\n"
  },
  {
    "path": "packages/bytebotd/src/mcp/computer-use.tools.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Tool } from '@rekog/mcp-nest';\nimport { z } from 'zod';\nimport { ComputerUseService } from '../computer-use/computer-use.service';\nimport { compressPngBase64Under1MB } from './compressor';\n\n@Injectable()\nexport class ComputerUseTools {\n  constructor(private readonly computerUse: ComputerUseService) {}\n\n  @Tool({\n    name: 'computer_move_mouse',\n    description: 'Moves the mouse cursor to the specified coordinates.',\n    parameters: z.object({\n      coordinates: z.object({\n        x: z.number().describe('The x-coordinate to move the mouse to.'),\n        y: z.number().describe('The y-coordinate to move the mouse to.'),\n      }),\n    }),\n  })\n  async moveMouse({ coordinates }: { coordinates: { x: number; y: number } }) {\n    try {\n      await this.computerUse.action({ action: 'move_mouse', coordinates });\n      return { content: [{ type: 'text', text: 'mouse moved' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error moving mouse: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_trace_mouse',\n    description:\n      'Moves the mouse cursor along a specified path of coordinates.',\n    parameters: z.object({\n      path: z\n        .array(\n          z.object({\n            x: z.number().describe('The x-coordinate to move the mouse to.'),\n            y: z.number().describe('The y-coordinate to move the mouse to.'),\n          }),\n        )\n        .describe('An array of coordinate objects representing the path.'),\n      holdKeys: z\n        .array(z.string())\n        .optional()\n        .describe('Optional array of keys to hold during the trace.'),\n    }),\n  })\n  async traceMouse({\n    path,\n    holdKeys,\n  }: {\n    path: { x: number; y: number }[];\n    holdKeys?: string[];\n  }) {\n    try {\n      await this.computerUse.action({ action: 'trace_mouse', path, holdKeys });\n      return {\n        content: [{ type: 'text', text: 'mouse traced' }],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error tracing mouse: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_click_mouse',\n    description:\n      'Performs a mouse click at the specified coordinates or current position.',\n    parameters: z.object({\n      coordinates: z\n        .object({\n          x: z.number().describe('The x-coordinate to move the mouse to.'),\n          y: z.number().describe('The y-coordinate to move the mouse to.'),\n        })\n        .optional()\n        .describe(\n          'Optional coordinates for the click. If not provided, clicks at the current mouse position.',\n        ),\n      button: z\n        .enum(['left', 'right', 'middle'])\n        .describe('The mouse button to click.'),\n      holdKeys: z\n        .array(z.string())\n        .optional()\n        .describe('Optional array of keys to hold during the click.'),\n      clickCount: z\n        .number()\n        .describe('Number of clicks to perform (e.g., 2 for double-click).'),\n    }),\n  })\n  async clickMouse({\n    coordinates,\n    button,\n    holdKeys,\n    clickCount,\n  }: {\n    coordinates?: { x: number; y: number };\n    button: 'left' | 'right' | 'middle';\n    holdKeys?: string[];\n    clickCount: number;\n  }) {\n    try {\n      await this.computerUse.action({\n        action: 'click_mouse',\n        coordinates,\n        button,\n        holdKeys,\n        clickCount,\n      });\n      return {\n        content: [{ type: 'text', text: 'mouse clicked' }],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error clicking mouse: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_press_mouse',\n    description:\n      'Presses or releases a specified mouse button at the given coordinates or current position.',\n    parameters: z.object({\n      coordinates: z\n        .object({\n          x: z.number().describe('The x-coordinate for the mouse action.'),\n          y: z.number().describe('The y-coordinate for the mouse action.'),\n        })\n        .optional()\n        .describe(\n          'Optional coordinates for the mouse press/release. If not provided, uses the current mouse position.',\n        ),\n      button: z\n        .enum(['left', 'right', 'middle'])\n        .describe('The mouse button to press or release.'),\n      press: z\n        .enum(['down', 'up'])\n        .describe('The action to perform (press or release).'),\n    }),\n  })\n  async pressMouse({\n    coordinates,\n    button,\n    press,\n  }: {\n    coordinates?: { x: number; y: number };\n    button: 'left' | 'right' | 'middle';\n    press: 'down' | 'up';\n  }) {\n    try {\n      await this.computerUse.action({\n        action: 'press_mouse',\n        coordinates,\n        button,\n        press,\n      });\n      return {\n        content: [{ type: 'text', text: 'mouse pressed' }],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error pressing mouse: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_drag_mouse',\n    description:\n      'Drags the mouse from a starting point along a path while holding a specified button.',\n    parameters: z.object({\n      path: z\n        .array(\n          z.object({\n            x: z\n              .number()\n              .describe('The x-coordinate of a point in the drag path.'),\n            y: z\n              .number()\n              .describe('The y-coordinate of a point in the drag path.'),\n          }),\n        )\n        .describe(\n          'An array of coordinate objects representing the drag path. The first coordinate is the start point.',\n        ),\n      button: z\n        .enum(['left', 'right', 'middle'])\n        .describe('The mouse button to hold while dragging.'),\n      holdKeys: z\n        .array(z.string())\n        .optional()\n        .describe('Optional array of keys to hold during the drag.'),\n    }),\n  })\n  async dragMouse({\n    path,\n    button,\n    holdKeys,\n  }: {\n    path: { x: number; y: number }[];\n    button: 'left' | 'right' | 'middle';\n    holdKeys?: string[];\n  }) {\n    try {\n      await this.computerUse.action({\n        action: 'drag_mouse',\n        path,\n        button,\n        holdKeys,\n      });\n      return {\n        content: [{ type: 'text', text: 'mouse dragged' }],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error dragging mouse: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_scroll',\n    description: 'Scrolls the mouse wheel up, down, left, or right.',\n    parameters: z.object({\n      coordinates: z\n        .object({\n          x: z\n            .number()\n            .describe(\n              'The x-coordinate for the scroll action (if applicable).',\n            ),\n          y: z\n            .number()\n            .describe(\n              'The y-coordinate for the scroll action (if applicable).',\n            ),\n        })\n        .optional()\n        .describe(\n          'Coordinates for where the scroll should occur. Behavior might depend on the OS/application.',\n        ),\n      direction: z\n        .enum(['up', 'down', 'left', 'right'])\n        .describe('The direction to scroll the mouse wheel.'),\n      scrollCount: z\n        .number()\n        .describe('The number of times to scroll the mouse wheel.'),\n      holdKeys: z\n        .array(z.string())\n        .optional()\n        .describe('Optional array of keys to hold during the scroll.'),\n    }),\n  })\n  async scroll({\n    coordinates,\n    direction,\n    scrollCount,\n    holdKeys,\n  }: {\n    coordinates?: { x: number; y: number };\n    direction: 'up' | 'down' | 'left' | 'right';\n    scrollCount: number;\n    holdKeys?: string[];\n  }) {\n    try {\n      await this.computerUse.action({\n        action: 'scroll',\n        coordinates,\n        direction,\n        scrollCount,\n        holdKeys,\n      });\n      return { content: [{ type: 'text', text: 'scrolled' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error scrolling: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_type_keys',\n    description: `Simulates typing a sequence of keys, often used for shortcuts involving modifier keys (e.g., Ctrl+C). Presses and releases each key in order.\n    \n────────────────────────\nVALID KEYS\n────────────────────────\nA, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp,  \nB, Backslash, Backspace,  \nC, CapsLock, Clear, Comma,  \nD, Decimal, Delete, Divide, Down,  \nE, End, Enter, Equal, Escape, F,  \nF1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,  \nFn,  \nG, Grave,  \nH, Home,  \nI, Insert,  \nJ, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin,  \nM, Menu, Minus, Multiply,  \nN, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock,  \nNumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9,  \nO, P, PageDown, PageUp, Pause, Period, Print,  \nQ, Quote,  \nR, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin,  \nS, ScrollLock, Semicolon, Slash, Space, Subtract,  \nT, Tab,  \nU, Up,  \nV, W, X, Y, Z`,\n    parameters: z.object({\n      keys: z\n        .array(z.string())\n        .describe(\n          'An array of key names to type in sequence (e.g., [\"control\", \"c\"]).',\n        ),\n      delay: z\n        .number()\n        .optional()\n        .describe('Optional delay in milliseconds between key presses.'),\n    }),\n  })\n  async typeKeys({ keys, delay }: { keys: string[]; delay?: number }) {\n    try {\n      await this.computerUse.action({ action: 'type_keys', keys, delay });\n      return { content: [{ type: 'text', text: 'keys typed' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error typing keys: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_press_keys',\n    description: `Simulates pressing down or releasing specific keys. Useful for holding modifier keys.     \n────────────────────────\nVALID KEYS\n────────────────────────\nA, Add, AudioForward, AudioMute, AudioNext, AudioPause, AudioPlay, AudioPrev, AudioRandom, AudioRepeat, AudioRewind, AudioStop, AudioVolDown, AudioVolUp,  \nB, Backslash, Backspace,  \nC, CapsLock, Clear, Comma,  \nD, Decimal, Delete, Divide, Down,  \nE, End, Enter, Equal, Escape, F,  \nF1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,  \nFn,  \nG, Grave,  \nH, Home,  \nI, Insert,  \nJ, K, L, Left, LeftAlt, LeftBracket, LeftCmd, LeftControl, LeftShift, LeftSuper, LeftWin,  \nM, Menu, Minus, Multiply,  \nN, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock,  \nNumPad0, NumPad1, NumPad2, NumPad3, NumPad4, NumPad5, NumPad6, NumPad7, NumPad8, NumPad9,  \nO, P, PageDown, PageUp, Pause, Period, Print,  \nQ, Quote,  \nR, Return, Right, RightAlt, RightBracket, RightCmd, RightControl, RightShift, RightSuper, RightWin,  \nS, ScrollLock, Semicolon, Slash, Space, Subtract,  \nT, Tab,  \nU, Up,  \nV, W, X, Y, Z  \n      `,\n    parameters: z.object({\n      keys: z\n        .array(z.string())\n        .describe(\n          'An array of key names to press or release (e.g., [\"shift\"]).',\n        ),\n      press: z\n        .enum(['down', 'up'])\n        .describe('Whether to press the keys down or release them up.'),\n    }),\n  })\n  async pressKeys({ keys, press }: { keys: string[]; press: 'down' | 'up' }) {\n    try {\n      await this.computerUse.action({ action: 'press_keys', keys, press });\n      return { content: [{ type: 'text', text: 'keys pressed' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error pressing keys: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_type_text',\n    description:\n      'Types a string of text character by character. Use this tool for strings less than 25 characters, or passwords/sensitive form fields.',\n    parameters: z.object({\n      text: z.string().describe('The text string to type.'),\n      delay: z\n        .number()\n        .optional()\n        .describe('Optional delay in milliseconds between key presses.'),\n    }),\n  })\n  async typeText({ text, delay }: { text: string; delay?: number }) {\n    try {\n      await this.computerUse.action({ action: 'type_text', text, delay });\n      return { content: [{ type: 'text', text: 'text typed' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error typing text: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_paste_text',\n    description:\n      'Copies text to the clipboard and pastes it. Use this tool for typing long text strings or special characters not on the standard keyboard.',\n    parameters: z.object({\n      text: z.string().describe('The text string to paste.'),\n    }),\n  })\n  async pasteText({ text }: { text: string }) {\n    try {\n      await this.computerUse.action({ action: 'paste_text', text });\n      return { content: [{ type: 'text', text: 'text pasted' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error pasting text: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_wait',\n    description: 'Pauses execution for a specified duration.',\n    parameters: z.object({\n      duration: z\n        .number()\n        .default(500)\n        .describe('The duration to wait in milliseconds.'),\n    }),\n  })\n  async wait({ duration }: { duration: number }) {\n    try {\n      await this.computerUse.action({ action: 'wait', duration });\n      return { content: [{ type: 'text', text: 'waiting done' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error waiting: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_application',\n    description:\n      'Opens or switches to the specified application and maximizes it.',\n    parameters: z.object({\n      application: z.enum([\n        'firefox',\n        '1password',\n        'thunderbird',\n        'vscode',\n        'terminal',\n        'desktop',\n        'directory',\n      ]),\n    }),\n  })\n  async application({\n    application,\n  }: {\n    application:\n      | 'firefox'\n      | '1password'\n      | 'thunderbird'\n      | 'vscode'\n      | 'terminal'\n      | 'desktop'\n      | 'directory';\n  }) {\n    try {\n      await this.computerUse.action({ action: 'application', application });\n      return { content: [{ type: 'text', text: 'application opened' }] };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error opening application: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_screenshot',\n    description: 'Captures a screenshot of the current screen.',\n  })\n  async screenshot() {\n    try {\n      const shot = (await this.computerUse.action({\n        action: 'screenshot',\n      })) as { image: string };\n      return {\n        content: [\n          {\n            type: 'image',\n            data: await compressPngBase64Under1MB(shot.image),\n            mimeType: 'image/png',\n          },\n        ],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error taking screenshot: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_cursor_position',\n    description: 'Gets the current (x, y) coordinates of the mouse cursor.',\n  })\n  async cursorPosition() {\n    try {\n      const pos = (await this.computerUse.action({\n        action: 'cursor_position',\n      })) as { x: number; y: number };\n      return {\n        content: [\n          {\n            type: 'text',\n            text: JSON.stringify(pos),\n          },\n        ],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error getting cursor position: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_write_file',\n    description:\n      'Writes a file to the specified path with base64 encoded data.',\n    parameters: z.object({\n      path: z\n        .string()\n        .describe('The file path where the file should be written.'),\n      data: z.string().describe('Base64 encoded file data to write.'),\n    }),\n  })\n  async writeFile({ path, data }: { path: string; data: string }) {\n    try {\n      const result = await this.computerUse.action({\n        action: 'write_file',\n        path,\n        data,\n      });\n      return {\n        content: [\n          {\n            type: 'text',\n            text: result.message || 'File written successfully',\n          },\n        ],\n      };\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error writing file: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n\n  @Tool({\n    name: 'computer_read_file',\n    description:\n      'Reads a file from the specified path and returns it as a document content block with base64 encoded data.',\n    parameters: z.object({\n      path: z.string().describe('The file path to read from.'),\n    }),\n  })\n  async readFile({ path }: { path: string }) {\n    try {\n      const result = await this.computerUse.action({\n        action: 'read_file',\n        path,\n      });\n\n      if (result.success && result.data) {\n        // Return document content block\n        return {\n          content: [\n            {\n              type: 'document',\n              source: {\n                type: 'base64',\n                media_type: result.mediaType || 'application/octet-stream',\n                data: result.data,\n              },\n              name: result.name || 'file',\n              size: result.size,\n            },\n          ],\n        };\n      } else {\n        return {\n          content: [\n            {\n              type: 'text',\n              text: result.message || 'Error reading file',\n            },\n          ],\n        };\n      }\n    } catch (err) {\n      return {\n        content: [\n          {\n            type: 'text',\n            text: `Error reading file: ${(err as Error).message}`,\n          },\n        ],\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/src/mcp/index.ts",
    "content": "export * from './bytebot-mcp.module';\n"
  },
  {
    "path": "packages/bytebotd/src/nut/nut.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { NutService } from './nut.service';\n\n@Module({\n  providers: [NutService],\n  exports: [NutService],\n})\nexport class NutModule {}\n"
  },
  {
    "path": "packages/bytebotd/src/nut/nut.service.ts",
    "content": "// src/nut/nut.service.ts\nimport { Injectable, Logger } from '@nestjs/common';\nimport {\n  keyboard,\n  mouse,\n  Point,\n  screen,\n  Key,\n  Button,\n  FileType,\n} from '@nut-tree-fork/nut-js';\nimport { spawn } from 'child_process';\nimport * as path from 'path';\n\n/**\n * Enum representing key codes supported by nut-js.\n * Maps to the same structure as QKeyCode for compatibility.\n */\n\nconst XKeySymToNutKeyMap: Record<string, Key> = {\n  // Alphanumeric Keys\n  '1': Key.Num1,\n  '2': Key.Num2,\n  '3': Key.Num3,\n  '4': Key.Num4,\n  '5': Key.Num5,\n  '6': Key.Num6,\n  '7': Key.Num7,\n  '8': Key.Num8,\n  '9': Key.Num9,\n  '0': Key.Num0,\n  bracketleft: Key.LeftBracket,\n  bracketright: Key.RightBracket,\n  apostrophe: Key.Quote,\n\n  // Modifier Keys\n  Shift: Key.LeftShift,\n  ctrl: Key.LeftControl,\n  Control: Key.LeftControl,\n  Super: Key.LeftSuper,\n  Alt: Key.LeftAlt,\n  Meta: Key.LeftMeta,\n  Shift_L: Key.LeftShift,\n  Shift_R: Key.RightShift,\n  Control_L: Key.LeftControl,\n  Control_R: Key.RightControl,\n  Super_L: Key.LeftSuper,\n  Super_R: Key.RightSuper,\n  Alt_L: Key.LeftAlt,\n  Alt_R: Key.RightAlt,\n  Meta_L: Key.LeftMeta,\n  Meta_R: Key.RightMeta,\n\n  // Lock and Toggle Keys\n  Caps_Lock: Key.CapsLock,\n  Num_Lock: Key.NumLock,\n  Scroll_Lock: Key.ScrollLock,\n\n  // Editing Keys\n  Page_Up: Key.PageUp,\n  Page_Down: Key.PageDown,\n\n  // Numpad Keys\n  KP_0: Key.NumPad0,\n  KP_1: Key.NumPad1,\n  KP_2: Key.NumPad2,\n  KP_3: Key.NumPad3,\n  KP_4: Key.NumPad4,\n  KP_5: Key.NumPad5,\n  KP_6: Key.NumPad6,\n  KP_7: Key.NumPad7,\n  KP_8: Key.NumPad8,\n  KP_9: Key.NumPad9,\n  KP_Add: Key.Add,\n  KP_Subtract: Key.Subtract,\n  KP_Multiply: Key.Multiply,\n  KP_Divide: Key.Divide,\n  KP_Decimal: Key.Decimal,\n  KP_Equal: Key.NumPadEqual,\n\n  // Multimedia Keys\n  AudioLowerVolume: Key.AudioVolDown,\n  AudioRaiseVolume: Key.AudioVolUp,\n  AudioRandomPlay: Key.AudioRandom,\n};\n\nconst XKeySymToNutKeyMapLowercase: Record<string, Key> = Object.entries(\n  XKeySymToNutKeyMap,\n).reduce(\n  (map, [key, value]) => {\n    map[key.toLowerCase()] = value;\n    return map;\n  },\n  {} as Record<string, Key>,\n);\n\nconst NutKeyMap = Object.entries(Key)\n  .filter(([name]) => isNaN(Number(name)))\n  .reduce(\n    (map, [name, value]) => {\n      map[name] = value as Key;\n      return map;\n    },\n    {} as Record<string, Key>,\n  );\n\n// Create a map of lowercase keys to nutjs keys\nconst NutKeyMapLowercase: Record<string, Key> = Object.entries(Key)\n  // we only want the string→number pairs (filter out the reverse numeric keys)\n  .filter(([name]) => isNaN(Number(name)))\n  .reduce(\n    (map, [name, value]) => {\n      map[name.toLowerCase()] = value as Key;\n      return map;\n    },\n    {} as Record<string, Key>,\n  );\n\n@Injectable()\nexport class NutService {\n  private readonly logger = new Logger(NutService.name);\n  private screenshotDir: string;\n\n  constructor() {\n    // Initialize nut-js settings\n    mouse.config.autoDelayMs = 100;\n    keyboard.config.autoDelayMs = 100;\n\n    // Create screenshot directory if it doesn't exist\n    this.screenshotDir = path.join('/tmp', 'bytebot-screenshots');\n    import('fs').then((fs) => {\n      fs.promises\n        .mkdir(this.screenshotDir, { recursive: true })\n        .catch((err) => {\n          this.logger.error(\n            `Failed to create screenshot directory: ${err.message}`,\n          );\n        });\n    });\n  }\n\n  /**\n   * Sends key events to the computer.\n   *\n   * @param keys An array of key strings.\n   * @param delay Delay between pressing and releasing keys in ms.\n   */\n  async sendKeys(keys: string[], delay: number = 100): Promise<any> {\n    this.logger.log(`Sending keys: ${keys}`);\n\n    try {\n      const nutKeys = keys.map((key) => this.validateKey(key));\n      await keyboard.pressKey(...nutKeys);\n      await this.delay(delay);\n      await keyboard.releaseKey(...nutKeys);\n      return { success: true };\n    } catch (error) {\n      throw new Error(`Failed to send keys: ${error.message}`);\n    }\n  }\n\n  /**\n   * Holds or releases keys.\n   *\n   * @param keys An array of key strings.\n   * @param down True to press the keys down, false to release them.\n   */\n  async holdKeys(keys: string[], down: boolean): Promise<any> {\n    try {\n      for (const key of keys) {\n        const nutKey = this.validateKey(key);\n        if (down) {\n          await keyboard.pressKey(nutKey);\n        } else {\n          await keyboard.releaseKey(nutKey);\n        }\n      }\n      return { success: true };\n    } catch (error) {\n      throw new Error(`Failed to hold keys: ${error.message}`);\n    }\n  }\n\n  /**\n   * Validates a key and returns the corresponding nut-js key.\n   *\n   * @param key The key to validate.\n   * @returns The corresponding nut-js key.\n   */\n  private validateKey(key: string): Key {\n    // Try exact matches first\n    let nutKey: Key | undefined = XKeySymToNutKeyMap[key] || NutKeyMap[key];\n\n    // If not found, try case-insensitive matching\n    if (nutKey === undefined) {\n      const lowerKey = key.toLowerCase();\n\n      // Try to find case-insensitive match in XKeySymToNutKeyMapLowercase or NutKeyMapLowercase\n      nutKey =\n        XKeySymToNutKeyMapLowercase[lowerKey] || NutKeyMapLowercase[lowerKey];\n    }\n\n    if (nutKey === undefined) {\n      throw new Error(\n        `Invalid key: '${key}'. Key not found in available key mappings.`,\n      );\n    }\n\n    return nutKey;\n  }\n\n  /**\n   * Types text on the keyboard.\n   *\n   * @param text The text to type.\n   * @param delayMs Delay between keypresses in ms.\n   */\n  async typeText(text: string, delayMs: number = 0): Promise<void> {\n    this.logger.log(`Typing text: ${text}`);\n\n    try {\n      for (let i = 0; i < text.length; i++) {\n        const char = text[i];\n        const keyInfo = this.charToKeyInfo(char);\n        if (keyInfo) {\n          if (keyInfo.withShift) {\n            // Hold shift key, press the character key, and release shift key\n            await keyboard.pressKey(Key.LeftShift, keyInfo.keyCode);\n            await keyboard.releaseKey(Key.LeftShift, keyInfo.keyCode);\n          } else {\n            await keyboard.pressKey(keyInfo.keyCode);\n            await keyboard.releaseKey(keyInfo.keyCode);\n          }\n          if (delayMs > 0 && i < text.length - 1) {\n            await new Promise((resolve) => setTimeout(resolve, delayMs));\n          }\n        } else {\n          throw new Error(`No key mapping found for character: ${char}`);\n        }\n      }\n    } catch (error) {\n      throw new Error(`Failed to type text: ${error.message}`);\n    }\n  }\n\n  async pasteText(text: string): Promise<void> {\n    this.logger.log(`Pasting text: ${text}`);\n\n    try {\n      // Copy text to clipboard using xclip via spawn\n      await new Promise<void>((resolve, reject) => {\n        const child = spawn('xclip', ['-selection', 'clipboard'], {\n          env: { ...process.env, DISPLAY: ':0.0' },\n          stdio: ['pipe', 'ignore', 'inherit'],\n        });\n\n        child.once('error', reject);\n        child.once('close', (code) => {\n          code === 0\n            ? resolve()\n            : reject(new Error(`xclip exited with code ${code}`));\n        });\n\n        child.stdin.write(text);\n        child.stdin.end();\n      });\n\n      // brief pause to ensure clipboard owner is set\n      await new Promise((resolve) => setTimeout(resolve, 100));\n\n      await keyboard.pressKey(Key.LeftControl, Key.V);\n      await keyboard.releaseKey(Key.LeftControl, Key.V);\n    } catch (error) {\n      throw new Error(`Failed to paste text: ${error.message}`);\n    }\n  }\n\n  /**\n   * Converts a character to its corresponding key information.\n   *\n   * @param char The character to convert.\n   * @returns An object containing the keyCode and whether shift is needed, or null if no mapping exists.\n   */\n  private charToKeyInfo(\n    char: string,\n  ): { keyCode: Key; withShift: boolean } | null {\n    // Handle lowercase letters\n    if (/^[a-z]$/.test(char)) {\n      return { keyCode: this.validateKey(char), withShift: false };\n    }\n\n    // Handle uppercase letters (need to send shift + lowercase)\n    if (/^[A-Z]$/.test(char)) {\n      return {\n        keyCode: this.validateKey(char.toLowerCase()),\n        withShift: true,\n      };\n    }\n\n    // Handle numbers\n    if (/^[0-9]$/.test(char)) {\n      return { keyCode: this.validateKey(char), withShift: false };\n    }\n\n    // Handle special characters\n    const specialCharMap: Record<string, { keyCode: Key; withShift: boolean }> =\n      {\n        ' ': { keyCode: Key.Space, withShift: false },\n        '.': { keyCode: Key.Period, withShift: false },\n        ',': { keyCode: Key.Comma, withShift: false },\n        ';': { keyCode: Key.Semicolon, withShift: false },\n        \"'\": { keyCode: Key.Quote, withShift: false },\n        '`': { keyCode: Key.Grave, withShift: false },\n        '-': { keyCode: Key.Minus, withShift: false },\n        '=': { keyCode: Key.Equal, withShift: false },\n        '[': { keyCode: Key.LeftBracket, withShift: false },\n        ']': { keyCode: Key.RightBracket, withShift: false },\n        '\\\\': { keyCode: Key.Backslash, withShift: false },\n        '/': { keyCode: Key.Slash, withShift: false },\n\n        // Characters that require shift\n        '!': { keyCode: Key.Num1, withShift: true },\n        '@': { keyCode: Key.Num2, withShift: true },\n        '#': { keyCode: Key.Num3, withShift: true },\n        $: { keyCode: Key.Num4, withShift: true },\n        '%': { keyCode: Key.Num5, withShift: true },\n        '^': { keyCode: Key.Num6, withShift: true },\n        '&': { keyCode: Key.Num7, withShift: true },\n        '*': { keyCode: Key.Num8, withShift: true },\n        '(': { keyCode: Key.Num9, withShift: true },\n        ')': { keyCode: Key.Num0, withShift: true },\n        _: { keyCode: Key.Minus, withShift: true },\n        '+': { keyCode: Key.Equal, withShift: true },\n        '{': { keyCode: Key.LeftBracket, withShift: true },\n        '}': { keyCode: Key.RightBracket, withShift: true },\n        '|': { keyCode: Key.Backslash, withShift: true },\n        ':': { keyCode: Key.Semicolon, withShift: true },\n        '\"': { keyCode: Key.Quote, withShift: true },\n        '<': { keyCode: Key.Comma, withShift: true },\n        '>': { keyCode: Key.Period, withShift: true },\n        '?': { keyCode: Key.Slash, withShift: true },\n        '~': { keyCode: Key.Grave, withShift: true },\n        '\\n': { keyCode: Key.Enter, withShift: false },\n      };\n\n    return specialCharMap[char] || null;\n  }\n\n  /**\n   * Moves the mouse to specified coordinates.\n   *\n   * @param coordinates The x and y coordinates.\n   */\n  async mouseMoveEvent({ x, y }: { x: number; y: number }): Promise<any> {\n    this.logger.log(`Moving mouse to coordinates: (${x}, ${y})`);\n    try {\n      const point = new Point(x, y);\n      await mouse.setPosition(point);\n      return { success: true };\n    } catch (error) {\n      throw new Error(`Failed to move mouse: ${error.message}`);\n    }\n  }\n\n  async mouseClickEvent(button: 'left' | 'right' | 'middle'): Promise<any> {\n    this.logger.log(`Clicking mouse button: ${button}`);\n    try {\n      switch (button) {\n        case 'left':\n          await mouse.click(Button.LEFT);\n          break;\n        case 'right':\n          await mouse.click(Button.RIGHT);\n          break;\n        case 'middle':\n          await mouse.click(Button.MIDDLE);\n          break;\n      }\n      return { success: true };\n    } catch (error) {\n      throw new Error(`Failed to click mouse button: ${error.message}`);\n    }\n  }\n\n  /**\n   * Presses or releases a mouse button.\n   *\n   * @param button The mouse button ('left', 'right', or 'middle').\n   * @param pressed True to press, false to release.\n   */\n  async mouseButtonEvent(\n    button: 'left' | 'right' | 'middle',\n    pressed: boolean,\n  ): Promise<any> {\n    this.logger.log(\n      `Mouse button event: ${button} ${pressed ? 'pressed' : 'released'}`,\n    );\n    try {\n      if (pressed) {\n        switch (button) {\n          case 'left':\n            await mouse.pressButton(Button.LEFT);\n            break;\n          case 'right':\n            await mouse.pressButton(Button.RIGHT);\n            break;\n          case 'middle':\n            await mouse.pressButton(Button.MIDDLE);\n            break;\n        }\n      } else {\n        switch (button) {\n          case 'left':\n            await mouse.releaseButton(Button.LEFT);\n            break;\n          case 'right':\n            await mouse.releaseButton(Button.RIGHT);\n            break;\n          case 'middle':\n            await mouse.releaseButton(Button.MIDDLE);\n            break;\n        }\n      }\n      return { success: true };\n    } catch (error) {\n      throw new Error(\n        `Failed to send mouse ${button} button ${pressed ? 'press' : 'release'} event: ${error.message}`,\n      );\n    }\n  }\n\n  /**\n   * Scrolls the mouse wheel.\n   *\n   * @param direction The scroll direction ('up', 'down', 'left', or 'right').\n   * @param amount The number of scroll steps.\n   */\n  async mouseWheelEvent(\n    direction: 'right' | 'left' | 'up' | 'down',\n    amount: number,\n  ): Promise<any> {\n    this.logger.log(`Mouse wheel event: ${direction} ${amount}`);\n    try {\n      switch (direction) {\n        case 'up':\n          await mouse.scrollUp(amount);\n          break;\n        case 'down':\n          await mouse.scrollDown(amount);\n          break;\n        case 'left':\n          await mouse.scrollLeft(amount);\n          break;\n        case 'right':\n          await mouse.scrollRight(amount);\n          break;\n      }\n\n      return { success: true };\n    } catch (error) {\n      throw new Error(`Failed to scroll: ${error.message}`);\n    }\n  }\n\n  /**\n   * Takes a screenshot of the screen.\n   *\n   * @returns A Promise that resolves with a Buffer containing the image.\n   */\n  async screendump(): Promise<Buffer> {\n    const filename = `screenshot-${Date.now()}.png`;\n    const filepath = path.join(this.screenshotDir, filename);\n    this.logger.log(`Taking screenshot to ${filepath}`);\n\n    try {\n      // Take screenshot\n      await screen.capture(filename, FileType.PNG, this.screenshotDir);\n\n      // Read the file back and return as buffer\n      return await import('fs').then((fs) => fs.promises.readFile(filepath));\n    } catch (error) {\n      this.logger.error(`Error taking screenshot: ${error.message}`);\n      throw error;\n    } finally {\n      // Clean up the temporary file\n      try {\n        await import('fs').then((fs) => fs.promises.unlink(filepath));\n      } catch (unlinkError) {\n        // Ignore if file doesn't exist\n        this.logger.warn(\n          `Failed to remove temporary screenshot file: ${unlinkError.message}`,\n        );\n      }\n    }\n  }\n\n  async getCursorPosition(): Promise<{ x: number; y: number }> {\n    this.logger.log(`Getting cursor position`);\n    try {\n      const position = await mouse.getPosition();\n      return { x: position.x, y: position.y };\n    } catch (error) {\n      this.logger.error(`Error getting cursor position: ${error.message}`);\n      throw error;\n    }\n  }\n\n  /**\n   * Utility method to create a delay.\n   *\n   * @param ms Milliseconds to wait\n   */\n  private async delay(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n}\n"
  },
  {
    "path": "packages/bytebotd/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\"]\n}\n"
  },
  {
    "path": "packages/bytebotd/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true,\n    \"skipLibCheck\": true,\n    \"strictNullChecks\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitAny\": false,\n    \"strictBindCallApply\": false,\n    \"noFallthroughCasesInSwitch\": false\n  }\n}\n"
  },
  {
    "path": "packages/shared/package.json",
    "content": "{\n  \"name\": \"@bytebot/shared\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Shared utilities and types for Bytebot packages\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"private\": true,\n  \"license\": \"UNLICENSED\",\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.json\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\"\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" --fix\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.esm.js\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.2.0\",\n    \"@eslint/js\": \"^9.18.0\",\n    \"eslint\": \"^9.18.0\",\n    \"eslint-config-prettier\": \"^10.0.1\",\n    \"eslint-plugin-prettier\": \"^5.2.2\",\n    \"globals\": \"^15.14.0\",\n    \"prettier\": \"^3.4.2\",\n    \"typescript\": \"^5.7.3\",\n    \"typescript-eslint\": \"^8.20.0\"\n  },\n  \"files\": [\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "packages/shared/src/index.ts",
    "content": "export * from \"./types/messageContent.types\";\nexport * from \"./utils/messageContent.utils\";\nexport * from \"./utils/computerAction.utils\";\nexport * from \"./types/computerAction.types\";\n"
  },
  {
    "path": "packages/shared/src/types/computerAction.types.ts",
    "content": "export type Coordinates = { x: number; y: number };\nexport type Button = \"left\" | \"right\" | \"middle\";\nexport type Press = \"up\" | \"down\";\nexport type Application =\n  | \"firefox\"\n  | \"1password\"\n  | \"thunderbird\"\n  | \"vscode\"\n  | \"terminal\"\n  | \"desktop\"\n  | \"directory\";\n\n// Define individual computer action types\nexport type MoveMouseAction = {\n  action: \"move_mouse\";\n  coordinates: Coordinates;\n};\n\nexport type TraceMouseAction = {\n  action: \"trace_mouse\";\n  path: Coordinates[];\n  holdKeys?: string[];\n};\n\nexport type ClickMouseAction = {\n  action: \"click_mouse\";\n  coordinates?: Coordinates;\n  button: Button;\n  holdKeys?: string[];\n  clickCount: number;\n};\n\nexport type PressMouseAction = {\n  action: \"press_mouse\";\n  coordinates?: Coordinates;\n  button: Button;\n  press: Press;\n};\n\nexport type DragMouseAction = {\n  action: \"drag_mouse\";\n  path: Coordinates[];\n  button: Button;\n  holdKeys?: string[];\n};\n\nexport type ScrollAction = {\n  action: \"scroll\";\n  coordinates?: Coordinates;\n  direction: \"up\" | \"down\" | \"left\" | \"right\";\n  scrollCount: number;\n  holdKeys?: string[];\n};\n\nexport type TypeKeysAction = {\n  action: \"type_keys\";\n  keys: string[];\n  delay?: number;\n};\n\nexport type PasteTextAction = {\n  action: \"paste_text\";\n  text: string;\n};\n\nexport type PressKeysAction = {\n  action: \"press_keys\";\n  keys: string[];\n  press: Press;\n};\n\nexport type TypeTextAction = {\n  action: \"type_text\";\n  text: string;\n  delay?: number;\n  sensitive?: boolean;\n};\n\nexport type WaitAction = {\n  action: \"wait\";\n  duration: number;\n};\n\nexport type ScreenshotAction = {\n  action: \"screenshot\";\n};\n\nexport type CursorPositionAction = {\n  action: \"cursor_position\";\n};\n\nexport type ApplicationAction = {\n  action: \"application\";\n  application: Application;\n};\n\nexport type WriteFileAction = {\n  action: \"write_file\";\n  path: string;\n  data: string; // Base64 encoded data\n};\n\nexport type ReadFileAction = {\n  action: \"read_file\";\n  path: string;\n};\n\n// Define the union type using the individual action types\nexport type ComputerAction =\n  | MoveMouseAction\n  | TraceMouseAction\n  | ClickMouseAction\n  | PressMouseAction\n  | DragMouseAction\n  | ScrollAction\n  | TypeKeysAction\n  | PressKeysAction\n  | TypeTextAction\n  | PasteTextAction\n  | WaitAction\n  | ScreenshotAction\n  | CursorPositionAction\n  | ApplicationAction\n  | WriteFileAction\n  | ReadFileAction;\n"
  },
  {
    "path": "packages/shared/src/types/messageContent.types.ts",
    "content": "import { Button, Coordinates, Press } from \"./computerAction.types\";\n\n// Content block types\nexport enum MessageContentType {\n  Text = \"text\",\n  Image = \"image\",\n  Document = \"document\",\n  ToolUse = \"tool_use\",\n  ToolResult = \"tool_result\",\n  Thinking = \"thinking\",\n  RedactedThinking = \"redacted_thinking\",\n  UserAction = \"user_action\",\n}\n\n// Base type with only the discriminator\nexport type MessageContentBlockBase = {\n  type: MessageContentType;\n  content?: MessageContentBlock[];\n};\n\nexport type TextContentBlock = {\n  type: MessageContentType.Text;\n  text: string;\n} & MessageContentBlockBase;\n\nexport type ImageContentBlock = {\n  type: MessageContentType.Image;\n  source: {\n    media_type: \"image/png\";\n    type: \"base64\";\n    data: string;\n  };\n} & MessageContentBlockBase;\n\nexport type DocumentContentBlock = {\n  type: MessageContentType.Document;\n  source: {\n    type: \"base64\";\n    media_type: string;\n    data: string;\n  };\n  name?: string;\n  size?: number;\n} & MessageContentBlockBase;\n\nexport type ThinkingContentBlock = {\n  type: MessageContentType.Thinking;\n  thinking: string;\n  signature: string;\n} & MessageContentBlockBase;\n\nexport type RedactedThinkingContentBlock = {\n  type: MessageContentType.RedactedThinking;\n  data: string;\n} & MessageContentBlockBase;\n\nexport type ToolUseContentBlock = {\n  type: MessageContentType.ToolUse;\n  name: string;\n  id: string;\n  input: Record<string, any>;\n} & MessageContentBlockBase;\n\nexport type MoveMouseToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_move_mouse\";\n  input: {\n    coordinates: Coordinates;\n  };\n};\n\nexport type TraceMouseToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_trace_mouse\";\n  input: {\n    path: Coordinates[];\n    holdKeys?: string[];\n  };\n};\n\nexport type ClickMouseToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_click_mouse\";\n  input: {\n    coordinates?: Coordinates;\n    button: Button;\n    holdKeys?: string[];\n    clickCount: number;\n  };\n};\n\nexport type PressMouseToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_press_mouse\";\n  input: {\n    coordinates?: Coordinates;\n    button: Button;\n    press: Press;\n  };\n};\n\nexport type DragMouseToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_drag_mouse\";\n  input: {\n    path: Coordinates[];\n    button: Button;\n    holdKeys?: string[];\n  };\n};\n\nexport type ScrollToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_scroll\";\n  input: {\n    coordinates?: Coordinates;\n    direction: \"up\" | \"down\" | \"left\" | \"right\";\n    scrollCount: number;\n    holdKeys?: string[];\n  };\n};\n\nexport type TypeKeysToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_type_keys\";\n  input: {\n    keys: string[];\n    delay?: number;\n  };\n};\n\nexport type PressKeysToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_press_keys\";\n  input: {\n    keys: string[];\n    press: Press;\n  };\n};\n\nexport type TypeTextToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_type_text\";\n  input: {\n    text: string;\n    isSensitive?: boolean;\n    delay?: number;\n  };\n};\n\nexport type PasteTextToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_paste_text\";\n  input: {\n    text: string;\n    isSensitive?: boolean;\n  };\n};\n\nexport type WaitToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_wait\";\n  input: {\n    duration: number;\n  };\n};\n\nexport type ScreenshotToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_screenshot\";\n};\n\nexport type CursorPositionToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_cursor_position\";\n};\n\nexport type ApplicationToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_application\";\n  input: {\n    application: string;\n  };\n};\n\nexport type WriteFileToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_write_file\";\n  input: {\n    path: string;\n    data: string;\n  };\n};\n\nexport type ReadFileToolUseBlock = ToolUseContentBlock & {\n  name: \"computer_read_file\";\n  input: {\n    path: string;\n  };\n};\n\nexport type ComputerToolUseContentBlock =\n  | MoveMouseToolUseBlock\n  | TraceMouseToolUseBlock\n  | ClickMouseToolUseBlock\n  | PressMouseToolUseBlock\n  | TypeKeysToolUseBlock\n  | PressKeysToolUseBlock\n  | TypeTextToolUseBlock\n  | PasteTextToolUseBlock\n  | WaitToolUseBlock\n  | ScreenshotToolUseBlock\n  | DragMouseToolUseBlock\n  | ScrollToolUseBlock\n  | CursorPositionToolUseBlock\n  | ApplicationToolUseBlock\n  | WriteFileToolUseBlock\n  | ReadFileToolUseBlock;\n\nexport type UserActionContentBlock = MessageContentBlockBase & {\n  type: MessageContentType.UserAction;\n  content: (\n    | ImageContentBlock\n    | MoveMouseToolUseBlock\n    | TraceMouseToolUseBlock\n    | ClickMouseToolUseBlock\n    | PressMouseToolUseBlock\n    | TypeKeysToolUseBlock\n    | PressKeysToolUseBlock\n    | TypeTextToolUseBlock\n    | DragMouseToolUseBlock\n    | ScrollToolUseBlock\n  )[];\n};\n\nexport type SetTaskStatusToolUseBlock = ToolUseContentBlock & {\n  name: \"set_task_status\";\n  input: {\n    status: \"completed\" | \"failed\" | \"needs_help\";\n    description: string;\n  };\n};\n\nexport type CreateTaskToolUseBlock = ToolUseContentBlock & {\n  name: \"create_task\";\n  input: {\n    name: string;\n    description: string;\n    type?: \"immediate\" | \"scheduled\";\n    scheduledFor?: string;\n    priority: \"low\" | \"medium\" | \"high\" | \"urgent\";\n  };\n};\n\nexport type ToolResultContentBlock = {\n  type: MessageContentType.ToolResult;\n  tool_use_id: string;\n  content: MessageContentBlock[];\n  is_error?: boolean;\n} & MessageContentBlockBase;\n\n// Union type of all possible content blocks\nexport type MessageContentBlock =\n  | TextContentBlock\n  | ImageContentBlock\n  | DocumentContentBlock\n  | ToolUseContentBlock\n  | ThinkingContentBlock\n  | RedactedThinkingContentBlock\n  | UserActionContentBlock\n  | ComputerToolUseContentBlock\n  | ToolResultContentBlock;\n"
  },
  {
    "path": "packages/shared/src/utils/computerAction.utils.ts",
    "content": "import {\n  ComputerAction,\n  ClickMouseAction,\n  DragMouseAction,\n  MoveMouseAction,\n  PressKeysAction,\n  PressMouseAction,\n  ScrollAction,\n  TraceMouseAction,\n  TypeKeysAction,\n  TypeTextAction,\n  WaitAction,\n  ScreenshotAction,\n  CursorPositionAction,\n  ApplicationAction,\n  PasteTextAction,\n  WriteFileAction,\n  ReadFileAction,\n} from \"../types/computerAction.types\";\nimport {\n  ComputerToolUseContentBlock,\n  MessageContentType,\n} from \"../types/messageContent.types\";\n\n/**\n * Type guard factory for computer actions\n */\nfunction createActionTypeGuard<T extends ComputerAction>(\n  actionType: T[\"action\"]\n): (obj: unknown) => obj is T {\n  return (obj: unknown): obj is T => {\n    if (!obj || typeof obj !== \"object\") {\n      return false;\n    }\n    const action = obj as Record<string, any>;\n    return action.action === actionType;\n  };\n}\n\n/**\n * Type guards for all computer actions\n */\nexport const isMoveMouseAction =\n  createActionTypeGuard<MoveMouseAction>(\"move_mouse\");\nexport const isTraceMouseAction =\n  createActionTypeGuard<TraceMouseAction>(\"trace_mouse\");\nexport const isClickMouseAction =\n  createActionTypeGuard<ClickMouseAction>(\"click_mouse\");\nexport const isPressMouseAction =\n  createActionTypeGuard<PressMouseAction>(\"press_mouse\");\nexport const isDragMouseAction =\n  createActionTypeGuard<DragMouseAction>(\"drag_mouse\");\nexport const isScrollAction = createActionTypeGuard<ScrollAction>(\"scroll\");\nexport const isTypeKeysAction =\n  createActionTypeGuard<TypeKeysAction>(\"type_keys\");\nexport const isPressKeysAction =\n  createActionTypeGuard<PressKeysAction>(\"press_keys\");\nexport const isTypeTextAction =\n  createActionTypeGuard<TypeTextAction>(\"type_text\");\nexport const isWaitAction = createActionTypeGuard<WaitAction>(\"wait\");\nexport const isScreenshotAction =\n  createActionTypeGuard<ScreenshotAction>(\"screenshot\");\nexport const isCursorPositionAction =\n  createActionTypeGuard<CursorPositionAction>(\"cursor_position\");\nexport const isApplicationAction =\n  createActionTypeGuard<ApplicationAction>(\"application\");\n\n/**\n * Base converter for creating tool use blocks\n */\nfunction createToolUseBlock(\n  toolName: string,\n  toolUseId: string,\n  input: Record<string, any>\n): ComputerToolUseContentBlock {\n  return {\n    type: MessageContentType.ToolUse,\n    id: toolUseId,\n    name: toolName as any,\n    input,\n  };\n}\n\n/**\n * Utility to conditionally add properties to objects\n */\nfunction conditionallyAdd<T extends Record<string, any>>(\n  obj: T,\n  conditions: Array<[boolean | undefined, string, any]>\n): T {\n  const result: Record<string, any> = { ...obj };\n  conditions.forEach(([condition, key, value]) => {\n    if (condition) {\n      result[key] = value;\n    }\n  });\n  return result as T;\n}\n\n/**\n * Converters for each action type\n */\nexport function convertMoveMouseActionToToolUseBlock(\n  action: MoveMouseAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_move_mouse\", toolUseId, {\n    coordinates: action.coordinates,\n  });\n}\n\nexport function convertTraceMouseActionToToolUseBlock(\n  action: TraceMouseAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_trace_mouse\",\n    toolUseId,\n    conditionallyAdd({ path: action.path }, [\n      [action.holdKeys !== undefined, \"holdKeys\", action.holdKeys],\n    ])\n  );\n}\n\nexport function convertClickMouseActionToToolUseBlock(\n  action: ClickMouseAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_click_mouse\",\n    toolUseId,\n    conditionallyAdd(\n      {\n        button: action.button,\n        clickCount: action.clickCount,\n      },\n      [\n        [action.coordinates !== undefined, \"coordinates\", action.coordinates],\n        [action.holdKeys !== undefined, \"holdKeys\", action.holdKeys],\n      ]\n    )\n  );\n}\n\nexport function convertPressMouseActionToToolUseBlock(\n  action: PressMouseAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_press_mouse\",\n    toolUseId,\n    conditionallyAdd(\n      {\n        button: action.button,\n        press: action.press,\n      },\n      [[action.coordinates !== undefined, \"coordinates\", action.coordinates]]\n    )\n  );\n}\n\nexport function convertDragMouseActionToToolUseBlock(\n  action: DragMouseAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_drag_mouse\",\n    toolUseId,\n    conditionallyAdd(\n      {\n        path: action.path,\n        button: action.button,\n      },\n      [[action.holdKeys !== undefined, \"holdKeys\", action.holdKeys]]\n    )\n  );\n}\n\nexport function convertScrollActionToToolUseBlock(\n  action: ScrollAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_scroll\",\n    toolUseId,\n    conditionallyAdd(\n      {\n        direction: action.direction,\n        scrollCount: action.scrollCount,\n      },\n      [\n        [action.coordinates !== undefined, \"coordinates\", action.coordinates],\n        [action.holdKeys !== undefined, \"holdKeys\", action.holdKeys],\n      ]\n    )\n  );\n}\n\nexport function convertTypeKeysActionToToolUseBlock(\n  action: TypeKeysAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_type_keys\",\n    toolUseId,\n    conditionallyAdd({ keys: action.keys }, [\n      [typeof action.delay === \"number\", \"delay\", action.delay],\n    ])\n  );\n}\n\nexport function convertPressKeysActionToToolUseBlock(\n  action: PressKeysAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_press_keys\", toolUseId, {\n    keys: action.keys,\n    press: action.press,\n  });\n}\n\nexport function convertTypeTextActionToToolUseBlock(\n  action: TypeTextAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\n    \"computer_type_text\",\n    toolUseId,\n    conditionallyAdd({ text: action.text }, [\n      [typeof action.delay === \"number\", \"delay\", action.delay],\n      [typeof action.sensitive === \"boolean\", \"isSensitive\", action.sensitive],\n    ])\n  );\n}\n\nexport function convertPasteTextActionToToolUseBlock(\n  action: PasteTextAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_paste_text\", toolUseId, {\n    text: action.text,\n  });\n}\n\nexport function convertWaitActionToToolUseBlock(\n  action: WaitAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_wait\", toolUseId, {\n    duration: action.duration,\n  });\n}\n\nexport function convertScreenshotActionToToolUseBlock(\n  action: ScreenshotAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_screenshot\", toolUseId, {});\n}\n\nexport function convertCursorPositionActionToToolUseBlock(\n  action: CursorPositionAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_cursor_position\", toolUseId, {});\n}\n\nexport function convertApplicationActionToToolUseBlock(\n  action: ApplicationAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_application\", toolUseId, {\n    application: action.application,\n  });\n}\n\nexport function convertWriteFileActionToToolUseBlock(\n  action: WriteFileAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_write_file\", toolUseId, {\n    path: action.path,\n    data: action.data,\n  });\n}\n\nexport function convertReadFileActionToToolUseBlock(\n  action: ReadFileAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  return createToolUseBlock(\"computer_read_file\", toolUseId, {\n    path: action.path,\n  });\n}\n\n/**\n * Generic converter that handles all action types\n */\nexport function convertComputerActionToToolUseBlock(\n  action: ComputerAction,\n  toolUseId: string\n): ComputerToolUseContentBlock {\n  switch (action.action) {\n    case \"move_mouse\":\n      return convertMoveMouseActionToToolUseBlock(action, toolUseId);\n    case \"trace_mouse\":\n      return convertTraceMouseActionToToolUseBlock(action, toolUseId);\n    case \"click_mouse\":\n      return convertClickMouseActionToToolUseBlock(action, toolUseId);\n    case \"press_mouse\":\n      return convertPressMouseActionToToolUseBlock(action, toolUseId);\n    case \"drag_mouse\":\n      return convertDragMouseActionToToolUseBlock(action, toolUseId);\n    case \"scroll\":\n      return convertScrollActionToToolUseBlock(action, toolUseId);\n    case \"type_keys\":\n      return convertTypeKeysActionToToolUseBlock(action, toolUseId);\n    case \"press_keys\":\n      return convertPressKeysActionToToolUseBlock(action, toolUseId);\n    case \"type_text\":\n      return convertTypeTextActionToToolUseBlock(action, toolUseId);\n    case \"paste_text\":\n      return convertPasteTextActionToToolUseBlock(action, toolUseId);\n    case \"wait\":\n      return convertWaitActionToToolUseBlock(action, toolUseId);\n    case \"screenshot\":\n      return convertScreenshotActionToToolUseBlock(action, toolUseId);\n    case \"cursor_position\":\n      return convertCursorPositionActionToToolUseBlock(action, toolUseId);\n    case \"application\":\n      return convertApplicationActionToToolUseBlock(action, toolUseId);\n    case \"write_file\":\n      return convertWriteFileActionToToolUseBlock(action, toolUseId);\n    case \"read_file\":\n      return convertReadFileActionToToolUseBlock(action, toolUseId);\n    default:\n      const exhaustiveCheck: never = action;\n      throw new Error(\n        `Unknown action type: ${(exhaustiveCheck as any).action}`\n      );\n  }\n}\n"
  },
  {
    "path": "packages/shared/src/utils/messageContent.utils.ts",
    "content": "import {\n  MessageContentBlock,\n  MessageContentType,\n  TextContentBlock,\n  ImageContentBlock,\n  DocumentContentBlock,\n  ToolUseContentBlock,\n  ComputerToolUseContentBlock,\n  ToolResultContentBlock,\n  MoveMouseToolUseBlock,\n  TraceMouseToolUseBlock,\n  ClickMouseToolUseBlock,\n  PressMouseToolUseBlock,\n  TypeKeysToolUseBlock,\n  PressKeysToolUseBlock,\n  TypeTextToolUseBlock,\n  WaitToolUseBlock,\n  ScreenshotToolUseBlock,\n  CursorPositionToolUseBlock,\n  DragMouseToolUseBlock,\n  ScrollToolUseBlock,\n  ApplicationToolUseBlock,\n  SetTaskStatusToolUseBlock,\n  CreateTaskToolUseBlock,\n  ThinkingContentBlock,\n  RedactedThinkingContentBlock,\n  PasteTextToolUseBlock,\n  WriteFileToolUseBlock,\n  ReadFileToolUseBlock,\n  UserActionContentBlock,\n} from \"../types/messageContent.types\";\n\n/**\n * Type guard to check if an object is a TextContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is TextContentBlock\n */\nexport function isTextContentBlock(obj: unknown): obj is TextContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<TextContentBlock>;\n  return (\n    block.type === MessageContentType.Text && typeof block.text === \"string\"\n  );\n}\n\nexport function isThinkingContentBlock(\n  obj: unknown\n): obj is ThinkingContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<ThinkingContentBlock>;\n  return (\n    block.type === MessageContentType.Thinking &&\n    typeof block.thinking === \"string\" &&\n    typeof block.signature === \"string\"\n  );\n}\n\nexport function isRedactedThinkingContentBlock(\n  obj: unknown\n): obj is RedactedThinkingContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<RedactedThinkingContentBlock>;\n  return (\n    block.type === MessageContentType.RedactedThinking &&\n    typeof block.data === \"string\"\n  );\n}\n\n/**\n * Type guard to check if an object is an ImageContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ImageContentBlock\n */\nexport function isImageContentBlock(obj: unknown): obj is ImageContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<ImageContentBlock>;\n  return (\n    block.type === MessageContentType.Image &&\n    block.source !== undefined &&\n    typeof block.source === \"object\" &&\n    typeof block.source.media_type === \"string\" &&\n    typeof block.source.type === \"string\" &&\n    typeof block.source.data === \"string\"\n  );\n}\n\nexport function isUserActionContentBlock(\n  obj: unknown\n): obj is UserActionContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<UserActionContentBlock>;\n\n  return block.type === MessageContentType.UserAction;\n}\n\n/**\n * Type guard to check if an object is a DocumentContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is DocumentContentBlock\n */\nexport function isDocumentContentBlock(\n  obj: unknown\n): obj is DocumentContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<DocumentContentBlock>;\n  return (\n    block.type === MessageContentType.Document &&\n    block.source !== undefined &&\n    typeof block.source === \"object\" &&\n    typeof block.source.type === \"string\" &&\n    typeof block.source.media_type === \"string\" &&\n    typeof block.source.data === \"string\"\n  );\n}\n\n/**\n * Type guard to check if an object is a ToolUseContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ToolUseContentBlock\n */\nexport function isToolUseContentBlock(\n  obj: unknown\n): obj is ToolUseContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<ToolUseContentBlock>;\n  return (\n    block.type === MessageContentType.ToolUse &&\n    typeof block.name === \"string\" &&\n    typeof block.id === \"string\" &&\n    block.input !== undefined &&\n    typeof block.input === \"object\"\n  );\n}\n\n/**\n * Type guard to check if an object is a ComputerToolUseContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ComputerToolUseContentBlock\n */\nexport function isComputerToolUseContentBlock(\n  obj: unknown\n): obj is ComputerToolUseContentBlock {\n  if (!isToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  return (obj as ToolUseContentBlock).name.startsWith(\"computer_\");\n}\n\n/**\n * Type guard to check if an object is a ToolResultContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ToolResultContentBlock\n */\nexport function isToolResultContentBlock(\n  obj: unknown\n): obj is ToolResultContentBlock {\n  if (!obj || typeof obj !== \"object\") {\n    return false;\n  }\n\n  const block = obj as Partial<ToolResultContentBlock>;\n  return (\n    block.type === MessageContentType.ToolResult &&\n    typeof block.tool_use_id === \"string\"\n  );\n}\n\n/**\n * Type guard to check if an object is any type of MessageContentBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is MessageContentBlock\n */\nexport function isMessageContentBlock(\n  obj: unknown\n): obj is MessageContentBlock {\n  return (\n    isTextContentBlock(obj) ||\n    isImageContentBlock(obj) ||\n    isDocumentContentBlock(obj) ||\n    isToolUseContentBlock(obj) ||\n    isToolResultContentBlock(obj) ||\n    isThinkingContentBlock(obj) ||\n    isRedactedThinkingContentBlock(obj) ||\n    isUserActionContentBlock(obj)\n  );\n}\n\n/**\n * Determines the specific type of MessageContentBlock for a given object.\n * This doesn't narrow the type but can be useful for debugging or logging.\n * @param obj The object to check (should be a MessageContentBlock)\n * @returns A string indicating the specific type, or null if not a valid MessageContentBlock\n */\nexport function getMessageContentBlockType(obj: unknown): string | null {\n  if (!obj || typeof obj !== \"object\") {\n    return null;\n  }\n\n  if (isTextContentBlock(obj)) {\n    return \"TextContentBlock\";\n  }\n\n  if (isImageContentBlock(obj)) {\n    return \"ImageContentBlock\";\n  }\n\n  if (isDocumentContentBlock(obj)) {\n    return \"DocumentContentBlock\";\n  }\n\n  if (isThinkingContentBlock(obj)) {\n    return \"ThinkingContentBlock\";\n  }\n\n  if (isRedactedThinkingContentBlock(obj)) {\n    return \"RedactedThinkingContentBlock\";\n  }\n\n  if (isComputerToolUseContentBlock(obj)) {\n    const computerBlock = obj as ComputerToolUseContentBlock;\n    if (computerBlock.input && typeof computerBlock.input === \"object\") {\n      return `ComputerToolUseContentBlock:${computerBlock.name.replace(\n        \"computer_\",\n        \"\"\n      )}`;\n    }\n    return \"ComputerToolUseContentBlock\";\n  }\n\n  if (isToolUseContentBlock(obj)) {\n    return \"ToolUseContentBlock\";\n  }\n\n  if (isToolResultContentBlock(obj)) {\n    return \"ToolResultContentBlock\";\n  }\n\n  return null;\n}\n\n/**\n * Type guard to check if an object is a MoveMouseToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is MoveMouseToolUseBlock\n */\nexport function isMoveMouseToolUseBlock(\n  obj: unknown\n): obj is MoveMouseToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_move_mouse\";\n}\n\n/**\n * Type guard to check if an object is a TraceMouseToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is TraceMouseToolUseBlock\n */\nexport function isTraceMouseToolUseBlock(\n  obj: unknown\n): obj is TraceMouseToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_trace_mouse\";\n}\n\n/**\n * Type guard to check if an object is a ClickMouseToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ClickMouseToolUseBlock\n */\nexport function isClickMouseToolUseBlock(\n  obj: unknown\n): obj is ClickMouseToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_click_mouse\";\n}\n\n/**\n * Type guard to check if an object is a CursorPositionToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is CursorPositionToolUseBlock\n */\nexport function isCursorPositionToolUseBlock(\n  obj: unknown\n): obj is CursorPositionToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_cursor_position\";\n}\n\n/**\n * Type guard to check if an object is a PressMouseToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is PressMouseToolUseBlock\n */\nexport function isPressMouseToolUseBlock(\n  obj: unknown\n): obj is PressMouseToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_press_mouse\";\n}\n\n/**\n * Type guard to check if an object is a DragMouseToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is DragMouseToolUseBlock\n */\nexport function isDragMouseToolUseBlock(\n  obj: unknown\n): obj is DragMouseToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_drag_mouse\";\n}\n\n/**\n * Type guard to check if an object is a ScrollToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ScrollToolUseBlock\n */\nexport function isScrollToolUseBlock(obj: unknown): obj is ScrollToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_scroll\";\n}\n\n/**\n * Type guard to check if an object is a TypeKeysToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is TypeKeysToolUseBlock\n */\nexport function isTypeKeysToolUseBlock(\n  obj: unknown\n): obj is TypeKeysToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_type_keys\";\n}\n\n/**\n * Type guard to check if an object is a PressKeysToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is PressKeysToolUseBlock\n */\nexport function isPressKeysToolUseBlock(\n  obj: unknown\n): obj is PressKeysToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_press_keys\";\n}\n\n/**\n * Type guard to check if an object is a TypeTextToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is TypeTextToolUseBlock\n */\nexport function isTypeTextToolUseBlock(\n  obj: unknown\n): obj is TypeTextToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_type_text\";\n}\n\nexport function isPasteTextToolUseBlock(\n  obj: unknown\n): obj is PasteTextToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_paste_text\";\n}\n\n/**\n * Type guard to check if an object is a WaitToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is WaitToolUseBlock\n */\nexport function isWaitToolUseBlock(obj: unknown): obj is WaitToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_wait\";\n}\n\n/**\n * Type guard to check if an object is a ScreenshotToolUseBlock\n * @param obj The object to validate\n * @returns Type predicate indicating obj is ScreenshotToolUseBlock\n */\nexport function isScreenshotToolUseBlock(\n  obj: unknown\n): obj is ScreenshotToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_screenshot\";\n}\n\nexport function isApplicationToolUseBlock(\n  obj: unknown\n): obj is ApplicationToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_application\";\n}\n\nexport function isSetTaskStatusToolUseBlock(\n  obj: unknown\n): obj is SetTaskStatusToolUseBlock {\n  if (!isToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"set_task_status\";\n}\n\nexport function isCreateTaskToolUseBlock(\n  obj: unknown\n): obj is CreateTaskToolUseBlock {\n  if (!isToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"create_task\";\n}\n\nexport function isWriteFileToolUseBlock(\n  obj: unknown\n): obj is WriteFileToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_write_file\";\n}\n\nexport function isReadFileToolUseBlock(\n  obj: unknown\n): obj is ReadFileToolUseBlock {\n  if (!isComputerToolUseContentBlock(obj)) {\n    return false;\n  }\n\n  const block = obj as Record<string, any>;\n  return block.name === \"computer_read_file\";\n}\n"
  },
  {
    "path": "packages/shared/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true,\n    \"skipLibCheck\": true,\n    \"strictNullChecks\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitAny\": false,\n    \"strictBindCallApply\": false,\n    \"noFallthroughCasesInSwitch\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  }
]