Merge branch 'development' of https://github.com/bytebot-ai/bytebot into development

This commit is contained in:
Carl Atupem
2025-06-27 10:54:19 -04:00
42 changed files with 2118 additions and 257 deletions
-1
View File
@@ -1 +0,0 @@
20.18.2
-1
View File
@@ -1 +0,0 @@
20.18.2
+13
View File
@@ -27,6 +27,19 @@ https://github.com/user-attachments/assets/32a76e83-ea3a-4d5e-b34b-3b57f3604948
https://github.com/user-attachments/assets/5f946df9-9161-4e7e-8262-9eda83ee7d22
## ☁️ Deploy on Railway (1-Click)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
1. Click the Deploy Now button in the Bytebot Railway template.
2. Paste your `ANTHROPIC_API_KEY` in the single required environment variable.
3. Press **Deploy**. Railway will spin up the Desktop, Agent, UI and Postgres services using pre-built container images, connect them via [private networking](https://docs.railway.com/guides/private-networking) and expose only the UI publicly.
4. In about two minutes your agent will be live at your project's public URL.
_For an in-depth guide see [here](https://docs.bytebot.ai/deployment/railway)._
---
## 🚀 Quick Start
### Prerequisites
+84
View File
@@ -0,0 +1,84 @@
---
title: "Deploying Bytebot on Railway"
description: "Comprehensive guide to deploying the full Bytebot stack on Railway using the official 1-click template"
---
> **TL;DR ** Click the button below, add your Anthropic key, and your personal Bytebot instance will be live in ~2 minutes.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
---
## Why Railway?
Railway provides a zero-ops PaaS experience with private networking and per-service logs that perfectly fits Bytebots multi-container architecture. The official template wires every service together using the latest container images pushed to the `edge` branch.
---
## What Gets Deployed
| Service | Container Image (edge) | Port | Exposed? | Purpose |
| ---------------- | -------------------------------------------------------------------- | ---- | -------- | ------------------------------------ |
| **bytebot-ui** | `ghcr.io/bytebot-ai/bytebot-ui:edge` | 9992 | **Yes** | Next.js web UI rendered to the world |
| **bytebot-agent**| `ghcr.io/bytebot-ai/bytebot-agent:edge` | 9991 | No | Task orchestration & LLM calls |
| **bytebot-desktop**| `ghcr.io/bytebot-ai/bytebot-desktop:edge` | 9990 | No | Containerised Ubuntu + XFCE desktop |
| **postgres** | `postgres:14-alpine` | 5432 | No | Persistence layer |
All internal traffic flows through Railways [private networking](https://docs.railway.com/guides/private-networking). Only `bytebot-ui` is assigned a public domain.
---
## Step-by-Step Walk-through
<Steps>
<Step title="1. Open the Template">
Click the **Deploy on Railway** button above or visit [https://railway.com/deploy/bytebot?referralCode=L9lKXQ](https://railway.com/deploy/bytebot?referralCode=L9lKXQ).
</Step>
<Step title="2. Configure Environment">
Railway automatically detects required variables. Paste your **Anthropic API key** into `ANTHROPIC_API_KEY` and keep other defaults.
</Step>
<Step title="3. Kick off the Deployment">
Press **Deploy**. Railway will pull the pre-built images, create the Postgres database and link all services on a private network.
</Step>
<Step title="4. Launch Bytebot">
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!
_Tip: You can tail logs for each service from the Railway dashboard._
</Step>
</Steps>
<Note>
The first deploy downloads several container layers expect ~2 minutes. Subsequent redeploys are much faster.
</Note>
---
## Private Networking & Security
• **Private networking** ensures that the agent, desktop and database can communicate securely without exposing their ports to the internet.
• **Public exposure** is limited to the UI which serves static assets and proxies WebSocket traffic.
• **Add authentication** by placing the UI behind Railways built-in password protection or an external provider (e.g. Cloudflare Access, Auth0, OAuth proxy).
• You can also point a custom domain to the UI from the Railway dashboard and enable Cloudflare for WAF/CDN protection.
---
## Customisation & Scaling
1. **Change images** Fork the repo, push your own images and edit the templates `Dockerfile` references.
2. **Increase resources** Each service has an independent CPU/RAM slider in Railway. Bump up the desktop or agent if you plan heavy automations.
---
## Troubleshooting
| Symptom | Likely Cause | Fix |
| ------- | ------------ | ---- |
| Web UI shows “connecting…” | Desktop not ready or private networking mis-config | Wait for `bytebot-desktop` container to finish starting, or restart service |
| Agent errors `401 Anthropic` | Missing/invalid API key | Re-enter `ANTHROPIC_API_KEY` in Railway variables |
| Slow desktop video | Free Railway plan throttling | Upgrade plan or reduce screen resolution in desktop settings |
---
## Next Steps
• Explore the [REST APIs](/api-reference/introduction) to script tasks programmatically.
• Join our [Discord](https://discord.com/invite/zcb5wA2t4u) community for support and showcase your automations!
+4
View File
@@ -17,6 +17,10 @@
"group": "Getting Started",
"pages": ["introduction", "quickstart"]
},
{
"group": "Deployment",
"pages": ["deployment/railway"]
},
{
"group": "Core Concepts",
"pages": [
+2 -2
View File
@@ -95,11 +95,11 @@ Bytebot consists of four integrated components working together:
Ubuntu 22.04 with XFCE4, pre-installed apps, and the automation daemon
</Card>
<Card title="AI Agent" icon="brain" href="/core-concepts/agent-system">
NestJS service that uses Claude AI to plan and execute tasks
NestJS service that uses an LLM to plan and execute tasks
</Card>
<Card
title="Task Interface"
icon="comments"
icon="window"
href="/core-concepts/agent-system#task-ui"
>
Next.js web app for creating and managing tasks
+26
View File
@@ -3,6 +3,32 @@ title: "Quick Start"
description: "Get your AI desktop agent running in 2 minutes"
---
# Host Bytebot on Railway
## ☁️ One-click Deploy on Railway
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/bytebot?referralCode=L9lKXQ)
<Steps>
<Step title="Visit the Template">
Click the Deploy Now button in the Bytebot template on Railway.
</Step>
<Step title="Add Anthropic Key">
Enter your `ANTHROPIC_API_KEY` as the single required environment variable.
</Step>
<Step title="Deploy &amp; Launch">
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!
</Step>
</Steps>
<Note>
Need more details? See the full <a href="/deployment/railway">Railway deployment guide</a>.
</Note>
---
# Self-host Bytebot
## Prerequisites
- Docker ≥ 20.10
+7 -7
View File
@@ -16,7 +16,7 @@ ENV DISPLAY=:0
# -----------------------------------------------------------------------------
# 2. System dependencies installation
# -----------------------------------------------------------------------------
RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get update && apt-get install -y \
# X11 / VNC
xvfb x11vnc xauth x11-xserver-utils \
x11-apps sudo software-properties-common \
@@ -45,7 +45,7 @@ RUN mkdir -p /run/dbus && \
# 3. Additional software installation
# -----------------------------------------------------------------------------
# Install Firefox
RUN apt-get update && apt-get install -y --no-install-recommends \
RUN apt-get update && apt-get install -y \
# Install necessary for adding PPA
software-properties-common apt-transport-https wget gnupg \
# Install Additional Graphics Libraries
@@ -61,7 +61,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
fonts-freefont-ttf \
&& add-apt-repository -y ppa:mozillateam/ppa \
&& apt-get update \
&& apt-get install -y --no-install-recommends firefox-esr thunderbird \
&& apt-get install -y firefox-esr thunderbird \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
# Set Firefox as default browser system-wide
&& update-alternatives --install /usr/bin/x-www-browser x-www-browser /usr/bin/firefox-esr 200 \
@@ -136,11 +136,11 @@ RUN ARCH=$(dpkg --print-architecture) && \
mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22 && \
curl -sS https://downloads.1password.com/linux/keys/1password.asc | \
gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg && \
apt-get update && apt-get install -y --no-install-recommends 1password && \
apt-get update && apt-get install -y 1password && \
apt-get clean && rm -rf /var/lib/apt/lists/*; \
elif [ "$ARCH" = "arm64" ]; then \
# Install from tar.gz for ARM64
apt-get update && apt-get install -y --no-install-recommends \
apt-get update && apt-get install -y \
libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 xdg-utils \
libatspi2.0-0 libdrm2 libgbm1 libxcb-dri3-0 libxkbcommon0 \
libsecret-1-0 && \
@@ -183,7 +183,7 @@ RUN ARCH=$(dpkg --print-architecture) && \
# Install Node.js
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get install -y nodejs \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Upgrade pip
@@ -205,7 +205,7 @@ RUN git clone https://github.com/novnc/noVNC.git /opt/noVNC \
# Install dependencies required to build libnut-core and uiohook-napi
RUN apt-get update && \
apt-get install -y --no-install-recommends \
apt-get install -y \
cmake \
libx11-dev \
libxtst-dev \
+5
View File
@@ -1 +1,6 @@
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/bytebotdb
ANTHROPIC_API_KEY=
BYTEBOT_DESKTOP_BASE_URL=http://localhost:9990
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=http://localhost:9991
AUTH_ENABLED=true
+1 -1
View File
@@ -1,5 +1,5 @@
# Base image
FROM node:20.18.2-alpine3.21
FROM node:20-alpine
# Create app directory
WORKDIR /app
+249 -1
View File
@@ -20,6 +20,8 @@
"@nestjs/schedule": "^6.0.0",
"@nestjs/websockets": "^11.1.1",
"@prisma/client": "^6.6.0",
"@thallesp/nestjs-better-auth": "^1.0.0",
"better-auth": "^1.2.9",
"class-validator": "^0.14.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
@@ -783,6 +785,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/@better-auth/utils": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.5.tgz",
"integrity": "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==",
"dependencies": {
"typescript": "^5.8.2",
"uncrypto": "^0.1.3"
}
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz",
"integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="
},
"node_modules/@bytebot/shared": {
"resolved": "../shared",
"link": true
@@ -986,6 +1002,11 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1985,6 +2006,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@levischuck/tiny-cbor": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="
},
"node_modules/@lukeed/csprng": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
@@ -2746,6 +2772,25 @@
}
}
},
"node_modules/@noble/ciphers": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz",
"integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2800,6 +2845,59 @@
"npm": ">=5.10.0"
}
},
"node_modules/@peculiar/asn1-android": {
"version": "2.3.16",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz",
"integrity": "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz",
"integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz",
"integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz",
"integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==",
"dependencies": {
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz",
"integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@pkgr/core": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.2.tgz",
@@ -2842,6 +2940,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/@simplewebauthn/browser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz",
"integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="
},
"node_modules/@simplewebauthn/server": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.1.tgz",
"integrity": "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==",
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
"@peculiar/asn1-android": "^2.3.10",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -3197,6 +3317,18 @@
"node": ">=14.16"
}
},
"node_modules/@thallesp/nestjs-better-auth": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@thallesp/nestjs-better-auth/-/nestjs-better-auth-1.0.0.tgz",
"integrity": "sha512-fOu1z8LLyPyWOATm4g0ylz8miiCKxyAoWIssx/bdCvJKmUQXPltSHA/ne1MveD5DBbUI0aO24gdF0FnEsCR2Zw==",
"peerDependencies": {
"@nestjs/common": "^11.0.10",
"@nestjs/core": "^11.0.10",
"better-auth": "^1.2.8",
"express": "^5.0.0",
"typescript": "^5"
}
},
"node_modules/@tokenizer/inflate": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
@@ -4898,6 +5030,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/asn1js": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
"integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
"dependencies": {
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.3",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -5088,6 +5233,36 @@
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/better-auth": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.2.9.tgz",
"integrity": "sha512-WLqBXDzuaCQetQctLGC5oTfGmL32zUvxnM4Y+LZkhwseMaZWq5EKI+c/ZATgz2YkFt7726q659PF8CfB9P1VuA==",
"dependencies": {
"@better-auth/utils": "0.2.5",
"@better-fetch/fetch": "^1.1.18",
"@noble/ciphers": "^0.6.0",
"@noble/hashes": "^1.6.1",
"@simplewebauthn/browser": "^13.0.0",
"@simplewebauthn/server": "^13.0.0",
"better-call": "^1.0.8",
"defu": "^6.1.4",
"jose": "^5.9.6",
"kysely": "^0.28.2",
"nanostores": "^0.11.3",
"zod": "^3.24.1"
}
},
"node_modules/better-call": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.9.tgz",
"integrity": "sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==",
"dependencies": {
"@better-fetch/fetch": "^1.1.4",
"rou3": "^0.5.1",
"set-cookie-parser": "^2.7.1",
"uncrypto": "^0.1.3"
}
},
"node_modules/bin-version": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz",
@@ -5984,6 +6159,11 @@
"node": ">=10"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -8656,6 +8836,14 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jose": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8780,6 +8968,14 @@
"node": ">=6"
}
},
"node_modules/kysely": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.2.tgz",
"integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -9230,6 +9426,20 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/nanostores": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz",
"integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -9944,6 +10154,22 @@
],
"license": "MIT"
},
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -10217,6 +10443,11 @@
"node": ">=0.10.0"
}
},
"node_modules/rou3": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz",
"integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -10424,6 +10655,11 @@
"node": ">= 18"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -11715,7 +11951,6 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -11783,6 +12018,11 @@
"through": "^2.3.8"
}
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -12431,6 +12671,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.25.67",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
"integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+42 -40
View File
@@ -22,49 +22,51 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@anthropic-ai/sdk": "0.39.0",
"@anthropic-ai/sdk": "^0.39.0",
"@bytebot/shared": "../shared",
"@nestjs/common": "11.0.1",
"@nestjs/config": "4.0.2",
"@nestjs/core": "11.0.1",
"@nestjs/platform-express": "11.1.3",
"@nestjs/event-emitter": "3.0.1",
"@nestjs/platform-socket.io": "11.1.1",
"@nestjs/schedule": "6.0.0",
"@nestjs/websockets": "11.1.1",
"@prisma/client": "6.6.0",
"class-validator": "0.14.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"socket.io": "4.8.1",
"socket.io-client": "4.8.1"
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/platform-express": "^11.1.3",
"@nestjs/platform-socket.io": "^11.1.1",
"@nestjs/schedule": "^6.0.0",
"@nestjs/websockets": "^11.1.1",
"@prisma/client": "^6.6.0",
"@thallesp/nestjs-better-auth": "^1.0.0",
"better-auth": "^1.2.9",
"class-validator": "^0.14.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.18.0",
"@nestjs/cli": "11.0.0",
"@nestjs/schematics": "11.0.0",
"@nestjs/testing": "11.0.1",
"@swc/cli": "0.6.0",
"@swc/core": "1.10.7",
"@types/express": "5.0.0",
"@types/jest": "29.5.14",
"@types/node": "22.10.7",
"@types/supertest": "6.0.2",
"eslint": "9.18.0",
"eslint-config-prettier": "10.0.1",
"eslint-plugin-prettier": "5.2.2",
"globals": "15.14.0",
"jest": "29.7.0",
"prettier": "3.4.2",
"source-map-support": "0.5.21",
"supertest": "7.0.0",
"ts-jest": "29.2.5",
"ts-loader": "9.5.2",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "5.7.3",
"typescript-eslint": "8.20.0"
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
@@ -0,0 +1,81 @@
-- AlterTable
ALTER TABLE "Message" ADD COLUMN "userId" TEXT;
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"accessToken" TEXT,
"refreshToken" TEXT,
"accessTokenExpiresAt" TIMESTAMP(3),
"refreshTokenExpiresAt" TIMESTAMP(3),
"scope" TEXT,
"idToken" TEXT,
"password" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Verification" (
"id" TEXT NOT NULL,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Verification_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token");
-- CreateIndex
CREATE UNIQUE INDEX "Account_providerId_accountId_key" ON "Account"("providerId", "accountId");
-- CreateIndex
CREATE UNIQUE INDEX "Verification_identifier_value_key" ON "Verification"("identifier", "value");
-- AddForeignKey
ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "userId" TEXT;
-- AddForeignKey
ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -58,6 +58,8 @@ model Task {
result Json?
messages Message[]
summaries Summary[]
user User? @relation(fields: [userId], references: [id])
userId String? // Optional foreign key to User for task ownership
}
model Summary {
@@ -92,4 +94,69 @@ model Message {
taskId String
summary Summary? @relation(fields: [summaryId], references: [id])
summaryId String? // Optional foreign key to Summary
user User? @relation(fields: [userId], references: [id])
userId String? // Optional foreign key to User for message ownership
}
// Better Auth Models
model User {
id String @id @default(uuid())
name String?
email String @unique
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
sessions Session[]
accounts Account[]
messages Message[]
tasks Task[]
}
model Session {
id String @id @default(uuid())
userId String
token String @unique
expiresAt DateTime
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Account {
id String @id @default(uuid())
userId String
accountId String
providerId String
accessToken String?
refreshToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
idToken String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([providerId, accountId])
}
model Verification {
id String @id @default(uuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, value])
}
@@ -47,7 +47,7 @@ export class InputCaptureService {
return;
}
this.socket = io(baseUrl, { transports: ['websocket'], secure: true });
this.socket = io(baseUrl, { transports: ['websocket'] });
this.socket.on('connect', () => {
this.logger.log('Input socket connected');
+7
View File
@@ -9,9 +9,16 @@ import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { AuthModule } from '@thallesp/nestjs-better-auth';
import { auth } from './auth';
const authEnabled = process.env.AUTH_ENABLED === 'true';
@Module({
imports: [
...(authEnabled ? [AuthModule.forRoot(auth, {
disableTrustedOriginsCors: false,
})] : []),
ScheduleModule.forRoot(),
EventEmitterModule.forRoot(),
ConfigModule.forRoot({
+23
View File
@@ -0,0 +1,23 @@
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const auth = process.env.AUTH_ENABLED === 'true' ? betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
trustedOrigins: [
'http://localhost:9992',
'http://localhost:9991',
],
}) : null;
// @ts-ignore
export type Session = typeof auth extends null ? null : typeof auth.$Infer.Session;
// @ts-ignore
export type User = typeof auth extends null ? null : typeof auth.$Infer.Session.user;
+7 -3
View File
@@ -11,13 +11,17 @@ async function bootstrap() {
console.log('Starting bytebot-agent application...');
try {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create(AppModule, {
bodyParser: false,
});
// Set global prefix for all routes
app.setGlobalPrefix('api');
// Enable CORS
app.enableCors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
});
await app.listen(process.env.PORT ?? 9991);
@@ -21,4 +21,8 @@ export class CreateTaskDto {
@IsOptional()
@IsString()
createdBy?: Role;
@IsOptional()
@IsString()
userId?: string;
}
@@ -8,7 +8,9 @@ import {
Delete,
HttpStatus,
HttpCode,
UseGuards,
Query,
Req,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
@@ -16,8 +18,13 @@ import { UpdateTaskDto } from './dto/update-task.dto';
import { Message, Task } from '@prisma/client';
import { GuideTaskDto } from './dto/guide-task.dto';
import { MessagesService } from 'src/messages/messages.service';
import { AuthGuard } from '@thallesp/nestjs-better-auth';
import { Request } from 'express';
const authEnabled = process.env.AUTH_ENABLED === 'true';
@Controller('tasks')
@UseGuards(...(authEnabled ? [AuthGuard] : []))
export class TasksController {
constructor(
private readonly tasksService: TasksService,
@@ -26,7 +33,14 @@ export class TasksController {
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createTaskDto: CreateTaskDto): Promise<Task> {
async create(
@Body() createTaskDto: CreateTaskDto,
@Req() req: Request,
): Promise<Task> {
// Add user ID if auth is enabled and user is authenticated
if (authEnabled && (req as any).user?.id) {
createTaskDto.userId = (req as any).user.id;
}
return this.tasksService.create(createTaskDto);
}
@@ -52,6 +52,7 @@ export class TasksService {
priority: createTaskDto.priority || TaskPriority.MEDIUM,
status: TaskStatus.PENDING,
createdBy: createTaskDto.createdBy || Role.USER,
...(createTaskDto.userId ? { userId: createTaskDto.userId } : {}),
...(createTaskDto.scheduledFor
? { scheduledFor: createTaskDto.scheduledFor }
: {}),
@@ -123,6 +124,15 @@ export class TasksService {
this.logger.log('Retrieving all tasks');
const tasks = await this.prisma.task.findMany({
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
@@ -140,6 +150,15 @@ export class TasksService {
try {
const task = await this.prisma.task.findUnique({
where: { id },
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
if (!task) {
+4
View File
@@ -0,0 +1,4 @@
BYTEBOT_AGENT_BASE_URL=http://localhost:9991
BYTEBOT_DESKTOP_VNC_URL=ws://localhost:9990/websockify
NEXT_PUBLIC_API_URL=http://localhost:9991
NEXT_PUBLIC_AUTH_ENABLED=true
+1 -1
View File
@@ -1,5 +1,5 @@
# Base image
FROM node:20.18.2-alpine3.21
FROM node:20-alpine
# Declare build arguments
ARG BYTEBOT_AGENT_BASE_URL
+769 -70
View File
File diff suppressed because it is too large Load Diff
+47 -44
View File
@@ -9,53 +9,56 @@
"lint": "next lint"
},
"dependencies": {
"@anthropic-ai/sdk": "0.39.0",
"@anthropic-ai/sdk": "^0.39.0",
"@bytebot/shared": "../shared",
"@hugeicons/core-free-icons": "1.0.14",
"@hugeicons/react": "1.0.5",
"@prisma/client": "6.5.0",
"@radix-ui/react-dropdown-menu": "2.1.6",
"@radix-ui/react-popover": "1.1.11",
"@radix-ui/react-scroll-area": "1.2.3",
"@radix-ui/react-select": "2.2.2",
"@radix-ui/react-separator": "1.1.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-switch": "1.1.3",
"@types/express": "5.0.1",
"@types/http-proxy": "1.17.16",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "4.1.0",
"dotenv": "16.4.7",
"express": "4.21.2",
"http-proxy": "1.18.1",
"http-proxy-middleware": "3.0.5",
"motion": "12.12.1",
"next": "15.2.4",
"next-themes": "0.4.6",
"next-transpile-modules": "10.0.1",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-markdown": "10.1.0",
"react-vnc": "3.1.0",
"socket.io-client": "4.8.1",
"tailwind-merge": "3.0.2",
"tsx": "4.19.3",
"tw-animate-css": "1.2.4",
"zod": "3.24.2"
"@hugeicons/core-free-icons": "^1.0.14",
"@hugeicons/react": "^1.0.5",
"@prisma/client": "^6.5.0",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@types/express": "^5.0.1",
"@types/http-proxy": "^1.17.16",
"better-auth": "^1.2.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
"lucide-react": "^0.517.0",
"motion": "^12.12.1",
"next": ">=15.2.4",
"next-themes": "^0.4.6",
"next-transpile-modules": "^10.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-vnc": "^3.1.0",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.0.2",
"tsx": "^4.19.3",
"tw-animate-css": "^1.2.4",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "3.2.0",
"@tailwindcss/postcss": "4.1.3",
"@types/node": "20.17.27",
"@types/react": "19.0.7",
"@types/react-dom": "19.0.3",
"eslint": "9.18.0",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.3",
"@types/node": "^20.17.27",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.2.3",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "0.6.11",
"prisma": "6.5.0",
"tailwindcss": "4.1.4",
"typescript": "5.7.3"
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"prisma": "^6.5.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
@@ -6,11 +6,17 @@ import { NextRequest } from "next/server";
async function proxy(req: NextRequest, path: string[]): Promise<Response> {
const BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL!;
const subPath = path.length ? `/${path.join("/")}` : "";
const url = `${BASE_URL}${subPath}${req.nextUrl.search}`;
const url = `${BASE_URL}/api${subPath}${req.nextUrl.search}`;
// Extract cookies from the incoming request
const cookies = req.headers.get('cookie');
const init: RequestInit = {
method: req.method,
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
...(cookies && { "Cookie": cookies })
},
body:
req.method === "GET" || req.method === "HEAD"
? undefined
@@ -20,9 +26,22 @@ async function proxy(req: NextRequest, path: string[]): Promise<Response> {
const res = await fetch(url, init);
const body = await res.text();
// Extract Set-Cookie headers from the backend response
const setCookieHeaders = res.headers.getSetCookie?.() || [];
// Create response headers
const responseHeaders = new Headers({
"Content-Type": "application/json"
});
// Add Set-Cookie headers if they exist
setCookieHeaders.forEach(cookie => {
responseHeaders.append("Set-Cookie", cookie);
});
return new Response(body, {
status: res.status,
headers: { "Content-Type": "application/json" },
headers: responseHeaders,
});
}
+4 -1
View File
@@ -2,6 +2,7 @@ import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { AuthProvider } from "@/components/providers/auth-provider"
const inter = Inter({ subsets: ["latin"] })
@@ -18,7 +19,9 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
{children}
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
)
+162
View File
@@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { signIn, signUp } from "@/lib/auth";
import Image from "next/image";
export default function LoginPage() {
const [isSignUp, setIsSignUp] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError("");
try {
if (isSignUp) {
const result = await signUp.email({
email,
password,
name,
});
if (result.error) {
setError(result.error.message || "Sign up failed");
} else {
router.push("/");
}
} else {
const result = await signIn.email({
email,
password,
});
if (result.error) {
setError(result.error.message || "Sign in failed");
} else {
router.push("/");
}
}
} catch (err) {
setError("An unexpected error occurred");
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8">
<div className="text-center">
<div className="flex justify-center mb-4">
<Image
src="/bytebot_square_light.svg"
alt="Bytebot"
width={64}
height={64}
className="h-12 w-12"
/>
</div>
<h2 className="text-2xl font-bold text-gray-900">
{isSignUp ? "Create your account" : "Sign in to Bytebot"}
</h2>
<p className="mt-2 text-sm text-gray-600">
{isSignUp
? "Join Bytebot to start automating your tasks"
: "Welcome back! Please sign in to continue"}
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<div className="space-y-4">
{isSignUp && (
<div>
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
name="name"
type="text"
required={isSignUp}
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-1"
placeholder="Enter your full name"
/>
</div>
)}
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
name="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1"
placeholder="Enter your email"
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1"
placeholder="Enter your password"
/>
</div>
</div>
<div>
<Button
type="submit"
className="w-full cursor-pointer"
disabled={isLoading}
>
{isLoading
? "Loading..."
: isSignUp
? "Create Account"
: "Sign In"}
</Button>
</div>
<div className="text-center">
<button
type="button"
className="text-sm text-gray-600 hover:text-gray-500 cursor-pointer"
onClick={() => setIsSignUp(!isSignUp)}
>
{isSignUp
? "Already have an account? Sign in"
: "Need an account? Sign up"}
</button>
</div>
</form>
</div>
</div>
);
}
+15 -12
View File
@@ -49,7 +49,7 @@ export default function Home() {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
buttonsRef.current &&
!buttonsRef.current.contains(event.target as Node)
@@ -144,7 +144,10 @@ export default function Home() {
>;
return (
<div className="relative mt-6 flex w-full flex-wrap justify-start gap-1" ref={popoverRef}>
<div
className="relative mt-6 flex w-full flex-wrap justify-start gap-1"
ref={popoverRef}
>
{/* Container for buttons */}
<div className="flex w-full flex-wrap gap-1" ref={buttonsRef}>
{topicNames.map((topic, index) => (
@@ -168,9 +171,9 @@ export default function Home() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.3,
ease: [0.4, 0.0, 0.2, 1]
transition={{
duration: 0.3,
ease: [0.4, 0.0, 0.2, 1],
}}
className="bg-bytebot-bronze-light-3 shadow-bytebot border-bytebot-bronze-light-7 absolute top-full left-0 z-40 mt-1 w-[500px] overflow-hidden rounded-xl border p-1.5"
>
@@ -181,10 +184,10 @@ export default function Home() {
key={idx}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.2,
transition={{
duration: 0.2,
delay: idx * 0.03,
ease: [0.4, 0.0, 0.2, 1]
ease: [0.4, 0.0, 0.2, 1],
}}
className="text-bytebot-bronze-light-12 hover:bg-bytebot-bronze-light-2 cursor-pointer rounded-md px-3 py-1.5 text-sm transition-colors"
onClick={() => {
@@ -232,12 +235,12 @@ export default function Home() {
minLines={3}
/>
<div className="mt-2">
<Select defaultValue="sonnet-4">
<Select defaultValue="opus-4">
<SelectTrigger className="w-auto">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sonnet-4">Model: Sonnet 4</SelectItem>
<SelectItem value="opus-4">Model: Opus 4</SelectItem>
</SelectContent>
</Select>
</div>
@@ -282,12 +285,12 @@ export default function Home() {
minLines={3}
/>
<div className="mt-2">
<Select defaultValue="sonnet-4">
<Select defaultValue="opus-4">
<SelectTrigger className="w-auto">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sonnet-4">Model: Sonnet 4</SelectItem>
<SelectItem value="opus-4">Model: Opus 4</SelectItem>
</SelectContent>
</Select>
</div>
@@ -209,7 +209,10 @@ export default function TaskPage() {
</div>
{/* Chat Area */}
<div ref={chatContainerRef} className="col-span-3 flex h-full flex-col overflow-scroll">
<div
ref={chatContainerRef}
className="col-span-3 flex h-full flex-col overflow-scroll"
>
{/* Messages scrollable area */}
<div className="flex-1 px-4 pt-4 pb-2">
<ChatContainer
@@ -235,12 +238,12 @@ export default function TaskPage() {
minLines={1}
/>
<div className="mt-2">
<Select value="sonnet-4">
<Select value="opus-4">
<SelectTrigger className="w-auto">
<SelectValue placeholder="Select an model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sonnet-4">Sonnet 4</SelectItem>
<SelectItem value="opus-4">Opus 4</SelectItem>
</SelectContent>
</Select>
</div>
@@ -2,11 +2,21 @@ import React, { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { useTheme } from "next-themes";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { signOut, useSession } from "@/lib/auth";
import { HugeiconsIcon } from "@hugeicons/react";
import {
DocumentCodeIcon,
TaskDaily01Icon,
Home01Icon,
User03Icon,
Logout01Icon,
ComputerIcon,
} from "@hugeicons/core-free-icons";
import { usePathname } from "next/navigation";
@@ -21,6 +31,7 @@ export function Header() {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const pathname = usePathname();
const { data: session } = useSession();
// After mounting, we can safely show the theme-dependent content
useEffect(() => {
@@ -93,7 +104,27 @@ export function Header() {
</Link>
</div>
</div>
<div className="flex items-center gap-3"></div>
<div className="flex items-center gap-3">
{session && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="flex items-center gap-2">
<HugeiconsIcon icon={User03Icon} className="w-4 h-4" />
<span className="text-sm">{session.user.name || session.user.email}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => signOut()}
className="flex items-center gap-2 text-red-600"
>
<HugeiconsIcon icon={Logout01Icon} className="w-4 h-4" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</header>
);
}
@@ -89,19 +89,18 @@ export function ChatContainer({
scrollRef,
}: ChatContainerProps) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const loadMoreRef = useRef<HTMLDivElement>(null);
// Group back-to-back messages from the same role
const groupedConversation = groupBackToBackMessages(filterMessages(messages));
// Infinite scroll handler
const handleScroll = useCallback(() => {
if (!scrollRef?.current || !loadMoreMessages) {
console.log('Scroll handler early return:', {
console.log("Scroll handler early return:", {
hasScrollRef: !!scrollRef?.current,
hasLoadMoreMessages: !!loadMoreMessages,
hasMoreMessages,
isLoadingMoreMessages
isLoadingMoreMessages,
});
return;
}
@@ -110,18 +109,18 @@ export function ChatContainer({
// Check if user scrolled to the bottom (within 20px - much more sensitive)
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
console.log('Scroll metrics:', {
console.log("Scroll metrics:", {
scrollTop,
scrollHeight,
clientHeight,
distanceFromBottom,
hasMoreMessages,
isLoadingMoreMessages
isLoadingMoreMessages,
});
if (distanceFromBottom <= 20 && hasMoreMessages && !isLoadingMoreMessages) {
console.log('Triggering loadMoreMessages');
console.log("Triggering loadMoreMessages");
loadMoreMessages();
}
}, [scrollRef, loadMoreMessages, hasMoreMessages, isLoadingMoreMessages]);
@@ -130,8 +129,8 @@ export function ChatContainer({
useEffect(() => {
const container = scrollRef?.current;
if (container) {
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}
}, [handleScroll, scrollRef]);
@@ -163,13 +162,13 @@ export function ChatContainer({
<MessageGroup group={group} messages={messages} />
</div>
))}
{taskStatus === TaskStatus.RUNNING && control === Role.ASSISTANT && (
<TextShimmer className="text-sm" duration={2}>
Bytebot is working...
</TextShimmer>
)}
{/* Loading indicator for infinite scroll at bottom */}
{isLoadingMoreMessages && (
<div className="flex justify-center py-4">
@@ -0,0 +1,45 @@
"use client";
import { useSession } from "@/lib/auth";
import { useRouter, usePathname } from "next/navigation";
import { useEffect } from "react";
interface AuthProviderProps {
children: React.ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const { data: session, isPending } = useSession();
const router = useRouter();
const pathname = usePathname();
const isPublicRoute = pathname === "/login";
useEffect(() => {
if (!isPending) {
if (!session && !isPublicRoute) {
// No session and not on a public route, redirect to login
router.push("/login");
} else if (session && isPublicRoute) {
// Has session but on login page, redirect to home
router.push("/");
}
}
}, [session, isPending, isPublicRoute, router]);
// Show loading spinner while checking auth
if (isPending) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="border-t-primary h-8 w-8 animate-spin rounded-full border-4 border-gray-300"></div>
</div>
);
}
// Show login page for unauthenticated users
if (!session && !isPublicRoute) {
return null; // Router will handle redirect
}
return <>{children}</>;
}
@@ -39,12 +39,24 @@ export const TaskItem: React.FC<TaskItemProps> = ({ task }) => {
return (
<Link href={`/tasks/${task.id}`} className="block">
<div className="shadow-bytebot bg-bytebot-bronze-light-2 border-bytebot-bronze-light-5 hover:bg-bytebot-bronze-light-3 flex min-h-24 items-start rounded-lg border-[0.5px] px-5 py-4 transition-colors">
<div className="flex-1 space-y-1">
<div className="flex-1 space-y-2">
<div className="text-byhtebot-bronze-dark-7 text-sm font-medium">
{capitalizeFirstChar(task.description)}
</div>
<div className="text-byhtebot-bronze-dark-11 text-xs">
{formatDate(task.createdAt)}
<div className="text-byhtebot-bronze-dark-11 text-xs flex items-center justify-start space-x-1.5">
<span className="text-byhtebot-bronze-dark-9">
{formatDate(task.createdAt)}
</span>
{task.user && (
<>
<span className="text-byhtebot-bronze-dark-9">
</span>
<span className="text-byhtebot-bronze-dark-9">
{task.user.name || task.user.email}
</span>
</>
)}
</div>
{task.status === TaskStatus.COMPLETED && (
<div className="bg-bytebot-green-3 border-bytebot-green-a5 inline-flex w-fit items-center space-x-1 rounded-full border px-1.5 py-0.5">
@@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { HugeiconsIcon } from "@hugeicons/react"
import { Tick01Icon, ArrowRight01Icon } from "@hugeicons/core-free-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"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",
inset && "pl-8",
className
)}
{...props}
>
{children}
<HugeiconsIcon icon={ArrowRight01Icon} className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"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",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"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",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"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",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"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",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<HugeiconsIcon icon={Tick01Icon} className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"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",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<div className="h-2 w-2 rounded-full bg-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+19
View File
@@ -0,0 +1,19 @@
import { createAuthClient } from 'better-auth/react';
const authEnabled = process.env.NEXT_PUBLIC_AUTH_ENABLED === 'true';
export const authClient = authEnabled ? createAuthClient() : null;
export const {
signIn,
signUp,
signOut,
useSession,
getSession,
} = authClient || {
signIn: () => Promise.resolve(null),
signUp: () => Promise.resolve(null),
signOut: () => Promise.resolve(null),
useSession: () => ({ data: null, isPending: false, error: null }),
getSession: () => Promise.resolve(null),
};
+21
View File
@@ -32,12 +32,33 @@ export enum TaskPriority {
URGENT = "URGENT",
}
export enum TaskType {
IMMEDIATE = "IMMEDIATE",
SCHEDULED = "SCHEDULED",
}
export interface User {
id: string;
name?: string;
email: string;
}
export interface Task {
id: string;
description: string;
type: TaskType;
status: TaskStatus;
priority: TaskPriority;
control: Role;
createdBy: Role;
createdAt: string;
updatedAt: string;
scheduledFor?: string;
executedAt?: string;
completedAt?: string;
queuedAt?: string;
error?: string;
result?: any;
userId?: string;
user?: User;
}
@@ -27,6 +27,7 @@ export async function fetchTaskMessages(
headers: {
"Content-Type": "application/json",
},
credentials: 'include', // Include cookies for auth
});
if (!response.ok) {
@@ -55,6 +56,7 @@ export async function fetchTaskById(taskId: string): Promise<Task | null> {
headers: {
"Content-Type": "application/json",
},
credentials: 'include',
},
);
@@ -85,6 +87,7 @@ export async function startTask(message: string): Promise<Task | null> {
"Content-Type": "application/json",
},
body: JSON.stringify({ description: message }),
credentials: 'include',
},
);
@@ -112,6 +115,7 @@ export async function guideTask(
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
credentials: 'include',
},
);
@@ -135,6 +139,7 @@ export async function fetchTasks(): Promise<Task[]> {
headers: {
"Content-Type": "application/json",
},
credentials: 'include',
},
);
@@ -159,6 +164,7 @@ export async function takeOverTask(taskId: string): Promise<Task | null> {
headers: {
"Content-Type": "application/json",
},
credentials: 'include',
},
);
@@ -182,6 +188,7 @@ export async function resumeTask(taskId: string): Promise<Task | null> {
headers: {
"Content-Type": "application/json",
},
credentials: 'include',
},
);
+40 -40
View File
@@ -26,48 +26,48 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "11.1.2",
"@nestjs/config": "4.0.1",
"@nestjs/core": "11.0.1",
"@nestjs/platform-express": "11.1.3",
"@nestjs/platform-socket.io": "11.1.2",
"@nestjs/serve-static": "5.0.3",
"@nestjs/websockets": "11.1.2",
"@nut-tree-fork/nut-js": "4.2.6",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"http-proxy-middleware": "3.0.5",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"socket.io": "4.8.1",
"uiohook-napi": "1.5.4"
"@nestjs/common": "^11.1.2",
"@nestjs/config": "^4.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.1.3",
"@nestjs/platform-socket.io": "^11.1.2",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/websockets": "^11.1.2",
"@nut-tree-fork/nut-js": "^4.2.6",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"http-proxy-middleware": "^3.0.5",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"uiohook-napi": "^1.5.4"
},
"devDependencies": {
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.18.0",
"@nestjs/cli": "11.0.0",
"@nestjs/schematics": "11.0.0",
"@nestjs/testing": "11.0.1",
"@swc/cli": "0.6.0",
"@swc/core": "1.10.7",
"@types/express": "5.0.0",
"@types/jest": "29.5.14",
"@types/node": "22.13.14",
"@types/supertest": "6.0.2",
"eslint": "9.18.0",
"eslint-config-prettier": "10.0.1",
"eslint-plugin-prettier": "5.2.2",
"globals": "15.14.0",
"jest": "29.7.0",
"prettier": "3.4.2",
"source-map-support": "0.5.21",
"supertest": "7.0.0",
"ts-jest": "29.2.5",
"ts-loader": "9.5.2",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "5.7.3",
"typescript-eslint": "8.20.0"
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.14",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
+9 -9
View File
@@ -19,15 +19,15 @@
}
},
"devDependencies": {
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.18.0",
"eslint": "9.18.0",
"eslint-config-prettier": "10.0.1",
"eslint-plugin-prettier": "5.2.2",
"globals": "15.14.0",
"prettier": "3.4.2",
"typescript": "5.7.3",
"typescript-eslint": "8.20.0"
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^15.14.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"files": [
"dist"