Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Larsen
9c18aebde8 Remove Copilot package.json and tsconfig.json files as part of project cleanup 2025-08-18 10:57:56 +01:00
3647 changed files with 141536 additions and 400298 deletions

View File

@@ -33,15 +33,6 @@ stop
nohup.out*
# Large directories not needed for Docker builds
E2E/playwright-report
E2E/test-results
Terraform
HelmChart
Scripts
.git
GoSDK
encrypted-credentials.tar
encrypted-credentials/

View File

@@ -1,17 +0,0 @@
---
applyTo: '**'
---
# Building and Compiling
If you would like to compile or build any project. Please cd into the directory and run the following command:
```
npm run compile
```
Ths will make sure there are no type / syntax errors.
# Typescript Types.
Please do not use "any" types. Please create proper types where required.

View File

@@ -10,6 +10,36 @@ on:
jobs:
docker-build-accounts:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Accounts/Dockerfile .
docker-build-isolated-vm:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./IsolatedVM/Dockerfile .
docker-build-home:
runs-on: ubuntu-latest
env:
@@ -19,19 +49,11 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./Home/Dockerfile .
run: sudo docker build -f ./Home/Dockerfile .
docker-build-worker:
runs-on: ubuntu-latest
@@ -42,20 +64,72 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./Worker/Dockerfile .
run: sudo docker build -f ./Worker/Dockerfile .
docker-build-workflow:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Workflow/Dockerfile .
docker-build-api-reference:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./APIReference/Dockerfile .
docker-build-docs:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Docs/Dockerfile .
docker-build-otel-collector:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./OTelCollector/Dockerfile .
docker-build-app:
runs-on: ubuntu-latest
@@ -66,22 +140,29 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./App/Dockerfile .
run: sudo docker build -f ./App/Dockerfile .
docker-build-copilot:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Copilot/Dockerfile .
docker-build-e2e:
runs-on: ubuntu-latest
env:
@@ -91,20 +172,42 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./E2E/Dockerfile .
run: sudo docker build -f ./E2E/Dockerfile .
docker-build-admin-dashboard:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for home
- name: build docker image
run: sudo docker build -f ./AdminDashboard/Dockerfile .
docker-build-dashboard:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for home
- name: build docker image
run: sudo docker build -f ./Dashboard/Dockerfile .
docker-build-probe:
runs-on: ubuntu-latest
@@ -115,21 +218,13 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image probe api
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./Probe/Dockerfile .
run: sudo docker build -f ./Probe/Dockerfile .
docker-build-telemetry:
docker-build-probe-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
@@ -138,62 +233,98 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image probe api
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./Telemetry/Dockerfile .
run: sudo docker build -f ./ProbeIngest/Dockerfile .
docker-build-server-monitor-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
docker-build-open-telemetry-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
docker-build-incoming-request-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
docker-build-fluent-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./FluentIngest/Dockerfile .
docker-build-status-page:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for home
- name: build docker image
run: sudo docker build -f ./StatusPage/Dockerfile .
docker-build-test-server:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
- name: Preinstall
run: npm run prerun
# build image for mail service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./TestServer/Dockerfile .
docker-build-ai-agent:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for ai agent service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./AIAgent/Dockerfile .
run: sudo docker build -f ./TestServer/Dockerfile .

View File

@@ -20,12 +20,19 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Accounts
uses: nick-fields/retry@v3
- run: cd Accounts && npm install && npm run compile && npm run dep-check
compile-isolated-vm:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/Accounts && npm install && npm run compile && npm run dep-check
node-version: latest
- run: cd Common && npm install
- run: cd IsolatedVM && npm install && npm run compile && npm run dep-check
compile-common:
runs-on: ubuntu-latest
@@ -36,12 +43,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Compile Common
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Common && npm install && npm run compile && npm run dep-check
- run: cd Common && npm install && npm run compile && npm run dep-check
compile-app:
runs-on: ubuntu-latest
@@ -53,12 +55,7 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile App
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App && npm install && npm run compile && npm run dep-check
- run: cd App && npm install && npm run compile && npm run dep-check
compile-home:
runs-on: ubuntu-latest
@@ -70,12 +67,7 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Home
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Home && npm install && npm run compile && npm run dep-check
- run: cd Home && npm install && npm run compile && npm run dep-check
compile-worker:
runs-on: ubuntu-latest
@@ -87,13 +79,55 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Worker
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Worker && npm install && npm run compile && npm run dep-check
- run: cd Worker && npm install && npm run compile && npm run dep-check
compile-workflow:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Workflow && npm install && npm run compile && npm run dep-check
compile-api-reference:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd APIReference && npm install && npm run compile && npm run dep-check
compile-docs-reference:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Docs && npm install && npm run compile && npm run dep-check
compile-copilot:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Copilot && npm install && npm run compile && npm run dep-check
compile-nginx:
runs-on: ubuntu-latest
@@ -106,12 +140,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Nginx
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Nginx && npm install && npm run compile && npm run dep-check
- run: cd Nginx && npm install && npm run compile && npm run dep-check
compile-infrastructure-agent:
runs-on: ubuntu-latest
@@ -121,12 +150,7 @@ jobs:
- uses: actions/checkout@v4
# Setup Go
- uses: actions/setup-go@v5
- name: Compile Infrastructure Agent
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd InfrastructureAgent && go build .
- run: cd InfrastructureAgent && go build .
compile-admin-dashboard:
@@ -140,12 +164,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Admin Dashboard
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/AdminDashboard && npm install && npm run compile && npm run dep-check
- run: cd AdminDashboard && npm install && npm run compile && npm run dep-check
compile-dashboard:
runs-on: ubuntu-latest
@@ -158,12 +177,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Dashboard
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/Dashboard && npm install && npm run compile && npm run dep-check
- run: cd Dashboard && npm install && npm run compile && npm run dep-check
compile-e2e:
@@ -177,12 +191,7 @@ jobs:
node-version: latest
- run: sudo apt-get update
- run: cd Common && npm install
- name: Compile E2E
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd E2E && npm install && npm run compile && npm run dep-check
- run: cd E2E && npm install && npm run compile && npm run dep-check
compile-probe:
runs-on: ubuntu-latest
@@ -194,14 +203,9 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Probe
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Probe && npm install && npm run compile && npm run dep-check
- run: cd Probe && npm install && npm run compile && npm run dep-check
compile-telemetry:
compile-probe-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
@@ -211,13 +215,57 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Telemetry
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Telemetry && npm install && npm run compile && npm run dep-check
- run: cd ProbeIngest && npm install && npm run compile && npm run dep-check
compile-server-monitor-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
compile-open-telemetry-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
compile-incoming-request-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
compile-fluent-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd FluentIngest && npm install && npm run compile && npm run dep-check
compile-status-page:
runs-on: ubuntu-latest
@@ -230,12 +278,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Status Page
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/StatusPage && npm install && npm run compile && npm run dep-check
- run: cd StatusPage && npm install && npm run compile && npm run dep-check
compile-test-server:
runs-on: ubuntu-latest
@@ -247,31 +290,9 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Test Server
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd TestServer && npm install && npm run compile && npm run dep-check
- run: cd TestServer && npm install && npm run compile && npm run dep-check
compile-mobile-app:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install && npm run compile
- name: Compile MobileApp
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MobileApp && npm install && npm run compile
compile-ai-agent:
compile-mcp:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
@@ -281,26 +302,4 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile AIAgent
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd AIAgent && npm install && npm run compile && npm run dep-check
compile-cli:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- name: Compile CLI
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd CLI && npm install && npm run compile && npm run dep-check
- run: cd MCP && npm install && npm run compile && npm run dep-check

View File

@@ -1,49 +0,0 @@
name: NPM Audit Fix
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
npm-audit-fix:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run npm audit fix across packages
run: npm run audit-fix
- name: Detect changes
id: changes
run: |
if git status --porcelain | grep .; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
- name: Create pull request
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore: npm audit fix"
title: "chore: npm audit fix"
body: |
Automated npm audit fix run.
Workflow: ${{ github.workflow }}
Run ID: ${{ github.run_id }}
branch: chore/npm-audit-fix
delete-branch: true

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
name: "OneUptime Reliability Copilot"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
# Run every day at midnight UTC
- cron: '0 0 * * *'
jobs:
analyze:
name: Analyze Code
runs-on: ubuntu-latest
steps:
# Run Reliability Copilot in Docker Container
- name: Run Copilot
run: |
docker run --rm \
-e ONEUPTIME_URL="https://test.oneuptime.com" \
-e ONEUPTIME_REPOSITORY_SECRET_KEY="${{ secrets.COPILOT_ONEUPTIME_REPOSITORY_SECRET_KEY }}" \
-e CODE_REPOSITORY_PASSWORD="${{ github.token }}" \
-e CODE_REPOSITORY_USERNAME="simlarsen" \
-e OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
--net=host oneuptime/copilot:test

View File

@@ -1,143 +0,0 @@
name: Terraform Provider E2E Tests
permissions:
contents: read
on:
pull_request:
push:
branches:
- main
- master
- develop
workflow_dispatch:
jobs:
terraform-e2e-tests:
runs-on: ubuntu-latest
timeout-minutes: 120
env:
CI_PIPELINE_ID: ${{ github.run_number }}
APP_TAG: latest
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Additional Disk Cleanup
run: |
echo "=== Initial disk space ==="
df -h
echo "=== Removing unnecessary tools and libraries ==="
# Remove Android SDK (if not already removed)
sudo rm -rf /usr/local/lib/android || true
# Remove .NET SDK and runtime
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /etc/skel/.dotnet || true
# Remove Haskell/GHC
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/.ghcup || true
# Remove CodeQL
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
# Remove Boost
sudo rm -rf /usr/local/share/boost || true
# Remove Swift
sudo rm -rf /usr/share/swift || true
# Remove Julia
sudo rm -rf /usr/local/julia* || true
# Remove Rust (cargo/rustup)
sudo rm -rf /usr/share/rust || true
sudo rm -rf /home/runner/.rustup || true
sudo rm -rf /home/runner/.cargo || true
# Remove unnecessary hostedtoolcache items
sudo rm -rf /opt/hostedtoolcache/Python || true
sudo rm -rf /opt/hostedtoolcache/PyPy || true
sudo rm -rf /opt/hostedtoolcache/Ruby || true
sudo rm -rf /opt/hostedtoolcache/Java* || true
# Remove additional large directories
sudo rm -rf /usr/share/miniconda || true
sudo rm -rf /usr/local/graalvm || true
sudo rm -rf /usr/local/share/chromium || true
sudo rm -rf /usr/local/share/powershell || true
sudo rm -rf /usr/share/az_* || true
# Remove documentation
sudo rm -rf /usr/share/doc || true
sudo rm -rf /usr/share/man || true
# Remove unnecessary locales
sudo rm -rf /usr/share/locale || true
# Clean apt cache
sudo apt-get clean || true
sudo rm -rf /var/lib/apt/lists/* || true
sudo rm -rf /var/cache/apt/archives/* || true
# Clean tmp
sudo rm -rf /tmp/* || true
echo "=== Moving Docker data to /mnt for more space ==="
# Stop docker
sudo systemctl stop docker || true
# Move docker data directory to /mnt (which has ~70GB)
sudo mv /var/lib/docker /mnt/docker || true
sudo mkdir -p /var/lib/docker || true
sudo mount --bind /mnt/docker /var/lib/docker || true
# Restart docker
sudo systemctl start docker || true
echo "=== Final disk space ==="
df -h
echo "=== Docker info ==="
docker info | grep -E "Docker Root Dir|Storage Driver" || true
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
cache: true
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.6.0"
terraform_wrapper: false
- name: Run E2E Tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 60
max_attempts: 3
command: |
chmod +x ./E2E/Terraform/e2e-tests/scripts/*.sh
./E2E/Terraform/e2e-tests/scripts/index.sh

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.21'
cache: true
- name: Install Common dependencies
@@ -77,21 +77,17 @@ jobs:
ls -la "$PROVIDER_DIR" || true
- name: Test Go build
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: |
PROVIDER_DIR="./Terraform"
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
cd "$PROVIDER_DIR"
echo "🔨 Testing Go build..."
go mod tidy
go build -v ./...
echo "✅ Go build successful"
else
echo "⚠️ Cannot test build - missing go.mod or provider directory"
fi
run: |
PROVIDER_DIR="./Terraform"
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
cd "$PROVIDER_DIR"
echo "🔨 Testing Go build..."
go mod tidy
go build -v ./...
echo "✅ Go build successful"
else
echo "⚠️ Cannot test build - missing go.mod or provider directory"
fi
- name: Upload Terraform provider as artifact
uses: actions/upload-artifact@v4

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
name: CLI Test
name: Fluent Ingest Test
on:
pull_request:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
@@ -17,5 +17,4 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd CLI && npm install && npm run test
- run: cd FluentIngest && npm install && npm run test

View File

@@ -0,0 +1,21 @@
name: Incoming Request Ingest Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd IncomingRequestIngest && npm install && npm run test

View File

@@ -1,4 +1,4 @@
name: Telemetry Test
name: MCP Server Test
on:
pull_request:
@@ -16,7 +16,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
node-version: latest
- run: cd Common && npm install
- run: cd Telemetry && npm install && npm run test
- run: cd MCP && npm install && npm run test

View File

@@ -1,39 +0,0 @@
name: MobileApp Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
expo-doctor:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd MobileApp && npm install
- name: Run Expo Doctor
run: cd MobileApp && npx expo-doctor@latest
expo-web-export:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd MobileApp && npm install
- name: Export Web Bundle
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MobileApp && npx expo export --platform web

View File

@@ -0,0 +1,21 @@
name: OpenTelemetryIngest Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd OpenTelemetryIngest && npm install && npm run test

View File

@@ -1,15 +1,13 @@
name: AIAgent Test
name: ProbeIngest Test
on:
pull_request:
pull_request:
push:
branches-ignore:
- 'hotfix-*'
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
@@ -19,5 +17,5 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd AIAgent && npm install && npm run test
- run: cd ProbeIngest && npm install && npm run test

19
.gitignore vendored
View File

@@ -65,7 +65,7 @@ secret.env
# This file is autogenerated from the template
*/.env
**/Dockerfile
*/Dockerfile
config.env
config.env.tmp
config.env.temp
@@ -116,8 +116,8 @@ InfrastructureAgent/oneuptime-infrastructure-agent
# Terraform generated files
openapi.json
Terraform/terraform-provider-oneuptime/**
Terraform/openapi.json
Terraform/**
TerraformTest/**
terraform-provider-example/**
@@ -127,17 +127,4 @@ MCP/build/
MCP/.env
MCP/node_modules
Dashboard/public/sw.js
App/Dashboard/public/sw.js
.claude/settings.local.json
Common/.claude/settings.local.json
E2E/Terraform/e2e-tests/test-env.sh
# Terraform state and plan files
*.tfplan
tfplan
terraform.tfstate
terraform.tfstate.backup
.terraform/
.terraform.lock.hcl
.claude/worktrees/**
App/FeatureSet/Dashboard/public/sw.js

15
.oneuptime/README.md Normal file
View File

@@ -0,0 +1,15 @@
## OneUptime Copilot
This folder contains the configuration files for the OneUptime Copilot. The Copilot is a tool that automatically improves your code. It can fix issues, improve code quality, and help you ship faster.
This folder has the following structure:
- `config.js`: The configuration file for the Copilot. You can customize the Copilot's behavior by changing this file.
- `scripts`: A folder containing scripts that the Copilot runs. These are hooks that run at different stages of the Copilot's process.
- `on-after-clone.sh`: A script that runs after the Copilot clones your repository.
- `on-before-code-change.sh`: A script that runs before the Copilot makes changes to your code.
- `on-after-code-change.sh`: A script that runs after the Copilot makes changes to your code.
- `on-before-commit.sh`: A script that runs before the Copilot commits changes to your repository.
- `on-after-commit.sh`: A script that runs after the Copilot commits changes to your repository.

10
.oneuptime/config.js Normal file
View File

@@ -0,0 +1,10 @@
// This is the configuration file for the oneuptime copilot.
const getCopilotConfig = () => {
return {
// The version of the schema for this configuration file.
schemaVersion: '1.0',
}
}
export default getCopilotConfig;

View File

@@ -0,0 +1,16 @@
# Description: Copilot clones your repository and to improve your code.
# This scirpt runs after the clone process is completed.
# Some of the common tasks you can do here are:
# 1. Install dependencies
# 2. Run linting
# 3. Run tests
# 4. Run build
# 5. Run any other command that you want to run after the clone process is completed.
# If this script fails, copilot will not proceed with the next steps to improve your code.
# This step is to ensure that the code is in a good state before we start improving it.
# If you want to skip this script, you can keep this file empty.
# It's highly recommended to run linting and tests in this script to ensure the code is in a good state.
# This scirpt will run on ubuntu machine. So, make sure the commands you run are compatible with ubuntu.
npm install
npm run lint

View File

@@ -0,0 +1,13 @@
# Description: Copilot will run this script after we make improvements to your code and write it to disk.
# Some of the common tasks you can do here are:
# 1. Run linting
# 2. Run tests
# 3. Run build
# 4. Run any other command that you want to run after the code is changed.
# If this script fails, copilot will not commit the changes to your repository.
# This step is to ensure that the code is in a good state before we commit the changes.
# If you want to skip this script, you can keep this file empty.
# It's highly recommended to run linting and tests in this script to ensure the code is in a good state.
# This scirpt will run on ubuntu machine. So, make sure the commands you run are compatible with ubuntu.
npm run fix

View File

@@ -0,0 +1 @@
# Description: Copilot will run this script after the commit process is completed.

View File

@@ -0,0 +1,9 @@
# Description: Copilot will run this script before we make changes to your code.
# Some of the common tasks you can do here are:
# 1. Install dependencies
# 2. Run any other command that you want to run before the code is changed.
# If this script fails, copilot will not make any changes to the code.
# This step is to ensure that the code is in a good state before we start making changes.
# If you want to skip this script, you can keep this file empty.
# It's highly recommended to run things like installing dependencies in this script.
# This scirpt will run on ubuntu machine. So, make sure the commands you run are compatible with ubuntu.

View File

@@ -0,0 +1 @@
# Description: Copilot will run this script before we commit the changes to your repository.

View File

@@ -49,4 +49,5 @@ LICENSE
marketing/*/*
licenses/*
certifications/*
ApiReference/public/assets/*
JavaScriptSDK/src/cli/server-monitor/out/scripts/prettify/*

102
.vscode/launch.json vendored
View File

@@ -19,6 +19,20 @@
}
],
"configurations": [
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/TestServer",
"name": "Copilot: Debug with Docker",
"port": 9985,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"name": "Debug Infrastructure Agent",
"type": "go",
@@ -105,6 +119,20 @@
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/APIReference",
"name": "API Reference: Debug with Docker",
"port": 8737,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/TestServer",
@@ -135,8 +163,50 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Telemetry",
"name": "Telemetry: Debug with Docker",
"localRoot": "${workspaceFolder}/ProbeIngest",
"name": "ProbeIngest: Debug with Docker",
"port": 9932,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/ServerMonitorIngest",
"name": "ServerMonitorIngest: Debug with Docker",
"port": 9941,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/IncomingRequestIngest",
"name": "IncomingRequestIngest: Debug with Docker",
"port": 9933,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/OpenTelemetryIngest",
"name": "OpenTelemetryIngest: Debug with Docker",
"port": 9938,
"remoteRoot": "/usr/src/app",
"request": "attach",
@@ -147,6 +217,34 @@
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/FluentIngest",
"name": "Fluent Ingest: Debug with Docker",
"port": 9937,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/IsolatedVM",
"name": "Isolated VM: Debug with Docker",
"port": 9974,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Workflow",

View File

@@ -1,95 +0,0 @@
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import { ONEUPTIME_URL } from "../Config";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPMethod from "Common/Types/API/HTTPMethod";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import API from "Common/Utils/API";
import logger from "Common/Server/Utils/Logger";
import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest";
const router: ExpressRouter = Express.getRouter();
/*
* Metrics endpoint for Keda autoscaling
* Returns the number of pending AI agent tasks
*/
router.get(
"/queue-size",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
/*
* Get the pending task count from OneUptime API
* This is the correct metric - the number of tasks waiting to be processed
*/
const pendingTaskCountUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent-task/get-pending-task-count");
logger.debug(
"Fetching pending task count from OneUptime API for KEDA scaling",
);
// Use AI Agent authentication (AI Agent key and AI Agent ID)
const requestBody: JSONObject = AIAgentAPIRequest.getDefaultRequestBody();
const result: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.fetch<JSONObject>({
method: HTTPMethod.POST,
url: pendingTaskCountUrl,
data: requestBody,
headers: {},
});
if (result instanceof HTTPErrorResponse) {
logger.error("Error fetching pending task count from OneUptime API");
logger.error(result);
throw result;
}
logger.debug(
"Pending task count fetched successfully from OneUptime API",
);
logger.debug(result.data);
// Extract count from the response - this is the number of tasks pending to be processed
let queueSize: number = (result.data["count"] as number) || 0;
// if string then convert to number
if (typeof queueSize === "string") {
const parsedQueueSize: number = parseInt(queueSize, 10);
if (!isNaN(parsedQueueSize)) {
queueSize = parsedQueueSize;
} else {
logger.warn(
"Pending task count is not a valid number, defaulting to 0",
);
queueSize = 0;
}
}
logger.debug(`Pending task count for KEDA: ${queueSize}`);
return Response.sendJsonObjectResponse(req, res, {
queueSize: queueSize,
});
} catch (err) {
logger.error("Error in metrics queue-size endpoint");
logger.error(err);
return next(err);
}
},
);
export default router;

View File

@@ -1,103 +0,0 @@
import {
CodeAgent,
CodeAgentType,
getCodeAgentDisplayName,
} from "./CodeAgentInterface";
import OpenCodeAgent from "./OpenCodeAgent";
import logger from "Common/Server/Utils/Logger";
// Factory class to create code agents
export default class CodeAgentFactory {
// Default agent type to use
private static defaultAgentType: CodeAgentType = CodeAgentType.OpenCode;
// Create an agent of the specified type
public static createAgent(type: CodeAgentType): CodeAgent {
logger.debug(`Creating code agent: ${getCodeAgentDisplayName(type)}`);
switch (type) {
case CodeAgentType.OpenCode:
return new OpenCodeAgent();
/*
* Future agents can be added here:
* case CodeAgentType.Goose:
* return new GooseAgent();
* case CodeAgentType.ClaudeCode:
* return new ClaudeCodeAgent();
*/
default:
throw new Error(`Unknown code agent type: ${type}`);
}
}
// Create the default agent
public static createDefaultAgent(): CodeAgent {
return this.createAgent(this.defaultAgentType);
}
// Set the default agent type
public static setDefaultAgentType(type: CodeAgentType): void {
this.defaultAgentType = type;
}
// Get the default agent type
public static getDefaultAgentType(): CodeAgentType {
return this.defaultAgentType;
}
// Get all available agent types
public static getAvailableAgentTypes(): Array<CodeAgentType> {
return Object.values(CodeAgentType);
}
// Check if an agent type is available on the system
public static async isAgentAvailable(type: CodeAgentType): Promise<boolean> {
try {
const agent: CodeAgent = this.createAgent(type);
return await agent.isAvailable();
} catch (error) {
logger.error(`Error checking agent availability for ${type}:`);
logger.error(error);
return false;
}
}
// Get the first available agent
public static async getFirstAvailableAgent(): Promise<CodeAgent | null> {
for (const type of this.getAvailableAgentTypes()) {
if (await this.isAgentAvailable(type)) {
return this.createAgent(type);
}
}
return null;
}
/*
* Create agent with fallback
* Tries to create the specified type, falls back to first available
*/
public static async createAgentWithFallback(
preferredType?: CodeAgentType,
): Promise<CodeAgent> {
// If preferred type is specified and available, use it
if (preferredType && (await this.isAgentAvailable(preferredType))) {
return this.createAgent(preferredType);
}
// Try the default type
if (await this.isAgentAvailable(this.defaultAgentType)) {
return this.createAgent(this.defaultAgentType);
}
// Fall back to first available
const agent: CodeAgent | null = await this.getFirstAvailableAgent();
if (!agent) {
throw new Error("No code agents are available on this system");
}
return agent;
}
}

View File

@@ -1,94 +0,0 @@
import LlmType from "Common/Types/LLM/LlmType";
import TaskLogger from "../Utils/TaskLogger";
// Configuration for the LLM provider
export interface CodeAgentLLMConfig {
llmType: LlmType;
apiKey?: string;
baseUrl?: string;
modelName?: string;
}
// The task to be executed by the code agent
export interface CodeAgentTask {
workingDirectory: string;
prompt: string;
context?: string;
timeoutMs?: number;
servicePath?: string; // Path within the repo where the service code lives
}
// Result from the code agent execution
export interface CodeAgentResult {
success: boolean;
filesModified: Array<string>;
summary: string;
logs: Array<string>;
error?: string;
exitCode?: number;
}
// Progress event from the code agent
export interface CodeAgentProgressEvent {
type: "stdout" | "stderr" | "status";
message: string;
timestamp: Date;
}
// Callback type for progress events
export type CodeAgentProgressCallback = (
event: CodeAgentProgressEvent,
) => void | Promise<void>;
/*
* Abstract interface for code agents
* This allows us to support multiple agents (OpenCode, Goose, Claude Code, etc.)
*/
export interface CodeAgent {
// Name of the agent (e.g., "OpenCode", "Goose", "ClaudeCode")
readonly name: string;
// Initialize the agent with LLM configuration
initialize(config: CodeAgentLLMConfig, logger?: TaskLogger): Promise<void>;
// Execute a task and return the result
executeTask(task: CodeAgentTask): Promise<CodeAgentResult>;
// Set a callback for progress events (streaming output)
onProgress(callback: CodeAgentProgressCallback): void;
// Check if the agent is available on the system
isAvailable(): Promise<boolean>;
// Abort the current task execution
abort(): Promise<void>;
// Clean up any resources used by the agent
cleanup(): Promise<void>;
}
// Enum for supported code agent types
export enum CodeAgentType {
OpenCode = "OpenCode",
/*
* Future agents:
* Goose = "Goose",
* ClaudeCode = "ClaudeCode",
* Aider = "Aider",
*/
}
// Helper function to get display name for agent type
export function getCodeAgentDisplayName(type: CodeAgentType): string {
switch (type) {
case CodeAgentType.OpenCode:
return "OpenCode AI";
default:
return type;
}
}
// Helper function to check if an agent type is valid
export function isValidCodeAgentType(type: string): type is CodeAgentType {
return Object.values(CodeAgentType).includes(type as CodeAgentType);
}

View File

@@ -1,15 +0,0 @@
// Export all code agent related types and classes
export {
CodeAgent,
CodeAgentLLMConfig,
CodeAgentTask,
CodeAgentResult,
CodeAgentProgressEvent,
CodeAgentProgressCallback,
CodeAgentType,
getCodeAgentDisplayName,
isValidCodeAgentType,
} from "./CodeAgentInterface";
export { default as CodeAgentFactory } from "./CodeAgentFactory";
export { default as OpenCodeAgent } from "./OpenCodeAgent";

View File

@@ -1,562 +0,0 @@
import {
CodeAgent,
CodeAgentLLMConfig,
CodeAgentTask,
CodeAgentResult,
CodeAgentProgressCallback,
CodeAgentProgressEvent,
} from "./CodeAgentInterface";
import TaskLogger from "../Utils/TaskLogger";
import Execute from "Common/Server/Utils/Execute";
import LocalFile from "Common/Server/Utils/LocalFile";
import LlmType from "Common/Types/LLM/LlmType";
import logger from "Common/Server/Utils/Logger";
import path from "path";
import { ChildProcess, spawn } from "child_process";
import BadDataException from "Common/Types/Exception/BadDataException";
// OpenCode configuration file structure
interface OpenCodeConfig {
provider?: Record<string, unknown>;
model?: string;
small_model?: string;
disabled_providers?: Array<string>;
enabled_providers?: Array<string>;
}
export default class OpenCodeAgent implements CodeAgent {
public readonly name: string = "OpenCode";
private config: CodeAgentLLMConfig | null = null;
private taskLogger: TaskLogger | null = null;
private progressCallback: CodeAgentProgressCallback | null = null;
private currentProcess: ChildProcess | null = null;
private aborted: boolean = false;
// Track original opencode.json content for restoration
private originalOpenCodeConfig: string | null = null;
private openCodeConfigPath: string | null = null;
// Default timeout: 30 minutes
private static readonly DEFAULT_TIMEOUT_MS: number = 30 * 60 * 1000;
public async initialize(
config: CodeAgentLLMConfig,
taskLogger?: TaskLogger,
): Promise<void> {
this.config = config;
if (taskLogger) {
this.taskLogger = taskLogger;
}
await this.log(`Initializing ${this.name} with ${config.llmType} provider`);
}
public async executeTask(task: CodeAgentTask): Promise<CodeAgentResult> {
if (!this.config) {
return this.createErrorResult(
"Agent not initialized. Call initialize() first.",
);
}
this.aborted = false;
const logs: Array<string> = [];
const timeoutMs: number =
task.timeoutMs || OpenCodeAgent.DEFAULT_TIMEOUT_MS;
try {
await this.log(`Executing task in directory: ${task.workingDirectory}`);
// Create OpenCode config file in the working directory
await this.createOpenCodeConfig(task.workingDirectory);
// Build the prompt
const fullPrompt: string = this.buildFullPrompt(task);
await this.log("Starting OpenCode execution...");
logs.push(`Prompt: ${fullPrompt.substring(0, 500)}...`);
// Execute OpenCode
const output: string = await this.runOpenCode(
task.workingDirectory,
fullPrompt,
timeoutMs,
(event: CodeAgentProgressEvent) => {
logs.push(`[${event.type}] ${event.message}`);
if (this.progressCallback) {
this.progressCallback(event);
}
},
);
logs.push(
`Output: ${output.substring(0, 1000)}${output.length > 1000 ? "..." : ""}`,
);
if (this.aborted) {
return this.createErrorResult("Task was aborted", logs);
}
// Check for modified files
const modifiedFiles: Array<string> = await this.getModifiedFiles(
task.workingDirectory,
);
// Restore or delete opencode.json before returning
await this.restoreOpenCodeConfig();
await this.log(
`OpenCode completed. ${modifiedFiles.length} files modified.`,
);
return {
success: true,
filesModified: modifiedFiles,
summary: this.extractSummary(output),
logs,
exitCode: 0,
};
} catch (error) {
const errorMessage: string =
error instanceof Error ? error.message : String(error);
// Restore or delete opencode.json on error
await this.restoreOpenCodeConfig();
await this.log(`OpenCode execution failed: ${errorMessage}`);
logs.push(`Error: ${errorMessage}`);
return this.createErrorResult(errorMessage, logs);
}
}
public onProgress(callback: CodeAgentProgressCallback): void {
this.progressCallback = callback;
}
public async isAvailable(): Promise<boolean> {
try {
const result: string = await Execute.executeCommandFile({
command: "opencode",
args: ["--version"],
cwd: process.cwd(),
});
logger.debug(`OpenCode version check: ${result}`);
return true;
} catch (error) {
logger.debug("OpenCode is not available:");
logger.debug(error);
return false;
}
}
public async abort(): Promise<void> {
this.aborted = true;
if (this.currentProcess) {
this.currentProcess.kill("SIGTERM");
this.currentProcess = null;
}
await this.log("OpenCode execution aborted");
}
public async cleanup(): Promise<void> {
if (this.currentProcess) {
this.currentProcess.kill("SIGTERM");
this.currentProcess = null;
}
this.config = null;
this.progressCallback = null;
}
// Create OpenCode configuration file in the workspace
private async createOpenCodeConfig(workingDirectory: string): Promise<void> {
if (!this.config) {
throw new Error("Config not initialized");
}
const configPath: string = path.join(workingDirectory, "opencode.json");
this.openCodeConfigPath = configPath;
// Check if opencode.json already exists and backup its content
try {
const existingContent: string = await LocalFile.read(configPath);
this.originalOpenCodeConfig = existingContent;
await this.log("Backed up existing opencode.json from repository");
} catch {
// File doesn't exist, which is the normal case
this.originalOpenCodeConfig = null;
}
const openCodeConfig: OpenCodeConfig = {
model: this.getModelString(),
small_model: this.getSmallModelString(),
};
// Set enabled providers based on LLM type
if (this.config.llmType === LlmType.Anthropic) {
openCodeConfig.enabled_providers = ["anthropic"];
} else if (this.config.llmType === LlmType.OpenAI) {
openCodeConfig.enabled_providers = ["openai"];
}
await LocalFile.write(configPath, JSON.stringify(openCodeConfig, null, 2));
await this.log(`Created OpenCode config at ${configPath}`);
}
// Restore or delete opencode.json after execution
private async restoreOpenCodeConfig(): Promise<void> {
if (!this.openCodeConfigPath) {
return;
}
try {
if (this.originalOpenCodeConfig !== null) {
// Restore the original file content
await LocalFile.write(
this.openCodeConfigPath,
this.originalOpenCodeConfig,
);
await this.log("Restored original opencode.json from repository");
} else {
// Delete the file we created
await LocalFile.deleteFile(this.openCodeConfigPath);
await this.log("Deleted generated opencode.json config file");
}
} catch (error) {
// Log but don't throw - cleanup failure shouldn't fail the task
logger.warn(`Failed to restore/delete opencode.json: ${error}`);
}
// Reset the tracking variables
this.openCodeConfigPath = null;
this.originalOpenCodeConfig = null;
}
// Get the model string in OpenCode format (provider/model)
private getModelString(): string {
if (!this.config) {
throw new Error("Config not initialized");
}
const provider: string = this.getProviderName();
const model: string = this.config.modelName || this.getDefaultModel();
return `${provider}/${model}`;
}
// Get the small model string for quick operations
private getSmallModelString(): string {
if (!this.config) {
throw new Error("Config not initialized");
}
const provider: string = this.getProviderName();
const smallModel: string = this.getDefaultSmallModel();
return `${provider}/${smallModel}`;
}
// Get provider name for OpenCode config
private getProviderName(): string {
if (!this.config) {
return "anthropic";
}
switch (this.config.llmType) {
case LlmType.Anthropic:
return "anthropic";
case LlmType.OpenAI:
return "openai";
case LlmType.Ollama:
return "ollama";
default:
throw new BadDataException("Unsupported LLM type for OpenCode agent");
}
}
// Get default model based on provider
private getDefaultModel(): string {
if (!this.config) {
return "claude-sonnet-4-20250514";
}
switch (this.config.llmType) {
case LlmType.Anthropic:
return "claude-sonnet-4-20250514";
case LlmType.OpenAI:
return "gpt-4o";
case LlmType.Ollama:
return "llama2";
default:
throw new BadDataException("Unsupported LLM type for OpenCode agent");
}
}
// Get default small model for quick operations
private getDefaultSmallModel(): string {
if (!this.config) {
return "claude-haiku-4-20250514";
}
switch (this.config.llmType) {
case LlmType.Anthropic:
return "claude-haiku-4-20250514";
case LlmType.OpenAI:
return "gpt-4o-mini";
case LlmType.Ollama:
return "llama2";
default:
throw new BadDataException("Unsupported LLM type for OpenCode agent");
}
}
// Build the full prompt including context
private buildFullPrompt(task: CodeAgentTask): string {
let prompt: string = task.prompt;
if (task.context) {
prompt = `${task.context}\n\n${prompt}`;
}
if (task.servicePath) {
prompt = `The service code is located at: ${task.servicePath}\n\n${prompt}`;
}
return prompt;
}
// Run OpenCode in non-interactive mode
private async runOpenCode(
workingDirectory: string,
prompt: string,
timeoutMs: number,
onOutput: (event: CodeAgentProgressEvent) => void,
): Promise<string> {
return new Promise(
(resolve: (value: string) => void, reject: (reason: Error) => void) => {
if (!this.config) {
reject(new Error("Config not initialized"));
return;
}
// Set environment variables for API key
const env: NodeJS.ProcessEnv = { ...process.env };
if (this.config.apiKey) {
switch (this.config.llmType) {
case LlmType.Anthropic:
env["ANTHROPIC_API_KEY"] = this.config.apiKey;
break;
case LlmType.OpenAI:
env["OPENAI_API_KEY"] = this.config.apiKey;
break;
case LlmType.Ollama:
if (this.config.baseUrl) {
env["OLLAMA_HOST"] = this.config.baseUrl;
}
break;
}
}
/*
* Use CLI mode flags to ensure output goes to stdout/stderr instead of TUI
* Pass prompt via stdin using "-" to avoid command line argument issues with long prompts
*/
const args: Array<string> = [
"run",
"--print-logs",
"--log-level",
"DEBUG",
"--format",
"default",
"-", // Read prompt from stdin
];
logger.debug(
`Running: opencode ${args.join(" ")} (prompt via stdin, ${prompt.length} chars)`,
);
const child: ChildProcess = spawn("opencode", args, {
cwd: workingDirectory,
env,
stdio: ["pipe", "pipe", "pipe"],
});
this.currentProcess = child;
// Write prompt to stdin and close it
if (child.stdin) {
child.stdin.write(prompt);
child.stdin.end();
}
let stdout: string = "";
let stderr: string = "";
// Set timeout
const timeout: ReturnType<typeof setTimeout> = setTimeout(() => {
if (child.pid) {
child.kill("SIGTERM");
reject(
new Error(
`OpenCode execution timed out after ${timeoutMs / 1000} seconds`,
),
);
}
}, timeoutMs);
child.stdout?.on("data", (data: Buffer) => {
const text: string = data.toString();
stdout += text;
// Stream to console immediately
const trimmedText: string = text.trim();
if (trimmedText) {
logger.info(`[OpenCode stdout] ${trimmedText}`);
// Stream to task logger for server-side logging
if (this.taskLogger) {
this.taskLogger
.info(`[OpenCode] ${trimmedText}`)
.catch((err: Error) => {
logger.error(`Failed to log OpenCode output: ${err.message}`);
});
}
}
onOutput({
type: "stdout",
message: trimmedText,
timestamp: new Date(),
});
});
child.stderr?.on("data", (data: Buffer) => {
const text: string = data.toString();
stderr += text;
// Stream to console immediately
const trimmedText: string = text.trim();
if (trimmedText) {
logger.warn(`[OpenCode stderr] ${trimmedText}`);
// Stream to task logger for server-side logging
if (this.taskLogger) {
this.taskLogger
.warning(`[OpenCode stderr] ${trimmedText}`)
.catch((err: Error) => {
logger.error(`Failed to log OpenCode stderr: ${err.message}`);
});
}
}
onOutput({
type: "stderr",
message: trimmedText,
timestamp: new Date(),
});
});
child.on("close", (code: number | null) => {
clearTimeout(timeout);
this.currentProcess = null;
if (this.aborted) {
reject(new Error("Execution aborted"));
return;
}
if (code === 0 || code === null) {
resolve(stdout);
} else {
reject(
new Error(
`OpenCode exited with code ${code}. stderr: ${stderr.substring(0, 500)}`,
),
);
}
});
child.on("error", (error: Error) => {
clearTimeout(timeout);
this.currentProcess = null;
reject(error);
});
},
);
}
// Get list of modified files using git
private async getModifiedFiles(
workingDirectory: string,
): Promise<Array<string>> {
try {
const result: string = await Execute.executeCommandFile({
command: "git",
args: ["status", "--porcelain"],
cwd: workingDirectory,
});
if (!result.trim()) {
return [];
}
return result
.split("\n")
.filter((line: string) => {
return line.trim().length > 0;
})
.map((line: string) => {
// Git status format: "XY filename"
return line.substring(3).trim();
});
} catch (error) {
logger.error("Error getting modified files:");
logger.error(error);
return [];
}
}
// Extract summary from OpenCode output
private extractSummary(output: string): string {
// Try to extract a meaningful summary from the output
const lines: Array<string> = output.split("\n").filter((line: string) => {
return line.trim().length > 0;
});
// Return last few meaningful lines as summary
const summaryLines: Array<string> = lines.slice(-5);
return summaryLines.join("\n") || "No summary available";
}
// Create error result helper
private createErrorResult(
errorMessage: string,
logs: Array<string> = [],
): CodeAgentResult {
return {
success: false,
filesModified: [],
summary: "",
logs,
error: errorMessage,
exitCode: 1,
};
}
// Logging helper
private async log(message: string): Promise<void> {
if (this.taskLogger) {
await this.taskLogger.info(`[${this.name}] ${message}`);
} else {
logger.debug(`[${this.name}] ${message}`);
}
}
}

View File

@@ -1,36 +0,0 @@
import URL from "Common/Types/API/URL";
import ObjectID from "Common/Types/ObjectID";
import logger from "Common/Server/Utils/Logger";
import Port from "Common/Types/Port";
if (!process.env["ONEUPTIME_URL"]) {
logger.error("ONEUPTIME_URL is not set");
process.exit();
}
export const ONEUPTIME_URL: URL = URL.fromString(
process.env["ONEUPTIME_URL"] || "https://oneuptime.com",
);
export const AI_AGENT_ID: ObjectID | null = process.env["AI_AGENT_ID"]
? new ObjectID(process.env["AI_AGENT_ID"])
: null;
if (!process.env["AI_AGENT_KEY"]) {
logger.error("AI_AGENT_KEY is not set");
process.exit();
}
export const AI_AGENT_KEY: string = process.env["AI_AGENT_KEY"];
export const AI_AGENT_NAME: string | null =
process.env["AI_AGENT_NAME"] || null;
export const AI_AGENT_DESCRIPTION: string | null =
process.env["AI_AGENT_DESCRIPTION"] || null;
export const HOSTNAME: string = process.env["HOSTNAME"] || "localhost";
export const PORT: Port = new Port(
process.env["PORT"] ? parseInt(process.env["PORT"]) : 3875,
);

View File

@@ -1,84 +0,0 @@
import { PORT } from "./Config";
import AliveJob from "./Jobs/Alive";
import startTaskProcessingLoop from "./Jobs/ProcessScheduledTasks";
import Register from "./Services/Register";
import MetricsAPI from "./API/Metrics";
import {
getTaskHandlerRegistry,
FixExceptionTaskHandler,
} from "./TaskHandlers/Index";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import "ejs";
const APP_NAME: string = "ai-agent";
const init: PromiseVoidFunction = async (): Promise<void> => {
try {
// Initialize telemetry
Telemetry.init({
serviceName: APP_NAME,
});
logger.info("AI Agent Service - Starting...");
// init the app
await App.init({
appName: APP_NAME,
port: PORT,
isFrontendApp: false,
statusOptions: {
liveCheck: async () => {},
readyCheck: async () => {},
},
});
// Add metrics API routes for KEDA autoscaling
const app: ExpressApplication = Express.getExpressApp();
app.use("/metrics", MetricsAPI);
// add default routes
await App.addDefaultRoutes();
try {
// Register this AI Agent.
await Register.registerAIAgent();
logger.debug("AI Agent registered");
AliveJob();
// Register task handlers
logger.debug("Registering task handlers...");
const taskHandlerRegistry: ReturnType<typeof getTaskHandlerRegistry> =
getTaskHandlerRegistry();
taskHandlerRegistry.register(new FixExceptionTaskHandler());
logger.debug(
`Registered ${taskHandlerRegistry.getHandlerCount()} task handler(s): ${taskHandlerRegistry.getRegisteredTaskTypes().join(", ")}`,
);
// Start task processing loop (runs in background)
startTaskProcessingLoop().catch((err: Error) => {
logger.error("Task processing loop failed:");
logger.error(err);
});
} catch (err) {
logger.error("Register AI Agent failed");
logger.error(err);
throw err;
}
} catch (err) {
logger.error("App Init Failed:");
logger.error(err);
throw err;
}
};
init().catch((err: Error) => {
logger.error(err);
logger.error("Exiting node process");
process.exit(1);
});

View File

@@ -1,56 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import Register from "../Services/Register";
import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import { EVERY_MINUTE } from "Common/Utils/CronTime";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import BasicCron from "Common/Server/Utils/BasicCron";
import logger from "Common/Server/Utils/Logger";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
const InitJob: VoidFunction = (): void => {
BasicCron({
jobName: "AIAgent:Alive",
options: {
schedule: EVERY_MINUTE,
runOnStartup: false,
},
runFunction: async () => {
logger.debug("Checking if AI Agent is alive...");
const aiAgentId: string | undefined = LocalCache.getString(
"AI_AGENT",
"AI_AGENT_ID",
);
if (!aiAgentId) {
logger.warn(
"AI Agent is not registered yet. Skipping alive check. Trying to register AI Agent again...",
);
await Register.registerAIAgent();
return;
}
logger.debug("AI Agent ID: " + aiAgentId.toString());
const aliveUrl: URL = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent/alive",
);
const result: HTTPResponse<JSONObject> = await API.post({
url: aliveUrl,
data: AIAgentAPIRequest.getDefaultRequestBody(),
});
if (result.isSuccess()) {
logger.debug("AI Agent update sent to server successfully.");
} else {
logger.error("Failed to send AI Agent update to server.");
}
},
});
};
export default InitJob;

View File

@@ -1,257 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest";
import AIAgentTaskLog from "../Utils/AIAgentTaskLog";
import TaskLogger from "../Utils/TaskLogger";
import BackendAPI from "../Utils/BackendAPI";
import {
getTaskHandlerRegistry,
TaskContext,
TaskMetadata,
TaskHandler,
TaskResult,
} from "../TaskHandlers/Index";
import TaskHandlerRegistry from "../TaskHandlers/TaskHandlerRegistry";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import logger from "Common/Server/Utils/Logger";
import AIAgentTaskStatus from "Common/Types/AI/AIAgentTaskStatus";
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import ObjectID from "Common/Types/ObjectID";
import Sleep from "Common/Types/Sleep";
// Type for task data from the API
interface AIAgentTaskData {
_id: string;
projectId: string;
taskType: AIAgentTaskType;
metadata: TaskMetadata;
createdAt: string;
status?: AIAgentTaskStatus;
}
// Type for API response containing task
interface GetPendingTaskResponse {
task: AIAgentTaskData | null;
}
const SLEEP_WHEN_NO_TASKS_MS: number = 60 * 1000; // 1 minute
type ExecuteTaskFunction = (task: AIAgentTaskData) => Promise<void>;
/**
* Execute an AI Agent task using the registered task handler
*/
const executeTask: ExecuteTaskFunction = async (
task: AIAgentTaskData,
): Promise<void> => {
const taskIdString: string = task._id;
const projectIdString: string = task.projectId;
const taskId: ObjectID = new ObjectID(taskIdString);
const projectId: ObjectID = new ObjectID(projectIdString);
const taskType: AIAgentTaskType = task.taskType;
const metadata: TaskMetadata = task.metadata || {};
const createdAt: Date = new Date(task.createdAt);
// Get the task handler from the registry
const registry: TaskHandlerRegistry = getTaskHandlerRegistry();
const handler: TaskHandler | undefined = registry.getHandler(taskType);
if (!handler) {
throw new Error(`No handler registered for task type: ${taskType}`);
}
// Create task logger
const taskLogger: TaskLogger = new TaskLogger({
taskId: taskIdString,
context: `${handler.name}`,
});
// Create backend API client
const backendAPI: BackendAPI = new BackendAPI();
// Build task context
const context: TaskContext = {
taskId,
projectId,
taskType,
metadata,
logger: taskLogger,
backendAPI,
createdAt,
startedAt: new Date(),
};
try {
// Log handler starting
await taskLogger.info(
`Starting ${handler.name} for task type: ${taskType}`,
);
// Validate metadata if the handler supports it
if (handler.validateMetadata && !handler.validateMetadata(metadata)) {
throw new Error(`Invalid metadata for task type: ${taskType}`);
}
// Execute the task handler
const result: TaskResult = await handler.execute(context);
// Log result
if (result.success) {
await taskLogger.info(`Task completed: ${result.message}`);
if (result.pullRequestsCreated && result.pullRequestsCreated > 0) {
await taskLogger.info(
`Created ${result.pullRequestsCreated} pull request(s): ${result.pullRequestUrls?.join(", ") || ""}`,
);
}
} else {
await taskLogger.warning(`Task did not succeed: ${result.message}`);
}
// Flush all pending logs
await taskLogger.flush();
/*
* If the task was not successful and we want to report it as an error
* Note: Based on user requirements, "no fix found" should be Completed, not Error
* Only throw if there was an actual error (not just "no action taken")
*/
if (!result.success && result.data?.["isError"]) {
throw new Error(result.message);
}
} catch (error) {
// Ensure logs are flushed even on error
await taskLogger.flush();
throw error;
}
};
const startTaskProcessingLoop: () => Promise<void> =
async (): Promise<void> => {
logger.info("Starting AI Agent task processing loop...");
const getPendingTaskUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent-task/get-pending-task");
const updateTaskStatusUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent-task/update-task-status");
/* Continuous loop to process tasks */
while (true) {
try {
/* Fetch one scheduled task */
const getPendingTaskResult: HTTPResponse<JSONObject> = await API.post({
url: getPendingTaskUrl,
data: AIAgentAPIRequest.getDefaultRequestBody(),
});
if (!getPendingTaskResult.isSuccess()) {
logger.error("Failed to fetch pending task from server");
logger.debug(
`Sleeping for ${SLEEP_WHEN_NO_TASKS_MS / 1000} seconds before retrying...`,
);
await Sleep.sleep(SLEEP_WHEN_NO_TASKS_MS);
continue;
}
const responseData: GetPendingTaskResponse =
getPendingTaskResult.data as unknown as GetPendingTaskResponse;
const task: AIAgentTaskData | null = responseData.task;
if (!task || !task._id) {
logger.debug("No pending tasks available");
logger.debug(
`Sleeping for ${SLEEP_WHEN_NO_TASKS_MS / 1000} seconds before checking again...`,
);
await Sleep.sleep(SLEEP_WHEN_NO_TASKS_MS);
continue;
}
const taskId: string = task._id;
const taskType: string = task.taskType || "Unknown";
logger.info(`Processing task: ${taskId} (type: ${taskType})`);
try {
/* Mark task as InProgress */
const inProgressResult: HTTPResponse<JSONObject> = await API.post({
url: updateTaskStatusUrl,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: AIAgentTaskStatus.InProgress,
},
});
if (!inProgressResult.isSuccess()) {
logger.error(
`Failed to mark task ${taskId} as InProgress. Skipping.`,
);
continue;
}
/* Send task started log */
await AIAgentTaskLog.sendTaskStartedLog(taskId);
/* Execute the task using the handler system */
await executeTask(task);
/* Mark task as Completed */
const completedResult: HTTPResponse<JSONObject> = await API.post({
url: updateTaskStatusUrl,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: AIAgentTaskStatus.Completed,
},
});
if (!completedResult.isSuccess()) {
logger.error(`Failed to mark task ${taskId} as Completed`);
} else {
/* Send task completed log */
await AIAgentTaskLog.sendTaskCompletedLog(taskId);
logger.info(`Task completed successfully: ${taskId}`);
}
} catch (error) {
/* Mark task as Error with error message */
const errorMessage: string =
error instanceof Error ? error.message : "Unknown error occurred";
const errorResult: HTTPResponse<JSONObject> = await API.post({
url: updateTaskStatusUrl,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: AIAgentTaskStatus.Error,
statusMessage: errorMessage,
},
});
if (!errorResult.isSuccess()) {
logger.error(
`Failed to mark task ${taskId} as Error: ${errorMessage}`,
);
}
/* Send task error log */
await AIAgentTaskLog.sendTaskErrorLog(taskId, errorMessage);
logger.error(`Task failed: ${taskId} - ${errorMessage}`);
logger.error(error);
}
} catch (error) {
logger.error("Error in task processing loop:");
logger.error(error);
logger.debug(
`Sleeping for ${SLEEP_WHEN_NO_TASKS_MS / 1000} seconds before retrying...`,
);
await Sleep.sleep(SLEEP_WHEN_NO_TASKS_MS);
}
}
};
export default startTaskProcessingLoop;

View File

@@ -1,127 +0,0 @@
import {
ONEUPTIME_URL,
AI_AGENT_ID,
AI_AGENT_KEY,
AI_AGENT_NAME,
AI_AGENT_DESCRIPTION,
} from "../Config";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import Sleep from "Common/Types/Sleep";
import API from "Common/Utils/API";
import { HasClusterKey } from "Common/Server/EnvironmentConfig";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import logger from "Common/Server/Utils/Logger";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
export default class Register {
public static async registerAIAgent(): Promise<void> {
// register AI agent with 10 retries and 30 second interval between each retry.
let currentRetry: number = 0;
const maxRetry: number = 10;
const retryIntervalInSeconds: number = 30;
while (currentRetry < maxRetry) {
try {
logger.debug(`Registering AI Agent. Attempt: ${currentRetry + 1}`);
await Register._registerAIAgent();
logger.debug(`AI Agent registered successfully.`);
break;
} catch (error) {
logger.error(
`Failed to register AI Agent. Retrying after ${retryIntervalInSeconds} seconds...`,
);
logger.error(error);
currentRetry++;
await Sleep.sleep(retryIntervalInSeconds * 1000);
}
}
}
private static async _registerAIAgent(): Promise<void> {
if (HasClusterKey) {
// Clustered mode: Auto-register and get ID from server
const aiAgentRegistrationUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent/register");
logger.debug("Registering AI Agent...");
logger.debug("Sending request to: " + aiAgentRegistrationUrl.toString());
const result: HTTPResponse<JSONObject> = await API.post({
url: aiAgentRegistrationUrl,
data: {
aiAgentKey: AI_AGENT_KEY,
aiAgentName: AI_AGENT_NAME,
aiAgentDescription: AI_AGENT_DESCRIPTION,
clusterKey: ClusterKeyAuthorization.getClusterKey(),
},
});
if (!result.isSuccess()) {
logger.error(
`Failed to register AI Agent. Status: ${result.statusCode}`,
);
logger.error(result.data);
throw new Error(
"Failed to register AI Agent: HTTP " + result.statusCode,
);
}
logger.debug("AI Agent Registered");
logger.debug(result.data);
const aiAgentId: string | undefined = result.data["_id"] as
| string
| undefined;
if (!aiAgentId) {
logger.error("AI Agent ID not found in response");
logger.error(result.data);
throw new Error("AI Agent ID not found in registration response");
}
LocalCache.setString("AI_AGENT", "AI_AGENT_ID", aiAgentId);
} else {
// Non-clustered mode: Validate AI agent by sending alive request
if (!AI_AGENT_ID) {
logger.error("AI_AGENT_ID or ONEUPTIME_SECRET should be set");
return process.exit();
}
const aliveUrl: URL = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent/alive",
);
logger.debug("Registering AI Agent...");
logger.debug("Sending request to: " + aliveUrl.toString());
const result: HTTPResponse<JSONObject> = await API.post({
url: aliveUrl,
data: {
aiAgentKey: AI_AGENT_KEY.toString(),
aiAgentId: AI_AGENT_ID.toString(),
},
});
if (result.isSuccess()) {
LocalCache.setString(
"AI_AGENT",
"AI_AGENT_ID",
AI_AGENT_ID.toString() as string,
);
logger.debug("AI Agent registered successfully");
} else {
throw new Error("Failed to register AI Agent: " + result.statusCode);
}
}
logger.debug(
`AI Agent ID: ${LocalCache.getString("AI_AGENT", "AI_AGENT_ID") || "Unknown"}`,
);
}
}

View File

@@ -1,454 +0,0 @@
import {
BaseTaskHandler,
TaskContext,
TaskResult,
TaskMetadata,
TaskResultData,
} from "./TaskHandlerInterface";
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import {
LLMConfig,
ExceptionDetails,
CodeRepositoryInfo,
RepositoryToken,
} from "../Utils/BackendAPI";
import RepositoryManager, {
RepositoryConfig,
CloneResult,
} from "../Utils/RepositoryManager";
import PullRequestCreator, {
PullRequestResult,
} from "../Utils/PullRequestCreator";
import WorkspaceManager, { WorkspaceInfo } from "../Utils/WorkspaceManager";
import {
CodeAgentFactory,
CodeAgent,
CodeAgentType,
CodeAgentTask,
CodeAgentResult,
CodeAgentProgressEvent,
CodeAgentLLMConfig,
} from "../CodeAgents/Index";
// Metadata structure for Fix Exception tasks
export interface FixExceptionMetadata extends TaskMetadata {
exceptionId: string;
serviceId?: string;
stackTrace?: string;
errorMessage?: string;
}
export default class FixExceptionTaskHandler extends BaseTaskHandler<FixExceptionMetadata> {
public readonly taskType: AIAgentTaskType = AIAgentTaskType.FixException;
public readonly name: string = "Fix Exception Handler";
// Default timeout for code agent execution (30 minutes)
private static readonly CODE_AGENT_TIMEOUT_MS: number = 30 * 60 * 1000;
public async execute(
context: TaskContext<FixExceptionMetadata>,
): Promise<TaskResult> {
const metadata: FixExceptionMetadata = context.metadata;
await this.log(
context,
`Starting Fix Exception task for exception: ${metadata.exceptionId} (taskId: ${context.taskId.toString()})`,
);
let workspace: WorkspaceInfo | null = null;
try {
// Step 1: Get LLM configuration for the project
await this.log(context, "Fetching LLM provider configuration...");
const llmConfig: LLMConfig = await context.backendAPI.getLLMConfig(
context.projectId.toString(),
);
await this.log(
context,
`Using LLM provider: ${llmConfig.llmType}${llmConfig.modelName ? ` (${llmConfig.modelName})` : ""}`,
);
// Step 2: Get exception details
await this.log(context, "Fetching exception details...");
const exceptionDetails: ExceptionDetails =
await context.backendAPI.getExceptionDetails(metadata.exceptionId);
if (!exceptionDetails.service) {
await this.log(context, "No service linked to this exception", "error");
return this.createFailureResult("No service linked to this exception", {
isError: true,
});
}
await this.log(
context,
`Exception: ${exceptionDetails.exception.message.substring(0, 100)}...`,
);
await this.log(context, `Service: ${exceptionDetails.service.name}`);
// Step 3: Get linked code repositories
await this.log(context, "Finding linked code repositories...");
const repositories: Array<CodeRepositoryInfo> =
await context.backendAPI.getCodeRepositories(
exceptionDetails.service.id,
);
if (repositories.length === 0) {
await this.log(
context,
"No code repositories linked to this service",
"error",
);
return this.createFailureResult(
"No code repositories linked to this service via Service Catalog",
{ isError: true },
);
}
await this.log(
context,
`Found ${repositories.length} linked code repository(ies)`,
);
// Step 4: Create workspace for the task
workspace = await WorkspaceManager.createWorkspace(
context.taskId.toString(),
);
await this.log(context, `Created workspace: ${workspace.workspacePath}`);
// Step 5: Process each repository
const pullRequestUrls: Array<string> = [];
const errors: Array<string> = [];
for (const repo of repositories) {
try {
await this.log(
context,
`Processing repository: ${repo.organizationName}/${repo.repositoryName}`,
);
const prUrl: string | null = await this.processRepository(
context,
repo,
exceptionDetails,
llmConfig,
workspace,
);
if (prUrl) {
pullRequestUrls.push(prUrl);
}
} catch (error) {
const errorMessage: string =
error instanceof Error ? error.message : String(error);
errors.push(
`${repo.organizationName}/${repo.repositoryName}: ${errorMessage}`,
);
await this.log(
context,
`Failed to process repository ${repo.organizationName}/${repo.repositoryName}: ${errorMessage}`,
"error",
);
// Continue with next repository
}
}
// Step 6: Return result
if (pullRequestUrls.length > 0) {
await this.log(
context,
`Successfully created ${pullRequestUrls.length} pull request(s)`,
);
const resultData: TaskResultData = {
pullRequests: pullRequestUrls,
};
if (errors.length > 0) {
resultData.errors = errors;
}
return {
success: true,
message: `Created ${pullRequestUrls.length} pull request(s)`,
pullRequestsCreated: pullRequestUrls.length,
pullRequestUrls,
data: resultData,
};
}
// No PRs created - mark as error
await this.log(
context,
"No fixes could be applied to any repository",
"error",
);
return this.createFailureResult(
errors.length > 0
? `No fixes could be applied. Errors: ${errors.join("; ")}`
: "No fixes could be applied to any repository",
{ isError: true },
);
} catch (error) {
const errorMessage: string =
error instanceof Error ? error.message : String(error);
await this.log(context, `Task failed: ${errorMessage}`, "error");
// Mark as an actual error (not just "no action taken") so task gets Error status
return this.createFailureResult(errorMessage, { isError: true });
} finally {
// Cleanup workspace
if (workspace) {
await this.log(context, "Cleaning up workspace...");
await WorkspaceManager.deleteWorkspace(workspace.workspacePath);
}
// Flush logs
await context.logger.flush();
}
}
// Process a single repository
private async processRepository(
context: TaskContext<FixExceptionMetadata>,
repo: CodeRepositoryInfo,
exceptionDetails: ExceptionDetails,
llmConfig: LLMConfig,
workspace: WorkspaceInfo,
): Promise<string | null> {
// Get access token for the repository
await this.log(
context,
`Getting access token for ${repo.organizationName}/${repo.repositoryName}...`,
);
const tokenData: RepositoryToken =
await context.backendAPI.getRepositoryToken(repo.id);
// Clone the repository
await this.log(
context,
`Cloning repository ${repo.organizationName}/${repo.repositoryName}...`,
);
const repoConfig: RepositoryConfig = {
organizationName: tokenData.organizationName,
repositoryName: tokenData.repositoryName,
token: tokenData.token,
repositoryUrl: tokenData.repositoryUrl,
};
const repoManager: RepositoryManager = new RepositoryManager(
context.logger,
);
const cloneResult: CloneResult = await repoManager.cloneRepository(
repoConfig,
workspace.workspacePath,
);
// Create a fix branch
const branchName: string = `oneuptime-fix-exception-${context.taskId.toString().substring(0, 8)}`;
await this.log(context, `Creating branch: ${branchName}`);
await repoManager.createBranch(cloneResult.repositoryPath, branchName);
// Build the prompt for the code agent
const prompt: string = this.buildFixPrompt(
exceptionDetails,
repo.servicePathInRepository,
);
// Initialize code agent
await this.log(context, "Initializing code agent...");
const agent: CodeAgent = CodeAgentFactory.createAgent(
CodeAgentType.OpenCode,
);
const agentConfig: CodeAgentLLMConfig = {
llmType: llmConfig.llmType,
};
if (llmConfig.apiKey) {
agentConfig.apiKey = llmConfig.apiKey;
}
if (llmConfig.baseUrl) {
agentConfig.baseUrl = llmConfig.baseUrl;
}
if (llmConfig.modelName) {
agentConfig.modelName = llmConfig.modelName;
}
await agent.initialize(agentConfig, context.logger);
// Set up progress callback to log agent output
agent.onProgress((event: CodeAgentProgressEvent) => {
context.logger.logProcessOutput("CodeAgent", event.message);
});
// Execute the code agent
await this.log(context, "Running code agent to fix exception...");
const codeAgentTask: CodeAgentTask = {
workingDirectory: cloneResult.repositoryPath,
prompt,
timeoutMs: FixExceptionTaskHandler.CODE_AGENT_TIMEOUT_MS,
};
if (repo.servicePathInRepository) {
codeAgentTask.servicePath = repo.servicePathInRepository;
}
const agentResult: CodeAgentResult = await agent.executeTask(codeAgentTask);
// Check if any changes were made
if (!agentResult.success || agentResult.filesModified.length === 0) {
await this.log(
context,
`Code agent did not make any changes: ${agentResult.error || agentResult.summary}`,
"warning",
);
await agent.cleanup();
return null;
}
await this.log(
context,
`Code agent modified ${agentResult.filesModified.length} file(s)`,
);
// Add all changes and commit
await this.log(context, "Committing changes...");
await repoManager.addAllChanges(cloneResult.repositoryPath);
const commitMessage: string = this.buildCommitMessage(exceptionDetails);
await repoManager.commitChanges(cloneResult.repositoryPath, commitMessage);
// Push the branch
await this.log(context, `Pushing branch ${branchName}...`);
await repoManager.pushBranch(
cloneResult.repositoryPath,
branchName,
repoConfig,
);
// Create pull request
await this.log(context, "Creating pull request...");
const prCreator: PullRequestCreator = new PullRequestCreator(
context.logger,
);
const prTitle: string = PullRequestCreator.generatePRTitle(
exceptionDetails.exception.message,
);
const prBody: string = PullRequestCreator.generatePRBody({
exceptionMessage: exceptionDetails.exception.message,
exceptionType: exceptionDetails.exception.exceptionType,
stackTrace: exceptionDetails.exception.stackTrace,
serviceName: exceptionDetails.service?.name || "Unknown Service",
summary: agentResult.summary,
});
const prResult: PullRequestResult = await prCreator.createPullRequest({
token: tokenData.token,
organizationName: tokenData.organizationName,
repositoryName: tokenData.repositoryName,
baseBranch: repo.mainBranchName || "main",
headBranch: branchName,
title: prTitle,
body: prBody,
});
await this.log(context, `Pull request created: ${prResult.htmlUrl}`);
// Record the PR in the backend
await context.backendAPI.recordPullRequest({
taskId: context.taskId.toString(),
codeRepositoryId: repo.id,
pullRequestUrl: prResult.htmlUrl,
pullRequestNumber: prResult.number,
pullRequestId: prResult.id,
title: prResult.title,
description: prBody.substring(0, 1000),
headRefName: branchName,
baseRefName: repo.mainBranchName || "main",
});
// Cleanup agent
await agent.cleanup();
return prResult.htmlUrl;
}
// Build the prompt for the code agent
private buildFixPrompt(
exceptionDetails: ExceptionDetails,
servicePathInRepository: string | null,
): string {
let prompt: string = `You are a software engineer fixing a bug in a codebase.
## Exception Information
**Exception Type:** ${exceptionDetails.exception.exceptionType}
**Error Message:**
${exceptionDetails.exception.message}
**Stack Trace:**
\`\`\`
${exceptionDetails.exception.stackTrace}
\`\`\`
## Task
Please analyze the stack trace and fix the exception. Here are the steps to follow:
1. Identify the root cause of the exception from the stack trace
2. Find the relevant source files in the codebase
3. Implement a fix for the issue
4. Make sure your fix handles edge cases appropriately
5. The fix should be minimal and focused - only change what's necessary
## Guidelines
- Do NOT add excessive error handling or logging unless necessary
- Do NOT refactor unrelated code
- Keep the fix simple and targeted
- Preserve existing code style and patterns
- If you cannot determine how to fix the issue, explain why
Please proceed with analyzing and fixing this exception.`;
if (servicePathInRepository) {
prompt = `The service code is located in the \`${servicePathInRepository}\` directory.\n\n${prompt}`;
}
return prompt;
}
// Build commit message for the fix
private buildCommitMessage(exceptionDetails: ExceptionDetails): string {
const shortMessage: string = exceptionDetails.exception.message
.replace(/\n/g, " ")
.replace(/\s+/g, " ")
.trim()
.substring(0, 50);
return `fix: ${shortMessage}
This commit fixes an exception detected by OneUptime.
Exception Type: ${exceptionDetails.exception.exceptionType}
Exception ID: ${exceptionDetails.exception.id}
Automatically generated by OneUptime AI Agent.`;
}
// Validate metadata
public validateMetadata(metadata: FixExceptionMetadata): boolean {
return Boolean(metadata.exceptionId);
}
// Get handler description
public getDescription(): string {
return "Analyzes exceptions detected by OneUptime and attempts to fix them by modifying the source code and creating a pull request.";
}
}

View File

@@ -1,16 +0,0 @@
// Export all task handler related types and classes
export {
TaskHandler,
TaskContext,
TaskResult,
TaskMetadata,
TaskResultData,
BaseTaskHandler,
} from "./TaskHandlerInterface";
export {
default as TaskHandlerRegistry,
getTaskHandlerRegistry,
} from "./TaskHandlerRegistry";
export { default as FixExceptionTaskHandler } from "./FixExceptionTaskHandler";

View File

@@ -1,161 +0,0 @@
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import ObjectID from "Common/Types/ObjectID";
import TaskLogger from "../Utils/TaskLogger";
import BackendAPI from "../Utils/BackendAPI";
// Base interface for task metadata - handlers should define their own specific metadata types
export interface TaskMetadata {
// All metadata must have at least these optional fields for extensibility
[key: string]: unknown;
}
// Base interface for task result data
export interface TaskResultData {
// Pull requests created (for Fix Exception tasks)
pullRequests?: Array<string>;
// Errors encountered during processing
errors?: Array<string>;
// Flag to indicate if this is an error result (not just "no action taken")
isError?: boolean;
// Additional data fields
[key: string]: unknown;
}
// Context provided to task handlers
export interface TaskContext<TMetadata extends TaskMetadata = TaskMetadata> {
// Task identification
taskId: ObjectID;
projectId: ObjectID;
taskType: AIAgentTaskType;
// Task metadata (varies by task type)
metadata: TMetadata;
// Utilities
logger: TaskLogger;
backendAPI: BackendAPI;
// Task timestamps
createdAt: Date;
startedAt: Date;
}
// Result returned by task handlers
export interface TaskResult {
// Whether the task completed successfully
success: boolean;
// Human-readable message describing the result
message: string;
// Additional data about the result (optional)
data?: TaskResultData;
// Number of PRs created (for Fix Exception tasks)
pullRequestsCreated?: number;
// List of PR URLs created
pullRequestUrls?: Array<string>;
}
// Interface that all task handlers must implement
export interface TaskHandler<TMetadata extends TaskMetadata = TaskMetadata> {
// The type of task this handler processes
readonly taskType: AIAgentTaskType;
// Human-readable name for the handler
readonly name: string;
// Execute the task and return a result
execute(context: TaskContext<TMetadata>): Promise<TaskResult>;
// Check if this handler can process a given task
canHandle(taskType: AIAgentTaskType): boolean;
// Optional: Validate task metadata before execution
validateMetadata?(metadata: TMetadata): boolean;
// Optional: Get a description of what this handler does
getDescription?(): string;
}
// Abstract base class that provides common functionality for task handlers
export abstract class BaseTaskHandler<
TMetadata extends TaskMetadata = TaskMetadata,
> implements TaskHandler<TMetadata>
{
public abstract readonly taskType: AIAgentTaskType;
public abstract readonly name: string;
public abstract execute(context: TaskContext<TMetadata>): Promise<TaskResult>;
public canHandle(taskType: AIAgentTaskType): boolean {
return taskType === this.taskType;
}
// Create a success result
protected createSuccessResult(
message: string,
data?: TaskResultData,
): TaskResult {
const result: TaskResult = {
success: true,
message,
};
if (data) {
result.data = data;
}
return result;
}
// Create a failure result
protected createFailureResult(
message: string,
data?: TaskResultData,
): TaskResult {
const result: TaskResult = {
success: false,
message,
};
if (data) {
result.data = data;
}
return result;
}
// Create a result for when no action was taken
protected createNoActionResult(message: string): TaskResult {
return {
success: true,
message,
pullRequestsCreated: 0,
};
}
// Log to the task logger
protected async log(
context: TaskContext<TMetadata>,
message: string,
level: "info" | "debug" | "warning" | "error" = "info",
): Promise<void> {
switch (level) {
case "debug":
await context.logger.debug(message);
break;
case "warning":
await context.logger.warning(message);
break;
case "error":
await context.logger.error(message);
break;
case "info":
default:
await context.logger.info(message);
break;
}
}
}

View File

@@ -1,93 +0,0 @@
import { TaskHandler } from "./TaskHandlerInterface";
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import logger from "Common/Server/Utils/Logger";
/*
* Registry for task handlers
* Allows dynamic registration and lookup of handlers by task type
*/
export default class TaskHandlerRegistry {
private static instance: TaskHandlerRegistry | null = null;
private handlers: Map<AIAgentTaskType, TaskHandler> = new Map();
// Private constructor for singleton pattern
private constructor() {}
// Get the singleton instance
public static getInstance(): TaskHandlerRegistry {
if (!TaskHandlerRegistry.instance) {
TaskHandlerRegistry.instance = new TaskHandlerRegistry();
}
return TaskHandlerRegistry.instance;
}
// Reset the singleton (useful for testing)
public static resetInstance(): void {
TaskHandlerRegistry.instance = null;
}
// Register a task handler
public register(handler: TaskHandler): void {
if (this.handlers.has(handler.taskType)) {
logger.warn(
`Overwriting existing handler for task type: ${handler.taskType}`,
);
}
this.handlers.set(handler.taskType, handler);
logger.debug(
`Registered handler "${handler.name}" for task type: ${handler.taskType}`,
);
}
// Register multiple handlers at once
public registerAll(handlers: Array<TaskHandler>): void {
for (const handler of handlers) {
this.register(handler);
}
}
// Unregister a handler
public unregister(taskType: AIAgentTaskType): void {
if (this.handlers.has(taskType)) {
this.handlers.delete(taskType);
logger.debug(`Unregistered handler for task type: ${taskType}`);
}
}
// Get a handler for a specific task type
public getHandler(taskType: AIAgentTaskType): TaskHandler | undefined {
return this.handlers.get(taskType);
}
// Check if a handler exists for a task type
public hasHandler(taskType: AIAgentTaskType): boolean {
return this.handlers.has(taskType);
}
// Get all registered task types
public getRegisteredTaskTypes(): Array<AIAgentTaskType> {
return Array.from(this.handlers.keys());
}
// Get all registered handlers
public getAllHandlers(): Array<TaskHandler> {
return Array.from(this.handlers.values());
}
// Get the number of registered handlers
public getHandlerCount(): number {
return this.handlers.size;
}
// Clear all handlers
public clear(): void {
this.handlers.clear();
logger.debug("Cleared all task handlers");
}
}
// Export a convenience function to get the registry instance
export function getTaskHandlerRegistry(): TaskHandlerRegistry {
return TaskHandlerRegistry.getInstance();
}

View File

@@ -1,17 +0,0 @@
import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from "Common/Types/ObjectID";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
export default class AIAgentUtil {
public static getAIAgentId(): ObjectID {
const id: string | undefined =
LocalCache.getString("AI_AGENT", "AI_AGENT_ID") ||
process.env["AI_AGENT_ID"];
if (!id) {
throw new BadDataException("AI Agent ID not found");
}
return new ObjectID(id);
}
}

View File

@@ -1,12 +0,0 @@
import { AI_AGENT_KEY } from "../Config";
import AIAgentUtil from "./AIAgent";
import { JSONObject } from "Common/Types/JSON";
export default class AIAgentAPIRequest {
public static getDefaultRequestBody(): JSONObject {
return {
aiAgentKey: AI_AGENT_KEY,
aiAgentId: AIAgentUtil.getAIAgentId().toString(),
};
}
}

View File

@@ -1,79 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "./AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import LogSeverity from "Common/Types/Log/LogSeverity";
import logger from "Common/Server/Utils/Logger";
export interface SendLogOptions {
taskId: string;
severity: LogSeverity;
message: string;
}
export default class AIAgentTaskLog {
private static createLogUrl: URL | null = null;
private static getCreateLogUrl(): URL {
if (!this.createLogUrl) {
this.createLogUrl = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent-task-log/create-log",
);
}
return this.createLogUrl;
}
public static async sendLog(options: SendLogOptions): Promise<boolean> {
try {
const result: HTTPResponse<JSONObject> = await API.post({
url: this.getCreateLogUrl(),
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: options.taskId,
severity: options.severity,
message: options.message,
},
});
if (!result.isSuccess()) {
logger.error(`Failed to send log for task ${options.taskId}`);
return false;
}
return true;
} catch (error) {
logger.error(`Error sending log for task ${options.taskId}:`);
logger.error(error);
return false;
}
}
public static async sendTaskStartedLog(taskId: string): Promise<boolean> {
return this.sendLog({
taskId,
severity: LogSeverity.Information,
message: "Task execution started",
});
}
public static async sendTaskCompletedLog(taskId: string): Promise<boolean> {
return this.sendLog({
taskId,
severity: LogSeverity.Information,
message: "Task execution completed successfully",
});
}
public static async sendTaskErrorLog(
taskId: string,
errorMessage: string,
): Promise<boolean> {
return this.sendLog({
taskId,
severity: LogSeverity.Error,
message: `Task execution failed: ${errorMessage}`,
});
}
}

View File

@@ -1,394 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "./AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import LlmType from "Common/Types/LLM/LlmType";
import AIAgentTaskStatus from "Common/Types/AI/AIAgentTaskStatus";
import logger from "Common/Server/Utils/Logger";
// API Response types
interface LLMConfigResponse {
llmType: LlmType;
apiKey?: string;
baseUrl?: string;
modelName?: string;
message?: string;
}
interface ExceptionResponse {
id: string;
message: string;
stackTrace: string;
exceptionType: string;
fingerprint: string;
}
interface ServiceResponse {
id: string;
name: string;
description: string;
}
interface ExceptionDetailsResponse {
exception: ExceptionResponse;
service: ServiceResponse | null;
message?: string;
}
interface CodeRepositoryResponse {
id: string;
name: string;
repositoryHostedAt: string;
organizationName: string;
repositoryName: string;
mainBranchName: string;
servicePathInRepository: string | null;
gitHubAppInstallationId: string | null;
}
interface CodeRepositoriesResponse {
repositories: Array<CodeRepositoryResponse>;
message?: string;
}
interface RepositoryTokenResponse {
token: string;
expiresAt: string;
repositoryUrl: string;
organizationName: string;
repositoryName: string;
message?: string;
}
interface RecordPullRequestResponse {
success: boolean;
pullRequestId: string;
message?: string;
}
interface UpdateTaskStatusResponse {
success?: boolean;
message?: string;
}
// Exported types
export interface LLMConfig {
llmType: LlmType;
apiKey?: string;
baseUrl?: string;
modelName?: string;
}
export interface ExceptionDetails {
exception: {
id: string;
message: string;
stackTrace: string;
exceptionType: string;
fingerprint: string;
};
service: {
id: string;
name: string;
description: string;
} | null;
}
export interface CodeRepositoryInfo {
id: string;
name: string;
repositoryHostedAt: string;
organizationName: string;
repositoryName: string;
mainBranchName: string;
servicePathInRepository: string | null;
gitHubAppInstallationId: string | null;
}
export interface RepositoryToken {
token: string;
expiresAt: Date;
repositoryUrl: string;
organizationName: string;
repositoryName: string;
}
export interface RecordPullRequestOptions {
taskId: string;
codeRepositoryId: string;
pullRequestUrl: string;
pullRequestNumber?: number;
pullRequestId?: number;
title: string;
description?: string;
headRefName?: string;
baseRefName?: string;
}
export interface RecordPullRequestResult {
success: boolean;
pullRequestId: string;
}
export default class BackendAPI {
private baseUrl: URL;
public constructor() {
this.baseUrl = URL.fromString(ONEUPTIME_URL.toString());
}
// Get LLM configuration for a project
public async getLLMConfig(projectId: string): Promise<LLMConfig> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-llm-config",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
projectId: projectId,
},
});
if (!response.isSuccess()) {
const data: LLMConfigResponse =
response.data as unknown as LLMConfigResponse;
const errorMessage: string = data?.message || "Failed to get LLM config";
throw new Error(errorMessage);
}
const data: LLMConfigResponse =
response.data as unknown as LLMConfigResponse;
logger.debug(`Got LLM config for project ${projectId}: ${data.llmType}`);
const llmConfig: LLMConfig = {
llmType: data.llmType,
};
if (data.apiKey) {
llmConfig.apiKey = data.apiKey;
}
if (data.baseUrl) {
llmConfig.baseUrl = data.baseUrl;
}
if (data.modelName) {
llmConfig.modelName = data.modelName;
}
return llmConfig;
}
// Get exception details with telemetry service info
public async getExceptionDetails(
exceptionId: string,
): Promise<ExceptionDetails> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-exception-details",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
exceptionId: exceptionId,
},
});
if (!response.isSuccess()) {
const data: ExceptionDetailsResponse =
response.data as unknown as ExceptionDetailsResponse;
const errorMessage: string =
data?.message || "Failed to get exception details";
throw new Error(errorMessage);
}
const data: ExceptionDetailsResponse =
response.data as unknown as ExceptionDetailsResponse;
logger.debug(
`Got exception details for ${exceptionId}: ${data.exception.message.substring(0, 100)}`,
);
return {
exception: {
id: data.exception.id,
message: data.exception.message,
stackTrace: data.exception.stackTrace,
exceptionType: data.exception.exceptionType,
fingerprint: data.exception.fingerprint,
},
service: data.service
? {
id: data.service.id,
name: data.service.name,
description: data.service.description,
}
: null,
};
}
// Get code repositories linked to a service
public async getCodeRepositories(
serviceId: string,
): Promise<Array<CodeRepositoryInfo>> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-code-repositories",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
serviceId: serviceId,
},
});
if (!response.isSuccess()) {
const data: CodeRepositoriesResponse =
response.data as unknown as CodeRepositoriesResponse;
const errorMessage: string =
data?.message || "Failed to get code repositories";
throw new Error(errorMessage);
}
const data: CodeRepositoriesResponse =
response.data as unknown as CodeRepositoriesResponse;
logger.debug(
`Got ${data.repositories.length} code repositories for service ${serviceId}`,
);
return data.repositories.map((repo: CodeRepositoryResponse) => {
return {
id: repo.id,
name: repo.name,
repositoryHostedAt: repo.repositoryHostedAt,
organizationName: repo.organizationName,
repositoryName: repo.repositoryName,
mainBranchName: repo.mainBranchName,
servicePathInRepository: repo.servicePathInRepository,
gitHubAppInstallationId: repo.gitHubAppInstallationId,
};
});
}
// Get access token for a code repository
public async getRepositoryToken(
codeRepositoryId: string,
): Promise<RepositoryToken> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-repository-token",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
codeRepositoryId: codeRepositoryId,
},
});
if (!response.isSuccess()) {
const data: RepositoryTokenResponse =
response.data as unknown as RepositoryTokenResponse;
const errorMessage: string =
data?.message || "Failed to get repository token";
throw new Error(errorMessage);
}
const data: RepositoryTokenResponse =
response.data as unknown as RepositoryTokenResponse;
logger.debug(
`Got access token for repository ${data.organizationName}/${data.repositoryName}`,
);
return {
token: data.token,
expiresAt: new Date(data.expiresAt),
repositoryUrl: data.repositoryUrl,
organizationName: data.organizationName,
repositoryName: data.repositoryName,
};
}
// Record a pull request created by the AI Agent
public async recordPullRequest(
options: RecordPullRequestOptions,
): Promise<RecordPullRequestResult> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/record-pull-request",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: options.taskId,
codeRepositoryId: options.codeRepositoryId,
pullRequestUrl: options.pullRequestUrl,
pullRequestNumber: options.pullRequestNumber,
pullRequestId: options.pullRequestId,
title: options.title,
description: options.description,
headRefName: options.headRefName,
baseRefName: options.baseRefName,
},
});
if (!response.isSuccess()) {
const data: RecordPullRequestResponse =
response.data as unknown as RecordPullRequestResponse;
const errorMessage: string =
data?.message || "Failed to record pull request";
throw new Error(errorMessage);
}
const data: RecordPullRequestResponse =
response.data as unknown as RecordPullRequestResponse;
logger.debug(`Recorded pull request: ${options.pullRequestUrl}`);
return {
success: data.success,
pullRequestId: data.pullRequestId,
};
}
// Update task status (wrapper around existing endpoint)
public async updateTaskStatus(
taskId: string,
status: AIAgentTaskStatus,
statusMessage?: string,
): Promise<void> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-task/update-task-status",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: status,
statusMessage: statusMessage,
},
});
if (!response.isSuccess()) {
const data: UpdateTaskStatusResponse =
response.data as unknown as UpdateTaskStatusResponse;
const errorMessage: string =
data?.message || "Failed to update task status";
throw new Error(errorMessage);
}
logger.debug(`Updated task ${taskId} status to ${status}`);
}
}

View File

@@ -1,369 +0,0 @@
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import URL from "Common/Types/API/URL";
import { JSONObject, JSONArray } from "Common/Types/JSON";
import logger from "Common/Server/Utils/Logger";
import Headers from "Common/Types/API/Headers";
import TaskLogger from "./TaskLogger";
export interface PullRequestOptions {
token: string;
organizationName: string;
repositoryName: string;
baseBranch: string;
headBranch: string;
title: string;
body: string;
draft?: boolean;
}
export interface PullRequestResult {
id: number;
number: number;
url: string;
htmlUrl: string;
state: string;
title: string;
}
export default class PullRequestCreator {
private static readonly GITHUB_API_BASE: string = "https://api.github.com";
private static readonly GITHUB_API_VERSION: string = "2022-11-28";
private logger: TaskLogger | null = null;
public constructor(taskLogger?: TaskLogger) {
if (taskLogger) {
this.logger = taskLogger;
}
}
// Create a pull request on GitHub
public async createPullRequest(
options: PullRequestOptions,
): Promise<PullRequestResult> {
await this.log(
`Creating pull request: ${options.title} (${options.headBranch} -> ${options.baseBranch})`,
);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${options.organizationName}/${options.repositoryName}/pulls`,
);
const headers: Headers = this.getHeaders(options.token);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
title: options.title,
body: options.body,
head: options.headBranch,
base: options.baseBranch,
draft: options.draft || false,
},
headers,
});
if (!response.isSuccess()) {
const errorData: JSONObject = response.data as JSONObject;
const errorMessage: string =
(errorData["message"] as string) || "Failed to create pull request";
logger.error(`GitHub API error: ${errorMessage}`);
throw new Error(`Failed to create pull request: ${errorMessage}`);
}
const data: JSONObject = response.data as JSONObject;
const result: PullRequestResult = {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
await this.log(`Pull request created: ${result.htmlUrl}`);
return result;
}
// Get an existing pull request by number
public async getPullRequest(
token: string,
organizationName: string,
repositoryName: string,
pullNumber: number,
): Promise<PullRequestResult | null> {
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls/${pullNumber}`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.get({
url,
headers,
});
if (!response.isSuccess()) {
return null;
}
const data: JSONObject = response.data as JSONObject;
return {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
}
// Check if a pull request already exists for a branch
public async findExistingPullRequest(
token: string,
organizationName: string,
repositoryName: string,
headBranch: string,
baseBranch: string,
): Promise<PullRequestResult | null> {
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONArray> | HTTPErrorResponse = await API.get(
{
url,
headers,
params: {
head: `${organizationName}:${headBranch}`,
base: baseBranch,
state: "open",
},
},
);
if (!response.isSuccess()) {
return null;
}
const pulls: JSONArray = response.data as JSONArray;
if (pulls.length > 0) {
const data: JSONObject = pulls[0] as JSONObject;
return {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
}
return null;
}
// Update an existing pull request
public async updatePullRequest(
token: string,
organizationName: string,
repositoryName: string,
pullNumber: number,
updates: { title?: string; body?: string; state?: "open" | "closed" },
): Promise<PullRequestResult> {
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls/${pullNumber}`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.patch({
url,
data: updates,
headers,
});
if (!response.isSuccess()) {
const errorData: JSONObject = response.data as JSONObject;
const errorMessage: string =
(errorData["message"] as string) || "Failed to update pull request";
throw new Error(`Failed to update pull request: ${errorMessage}`);
}
const data: JSONObject = response.data as JSONObject;
return {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
}
// Add labels to a pull request
public async addLabels(
token: string,
organizationName: string,
repositoryName: string,
issueNumber: number,
labels: Array<string>,
): Promise<void> {
await this.log(`Adding labels to PR #${issueNumber}: ${labels.join(", ")}`);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/issues/${issueNumber}/labels`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: { labels },
headers,
});
if (!response.isSuccess()) {
logger.warn(`Failed to add labels to PR #${issueNumber}`);
}
}
// Add reviewers to a pull request
public async requestReviewers(
token: string,
organizationName: string,
repositoryName: string,
pullNumber: number,
reviewers: Array<string>,
teamReviewers?: Array<string>,
): Promise<void> {
await this.log(`Requesting reviewers for PR #${pullNumber}`);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls/${pullNumber}/requested_reviewers`,
);
const headers: Headers = this.getHeaders(token);
const data: JSONObject = {
reviewers,
};
if (teamReviewers && teamReviewers.length > 0) {
data["team_reviewers"] = teamReviewers;
}
const response: HTTPResponse<JSONObject> = await API.post({
url,
data,
headers,
});
if (!response.isSuccess()) {
logger.warn(`Failed to request reviewers for PR #${pullNumber}`);
}
}
// Add a comment to a pull request
public async addComment(
token: string,
organizationName: string,
repositoryName: string,
issueNumber: number,
comment: string,
): Promise<void> {
await this.log(`Adding comment to PR #${issueNumber}`);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/issues/${issueNumber}/comments`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: { body: comment },
headers,
});
if (!response.isSuccess()) {
logger.warn(`Failed to add comment to PR #${issueNumber}`);
}
}
// Generate PR body from exception details
public static generatePRBody(data: {
exceptionMessage: string;
exceptionType: string;
stackTrace: string;
serviceName: string;
summary: string;
}): string {
return `## Exception Fix
This pull request was automatically generated by OneUptime AI Agent to fix an exception.
### Exception Details
**Service:** ${data.serviceName}
**Type:** ${data.exceptionType}
**Message:** ${data.exceptionMessage}
### Stack Trace
\`\`\`
${data.stackTrace.substring(0, 2000)}${data.stackTrace.length > 2000 ? "\n...(truncated)" : ""}
\`\`\`
### Summary of Changes
${data.summary}
---
*This PR was automatically generated by [OneUptime AI Agent](https://oneuptime.com)*`;
}
// Generate PR title from exception
public static generatePRTitle(exceptionMessage: string): string {
// Truncate and clean the message for use as a title
const cleanMessage: string = exceptionMessage
.replace(/\n/g, " ")
.replace(/\s+/g, " ")
.trim();
const maxLength: number = 70;
if (cleanMessage.length <= maxLength) {
return `fix: ${cleanMessage}`;
}
return `fix: ${cleanMessage.substring(0, maxLength - 3)}...`;
}
// Helper method to get GitHub API headers
private getHeaders(token: string): Headers {
return {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": PullRequestCreator.GITHUB_API_VERSION,
"Content-Type": "application/json",
};
}
// Helper method for logging
private async log(message: string): Promise<void> {
if (this.logger) {
await this.logger.info(message);
} else {
logger.debug(message);
}
}
}

View File

@@ -1,313 +0,0 @@
import Execute from "Common/Server/Utils/Execute";
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
import path from "path";
import TaskLogger from "./TaskLogger";
export interface CloneResult {
workingDirectory: string;
repositoryPath: string;
}
export interface RepositoryConfig {
organizationName: string;
repositoryName: string;
token: string;
repositoryUrl?: string;
}
export default class RepositoryManager {
private logger: TaskLogger | null = null;
public constructor(taskLogger?: TaskLogger) {
if (taskLogger) {
this.logger = taskLogger;
}
}
// Clone a repository with token-based authentication
public async cloneRepository(
config: RepositoryConfig,
workDir: string,
): Promise<CloneResult> {
await this.log(
`Cloning repository ${config.organizationName}/${config.repositoryName}...`,
);
// Build the authenticated URL
const authUrl: string = this.buildAuthenticatedUrl(config);
// Ensure the working directory exists
await LocalFile.makeDirectory(workDir);
// Clone the repository
await this.runGitCommand(workDir, ["clone", authUrl]);
const repositoryPath: string = path.join(workDir, config.repositoryName);
await this.log(`Repository cloned to ${repositoryPath}`);
// Set git config for the repository
await this.setGitConfig(repositoryPath);
return {
workingDirectory: workDir,
repositoryPath: repositoryPath,
};
}
// Build URL with embedded token for authentication
private buildAuthenticatedUrl(config: RepositoryConfig): string {
if (config.repositoryUrl) {
// If URL is provided, inject token
const url: URL = new URL(config.repositoryUrl);
url.username = "x-access-token";
url.password = config.token;
return url.toString();
}
// Default to GitHub
return `https://x-access-token:${config.token}@github.com/${config.organizationName}/${config.repositoryName}.git`;
}
// Set git user config for commits
private async setGitConfig(repoPath: string): Promise<void> {
await this.runGitCommand(repoPath, [
"config",
"user.name",
"OneUptime AI Agent",
]);
await this.runGitCommand(repoPath, [
"config",
"user.email",
"ai-agent@oneuptime.com",
]);
}
// Create a new branch
public async createBranch(
repoPath: string,
branchName: string,
): Promise<void> {
await this.log(`Creating branch: ${branchName}`);
await this.runGitCommand(repoPath, ["checkout", "-b", branchName]);
await this.log(`Branch ${branchName} created and checked out`);
}
// Checkout existing branch
public async checkoutBranch(
repoPath: string,
branchName: string,
): Promise<void> {
await this.log(`Checking out branch: ${branchName}`);
await this.runGitCommand(repoPath, ["checkout", branchName]);
}
// Create branch if doesn't exist, or checkout if it does
public async createOrCheckoutBranch(
repoPath: string,
branchName: string,
): Promise<void> {
try {
// Check if branch exists locally
await this.runGitCommand(repoPath, ["rev-parse", "--verify", branchName]);
await this.checkoutBranch(repoPath, branchName);
} catch {
// Branch doesn't exist, create it
await this.createBranch(repoPath, branchName);
}
}
// Add all changes to staging
public async addAllChanges(repoPath: string): Promise<void> {
await this.log("Adding all changes to git staging...");
await this.runGitCommand(repoPath, ["add", "-A"]);
}
// Check if there are any changes to commit
public async hasChanges(repoPath: string): Promise<boolean> {
try {
const status: string = await this.runGitCommand(repoPath, [
"status",
"--porcelain",
]);
return status.trim().length > 0;
} catch (error) {
logger.error("Error checking for changes:");
logger.error(error);
return false;
}
}
// Get list of changed files
public async getChangedFiles(repoPath: string): Promise<Array<string>> {
const status: string = await this.runGitCommand(repoPath, [
"status",
"--porcelain",
]);
if (!status.trim()) {
return [];
}
return status
.split("\n")
.filter((line: string) => {
return line.trim().length > 0;
})
.map((line: string) => {
// Status output format is "XY filename" where XY is the status
return line.substring(3).trim();
});
}
// Commit changes
public async commitChanges(repoPath: string, message: string): Promise<void> {
await this.log(`Committing changes: ${message.substring(0, 50)}...`);
await Execute.executeCommandFile({
command: "git",
args: ["commit", "-m", message],
cwd: repoPath,
});
await this.log("Changes committed successfully");
}
// Push branch to remote
public async pushBranch(
repoPath: string,
branchName: string,
config: RepositoryConfig,
): Promise<void> {
await this.log(`Pushing branch ${branchName} to remote...`);
// Set the remote URL with authentication
const authUrl: string = this.buildAuthenticatedUrl(config);
// Update the remote URL
await this.runGitCommand(repoPath, [
"remote",
"set-url",
"origin",
authUrl,
]);
// Push with tracking
await this.runGitCommand(repoPath, ["push", "-u", "origin", branchName]);
await this.log(`Branch ${branchName} pushed to remote`);
}
// Get the current branch name
public async getCurrentBranch(repoPath: string): Promise<string> {
const branch: string = await this.runGitCommand(repoPath, [
"rev-parse",
"--abbrev-ref",
"HEAD",
]);
return branch.trim();
}
// Get the current commit hash
public async getCurrentCommitHash(repoPath: string): Promise<string> {
const hash: string = await this.runGitCommand(repoPath, [
"rev-parse",
"HEAD",
]);
return hash.trim();
}
// Pull latest changes from remote
public async pullChanges(repoPath: string): Promise<void> {
await this.log("Pulling latest changes from remote...");
await this.runGitCommand(repoPath, ["pull"]);
}
// Discard all local changes
public async discardChanges(repoPath: string): Promise<void> {
await this.log("Discarding all local changes...");
await this.runGitCommand(repoPath, ["checkout", "."]);
await this.runGitCommand(repoPath, ["clean", "-fd"]);
}
// Clean up the repository directory
public async cleanup(workDir: string): Promise<void> {
await this.log(`Cleaning up workspace: ${workDir}`);
try {
await LocalFile.deleteDirectory(workDir);
await this.log("Workspace cleaned up successfully");
} catch (error) {
logger.error(`Error cleaning up workspace ${workDir}:`);
logger.error(error);
}
}
// Get diff of current changes
public async getDiff(repoPath: string): Promise<string> {
try {
const diff: string = await this.runGitCommand(repoPath, ["diff"]);
return diff;
} catch (error) {
logger.error("Error getting diff:");
logger.error(error);
return "";
}
}
// Get staged diff
public async getStagedDiff(repoPath: string): Promise<string> {
try {
const diff: string = await this.runGitCommand(repoPath, [
"diff",
"--staged",
]);
return diff;
} catch (error) {
logger.error("Error getting staged diff:");
logger.error(error);
return "";
}
}
// Helper method to run git commands
private async runGitCommand(
repoPath: string,
args: Array<string>,
): Promise<string> {
const cwd: string = path.resolve(repoPath);
const logArgs: Array<string> = args.map((arg: string) => {
// Mask tokens in URLs
if (arg.includes("x-access-token:")) {
return arg.replace(/x-access-token:[^@]+@/, "x-access-token:***@");
}
return arg.includes(" ") ? `"${arg}"` : arg;
});
logger.debug(`Executing git command in ${cwd}: git ${logArgs.join(" ")}`);
return Execute.executeCommandFile({
command: "git",
args,
cwd,
});
}
// Helper method for logging
private async log(message: string): Promise<void> {
if (this.logger) {
await this.logger.info(message);
} else {
logger.debug(message);
}
}
}

View File

@@ -1,229 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "./AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import LogSeverity from "Common/Types/Log/LogSeverity";
import logger from "Common/Server/Utils/Logger";
import OneUptimeDate from "Common/Types/Date";
export interface TaskLoggerOptions {
taskId: string;
context?: string;
batchSize?: number;
flushIntervalMs?: number;
}
interface LogEntry {
severity: LogSeverity;
message: string;
timestamp: Date;
}
export default class TaskLogger {
private taskId: string;
private context: string | undefined;
private logBuffer: Array<LogEntry> = [];
private batchSize: number;
private flushIntervalMs: number;
private flushTimer: ReturnType<typeof setInterval> | null = null;
private createLogUrl: URL | null = null;
public constructor(options: TaskLoggerOptions) {
this.taskId = options.taskId;
this.context = options.context;
this.batchSize = options.batchSize || 10;
this.flushIntervalMs = options.flushIntervalMs || 5000; // 5 seconds default
// Start periodic flush timer
this.startFlushTimer();
}
private getCreateLogUrl(): URL {
if (!this.createLogUrl) {
this.createLogUrl = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent-task-log/create-log",
);
}
return this.createLogUrl;
}
private startFlushTimer(): void {
this.flushTimer = setInterval(() => {
this.flush().catch((err: Error) => {
logger.error(`Error flushing logs: ${err.message}`);
});
}, this.flushIntervalMs);
}
private stopFlushTimer(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = null;
}
}
private formatMessage(
severity: LogSeverity,
message: string,
timestamp: Date,
): string {
const timestampStr: string = OneUptimeDate.toDateTimeLocalString(timestamp);
const severityStr: string = severity.toUpperCase().padEnd(7);
const contextStr: string = this.context ? `[${this.context}] ` : "";
return `[${timestampStr}] [${severityStr}] ${contextStr}${message}`;
}
private addToBuffer(severity: LogSeverity, message: string): void {
const entry: LogEntry = {
severity,
message,
timestamp: OneUptimeDate.getCurrentDate(),
};
this.logBuffer.push(entry);
// Also log locally for debugging
logger.debug(
`[Task ${this.taskId}] ${this.formatMessage(entry.severity, entry.message, entry.timestamp)}`,
);
// Auto-flush if buffer is full
if (this.logBuffer.length >= this.batchSize) {
this.flush().catch((err: Error) => {
logger.error(`Error auto-flushing logs: ${err.message}`);
});
}
}
private async sendLogToServer(
severity: LogSeverity,
message: string,
): Promise<boolean> {
try {
const result: HTTPResponse<JSONObject> = await API.post({
url: this.getCreateLogUrl(),
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: this.taskId,
severity: severity,
message: message,
},
});
if (!result.isSuccess()) {
logger.error(`Failed to send log for task ${this.taskId}`);
return false;
}
return true;
} catch (error) {
logger.error(`Error sending log for task ${this.taskId}:`);
logger.error(error);
return false;
}
}
// Public logging methods
public async debug(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Debug, message);
}
public async info(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Information, message);
}
public async warning(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Warning, message);
}
public async error(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Error, message);
// Immediately flush on errors
await this.flush();
}
public async trace(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Trace, message);
}
// Log output from external processes like OpenCode
public async logProcessOutput(
processName: string,
output: string,
): Promise<void> {
const lines: Array<string> = output.split("\n").filter((line: string) => {
return line.trim().length > 0;
});
for (const line of lines) {
this.addToBuffer(LogSeverity.Information, `[${processName}] ${line}`);
}
}
// Log a code block (useful for stack traces, code snippets, etc.)
public async logCodeBlock(
title: string,
code: string,
severity: LogSeverity = LogSeverity.Information,
): Promise<void> {
const formattedCode: string = `${title}:\n\`\`\`\n${code}\n\`\`\``;
this.addToBuffer(severity, formattedCode);
}
// Flush all buffered logs to the server
public async flush(): Promise<void> {
if (this.logBuffer.length === 0) {
return;
}
// Get all entries and clear buffer
const entries: Array<LogEntry> = [...this.logBuffer];
this.logBuffer = [];
// Send each log entry separately to preserve individual log lines
for (const entry of entries) {
const formattedMessage: string = this.formatMessage(
entry.severity,
entry.message,
entry.timestamp,
);
await this.sendLogToServer(entry.severity, formattedMessage);
}
}
// Cleanup method - call when task is done
public async dispose(): Promise<void> {
this.stopFlushTimer();
await this.flush();
}
// Helper methods for common log patterns
public async logStepStart(stepName: string): Promise<void> {
await this.info(`Starting: ${stepName}`);
}
public async logStepComplete(stepName: string): Promise<void> {
await this.info(`Completed: ${stepName}`);
}
public async logStepFailed(stepName: string, error: string): Promise<void> {
await this.error(`Failed: ${stepName} - ${error}`);
}
// Create a child logger with additional context
public createChildLogger(childContext: string): TaskLogger {
const fullContext: string = this.context
? `${this.context}:${childContext}`
: childContext;
return new TaskLogger({
taskId: this.taskId,
context: fullContext,
batchSize: this.batchSize,
flushIntervalMs: this.flushIntervalMs,
});
}
}

View File

@@ -1,221 +0,0 @@
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
import ObjectID from "Common/Types/ObjectID";
import path from "path";
import os from "os";
export interface WorkspaceInfo {
workspacePath: string;
taskId: string;
createdAt: Date;
}
export default class WorkspaceManager {
private static readonly BASE_TEMP_DIR: string = path.join(
os.tmpdir(),
"oneuptime-ai-agent",
);
// Create a new workspace for a task
public static async createWorkspace(taskId: string): Promise<WorkspaceInfo> {
const timestamp: number = Date.now();
const uniqueId: string = ObjectID.generate().toString().substring(0, 8);
const workspaceName: string = `task-${taskId}-${timestamp}-${uniqueId}`;
const workspacePath: string = path.join(this.BASE_TEMP_DIR, workspaceName);
logger.debug(`Creating workspace: ${workspacePath}`);
// Create the workspace directory
await LocalFile.makeDirectory(workspacePath);
return {
workspacePath,
taskId,
createdAt: new Date(),
};
}
// Create a subdirectory within a workspace
public static async createSubdirectory(
workspacePath: string,
subdirectoryName: string,
): Promise<string> {
const subdirectoryPath: string = path.join(workspacePath, subdirectoryName);
await LocalFile.makeDirectory(subdirectoryPath);
return subdirectoryPath;
}
// Check if workspace exists
public static async workspaceExists(workspacePath: string): Promise<boolean> {
try {
await LocalFile.readDirectory(workspacePath);
return true;
} catch {
return false;
}
}
// Delete a workspace and all its contents
public static async deleteWorkspace(workspacePath: string): Promise<void> {
logger.debug(`Deleting workspace: ${workspacePath}`);
try {
// Verify the path is within our temp directory to prevent accidental deletion
const normalizedPath: string = path.normalize(workspacePath);
const normalizedBase: string = path.normalize(this.BASE_TEMP_DIR);
if (!normalizedPath.startsWith(normalizedBase)) {
throw new Error(
`Security error: Cannot delete path outside workspace base: ${workspacePath}`,
);
}
await LocalFile.deleteDirectory(workspacePath);
logger.debug(`Workspace deleted: ${workspacePath}`);
} catch (error) {
logger.error(`Error deleting workspace ${workspacePath}:`);
logger.error(error);
}
}
// Write a file to workspace
public static async writeFile(
workspacePath: string,
relativePath: string,
content: string,
): Promise<string> {
const filePath: string = path.join(workspacePath, relativePath);
// Ensure parent directory exists
const parentDir: string = path.dirname(filePath);
await LocalFile.makeDirectory(parentDir);
await LocalFile.write(filePath, content);
return filePath;
}
// Read a file from workspace
public static async readFile(
workspacePath: string,
relativePath: string,
): Promise<string> {
const filePath: string = path.join(workspacePath, relativePath);
return LocalFile.read(filePath);
}
// Check if a file exists in workspace
public static async fileExists(
workspacePath: string,
relativePath: string,
): Promise<boolean> {
try {
const filePath: string = path.join(workspacePath, relativePath);
await LocalFile.read(filePath);
return true;
} catch {
return false;
}
}
// Delete a file from workspace
public static async deleteFile(
workspacePath: string,
relativePath: string,
): Promise<void> {
const filePath: string = path.join(workspacePath, relativePath);
await LocalFile.deleteFile(filePath);
}
// List files in workspace directory
public static async listFiles(workspacePath: string): Promise<Array<string>> {
const entries: Array<{ name: string; isDirectory(): boolean }> =
await LocalFile.readDirectory(workspacePath);
return entries.map((entry: { name: string }) => {
return entry.name;
});
}
// Get the full path for a relative path in workspace
public static getFullPath(
workspacePath: string,
relativePath: string,
): string {
return path.join(workspacePath, relativePath);
}
// Clean up old workspaces (older than specified hours)
public static async cleanupOldWorkspaces(
maxAgeHours: number = 24,
): Promise<number> {
logger.debug(`Cleaning up workspaces older than ${maxAgeHours} hours`);
let cleanedCount: number = 0;
try {
// Ensure base directory exists
try {
await LocalFile.readDirectory(this.BASE_TEMP_DIR);
} catch {
// Base directory doesn't exist, nothing to clean
return 0;
}
const entries: Array<{ name: string; isDirectory(): boolean }> =
await LocalFile.readDirectory(this.BASE_TEMP_DIR);
const maxAge: number = maxAgeHours * 60 * 60 * 1000; // Convert to milliseconds
const now: number = Date.now();
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const workspacePath: string = path.join(this.BASE_TEMP_DIR, entry.name);
/*
* Try to extract timestamp from directory name
* Format: task-{taskId}-{timestamp}-{uniqueId}
*/
const match: RegExpMatchArray | null = entry.name.match(
/task-[^-]+-(\d+)-[^-]+/,
);
if (match) {
const timestamp: number = parseInt(match[1] || "0", 10);
if (now - timestamp > maxAge) {
await this.deleteWorkspace(workspacePath);
cleanedCount++;
}
}
}
} catch (error) {
logger.error("Error during workspace cleanup:");
logger.error(error);
}
logger.debug(`Cleaned up ${cleanedCount} old workspaces`);
return cleanedCount;
}
// Initialize workspace manager (create base directory if needed)
public static async initialize(): Promise<void> {
try {
await LocalFile.makeDirectory(this.BASE_TEMP_DIR);
logger.debug(
`Workspace base directory initialized: ${this.BASE_TEMP_DIR}`,
);
} catch (error) {
logger.error("Error initializing workspace manager:");
logger.error(error);
}
}
// Get the base temp directory path
public static getBaseTempDir(): string {
return this.BASE_TEMP_DIR;
}
}

View File

@@ -1,11 +0,0 @@
{
"watch": [
"./",
"../Common"
],
"ext": "ts,tsx",
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -1,45 +0,0 @@
{
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"resolveJsonModule": true
}
},
"compilerOptions": {
"target": "es2017",
"jsx": "react",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"rootDir": "",
"moduleResolution": "node",
"typeRoots": [
"./node_modules/@types"
],
"types": ["node", "jest"],
"sourceMap": true,
"outDir": "build/dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["/**/*.ts"],
"exclude": ["node_modules"]
}

30
APIReference/.gitignore vendored Executable file
View File

@@ -0,0 +1,30 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
#/backend/node_modules
/kubernetes
/node_modules
.idea
# misc
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
**/*/paymentService.test.js
apiTest.rest
application_security_dir
container_security_dir
# coverage
/coverage
/.nyc_output
/greenlock.d/config.json
/greenlock.d/config.json.bak
/.greenlockrc

View File

@@ -0,0 +1,72 @@
#
# OneUptime-App Dockerfile
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
RUN npm config set fetch-retry-mintimeout 100000
RUN npm config set fetch-retry-maxtimeout 600000
ARG GIT_SHA
ARG APP_VERSION
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
# IF APP_VERSION is not set, set it to 1.0.0
RUN if [ -z "$APP_VERSION" ]; then export APP_VERSION=1.0.0; fi
# Install bash.
RUN apk add bash && apk add curl
# Install python
RUN apk update && apk add --no-cache --virtual .gyp python3 make g++
#Use bash shell by default
SHELL ["/bin/bash", "-c"]
RUN mkdir /usr/src
WORKDIR /usr/src/Common
COPY ./Common/package*.json /usr/src/Common/
# Set version in ./Common/package.json to the APP_VERSION
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/Common/package.json
RUN npm install
COPY ./Common /usr/src/Common
ENV PRODUCTION=true
WORKDIR /usr/src/app
# Install app dependencies
COPY ./APIReference/package*.json /usr/src/app/
# Set version in ./App/package.json to the APP_VERSION
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/app/package.json
RUN npm install
# Expose ports.
# - 1446: OneUptime-api-reference
EXPOSE 1446
{{ if eq .Env.ENVIRONMENT "development" }}
#Run the app
CMD [ "npm", "run", "dev" ]
{{ else }}
# Copy app source
COPY ./APIReference /usr/src/app
# Bundle app source
RUN npm run compile
# Set permission to write logs and cache in case container run as non root
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
#Run the app
CMD [ "npm", "start" ]
{{ end }}

52
APIReference/Index.ts Executable file
View File

@@ -0,0 +1,52 @@
import APIReferenceRoutes from "./Routes";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import InfrastructureStatus from "Common/Server/Infrastructure/Status";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import "ejs";
const APP_NAME: string = "reference";
const init: PromiseVoidFunction = async (): Promise<void> => {
try {
// Initialize telemetry
Telemetry.init({
serviceName: APP_NAME,
});
const statusCheck: PromiseVoidFunction = async (): Promise<void> => {
// Check the status of infrastructure components
return await InfrastructureStatus.checkStatusWithRetry({
checkClickhouseStatus: false,
checkPostgresStatus: false,
checkRedisStatus: false,
retryCount: 3,
});
};
// Initialize the app with service name and status checks
await App.init({
appName: APP_NAME,
statusOptions: {
liveCheck: statusCheck,
readyCheck: statusCheck,
},
});
await APIReferenceRoutes.init();
// Add default routes to the app
await App.addDefaultRoutes();
} catch (err) {
logger.error("App Init Failed:");
logger.error(err);
throw err;
}
};
init().catch((err: Error) => {
logger.error(err);
logger.error("Exiting node process");
process.exit(1);
});

29
APIReference/README.md Executable file
View File

@@ -0,0 +1,29 @@
# README
This README would normally document whatever steps are necessary to get your application up and running.
### What is this repository for?
- Quick summary
- Version
- [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo)
### How do I get set up?
- Summary of set up
- Configuration
- Dependencies
- Database configuration
- How to run tests
- Deployment instructions
### Contribution guidelines
- Writing tests
- Code review
- Other guidelines
### Who do I talk to?
- Repo owner or admin
- Other community or team contact

View File

@@ -1,6 +1,5 @@
import AuthenticationServiceHandler from "./Service/Authentication";
import DataTypeServiceHandler from "./Service/DataType";
import DataTypeDetailServiceHandler from "./Service/DataTypeDetail";
import ErrorServiceHandler from "./Service/Errors";
import OpenAPIServiceHandler from "./Service/OpenAPI";
import IntroductionServiceHandler from "./Service/Introduction";
@@ -11,7 +10,6 @@ import PermissionServiceHandler from "./Service/Permissions";
import StatusServiceHandler from "./Service/Status";
import { StaticPath } from "./Utils/Config";
import ResourceUtil, { ModelDocumentation } from "./Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "./Utils/DataTypes";
import Dictionary from "Common/Types/Dictionary";
import FeatureSet from "Common/Server/Types/FeatureSet";
import Express, {
@@ -26,9 +24,6 @@ const APIReferenceFeatureSet: FeatureSet = {
const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath();
const DataTypeDictionary: Dictionary<DataTypeDocumentation> =
DataTypeUtil.getDataTypeDictionaryByPath();
const app: ExpressApplication = Express.getExpressApp();
// Serve static files for the API reference with a cache max age of 30 days
@@ -77,8 +72,6 @@ const APIReferenceFeatureSet: FeatureSet = {
return StatusServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "data-types") {
return DataTypeServiceHandler.executeResponse(req, res);
} else if (DataTypeDictionary[page]) {
return DataTypeDetailServiceHandler.executeResponse(req, res);
} else if (currentResource) {
return ModelServiceHandler.executeResponse(req, res);
}

View File

@@ -1,13 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Retrieve resources documentation
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -19,7 +16,7 @@ export default class ServiceHandler {
// Extract page parameter from request
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set default page title and description for the authentication page
pageTitle = "Authentication";
@@ -29,7 +26,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,23 +1,20 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
_req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
pageData["selectCode"] = await LocalCache.getOrSetString(
pageData.selectCode = await LocalCache.getOrSetString(
"data-type",
"select",
async () => {
@@ -25,7 +22,7 @@ export default class ServiceHandler {
},
);
pageData["sortCode"] = await LocalCache.getOrSetString(
pageData.sortCode = await LocalCache.getOrSetString(
"data-type",
"sort",
async () => {
@@ -33,7 +30,7 @@ export default class ServiceHandler {
},
);
pageData["equalToCode"] = await LocalCache.getOrSetString(
pageData.equalToCode = await LocalCache.getOrSetString(
"data-type",
"equal-to",
async () => {
@@ -41,7 +38,7 @@ export default class ServiceHandler {
},
);
pageData["equalToOrNullCode"] = await LocalCache.getOrSetString(
pageData.equalToOrNullCode = await LocalCache.getOrSetString(
"data-type",
"equal-to-or-null",
async () => {
@@ -51,7 +48,7 @@ export default class ServiceHandler {
},
);
pageData["greaterThanCode"] = await LocalCache.getOrSetString(
pageData.greaterThanCode = await LocalCache.getOrSetString(
"data-type",
"greater-than",
async () => {
@@ -61,7 +58,7 @@ export default class ServiceHandler {
},
);
pageData["greaterThanOrEqualCode"] = await LocalCache.getOrSetString(
pageData.greaterThanOrEqualCode = await LocalCache.getOrSetString(
"data-type",
"greater-than-or-equal",
async () => {
@@ -71,7 +68,7 @@ export default class ServiceHandler {
},
);
pageData["lessThanCode"] = await LocalCache.getOrSetString(
pageData.lessThanCode = await LocalCache.getOrSetString(
"data-type",
"less-than",
async () => {
@@ -81,7 +78,7 @@ export default class ServiceHandler {
},
);
pageData["lessThanOrEqualCode"] = await LocalCache.getOrSetString(
pageData.lessThanOrEqualCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
@@ -91,7 +88,7 @@ export default class ServiceHandler {
},
);
pageData["includesCode"] = await LocalCache.getOrSetString(
pageData.includesCode = await LocalCache.getOrSetString(
"data-type",
"includes",
async () => {
@@ -101,7 +98,7 @@ export default class ServiceHandler {
},
);
pageData["lessThanOrNullCode"] = await LocalCache.getOrSetString(
pageData.lessThanOrNullCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
@@ -111,7 +108,7 @@ export default class ServiceHandler {
},
);
pageData["greaterThanOrNullCode"] = await LocalCache.getOrSetString(
pageData.greaterThanOrNullCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
@@ -121,7 +118,7 @@ export default class ServiceHandler {
},
);
pageData["isNullCode"] = await LocalCache.getOrSetString(
pageData.isNullCode = await LocalCache.getOrSetString(
"data-type",
"is-null",
async () => {
@@ -129,7 +126,7 @@ export default class ServiceHandler {
},
);
pageData["notNullCode"] = await LocalCache.getOrSetString(
pageData.notNullCode = await LocalCache.getOrSetString(
"data-type",
"not-null",
async () => {
@@ -137,7 +134,7 @@ export default class ServiceHandler {
},
);
pageData["notEqualToCode"] = await LocalCache.getOrSetString(
pageData.notEqualToCode = await LocalCache.getOrSetString(
"data-type",
"not-equals",
async () => {
@@ -155,7 +152,6 @@ export default class ServiceHandler {
pageDescription:
"Data Types that can be used to interact with OneUptime API",
resources: Resources,
dataTypes: DataTypes,
pageData: pageData,
});
}

View File

@@ -1,13 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// Handles the HTTP response for a given request
@@ -20,7 +17,7 @@ export default class ServiceHandler {
// Get the 'page' parameter from the request
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set the default page title and description
pageTitle = "Errors";
@@ -30,7 +27,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,13 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Get all resources and featured resources from ResourceUtil
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
const FeaturedResources: Array<ModelDocumentation> =
ResourceUtil.getFeaturedResources();
@@ -23,10 +20,10 @@ export default class ServiceHandler {
// Get the requested page from the URL parameters
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set featured resources for the page
pageData["featuredResources"] = FeaturedResources;
pageData.featuredResources = FeaturedResources;
// Set page title and description
pageTitle = "Introduction";
@@ -36,7 +33,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -0,0 +1,276 @@
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import PageNotFoundServiceHandler from "./PageNotFound";
import { AppApiRoute } from "Common/ServiceRoute";
import { ColumnAccessControl } from "Common/Types/BaseDatabase/AccessControl";
import { getTableColumns } from "Common/Types/Database/TableColumn";
import Dictionary from "Common/Types/Dictionary";
import ObjectID from "Common/Types/ObjectID";
import Permission, {
PermissionHelper,
PermissionProps,
} from "Common/Types/Permission";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
// Get all resources and resource dictionary
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath();
// Get all permission props
const PermissionDictionary: Dictionary<PermissionProps> =
PermissionHelper.getAllPermissionPropsAsDictionary();
export default class ServiceHandler {
// Execute response for a given page
public static async executeResponse(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
let pageTitle: string = "";
let pageDescription: string = "";
let page: string | undefined = req.params["page"];
const pageData: any = {};
// Check if page is provided
if (!page) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
// Get current resource
const currentResource: ModelDocumentation | undefined =
ResourceDictionary[page];
// Check if current resource exists
if (!currentResource) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
// Set page title and description
pageTitle = currentResource.name;
pageDescription = currentResource.description;
page = "model";
// Get table columns for current resource
const tableColumns: any = getTableColumns(currentResource.model);
// Filter out columns with no access
for (const key in tableColumns) {
const accessControl: ColumnAccessControl | null =
currentResource.model.getColumnAccessControlFor(key);
if (!accessControl) {
delete tableColumns[key];
continue;
}
if (
accessControl?.create.length === 0 &&
accessControl?.read.length === 0 &&
accessControl?.update.length === 0
) {
delete tableColumns[key];
continue;
}
if (tableColumns[key].hideColumnInDocumentation) {
delete tableColumns[key];
continue;
}
tableColumns[key].permissions = accessControl;
}
// Remove unnecessary columns
delete tableColumns["deletedAt"];
delete tableColumns["deletedByUserId"];
delete tableColumns["deletedByUser"];
delete tableColumns["version"];
// Set page data
pageData.title = currentResource.model.singularName;
pageData.description = currentResource.model.tableDescription;
pageData.columns = tableColumns;
pageData.tablePermissions = {
read: currentResource.model.readRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
update: currentResource.model.updateRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
delete: currentResource.model.deleteRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
create: currentResource.model.createRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
};
// Cache the list request data
pageData.listRequest = await LocalCache.getOrSetString(
"model",
"list-request",
async () => {
// Read the list request data from a file
return await LocalFile.read(`${CodeExamplesPath}/Model/ListRequest.md`);
},
);
// Cache the item request data
pageData.itemRequest = await LocalCache.getOrSetString(
"model",
"item-request",
async () => {
// Read the item request data from a file
return await LocalFile.read(`${CodeExamplesPath}/Model/ItemRequest.md`);
},
);
// Cache the item response data
pageData.itemResponse = await LocalCache.getOrSetString(
"model",
"item-response",
async () => {
// Read the item response data from a file
return await LocalFile.read(
`${CodeExamplesPath}/Model/ItemResponse.md`,
);
},
);
// Cache the count request data
pageData.countRequest = await LocalCache.getOrSetString(
"model",
"count-request",
async () => {
// Read the count request data from a file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountRequest.md`,
);
},
);
// Cache the count response data
pageData.countResponse = await LocalCache.getOrSetString(
"model",
"count-response",
async () => {
// Read the CountResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountResponse.md`,
);
},
);
pageData.updateRequest = await LocalCache.getOrSetString(
"model",
"update-request",
async () => {
// Read the UpdateRequest.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateRequest.md`,
);
},
);
pageData.updateResponse = await LocalCache.getOrSetString(
"model",
"update-response",
async () => {
// Read the UpdateResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateResponse.md`,
);
},
);
pageData.createRequest = await LocalCache.getOrSetString(
"model",
"create-request",
async () => {
// Read the CreateRequest.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateRequest.md`,
);
},
);
pageData.createResponse = await LocalCache.getOrSetString(
"model",
"create-response",
async () => {
// Read the CreateResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateResponse.md`,
);
},
);
pageData.deleteRequest = await LocalCache.getOrSetString(
"model",
"delete-request",
async () => {
// Read the DeleteRequest.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteRequest.md`,
);
},
);
pageData.deleteResponse = await LocalCache.getOrSetString(
"model",
"delete-response",
async () => {
// Read the DeleteResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteResponse.md`,
);
},
);
// Get list response from cache or set it if it's not available
pageData.listResponse = await LocalCache.getOrSetString(
"model",
"list-response",
async () => {
// Read the list response from a file
return await LocalFile.read(
`${CodeExamplesPath}/Model/ListResponse.md`,
);
},
);
// Generate a unique ID for the example object
pageData.exampleObjectID = ObjectID.generate();
// Construct the API path for the current resource
pageData.apiPath =
AppApiRoute.toString() + currentResource.model.crudApiPath?.toString();
// Check if the current resource is a master admin API
pageData.isMasterAdminApiDocs = currentResource.model.isMasterAdminApiDocs;
// Render the index page with the required data
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,
pageData: pageData,
});
}
}

View File

@@ -5,14 +5,11 @@ import {
} from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import URL from "Common/Types/API/URL";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// Handles the HTTP response for a given request
@@ -25,7 +22,7 @@ export default class ServiceHandler {
// Get the 'page' parameter from the request
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {
const pageData: any = {
hostUrl: new URL(HttpProtocol, Host).toString(),
};
@@ -38,7 +35,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,11 +1,9 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get an array of model documentation resources
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// This is a static method that handles the response
@@ -23,7 +21,6 @@ export default class ServiceHandler {
enableGoogleTagManager: IsBillingEnabled,
pageDescription: "Page you're looking for is not found.", // The page description
resources: Resources, // The array of model documentation resources
dataTypes: DataTypes,
pageData: {}, // An empty object to hold any additional page data
});
}

View File

@@ -1,14 +1,11 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get all resources from ResourceUtil
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -18,14 +15,14 @@ export default class ServiceHandler {
let pageTitle: string = ""; // Initialize page title
let pageDescription: string = ""; // Initialize page description
const page: string | undefined = req.params["page"]; // Get the page parameter from the request
const pageData: Dictionary<unknown> = {}; // Initialize page data object
const pageData: any = {}; // Initialize page data object
// Set page title and description
pageTitle = "Pagination";
pageDescription = "Learn how to paginate requests with OneUptime API";
// Get response and request code from LocalCache or LocalFile
pageData["responseCode"] = await LocalCache.getOrSetString(
pageData.responseCode = await LocalCache.getOrSetString(
"pagination",
"response",
async () => {
@@ -36,7 +33,7 @@ export default class ServiceHandler {
},
);
pageData["requestCode"] = await LocalCache.getOrSetString(
pageData.requestCode = await LocalCache.getOrSetString(
"pagination",
"request",
async () => {
@@ -51,7 +48,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page, // Pass the page parameter
resources: Resources, // Pass all resources
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled, // Pass the page title
pageDescription: pageDescription, // Pass the page description

View File

@@ -1,17 +1,10 @@
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import {
PermissionGroup,
PermissionHelper,
PermissionProps,
} from "Common/Types/Permission";
import { PermissionHelper, PermissionProps } from "Common/Types/Permission";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -24,46 +17,23 @@ export default class ServiceHandler {
// Get the requested page
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set page title and description
pageTitle = "Permissions";
pageDescription = "Learn how permissions work with OneUptime";
// Filter permissions to only include those assignable to tenants
const tenantPermissions: Array<PermissionProps> =
PermissionHelper.getAllPermissionProps().filter((i: PermissionProps) => {
pageData.permissions = PermissionHelper.getAllPermissionProps().filter(
(i: PermissionProps) => {
return i.isAssignableToTenant;
});
// Group permissions by PermissionGroup
const permissionGroups: Array<{
group: string;
permissions: Array<PermissionProps>;
}> = [];
for (const group of Object.values(PermissionGroup)) {
const groupPermissions: Array<PermissionProps> = tenantPermissions.filter(
(p: PermissionProps) => {
return p.group === group;
},
);
if (groupPermissions.length > 0) {
permissionGroups.push({
group: group,
permissions: groupPermissions,
});
}
}
pageData["permissionGroups"] = permissionGroups;
},
);
// Render the page
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,12 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
// Retrieve resources from ResourceUtil
const resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const dataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -23,7 +21,6 @@ export default class ServiceHandler {
enableGoogleTagManager: IsBillingEnabled,
pageDescription: "200 - Success",
resources: resources, // Pass resources to the template
dataTypes: dataTypes,
pageData: {}, // Pass empty data to the template
});
}

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Some files were not shown because too many files have changed in this diff Show More