diff --git a/.github/workflows/npm-publish-computer.yml b/.github/workflows/npm-publish-computer.yml new file mode 100644 index 00000000..328378d7 --- /dev/null +++ b/.github/workflows/npm-publish-computer.yml @@ -0,0 +1,35 @@ +name: Publish @trycua/computer to npm + +on: + push: + tags: + - "computer-v*" + +jobs: + publish: + permissions: + id-token: write + contents: read + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: "24.x" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + working-directory: ./libs/typescript/computer + run: npm ci + + - name: Build package + working-directory: ./libs/typescript/computer + run: npm run build --if-present + + - name: Publish to npm + working-directory: ./libs/typescript/computer + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/npm-publish-core.yml b/.github/workflows/npm-publish-core.yml new file mode 100644 index 00000000..dc06628d --- /dev/null +++ b/.github/workflows/npm-publish-core.yml @@ -0,0 +1,35 @@ +name: Publish @trycua/core to npm + +on: + push: + tags: + - "core-v*" + +jobs: + publish: + permissions: + id-token: write + contents: read + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: "24.x" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + working-directory: ./libs/typescript/core + run: npm ci + + - name: Build package + working-directory: ./libs/typescript/core + run: npm run build --if-present + + - name: Publish to npm + working-directory: ./libs/typescript/core + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish-agent.yml b/.github/workflows/publish-agent.yml deleted file mode 100644 index ea03edd6..00000000 --- a/.github/workflows/publish-agent.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: Publish Agent Package - -on: - push: - tags: - - 'agent-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - prepare: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - computer_version: ${{ steps.update-deps.outputs.computer_version }} - som_version: ${{ steps.update-deps.outputs.som_version }} - core_version: ${{ steps.update-deps.outputs.core_version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/agent-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for agent" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Update dependencies to latest versions - id: update-deps - run: | - cd libs/agent - - # Install required package for PyPI API access - pip install requests - - # Create a more robust Python script for PyPI version checking - cat > get_latest_versions.py << 'EOF' - import requests - import json - import sys - - def get_package_version(package_name, fallback="0.1.0"): - try: - response = requests.get(f'https://pypi.org/pypi/{package_name}/json') - print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr) - - if response.status_code != 200: - print(f"API request failed for {package_name}, using fallback version", file=sys.stderr) - return fallback - - data = json.loads(response.text) - - if 'info' not in data: - print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr) - return fallback - - return data['info']['version'] - except Exception as e: - print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr) - return fallback - - # Get latest versions - print(get_package_version('cua-computer')) - print(get_package_version('cua-som')) - print(get_package_version('cua-core')) - EOF - - # Execute the script to get the versions - VERSIONS=($(python get_latest_versions.py)) - LATEST_COMPUTER=${VERSIONS[0]} - LATEST_SOM=${VERSIONS[1]} - LATEST_CORE=${VERSIONS[2]} - - echo "Latest cua-computer version: $LATEST_COMPUTER" - echo "Latest cua-som version: $LATEST_SOM" - echo "Latest cua-core version: $LATEST_CORE" - - # Output the versions for the next job - echo "computer_version=$LATEST_COMPUTER" >> $GITHUB_OUTPUT - echo "som_version=$LATEST_SOM" >> $GITHUB_OUTPUT - echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT - - # Determine major version for version constraint - COMPUTER_MAJOR=$(echo $LATEST_COMPUTER | cut -d. -f1) - SOM_MAJOR=$(echo $LATEST_SOM | cut -d. -f1) - CORE_MAJOR=$(echo $LATEST_CORE | cut -d. -f1) - - NEXT_COMPUTER_MAJOR=$((COMPUTER_MAJOR + 1)) - NEXT_SOM_MAJOR=$((SOM_MAJOR + 1)) - NEXT_CORE_MAJOR=$((CORE_MAJOR + 1)) - - # Update dependencies in pyproject.toml - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS version of sed needs an empty string for -i - sed -i '' "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml - sed -i '' "s/\"cua-som>=.*,<.*\"/\"cua-som>=$LATEST_SOM,<$NEXT_SOM_MAJOR.0.0\"/" pyproject.toml - sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml - else - # Linux version - sed -i "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml - sed -i "s/\"cua-som>=.*,<.*\"/\"cua-som>=$LATEST_SOM,<$NEXT_SOM_MAJOR.0.0\"/" pyproject.toml - sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml - fi - - # Display the updated dependencies - echo "Updated dependencies in pyproject.toml:" - grep -E "cua-computer|cua-som|cua-core" pyproject.toml - - publish: - needs: prepare - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "agent" - package_dir: "libs/agent" - version: ${{ needs.prepare.outputs.version }} - is_lume_package: false - base_package_name: "cua-agent" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - - set-env-variables: - needs: [prepare, publish] - runs-on: macos-latest - steps: - - name: Set environment variables for use in other jobs - run: | - echo "COMPUTER_VERSION=${{ needs.prepare.outputs.computer_version }}" >> $GITHUB_ENV - echo "SOM_VERSION=${{ needs.prepare.outputs.som_version }}" >> $GITHUB_ENV - echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/publish-computer-server.yml b/.github/workflows/publish-computer-server.yml deleted file mode 100644 index 15eca348..00000000 --- a/.github/workflows/publish-computer-server.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Publish Computer Server Package - -on: - push: - tags: - - 'computer-server-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - outputs: - version: - description: "The version that was published" - value: ${{ jobs.prepare.outputs.version }} - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - prepare: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/computer-server-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for computer-server" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - publish: - needs: prepare - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "computer-server" - package_dir: "libs/computer-server" - version: ${{ needs.prepare.outputs.version }} - is_lume_package: false - base_package_name: "cua-computer-server" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - - set-env-variables: - needs: [prepare, publish] - runs-on: macos-latest - steps: - - name: Set environment variables for use in other jobs - run: | - echo "COMPUTER_VERSION=${{ needs.prepare.outputs.version }}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/publish-computer.yml b/.github/workflows/publish-computer.yml deleted file mode 100644 index b2a9fb25..00000000 --- a/.github/workflows/publish-computer.yml +++ /dev/null @@ -1,140 +0,0 @@ -name: Publish Computer Package - -on: - push: - tags: - - 'computer-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - prepare: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - core_version: ${{ steps.update-deps.outputs.core_version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/computer-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for computer" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Update dependencies to latest versions - id: update-deps - run: | - cd libs/computer - # Install required package for PyPI API access - pip install requests - - # Create a more robust Python script for PyPI version checking - cat > get_latest_versions.py << 'EOF' - import requests - import json - import sys - - def get_package_version(package_name, fallback="0.1.0"): - try: - response = requests.get(f'https://pypi.org/pypi/{package_name}/json') - print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr) - - if response.status_code != 200: - print(f"API request failed for {package_name}, using fallback version", file=sys.stderr) - return fallback - - data = json.loads(response.text) - - if 'info' not in data: - print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr) - return fallback - - return data['info']['version'] - except Exception as e: - print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr) - return fallback - - # Get latest versions - print(get_package_version('cua-core')) - EOF - - # Execute the script to get the versions - VERSIONS=($(python get_latest_versions.py)) - LATEST_CORE=${VERSIONS[0]} - - echo "Latest cua-core version: $LATEST_CORE" - - # Output the versions for the next job - echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT - - # Determine major version for version constraint - CORE_MAJOR=$(echo $LATEST_CORE | cut -d. -f1) - NEXT_CORE_MAJOR=$((CORE_MAJOR + 1)) - - # Update dependencies in pyproject.toml - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS version of sed needs an empty string for -i - sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml - else - # Linux version - sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml - fi - - # Display the updated dependencies - echo "Updated dependencies in pyproject.toml:" - grep -E "cua-core" pyproject.toml - - publish: - needs: prepare - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "computer" - package_dir: "libs/computer" - version: ${{ needs.prepare.outputs.version }} - is_lume_package: false - base_package_name: "cua-computer" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - - set-env-variables: - needs: [prepare, publish] - runs-on: macos-latest - steps: - - name: Set environment variables for use in other jobs - run: | - echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/publish-core.yml b/.github/workflows/publish-core.yml deleted file mode 100644 index 4f868f26..00000000 --- a/.github/workflows/publish-core.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Publish Core Package - -on: - push: - tags: - - 'core-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - prepare: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/core-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for core" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - publish: - needs: prepare - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "core" - package_dir: "libs/core" - version: ${{ needs.prepare.outputs.version }} - is_lume_package: false - base_package_name: "cua-core" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-lume.yml b/.github/workflows/publish-lume.yml index ec5e7550..b2ea3c4b 100644 --- a/.github/workflows/publish-lume.yml +++ b/.github/workflows/publish-lume.yml @@ -3,17 +3,17 @@ name: Publish Notarized Lume on: push: tags: - - 'lume-v*' + - "lume-v*" workflow_dispatch: inputs: version: - description: 'Version to notarize (without v prefix)' + description: "Version to notarize (without v prefix)" required: true - default: '0.1.0' + default: "0.1.0" workflow_call: inputs: version: - description: 'Version to notarize' + description: "Version to notarize" required: true type: string secrets: @@ -64,7 +64,7 @@ jobs: - name: Create .release directory run: mkdir -p .release - + - name: Set version id: set_version run: | @@ -82,11 +82,11 @@ jobs: echo "Error: No version found in tag or input" exit 1 fi - + # Update version in Main.swift echo "Updating version in Main.swift to $VERSION" sed -i '' "s/static let current: String = \".*\"/static let current: String = \"$VERSION\"/" libs/lume/src/Main.swift - + # Set output for later steps echo "version=$VERSION" >> $GITHUB_OUTPUT @@ -106,34 +106,34 @@ jobs: # Import certificates echo $APPLICATION_CERT_BASE64 | base64 --decode > application.p12 echo $INSTALLER_CERT_BASE64 | base64 --decode > installer.p12 - + # Import certificates silently (minimize output) security import application.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/pkgbuild > /dev/null 2>&1 security import installer.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/pkgbuild > /dev/null 2>&1 - + # Allow codesign to access the certificates (minimal output) security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain > /dev/null 2>&1 - + # Verify certificates were imported echo "Verifying signing identities..." CERT_COUNT=$(security find-identity -v -p codesigning build.keychain | grep -c "Developer ID Application" || echo "0") INSTALLER_COUNT=$(security find-identity -v build.keychain | grep -c "Developer ID Installer" || echo "0") - + if [ "$CERT_COUNT" -eq 0 ]; then echo "Error: No Developer ID Application certificate found" security find-identity -v -p codesigning build.keychain exit 1 fi - + if [ "$INSTALLER_COUNT" -eq 0 ]; then echo "Error: No Developer ID Installer certificate found" security find-identity -v build.keychain exit 1 fi - + echo "Found $CERT_COUNT Developer ID Application certificate(s) and $INSTALLER_COUNT Developer ID Installer certificate(s)" echo "All required certificates verified successfully" - + # Clean up certificate files rm application.p12 installer.p12 @@ -153,32 +153,32 @@ jobs: echo "Starting build process..." echo "Swift version: $(swift --version | head -n 1)" echo "Building version: $VERSION" - + # Ensure .release directory exists mkdir -p .release chmod 755 .release - + # Build the project first (redirect verbose output) echo "Building project..." swift build --configuration release > build.log 2>&1 echo "Build completed." - + # Run the notarization script with LOG_LEVEL env var chmod +x scripts/build/build-release-notarized.sh cd scripts/build LOG_LEVEL=minimal ./build-release-notarized.sh - + # Return to the lume directory cd ../.. - + # Debug: List what files were actually created echo "Files in .release directory:" find .release -type f -name "*.tar.gz" -o -name "*.pkg.tar.gz" - + # Get architecture for output filename ARCH=$(uname -m) OS_IDENTIFIER="darwin-${ARCH}" - + # Output paths for later use echo "tarball_path=.release/lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" >> $GITHUB_OUTPUT echo "pkg_path=.release/lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" >> $GITHUB_OUTPUT @@ -197,12 +197,12 @@ jobs: shasum -a 256 lume-*.tar.gz >> checksums.txt echo '```' >> checksums.txt fi - + checksums=$(cat checksums.txt) echo "checksums<> $GITHUB_OUTPUT echo "$checksums" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - + # Debug: Show all files in the release directory echo "All files in release directory:" ls -la @@ -213,15 +213,15 @@ jobs: VERSION=${{ steps.set_version.outputs.version }} ARCH=$(uname -m) OS_IDENTIFIER="darwin-${ARCH}" - + # Create OS-tagged symlinks ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" "lume-darwin.tar.gz" ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" "lume-darwin.pkg.tar.gz" - + # Create simple symlinks ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.tar.gz" "lume.tar.gz" ln -sf "lume-${VERSION}-${OS_IDENTIFIER}.pkg.tar.gz" "lume.pkg.tar.gz" - + # List all files (including symlinks) echo "Files with symlinks in release directory:" ls -la @@ -253,10 +253,10 @@ jobs: ./libs/lume/.release/lume.pkg.tar.gz body: | ${{ steps.generate_checksums.outputs.checksums }} - + ### Installation with script /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)" ``` generate_release_notes: true - make_latest: true \ No newline at end of file + make_latest: true diff --git a/.github/workflows/publish-mcp-server.yml b/.github/workflows/publish-mcp-server.yml deleted file mode 100644 index e6eccd5a..00000000 --- a/.github/workflows/publish-mcp-server.yml +++ /dev/null @@ -1,157 +0,0 @@ -name: Publish MCP Server Package - -on: - push: - tags: - - 'mcp-server-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - outputs: - version: - description: "The version that was published" - value: ${{ jobs.prepare.outputs.version }} - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - prepare: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - agent_version: ${{ steps.update-deps.outputs.agent_version }} - computer_version: ${{ steps.update-deps.outputs.computer_version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/mcp-server-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for mcp-server" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Update dependencies to latest versions - id: update-deps - run: | - cd libs/mcp-server - - # Install required package for PyPI API access - pip install requests - - # Create a Python script for PyPI version checking - cat > get_latest_versions.py << 'EOF' - import requests - import json - import sys - - def get_package_version(package_name, fallback="0.1.0"): - try: - response = requests.get(f'https://pypi.org/pypi/{package_name}/json') - print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr) - - if response.status_code != 200: - print(f"API request failed for {package_name}, using fallback version", file=sys.stderr) - return fallback - - data = json.loads(response.text) - - if 'info' not in data: - print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr) - return fallback - - return data['info']['version'] - except Exception as e: - print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr) - return fallback - - # Get latest versions - print(get_package_version('cua-agent')) - print(get_package_version('cua-computer')) - EOF - - # Execute the script to get the versions - VERSIONS=($(python get_latest_versions.py)) - LATEST_AGENT=${VERSIONS[0]} - LATEST_COMPUTER=${VERSIONS[1]} - - echo "Latest cua-agent version: $LATEST_AGENT" - echo "Latest cua-computer version: $LATEST_COMPUTER" - - # Output the versions for the next job - echo "agent_version=$LATEST_AGENT" >> $GITHUB_OUTPUT - echo "computer_version=$LATEST_COMPUTER" >> $GITHUB_OUTPUT - - # Determine major version for version constraint - AGENT_MAJOR=$(echo $LATEST_AGENT | cut -d. -f1) - COMPUTER_MAJOR=$(echo $LATEST_COMPUTER | cut -d. -f1) - - NEXT_AGENT_MAJOR=$((AGENT_MAJOR + 1)) - NEXT_COMPUTER_MAJOR=$((COMPUTER_MAJOR + 1)) - - # Update dependencies in pyproject.toml - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS version of sed needs an empty string for -i - # Update cua-agent with all extras - sed -i '' "s/\"cua-agent\[all\]>=.*,<.*\"/\"cua-agent[all]>=$LATEST_AGENT,<$NEXT_AGENT_MAJOR.0.0\"/" pyproject.toml - sed -i '' "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml - else - # Linux version - sed -i "s/\"cua-agent\[all\]>=.*,<.*\"/\"cua-agent[all]>=$LATEST_AGENT,<$NEXT_AGENT_MAJOR.0.0\"/" pyproject.toml - sed -i "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml - fi - - # Display the updated dependencies - echo "Updated dependencies in pyproject.toml:" - grep -E "cua-agent|cua-computer" pyproject.toml - - publish: - needs: prepare - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "mcp-server" - package_dir: "libs/mcp-server" - version: ${{ needs.prepare.outputs.version }} - is_lume_package: false - base_package_name: "cua-mcp-server" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - - set-env-variables: - needs: [prepare, publish] - runs-on: macos-latest - steps: - - name: Set environment variables for use in other jobs - run: | - echo "AGENT_VERSION=${{ needs.prepare.outputs.agent_version }}" >> $GITHUB_ENV - echo "COMPUTER_VERSION=${{ needs.prepare.outputs.computer_version }}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/publish-pylume.yml b/.github/workflows/publish-pylume.yml deleted file mode 100644 index c5bd4f6f..00000000 --- a/.github/workflows/publish-pylume.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Publish Pylume Package - -on: - push: - tags: - - 'pylume-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - outputs: - version: - description: "The version that was published" - value: ${{ jobs.determine-version.outputs.version }} - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - determine-version: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/pylume-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for pylume" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - validate-version: - runs-on: macos-latest - needs: determine-version - steps: - - uses: actions/checkout@v4 - - name: Validate version - id: validate-version - run: | - CODE_VERSION=$(grep '__version__' libs/pylume/pylume/__init__.py | cut -d'"' -f2) - if [ "${{ needs.determine-version.outputs.version }}" != "$CODE_VERSION" ]; then - echo "Version mismatch: expected $CODE_VERSION, got ${{ needs.determine-version.outputs.version }}" - exit 1 - fi - echo "Version validated: $CODE_VERSION" - - publish: - needs: determine-version - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "pylume" - package_dir: "libs/pylume" - version: ${{ needs.determine-version.outputs.version }} - is_lume_package: true - base_package_name: "pylume" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/publish-som.yml b/.github/workflows/publish-som.yml deleted file mode 100644 index b1d53ac8..00000000 --- a/.github/workflows/publish-som.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Publish SOM Package - -on: - push: - tags: - - 'som-v*' - workflow_dispatch: - inputs: - version: - description: 'Version to publish (without v prefix)' - required: true - default: '0.1.0' - workflow_call: - inputs: - version: - description: 'Version to publish' - required: true - type: string - outputs: - version: - description: "The version that was published" - value: ${{ jobs.determine-version.outputs.version }} - -# Adding permissions at workflow level -permissions: - contents: write - -jobs: - determine-version: - runs-on: macos-latest - outputs: - version: ${{ steps.get-version.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Determine version - id: get-version - run: | - if [ "${{ github.event_name }}" == "push" ]; then - # Extract version from tag (for package-specific tags) - if [[ "${{ github.ref }}" =~ ^refs/tags/som-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - echo "Invalid tag format for som" - exit 1 - fi - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - # Use version from workflow dispatch - VERSION=${{ github.event.inputs.version }} - else - # Use version from workflow_call - VERSION=${{ inputs.version }} - fi - echo "VERSION=$VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT - - publish: - needs: determine-version - uses: ./.github/workflows/reusable-publish.yml - with: - package_name: "som" - package_dir: "libs/som" - version: ${{ needs.determine-version.outputs.version }} - is_lume_package: false - base_package_name: "cua-som" - secrets: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pypi-publish-agent.yml b/.github/workflows/pypi-publish-agent.yml new file mode 100644 index 00000000..c36c1c1b --- /dev/null +++ b/.github/workflows/pypi-publish-agent.yml @@ -0,0 +1,162 @@ +name: Publish Agent Package + +on: + push: + tags: + - "agent-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + prepare: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + computer_version: ${{ steps.update-deps.outputs.computer_version }} + som_version: ${{ steps.update-deps.outputs.som_version }} + core_version: ${{ steps.update-deps.outputs.core_version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/agent-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for agent" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Update dependencies to latest versions + id: update-deps + run: | + cd libs/python/agent + + # Install required package for PyPI API access + pip install requests + + # Create a more robust Python script for PyPI version checking + cat > get_latest_versions.py << 'EOF' + import requests + import json + import sys + + def get_package_version(package_name, fallback="0.1.0"): + try: + response = requests.get(f'https://pypi.org/pypi/{package_name}/json') + print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr) + + if response.status_code != 200: + print(f"API request failed for {package_name}, using fallback version", file=sys.stderr) + return fallback + + data = json.loads(response.text) + + if 'info' not in data: + print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr) + return fallback + + return data['info']['version'] + except Exception as e: + print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr) + return fallback + + # Get latest versions + print(get_package_version('cua-computer')) + print(get_package_version('cua-som')) + print(get_package_version('cua-core')) + EOF + + # Execute the script to get the versions + VERSIONS=($(python get_latest_versions.py)) + LATEST_COMPUTER=${VERSIONS[0]} + LATEST_SOM=${VERSIONS[1]} + LATEST_CORE=${VERSIONS[2]} + + echo "Latest cua-computer version: $LATEST_COMPUTER" + echo "Latest cua-som version: $LATEST_SOM" + echo "Latest cua-core version: $LATEST_CORE" + + # Output the versions for the next job + echo "computer_version=$LATEST_COMPUTER" >> $GITHUB_OUTPUT + echo "som_version=$LATEST_SOM" >> $GITHUB_OUTPUT + echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT + + # Determine major version for version constraint + COMPUTER_MAJOR=$(echo $LATEST_COMPUTER | cut -d. -f1) + SOM_MAJOR=$(echo $LATEST_SOM | cut -d. -f1) + CORE_MAJOR=$(echo $LATEST_CORE | cut -d. -f1) + + NEXT_COMPUTER_MAJOR=$((COMPUTER_MAJOR + 1)) + NEXT_SOM_MAJOR=$((SOM_MAJOR + 1)) + NEXT_CORE_MAJOR=$((CORE_MAJOR + 1)) + + # Update dependencies in pyproject.toml + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version of sed needs an empty string for -i + sed -i '' "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml + sed -i '' "s/\"cua-som>=.*,<.*\"/\"cua-som>=$LATEST_SOM,<$NEXT_SOM_MAJOR.0.0\"/" pyproject.toml + sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml + else + # Linux version + sed -i "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml + sed -i "s/\"cua-som>=.*,<.*\"/\"cua-som>=$LATEST_SOM,<$NEXT_SOM_MAJOR.0.0\"/" pyproject.toml + sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml + fi + + # Display the updated dependencies + echo "Updated dependencies in pyproject.toml:" + grep -E "cua-computer|cua-som|cua-core" pyproject.toml + + publish: + needs: prepare + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "agent" + package_dir: "libs/python/agent" + version: ${{ needs.prepare.outputs.version }} + is_lume_package: false + base_package_name: "cua-agent" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + + set-env-variables: + needs: [prepare, publish] + runs-on: macos-latest + steps: + - name: Set environment variables for use in other jobs + run: | + echo "COMPUTER_VERSION=${{ needs.prepare.outputs.computer_version }}" >> $GITHUB_ENV + echo "SOM_VERSION=${{ needs.prepare.outputs.som_version }}" >> $GITHUB_ENV + echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV diff --git a/.github/workflows/pypi-publish-computer-server.yml b/.github/workflows/pypi-publish-computer-server.yml new file mode 100644 index 00000000..899cc605 --- /dev/null +++ b/.github/workflows/pypi-publish-computer-server.yml @@ -0,0 +1,80 @@ +name: Publish Computer Server Package + +on: + push: + tags: + - "computer-server-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + outputs: + version: + description: "The version that was published" + value: ${{ jobs.prepare.outputs.version }} + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + prepare: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/computer-server-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for computer-server" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + publish: + needs: prepare + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "computer-server" + package_dir: "libs/python/computer-server" + version: ${{ needs.prepare.outputs.version }} + is_lume_package: false + base_package_name: "cua-computer-server" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + + set-env-variables: + needs: [prepare, publish] + runs-on: macos-latest + steps: + - name: Set environment variables for use in other jobs + run: | + echo "COMPUTER_VERSION=${{ needs.prepare.outputs.version }}" >> $GITHUB_ENV diff --git a/.github/workflows/pypi-publish-computer.yml b/.github/workflows/pypi-publish-computer.yml new file mode 100644 index 00000000..ea5d644b --- /dev/null +++ b/.github/workflows/pypi-publish-computer.yml @@ -0,0 +1,140 @@ +name: Publish Computer Package + +on: + push: + tags: + - "computer-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + prepare: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + core_version: ${{ steps.update-deps.outputs.core_version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/computer-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for computer" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Update dependencies to latest versions + id: update-deps + run: | + cd libs/python/computer + # Install required package for PyPI API access + pip install requests + + # Create a more robust Python script for PyPI version checking + cat > get_latest_versions.py << 'EOF' + import requests + import json + import sys + + def get_package_version(package_name, fallback="0.1.0"): + try: + response = requests.get(f'https://pypi.org/pypi/{package_name}/json') + print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr) + + if response.status_code != 200: + print(f"API request failed for {package_name}, using fallback version", file=sys.stderr) + return fallback + + data = json.loads(response.text) + + if 'info' not in data: + print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr) + return fallback + + return data['info']['version'] + except Exception as e: + print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr) + return fallback + + # Get latest versions + print(get_package_version('cua-core')) + EOF + + # Execute the script to get the versions + VERSIONS=($(python get_latest_versions.py)) + LATEST_CORE=${VERSIONS[0]} + + echo "Latest cua-core version: $LATEST_CORE" + + # Output the versions for the next job + echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT + + # Determine major version for version constraint + CORE_MAJOR=$(echo $LATEST_CORE | cut -d. -f1) + NEXT_CORE_MAJOR=$((CORE_MAJOR + 1)) + + # Update dependencies in pyproject.toml + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version of sed needs an empty string for -i + sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml + else + # Linux version + sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml + fi + + # Display the updated dependencies + echo "Updated dependencies in pyproject.toml:" + grep -E "cua-core" pyproject.toml + + publish: + needs: prepare + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "computer" + package_dir: "libs/python/computer" + version: ${{ needs.prepare.outputs.version }} + is_lume_package: false + base_package_name: "cua-computer" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + + set-env-variables: + needs: [prepare, publish] + runs-on: macos-latest + steps: + - name: Set environment variables for use in other jobs + run: | + echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV diff --git a/.github/workflows/pypi-publish-core.yml b/.github/workflows/pypi-publish-core.yml new file mode 100644 index 00000000..a5b993f6 --- /dev/null +++ b/.github/workflows/pypi-publish-core.yml @@ -0,0 +1,63 @@ +name: Publish Core Package + +on: + push: + tags: + - "core-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + prepare: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/core-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for core" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + publish: + needs: prepare + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "core" + package_dir: "libs/python/core" + version: ${{ needs.prepare.outputs.version }} + is_lume_package: false + base_package_name: "cua-core" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/pypi-publish-mcp-server.yml b/.github/workflows/pypi-publish-mcp-server.yml new file mode 100644 index 00000000..cd1b0c2f --- /dev/null +++ b/.github/workflows/pypi-publish-mcp-server.yml @@ -0,0 +1,157 @@ +name: Publish MCP Server Package + +on: + push: + tags: + - "mcp-server-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + outputs: + version: + description: "The version that was published" + value: ${{ jobs.prepare.outputs.version }} + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + prepare: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + agent_version: ${{ steps.update-deps.outputs.agent_version }} + computer_version: ${{ steps.update-deps.outputs.computer_version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/mcp-server-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for mcp-server" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Update dependencies to latest versions + id: update-deps + run: | + cd libs/python/mcp-server + + # Install required package for PyPI API access + pip install requests + + # Create a Python script for PyPI version checking + cat > get_latest_versions.py << 'EOF' + import requests + import json + import sys + + def get_package_version(package_name, fallback="0.1.0"): + try: + response = requests.get(f'https://pypi.org/pypi/{package_name}/json') + print(f"API Response Status for {package_name}: {response.status_code}", file=sys.stderr) + + if response.status_code != 200: + print(f"API request failed for {package_name}, using fallback version", file=sys.stderr) + return fallback + + data = json.loads(response.text) + + if 'info' not in data: + print(f"Missing 'info' key in API response for {package_name}, using fallback version", file=sys.stderr) + return fallback + + return data['info']['version'] + except Exception as e: + print(f"Error fetching version for {package_name}: {str(e)}", file=sys.stderr) + return fallback + + # Get latest versions + print(get_package_version('cua-agent')) + print(get_package_version('cua-computer')) + EOF + + # Execute the script to get the versions + VERSIONS=($(python get_latest_versions.py)) + LATEST_AGENT=${VERSIONS[0]} + LATEST_COMPUTER=${VERSIONS[1]} + + echo "Latest cua-agent version: $LATEST_AGENT" + echo "Latest cua-computer version: $LATEST_COMPUTER" + + # Output the versions for the next job + echo "agent_version=$LATEST_AGENT" >> $GITHUB_OUTPUT + echo "computer_version=$LATEST_COMPUTER" >> $GITHUB_OUTPUT + + # Determine major version for version constraint + AGENT_MAJOR=$(echo $LATEST_AGENT | cut -d. -f1) + COMPUTER_MAJOR=$(echo $LATEST_COMPUTER | cut -d. -f1) + + NEXT_AGENT_MAJOR=$((AGENT_MAJOR + 1)) + NEXT_COMPUTER_MAJOR=$((COMPUTER_MAJOR + 1)) + + # Update dependencies in pyproject.toml + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version of sed needs an empty string for -i + # Update cua-agent with all extras + sed -i '' "s/\"cua-agent\[all\]>=.*,<.*\"/\"cua-agent[all]>=$LATEST_AGENT,<$NEXT_AGENT_MAJOR.0.0\"/" pyproject.toml + sed -i '' "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml + else + # Linux version + sed -i "s/\"cua-agent\[all\]>=.*,<.*\"/\"cua-agent[all]>=$LATEST_AGENT,<$NEXT_AGENT_MAJOR.0.0\"/" pyproject.toml + sed -i "s/\"cua-computer>=.*,<.*\"/\"cua-computer>=$LATEST_COMPUTER,<$NEXT_COMPUTER_MAJOR.0.0\"/" pyproject.toml + fi + + # Display the updated dependencies + echo "Updated dependencies in pyproject.toml:" + grep -E "cua-agent|cua-computer" pyproject.toml + + publish: + needs: prepare + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "mcp-server" + package_dir: "libs/python/mcp-server" + version: ${{ needs.prepare.outputs.version }} + is_lume_package: false + base_package_name: "cua-mcp-server" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + + set-env-variables: + needs: [prepare, publish] + runs-on: macos-latest + steps: + - name: Set environment variables for use in other jobs + run: | + echo "AGENT_VERSION=${{ needs.prepare.outputs.agent_version }}" >> $GITHUB_ENV + echo "COMPUTER_VERSION=${{ needs.prepare.outputs.computer_version }}" >> $GITHUB_ENV diff --git a/.github/workflows/pypi-publish-pylume.yml b/.github/workflows/pypi-publish-pylume.yml new file mode 100644 index 00000000..91278c00 --- /dev/null +++ b/.github/workflows/pypi-publish-pylume.yml @@ -0,0 +1,82 @@ +name: Publish Pylume Package + +on: + push: + tags: + - "pylume-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + outputs: + version: + description: "The version that was published" + value: ${{ jobs.determine-version.outputs.version }} + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + determine-version: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/pylume-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for pylume" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + validate-version: + runs-on: macos-latest + needs: determine-version + steps: + - uses: actions/checkout@v4 + - name: Validate version + id: validate-version + run: | + CODE_VERSION=$(grep '__version__' libs/python/pylume/pylume/__init__.py | cut -d'"' -f2) + if [ "${{ needs.determine-version.outputs.version }}" != "$CODE_VERSION" ]; then + echo "Version mismatch: expected $CODE_VERSION, got ${{ needs.determine-version.outputs.version }}" + exit 1 + fi + echo "Version validated: $CODE_VERSION" + + publish: + needs: determine-version + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "pylume" + package_dir: "libs/python/pylume" + version: ${{ needs.determine-version.outputs.version }} + is_lume_package: true + base_package_name: "pylume" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/pypi-publish-som.yml b/.github/workflows/pypi-publish-som.yml new file mode 100644 index 00000000..fd80d3e8 --- /dev/null +++ b/.github/workflows/pypi-publish-som.yml @@ -0,0 +1,67 @@ +name: Publish SOM Package + +on: + push: + tags: + - "som-v*" + workflow_dispatch: + inputs: + version: + description: "Version to publish (without v prefix)" + required: true + default: "0.1.0" + workflow_call: + inputs: + version: + description: "Version to publish" + required: true + type: string + outputs: + version: + description: "The version that was published" + value: ${{ jobs.determine-version.outputs.version }} + +# Adding permissions at workflow level +permissions: + contents: write + +jobs: + determine-version: + runs-on: macos-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: get-version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + # Extract version from tag (for package-specific tags) + if [[ "${{ github.ref }}" =~ ^refs/tags/som-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + echo "Invalid tag format for som" + exit 1 + fi + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + # Use version from workflow dispatch + VERSION=${{ github.event.inputs.version }} + else + # Use version from workflow_call + VERSION=${{ inputs.version }} + fi + echo "VERSION=$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + publish: + needs: determine-version + uses: ./.github/workflows/pypi-reusable-publish.yml + with: + package_name: "som" + package_dir: "libs/python/som" + version: ${{ needs.determine-version.outputs.version }} + is_lume_package: false + base_package_name: "cua-som" + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/pypi-reusable-publish.yml b/.github/workflows/pypi-reusable-publish.yml new file mode 100644 index 00000000..f1eb045e --- /dev/null +++ b/.github/workflows/pypi-reusable-publish.yml @@ -0,0 +1,280 @@ +name: Reusable Package Publish Workflow + +on: + workflow_call: + inputs: + package_name: + description: "Name of the package (e.g. pylume, computer, agent)" + required: true + type: string + package_dir: + description: "Directory containing the package relative to workspace root (e.g. libs/python/pylume)" + required: true + type: string + version: + description: "Version to publish" + required: true + type: string + is_lume_package: + description: "Whether this package includes the lume binary" + required: false + type: boolean + default: false + base_package_name: + description: "PyPI package name (e.g. pylume, cua-agent)" + required: true + type: string + make_latest: + description: "Whether to mark this release as latest (should only be true for lume)" + required: false + type: boolean + default: false + secrets: + PYPI_TOKEN: + required: true + outputs: + version: + description: "The version that was published" + value: ${{ jobs.build-and-publish.outputs.version }} + +jobs: + build-and-publish: + runs-on: macos-latest + permissions: + contents: write # This permission is needed for creating releases + outputs: + version: ${{ steps.set-version.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for release creation + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Create root pdm.lock file + run: | + # Create an empty pdm.lock file in the root + touch pdm.lock + + - name: Install PDM + uses: pdm-project/setup-pdm@v3 + with: + python-version: "3.11" + cache: true + + - name: Set version + id: set-version + run: | + echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + + - name: Initialize PDM in package directory + run: | + # Make sure we're working with a properly initialized PDM project + cd ${{ inputs.package_dir }} + + # Create pdm.lock if it doesn't exist + if [ ! -f "pdm.lock" ]; then + echo "No pdm.lock found, initializing PDM project..." + pdm lock + fi + + - name: Set version in package + run: | + cd ${{ inputs.package_dir }} + # Replace pdm bump with direct edit of pyproject.toml + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version of sed needs an empty string for -i + sed -i '' "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml + else + # Linux version + sed -i "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml + fi + # Verify version was updated + echo "Updated version in pyproject.toml:" + grep "version =" pyproject.toml + + # Conditional step for lume binary download (only for pylume package) + - name: Download and setup lume binary + if: inputs.is_lume_package + run: | + # Create a temporary directory for extraction + mkdir -p temp_lume + + # Download the latest lume release directly + echo "Downloading latest lume version..." + curl -sL "https://github.com/trycua/lume/releases/latest/download/lume.tar.gz" -o temp_lume/lume.tar.gz + + # Extract the tar file (ignore ownership and suppress warnings) + cd temp_lume && tar --no-same-owner -xzf lume.tar.gz + + # Make the binary executable + chmod +x lume + + # Copy the lume binary to the correct location in the pylume package + mkdir -p "${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pylume" + cp lume "${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pylume/lume" + + # Verify the binary exists and is executable + test -x "${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pylume/lume" || { echo "lume binary not found or not executable"; exit 1; } + + # Get the version from the downloaded binary for reference + LUME_VERSION=$(./lume --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown") + echo "Using lume version: $LUME_VERSION" + + # Cleanup + cd "${GITHUB_WORKSPACE}" && rm -rf temp_lume + + # Save the lume version for reference + echo "LUME_VERSION=${LUME_VERSION}" >> $GITHUB_ENV + + - name: Build and publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: | + cd ${{ inputs.package_dir }} + # Build with PDM + pdm build + + # For pylume package, verify the binary is in the wheel + if [ "${{ inputs.is_lume_package }}" = "true" ]; then + python -m pip install wheel + wheel unpack dist/*.whl --dest temp_wheel + echo "Listing contents of wheel directory:" + find temp_wheel -type f + test -f temp_wheel/pylume-*/pylume/lume || { echo "lume binary not found in wheel"; exit 1; } + rm -rf temp_wheel + echo "Publishing ${{ inputs.base_package_name }} ${VERSION} with lume ${LUME_VERSION}" + else + echo "Publishing ${{ inputs.base_package_name }} ${VERSION}" + fi + + # Install and use twine directly instead of PDM publish + echo "Installing twine for direct publishing..." + pip install twine + + echo "Publishing to PyPI using twine..." + TWINE_USERNAME="__token__" TWINE_PASSWORD="$PYPI_TOKEN" python -m twine upload dist/* + + # Save the wheel file path for the release + WHEEL_FILE=$(ls dist/*.whl | head -1) + echo "WHEEL_FILE=${WHEEL_FILE}" >> $GITHUB_ENV + + - name: Prepare Simple Release Notes + if: startsWith(github.ref, 'refs/tags/') + run: | + # Create release notes based on package type + echo "# ${{ inputs.base_package_name }} v${VERSION}" > release_notes.md + echo "" >> release_notes.md + + if [ "${{ inputs.package_name }}" = "pylume" ]; then + echo "## Python SDK for lume - run macOS and Linux VMs on Apple Silicon" >> release_notes.md + echo "" >> release_notes.md + echo "This package provides Python bindings for the lume virtualization tool." >> release_notes.md + echo "" >> release_notes.md + echo "## Dependencies" >> release_notes.md + echo "* lume binary: v${LUME_VERSION}" >> release_notes.md + elif [ "${{ inputs.package_name }}" = "computer" ]; then + echo "## Computer control library for the Computer Universal Automation (CUA) project" >> release_notes.md + echo "" >> release_notes.md + echo "## Dependencies" >> release_notes.md + echo "* pylume: ${PYLUME_VERSION:-latest}" >> release_notes.md + elif [ "${{ inputs.package_name }}" = "agent" ]; then + echo "## Dependencies" >> release_notes.md + echo "* cua-computer: ${COMPUTER_VERSION:-latest}" >> release_notes.md + echo "* cua-som: ${SOM_VERSION:-latest}" >> release_notes.md + echo "" >> release_notes.md + echo "## Installation Options" >> release_notes.md + echo "" >> release_notes.md + echo "### Basic installation with Anthropic" >> release_notes.md + echo '```bash' >> release_notes.md + echo "pip install cua-agent[anthropic]==${VERSION}" >> release_notes.md + echo '```' >> release_notes.md + echo "" >> release_notes.md + echo "### With SOM (recommended)" >> release_notes.md + echo '```bash' >> release_notes.md + echo "pip install cua-agent[som]==${VERSION}" >> release_notes.md + echo '```' >> release_notes.md + echo "" >> release_notes.md + echo "### All features" >> release_notes.md + echo '```bash' >> release_notes.md + echo "pip install cua-agent[all]==${VERSION}" >> release_notes.md + echo '```' >> release_notes.md + elif [ "${{ inputs.package_name }}" = "som" ]; then + echo "## Computer Vision and OCR library for detecting and analyzing UI elements" >> release_notes.md + echo "" >> release_notes.md + echo "This package provides enhanced UI understanding capabilities through computer vision and OCR." >> release_notes.md + elif [ "${{ inputs.package_name }}" = "computer-server" ]; then + echo "## Computer Server for the Computer Universal Automation (CUA) project" >> release_notes.md + echo "" >> release_notes.md + echo "A FastAPI-based server implementation for computer control." >> release_notes.md + echo "" >> release_notes.md + echo "## Dependencies" >> release_notes.md + echo "* cua-computer: ${COMPUTER_VERSION:-latest}" >> release_notes.md + echo "" >> release_notes.md + echo "## Usage" >> release_notes.md + echo '```bash' >> release_notes.md + echo "# Run the server" >> release_notes.md + echo "cua-computer-server" >> release_notes.md + echo '```' >> release_notes.md + elif [ "${{ inputs.package_name }}" = "mcp-server" ]; then + echo "## MCP Server for the Computer-Use Agent (CUA)" >> release_notes.md + echo "" >> release_notes.md + echo "This package provides MCP (Model Context Protocol) integration for CUA agents, allowing them to be used with Claude Desktop, Cursor, and other MCP clients." >> release_notes.md + echo "" >> release_notes.md + echo "## Dependencies" >> release_notes.md + echo "* cua-computer: ${COMPUTER_VERSION:-latest}" >> release_notes.md + echo "* cua-agent: ${AGENT_VERSION:-latest}" >> release_notes.md + echo "" >> release_notes.md + echo "## Usage" >> release_notes.md + echo '```bash' >> release_notes.md + echo "# Run the MCP server directly" >> release_notes.md + echo "cua-mcp-server" >> release_notes.md + echo '```' >> release_notes.md + echo "" >> release_notes.md + echo "## Claude Desktop Integration" >> release_notes.md + echo "Add to your Claude Desktop configuration (~/.config/claude-desktop/claude_desktop_config.json or OS-specific location):" >> release_notes.md + echo '```json' >> release_notes.md + echo '"mcpServers": {' >> release_notes.md + echo ' "cua-agent": {' >> release_notes.md + echo ' "command": "cua-mcp-server",' >> release_notes.md + echo ' "args": [],' >> release_notes.md + echo ' "env": {' >> release_notes.md + echo ' "CUA_AGENT_LOOP": "OMNI",' >> release_notes.md + echo ' "CUA_MODEL_PROVIDER": "ANTHROPIC",' >> release_notes.md + echo ' "CUA_MODEL_NAME": "claude-3-opus-20240229",' >> release_notes.md + echo ' "ANTHROPIC_API_KEY": "your-api-key",' >> release_notes.md + echo ' "PYTHONIOENCODING": "utf-8"' >> release_notes.md + echo ' }' >> release_notes.md + echo ' }' >> release_notes.md + echo '}' >> release_notes.md + echo '```' >> release_notes.md + fi + + # Add installation section if not agent (which has its own installation section) + if [ "${{ inputs.package_name }}" != "agent" ]; then + echo "" >> release_notes.md + echo "## Installation" >> release_notes.md + echo '```bash' >> release_notes.md + echo "pip install ${{ inputs.base_package_name }}==${VERSION}" >> release_notes.md + echo '```' >> release_notes.md + fi + + echo "Release notes created:" + cat release_notes.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + name: "${{ inputs.base_package_name }} v${{ env.VERSION }}" + body_path: release_notes.md + files: ${{ inputs.package_dir }}/${{ env.WHEEL_FILE }} + draft: false + prerelease: false + make_latest: ${{ inputs.package_name == 'lume' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable-publish.yml b/.github/workflows/reusable-publish.yml deleted file mode 100644 index 8856b60d..00000000 --- a/.github/workflows/reusable-publish.yml +++ /dev/null @@ -1,280 +0,0 @@ -name: Reusable Package Publish Workflow - -on: - workflow_call: - inputs: - package_name: - description: 'Name of the package (e.g. pylume, computer, agent)' - required: true - type: string - package_dir: - description: 'Directory containing the package relative to workspace root (e.g. libs/pylume)' - required: true - type: string - version: - description: 'Version to publish' - required: true - type: string - is_lume_package: - description: 'Whether this package includes the lume binary' - required: false - type: boolean - default: false - base_package_name: - description: 'PyPI package name (e.g. pylume, cua-agent)' - required: true - type: string - make_latest: - description: 'Whether to mark this release as latest (should only be true for lume)' - required: false - type: boolean - default: false - secrets: - PYPI_TOKEN: - required: true - outputs: - version: - description: "The version that was published" - value: ${{ jobs.build-and-publish.outputs.version }} - -jobs: - build-and-publish: - runs-on: macos-latest - permissions: - contents: write # This permission is needed for creating releases - outputs: - version: ${{ steps.set-version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Full history for release creation - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Create root pdm.lock file - run: | - # Create an empty pdm.lock file in the root - touch pdm.lock - - - name: Install PDM - uses: pdm-project/setup-pdm@v3 - with: - python-version: '3.11' - cache: true - - - name: Set version - id: set-version - run: | - echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV - echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT - - - name: Initialize PDM in package directory - run: | - # Make sure we're working with a properly initialized PDM project - cd ${{ inputs.package_dir }} - - # Create pdm.lock if it doesn't exist - if [ ! -f "pdm.lock" ]; then - echo "No pdm.lock found, initializing PDM project..." - pdm lock - fi - - - name: Set version in package - run: | - cd ${{ inputs.package_dir }} - # Replace pdm bump with direct edit of pyproject.toml - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS version of sed needs an empty string for -i - sed -i '' "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml - else - # Linux version - sed -i "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml - fi - # Verify version was updated - echo "Updated version in pyproject.toml:" - grep "version =" pyproject.toml - - # Conditional step for lume binary download (only for pylume package) - - name: Download and setup lume binary - if: inputs.is_lume_package - run: | - # Create a temporary directory for extraction - mkdir -p temp_lume - - # Download the latest lume release directly - echo "Downloading latest lume version..." - curl -sL "https://github.com/trycua/lume/releases/latest/download/lume.tar.gz" -o temp_lume/lume.tar.gz - - # Extract the tar file (ignore ownership and suppress warnings) - cd temp_lume && tar --no-same-owner -xzf lume.tar.gz - - # Make the binary executable - chmod +x lume - - # Copy the lume binary to the correct location in the pylume package - mkdir -p "${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pylume" - cp lume "${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pylume/lume" - - # Verify the binary exists and is executable - test -x "${GITHUB_WORKSPACE}/${{ inputs.package_dir }}/pylume/lume" || { echo "lume binary not found or not executable"; exit 1; } - - # Get the version from the downloaded binary for reference - LUME_VERSION=$(./lume --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown") - echo "Using lume version: $LUME_VERSION" - - # Cleanup - cd "${GITHUB_WORKSPACE}" && rm -rf temp_lume - - # Save the lume version for reference - echo "LUME_VERSION=${LUME_VERSION}" >> $GITHUB_ENV - - - name: Build and publish - env: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - run: | - cd ${{ inputs.package_dir }} - # Build with PDM - pdm build - - # For pylume package, verify the binary is in the wheel - if [ "${{ inputs.is_lume_package }}" = "true" ]; then - python -m pip install wheel - wheel unpack dist/*.whl --dest temp_wheel - echo "Listing contents of wheel directory:" - find temp_wheel -type f - test -f temp_wheel/pylume-*/pylume/lume || { echo "lume binary not found in wheel"; exit 1; } - rm -rf temp_wheel - echo "Publishing ${{ inputs.base_package_name }} ${VERSION} with lume ${LUME_VERSION}" - else - echo "Publishing ${{ inputs.base_package_name }} ${VERSION}" - fi - - # Install and use twine directly instead of PDM publish - echo "Installing twine for direct publishing..." - pip install twine - - echo "Publishing to PyPI using twine..." - TWINE_USERNAME="__token__" TWINE_PASSWORD="$PYPI_TOKEN" python -m twine upload dist/* - - # Save the wheel file path for the release - WHEEL_FILE=$(ls dist/*.whl | head -1) - echo "WHEEL_FILE=${WHEEL_FILE}" >> $GITHUB_ENV - - - name: Prepare Simple Release Notes - if: startsWith(github.ref, 'refs/tags/') - run: | - # Create release notes based on package type - echo "# ${{ inputs.base_package_name }} v${VERSION}" > release_notes.md - echo "" >> release_notes.md - - if [ "${{ inputs.package_name }}" = "pylume" ]; then - echo "## Python SDK for lume - run macOS and Linux VMs on Apple Silicon" >> release_notes.md - echo "" >> release_notes.md - echo "This package provides Python bindings for the lume virtualization tool." >> release_notes.md - echo "" >> release_notes.md - echo "## Dependencies" >> release_notes.md - echo "* lume binary: v${LUME_VERSION}" >> release_notes.md - elif [ "${{ inputs.package_name }}" = "computer" ]; then - echo "## Computer control library for the Computer Universal Automation (CUA) project" >> release_notes.md - echo "" >> release_notes.md - echo "## Dependencies" >> release_notes.md - echo "* pylume: ${PYLUME_VERSION:-latest}" >> release_notes.md - elif [ "${{ inputs.package_name }}" = "agent" ]; then - echo "## Dependencies" >> release_notes.md - echo "* cua-computer: ${COMPUTER_VERSION:-latest}" >> release_notes.md - echo "* cua-som: ${SOM_VERSION:-latest}" >> release_notes.md - echo "" >> release_notes.md - echo "## Installation Options" >> release_notes.md - echo "" >> release_notes.md - echo "### Basic installation with Anthropic" >> release_notes.md - echo '```bash' >> release_notes.md - echo "pip install cua-agent[anthropic]==${VERSION}" >> release_notes.md - echo '```' >> release_notes.md - echo "" >> release_notes.md - echo "### With SOM (recommended)" >> release_notes.md - echo '```bash' >> release_notes.md - echo "pip install cua-agent[som]==${VERSION}" >> release_notes.md - echo '```' >> release_notes.md - echo "" >> release_notes.md - echo "### All features" >> release_notes.md - echo '```bash' >> release_notes.md - echo "pip install cua-agent[all]==${VERSION}" >> release_notes.md - echo '```' >> release_notes.md - elif [ "${{ inputs.package_name }}" = "som" ]; then - echo "## Computer Vision and OCR library for detecting and analyzing UI elements" >> release_notes.md - echo "" >> release_notes.md - echo "This package provides enhanced UI understanding capabilities through computer vision and OCR." >> release_notes.md - elif [ "${{ inputs.package_name }}" = "computer-server" ]; then - echo "## Computer Server for the Computer Universal Automation (CUA) project" >> release_notes.md - echo "" >> release_notes.md - echo "A FastAPI-based server implementation for computer control." >> release_notes.md - echo "" >> release_notes.md - echo "## Dependencies" >> release_notes.md - echo "* cua-computer: ${COMPUTER_VERSION:-latest}" >> release_notes.md - echo "" >> release_notes.md - echo "## Usage" >> release_notes.md - echo '```bash' >> release_notes.md - echo "# Run the server" >> release_notes.md - echo "cua-computer-server" >> release_notes.md - echo '```' >> release_notes.md - elif [ "${{ inputs.package_name }}" = "mcp-server" ]; then - echo "## MCP Server for the Computer-Use Agent (CUA)" >> release_notes.md - echo "" >> release_notes.md - echo "This package provides MCP (Model Context Protocol) integration for CUA agents, allowing them to be used with Claude Desktop, Cursor, and other MCP clients." >> release_notes.md - echo "" >> release_notes.md - echo "## Dependencies" >> release_notes.md - echo "* cua-computer: ${COMPUTER_VERSION:-latest}" >> release_notes.md - echo "* cua-agent: ${AGENT_VERSION:-latest}" >> release_notes.md - echo "" >> release_notes.md - echo "## Usage" >> release_notes.md - echo '```bash' >> release_notes.md - echo "# Run the MCP server directly" >> release_notes.md - echo "cua-mcp-server" >> release_notes.md - echo '```' >> release_notes.md - echo "" >> release_notes.md - echo "## Claude Desktop Integration" >> release_notes.md - echo "Add to your Claude Desktop configuration (~/.config/claude-desktop/claude_desktop_config.json or OS-specific location):" >> release_notes.md - echo '```json' >> release_notes.md - echo '"mcpServers": {' >> release_notes.md - echo ' "cua-agent": {' >> release_notes.md - echo ' "command": "cua-mcp-server",' >> release_notes.md - echo ' "args": [],' >> release_notes.md - echo ' "env": {' >> release_notes.md - echo ' "CUA_AGENT_LOOP": "OMNI",' >> release_notes.md - echo ' "CUA_MODEL_PROVIDER": "ANTHROPIC",' >> release_notes.md - echo ' "CUA_MODEL_NAME": "claude-3-opus-20240229",' >> release_notes.md - echo ' "ANTHROPIC_API_KEY": "your-api-key",' >> release_notes.md - echo ' "PYTHONIOENCODING": "utf-8"' >> release_notes.md - echo ' }' >> release_notes.md - echo ' }' >> release_notes.md - echo '}' >> release_notes.md - echo '```' >> release_notes.md - fi - - # Add installation section if not agent (which has its own installation section) - if [ "${{ inputs.package_name }}" != "agent" ]; then - echo "" >> release_notes.md - echo "## Installation" >> release_notes.md - echo '```bash' >> release_notes.md - echo "pip install ${{ inputs.base_package_name }}==${VERSION}" >> release_notes.md - echo '```' >> release_notes.md - fi - - echo "Release notes created:" - cat release_notes.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') - with: - name: "${{ inputs.base_package_name }} v${{ env.VERSION }}" - body_path: release_notes.md - files: ${{ inputs.package_dir }}/${{ env.WHEEL_FILE }} - draft: false - prerelease: false - make_latest: ${{ inputs.package_name == 'lume' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.vscode/libs-ts.code-workspace b/.vscode/libs-ts.code-workspace new file mode 100644 index 00000000..732316f2 --- /dev/null +++ b/.vscode/libs-ts.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "name": "libs-ts", + "path": "../libs/typescript" + } + ], + "extensions": { + "recommendations": [ + "biomejs.biome", + ] + } +} \ No newline at end of file diff --git a/examples/computer-example-ts/.env.example b/examples/computer-example-ts/.env.example new file mode 100644 index 00000000..0496a574 --- /dev/null +++ b/examples/computer-example-ts/.env.example @@ -0,0 +1,3 @@ +OPENAI_KEY= +CUA_KEY= +CUA_CONTAINER_NAME= \ No newline at end of file diff --git a/examples/computer-example-ts/.gitignore b/examples/computer-example-ts/.gitignore new file mode 100644 index 00000000..9bdf3559 --- /dev/null +++ b/examples/computer-example-ts/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +.env \ No newline at end of file diff --git a/examples/computer-example-ts/.prettierrc b/examples/computer-example-ts/.prettierrc new file mode 100644 index 00000000..23eaef29 --- /dev/null +++ b/examples/computer-example-ts/.prettierrc @@ -0,0 +1,7 @@ +{ + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true +} \ No newline at end of file diff --git a/examples/computer-example-ts/README.md b/examples/computer-example-ts/README.md new file mode 100644 index 00000000..1e61eab0 --- /dev/null +++ b/examples/computer-example-ts/README.md @@ -0,0 +1,47 @@ +# cua-cloud-openai Example + +This example demonstrates how to control a c/ua Cloud container using the OpenAI `computer-use-preview` model and the `@trycua/computer` TypeScript library. + +## Overview + +- Connects to a c/ua Cloud container via the `@trycua/computer` library +- Sends screenshots and instructions to OpenAI's computer-use model +- Executes AI-generated actions (clicks, typing, etc.) inside the container +- Designed for Linux containers, but can be adapted for other OS types + +## Getting Started + +1. **Install dependencies:** + + ```bash + npm install + ``` + +2. **Set up environment variables:** + Create a `.env` file with the following variables: + - `OPENAI_KEY` — your OpenAI API key + - `CUA_KEY` — your c/ua Cloud API key + - `CUA_CONTAINER_NAME` — the name of your provisioned container + +3. **Run the example:** + + ```bash + npx tsx src/index.ts + ``` + +## Files + +- `src/index.ts` — Main example script +- `src/helpers.ts` — Helper for executing actions on the container + +## Further Reading + +For a step-by-step tutorial and more detailed explanation, see the accompanying blog post: + +➡️ [Controlling a c/ua Cloud Container with JavaScript](https://placeholder-url-to-blog-post.com) + +_(This link will be updated once the article is published.)_ + +--- + +If you have questions or issues, please open an issue or contact the maintainers. diff --git a/examples/computer-example-ts/package.json b/examples/computer-example-ts/package.json new file mode 100644 index 00000000..4e961528 --- /dev/null +++ b/examples/computer-example-ts/package.json @@ -0,0 +1,25 @@ +{ + "name": "cua-cloud-openai", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "tsx src/index.ts" + }, + "keywords": [], + "author": "", + "license": "MIT", + "packageManager": "pnpm@10.12.3", + "dependencies": { + "@trycua/computer": "link:../../libs/typescript/computer", + "dotenv": "^16.5.0", + "openai": "^5.7.0" + }, + "devDependencies": { + "@types/node": "^22.15.33", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } +} \ No newline at end of file diff --git a/examples/computer-example-ts/pnpm-lock.yaml b/examples/computer-example-ts/pnpm-lock.yaml new file mode 100644 index 00000000..6d0882b3 --- /dev/null +++ b/examples/computer-example-ts/pnpm-lock.yaml @@ -0,0 +1,362 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@trycua/computer': + specifier: link:../../libs/typescript/computer + version: link:../../libs/typescript/computer + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + openai: + specifier: ^5.7.0 + version: 5.8.2 + devDependencies: + '@types/node': + specifier: ^22.15.33 + version: 22.15.34 + tsx: + specifier: ^4.20.3 + version: 4.20.3 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + +packages: + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@22.15.34': + resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + openai@5.8.2: + resolution: {integrity: sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@types/node@22.15.34': + dependencies: + undici-types: 6.21.0 + + dotenv@16.6.1: {} + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + openai@5.8.2: {} + + resolve-pkg-maps@1.0.0: {} + + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.8.3: {} + + undici-types@6.21.0: {} diff --git a/examples/computer-example-ts/src/helpers.ts b/examples/computer-example-ts/src/helpers.ts new file mode 100644 index 00000000..adad2347 --- /dev/null +++ b/examples/computer-example-ts/src/helpers.ts @@ -0,0 +1,63 @@ +import type { Computer } from "@trycua/computer"; +import type OpenAI from "openai"; + +export async function executeAction( + computer: Computer, + action: OpenAI.Responses.ResponseComputerToolCall["action"], +) { + switch (action.type) { + case "click": { + const { x, y, button } = action; + console.log(`Executing click at (${x}, ${y}) with button '${button}'.`); + await computer.interface.moveCursor(x, y); + if (button === "right") await computer.interface.rightClick(); + else await computer.interface.leftClick(); + break; + } + case "type": + { + const { text } = action; + console.log(`Typing text: ${text}`); + await computer.interface.typeText(text); + } + break; + case "scroll": { + const { x: locX, y: locY, scroll_x, scroll_y } = action; + console.log( + `Scrolling at (${locX}, ${locY}) with offsets (scroll_x=${scroll_x}, scroll_y=${scroll_y}).`, + ); + await computer.interface.moveCursor(locX, locY); + await computer.interface.scroll(scroll_x, scroll_y); + break; + } + case "keypress": { + const { keys } = action; + for (const key of keys) { + console.log(`Pressing key: ${key}.`); + // Map common key names to CUA equivalents + if (key.toLowerCase() === "enter") { + await computer.interface.pressKey("return"); + } else if (key.toLowerCase() === "space") { + await computer.interface.pressKey("space"); + } else { + await computer.interface.pressKey(key); + } + } + break; + } + case "wait": { + console.log(`Waiting for 3 seconds.`); + await new Promise((resolve) => setTimeout(resolve, 3 * 1000)); + break; + } + case "screenshot": { + console.log("Taking screenshot."); + // This is handled automatically in the main loop, but we can take an extra one if requested + const screenshot = await computer.interface.screenshot(); + return screenshot; + } + default: + console.log(`Unrecognized action: ${action.type}`); + break; + } +} diff --git a/examples/computer-example-ts/src/index.ts b/examples/computer-example-ts/src/index.ts new file mode 100644 index 00000000..1077e088 --- /dev/null +++ b/examples/computer-example-ts/src/index.ts @@ -0,0 +1,104 @@ +import { Computer, OSType } from "@trycua/computer"; +import OpenAI from "openai"; +import { executeAction } from "./helpers"; + +import "dotenv/config"; + +const openai = new OpenAI({ apiKey: process.env.OPENAI_KEY }); + +const COMPUTER_USE_PROMPT = "Open firefox and go to trycua.com"; + +// Initialize the Computer Connection +const computer = new Computer({ + apiKey: process.env.CUA_KEY!, + name: process.env.CUA_CONTAINER_NAME!, + osType: OSType.LINUX, +}); + +await computer.run(); +// Take the initial screenshot +const screenshot = await computer.interface.screenshot(); +const screenshotBase64 = screenshot.toString("base64"); + +// Setup openai config for computer use +const computerUseConfig: OpenAI.Responses.ResponseCreateParamsNonStreaming = { + model: "computer-use-preview", + tools: [ + { + type: "computer_use_preview", + display_width: 1024, + display_height: 768, + environment: "linux", // we're using a linux vm + }, + ], + truncation: "auto", +}; + +// Send initial screenshot to the openai computer use model +let res = await openai.responses.create({ + ...computerUseConfig, + input: [ + { + role: "user", + content: [ + // what we want the ai to do + { type: "input_text", text: COMPUTER_USE_PROMPT }, + // current screenshot of the vm + { + type: "input_image", + image_url: `data:image/png;base64,${screenshotBase64}`, + detail: "auto", + }, + ], + }, + ], +}); + +// Loop until there are no more computer use actions. +while (true) { + const computerCalls = res.output.filter((o) => o.type === "computer_call"); + if (computerCalls.length < 1) { + console.log("No more computer calls. Loop complete."); + break; + } + // Get the first call + const call = computerCalls[0]; + const action = call.action; + console.log("Received action from OpenAI Responses API:", action); + let ackChecks: OpenAI.Responses.ResponseComputerToolCall.PendingSafetyCheck[] = + []; + if (call.pending_safety_checks.length > 0) { + console.log("Safety checks pending:", call.pending_safety_checks); + // In a real implementation, you would want to get user confirmation here + ackChecks = call.pending_safety_checks; + } + + // Execute the action in the container + await executeAction(computer, action); + // Wait for changes to process within the container (1sec) + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Capture new screenshot + const newScreenshot = await computer.interface.screenshot(); + const newScreenshotBase64 = newScreenshot.toString("base64"); + + // Screenshot back as computer_call_output + + res = await openai.responses.create({ + ...computerUseConfig, + previous_response_id: res.id, + input: [ + { + type: "computer_call_output", + call_id: call.call_id, + acknowledged_safety_checks: ackChecks, + output: { + type: "computer_screenshot", + image_url: `data:image/png;base64,${newScreenshotBase64}`, + }, + }, + ], + }); +} + +process.exit(); diff --git a/examples/computer-example-ts/tsconfig.json b/examples/computer-example-ts/tsconfig.json new file mode 100644 index 00000000..c606e279 --- /dev/null +++ b/examples/computer-example-ts/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": [ + "es2023" + ], + "moduleDetection": "force", + "module": "preserve", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "types": [ + "node" + ], + "allowSyntheticDefaultImports": true, + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "outDir": "build", + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/libs/agent/README.md b/libs/python/agent/README.md similarity index 100% rename from libs/agent/README.md rename to libs/python/agent/README.md diff --git a/libs/agent/agent/__init__.py b/libs/python/agent/agent/__init__.py similarity index 100% rename from libs/agent/agent/__init__.py rename to libs/python/agent/agent/__init__.py diff --git a/libs/agent/agent/core/__init__.py b/libs/python/agent/agent/core/__init__.py similarity index 100% rename from libs/agent/agent/core/__init__.py rename to libs/python/agent/agent/core/__init__.py diff --git a/libs/agent/agent/core/agent.py b/libs/python/agent/agent/core/agent.py similarity index 100% rename from libs/agent/agent/core/agent.py rename to libs/python/agent/agent/core/agent.py diff --git a/libs/agent/agent/core/base.py b/libs/python/agent/agent/core/base.py similarity index 100% rename from libs/agent/agent/core/base.py rename to libs/python/agent/agent/core/base.py diff --git a/libs/agent/agent/core/callbacks.py b/libs/python/agent/agent/core/callbacks.py similarity index 100% rename from libs/agent/agent/core/callbacks.py rename to libs/python/agent/agent/core/callbacks.py diff --git a/libs/agent/agent/core/experiment.py b/libs/python/agent/agent/core/experiment.py similarity index 100% rename from libs/agent/agent/core/experiment.py rename to libs/python/agent/agent/core/experiment.py diff --git a/libs/agent/agent/core/factory.py b/libs/python/agent/agent/core/factory.py similarity index 100% rename from libs/agent/agent/core/factory.py rename to libs/python/agent/agent/core/factory.py diff --git a/libs/agent/agent/core/messages.py b/libs/python/agent/agent/core/messages.py similarity index 100% rename from libs/agent/agent/core/messages.py rename to libs/python/agent/agent/core/messages.py diff --git a/libs/agent/agent/core/provider_config.py b/libs/python/agent/agent/core/provider_config.py similarity index 100% rename from libs/agent/agent/core/provider_config.py rename to libs/python/agent/agent/core/provider_config.py diff --git a/libs/agent/agent/core/telemetry.py b/libs/python/agent/agent/core/telemetry.py similarity index 100% rename from libs/agent/agent/core/telemetry.py rename to libs/python/agent/agent/core/telemetry.py diff --git a/libs/agent/agent/core/tools.py b/libs/python/agent/agent/core/tools.py similarity index 100% rename from libs/agent/agent/core/tools.py rename to libs/python/agent/agent/core/tools.py diff --git a/libs/agent/agent/core/tools/__init__.py b/libs/python/agent/agent/core/tools/__init__.py similarity index 100% rename from libs/agent/agent/core/tools/__init__.py rename to libs/python/agent/agent/core/tools/__init__.py diff --git a/libs/agent/agent/core/tools/base.py b/libs/python/agent/agent/core/tools/base.py similarity index 100% rename from libs/agent/agent/core/tools/base.py rename to libs/python/agent/agent/core/tools/base.py diff --git a/libs/agent/agent/core/tools/bash.py b/libs/python/agent/agent/core/tools/bash.py similarity index 100% rename from libs/agent/agent/core/tools/bash.py rename to libs/python/agent/agent/core/tools/bash.py diff --git a/libs/agent/agent/core/tools/collection.py b/libs/python/agent/agent/core/tools/collection.py similarity index 100% rename from libs/agent/agent/core/tools/collection.py rename to libs/python/agent/agent/core/tools/collection.py diff --git a/libs/agent/agent/core/tools/computer.py b/libs/python/agent/agent/core/tools/computer.py similarity index 100% rename from libs/agent/agent/core/tools/computer.py rename to libs/python/agent/agent/core/tools/computer.py diff --git a/libs/agent/agent/core/tools/edit.py b/libs/python/agent/agent/core/tools/edit.py similarity index 100% rename from libs/agent/agent/core/tools/edit.py rename to libs/python/agent/agent/core/tools/edit.py diff --git a/libs/agent/agent/core/tools/manager.py b/libs/python/agent/agent/core/tools/manager.py similarity index 100% rename from libs/agent/agent/core/tools/manager.py rename to libs/python/agent/agent/core/tools/manager.py diff --git a/libs/agent/agent/core/types.py b/libs/python/agent/agent/core/types.py similarity index 100% rename from libs/agent/agent/core/types.py rename to libs/python/agent/agent/core/types.py diff --git a/libs/agent/agent/core/visualization.py b/libs/python/agent/agent/core/visualization.py similarity index 100% rename from libs/agent/agent/core/visualization.py rename to libs/python/agent/agent/core/visualization.py diff --git a/libs/agent/agent/providers/__init__.py b/libs/python/agent/agent/providers/__init__.py similarity index 100% rename from libs/agent/agent/providers/__init__.py rename to libs/python/agent/agent/providers/__init__.py diff --git a/libs/agent/agent/providers/anthropic/__init__.py b/libs/python/agent/agent/providers/anthropic/__init__.py similarity index 100% rename from libs/agent/agent/providers/anthropic/__init__.py rename to libs/python/agent/agent/providers/anthropic/__init__.py diff --git a/libs/agent/agent/providers/anthropic/api/client.py b/libs/python/agent/agent/providers/anthropic/api/client.py similarity index 100% rename from libs/agent/agent/providers/anthropic/api/client.py rename to libs/python/agent/agent/providers/anthropic/api/client.py diff --git a/libs/agent/agent/providers/anthropic/api/logging.py b/libs/python/agent/agent/providers/anthropic/api/logging.py similarity index 100% rename from libs/agent/agent/providers/anthropic/api/logging.py rename to libs/python/agent/agent/providers/anthropic/api/logging.py diff --git a/libs/agent/agent/providers/anthropic/api_handler.py b/libs/python/agent/agent/providers/anthropic/api_handler.py similarity index 100% rename from libs/agent/agent/providers/anthropic/api_handler.py rename to libs/python/agent/agent/providers/anthropic/api_handler.py diff --git a/libs/agent/agent/providers/anthropic/callbacks/__init__.py b/libs/python/agent/agent/providers/anthropic/callbacks/__init__.py similarity index 100% rename from libs/agent/agent/providers/anthropic/callbacks/__init__.py rename to libs/python/agent/agent/providers/anthropic/callbacks/__init__.py diff --git a/libs/agent/agent/providers/anthropic/callbacks/manager.py b/libs/python/agent/agent/providers/anthropic/callbacks/manager.py similarity index 100% rename from libs/agent/agent/providers/anthropic/callbacks/manager.py rename to libs/python/agent/agent/providers/anthropic/callbacks/manager.py diff --git a/libs/agent/agent/providers/anthropic/loop.py b/libs/python/agent/agent/providers/anthropic/loop.py similarity index 100% rename from libs/agent/agent/providers/anthropic/loop.py rename to libs/python/agent/agent/providers/anthropic/loop.py diff --git a/libs/agent/agent/providers/anthropic/prompts.py b/libs/python/agent/agent/providers/anthropic/prompts.py similarity index 100% rename from libs/agent/agent/providers/anthropic/prompts.py rename to libs/python/agent/agent/providers/anthropic/prompts.py diff --git a/libs/agent/agent/providers/anthropic/response_handler.py b/libs/python/agent/agent/providers/anthropic/response_handler.py similarity index 100% rename from libs/agent/agent/providers/anthropic/response_handler.py rename to libs/python/agent/agent/providers/anthropic/response_handler.py diff --git a/libs/agent/agent/providers/anthropic/tools/__init__.py b/libs/python/agent/agent/providers/anthropic/tools/__init__.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/__init__.py rename to libs/python/agent/agent/providers/anthropic/tools/__init__.py diff --git a/libs/agent/agent/providers/anthropic/tools/base.py b/libs/python/agent/agent/providers/anthropic/tools/base.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/base.py rename to libs/python/agent/agent/providers/anthropic/tools/base.py diff --git a/libs/agent/agent/providers/anthropic/tools/bash.py b/libs/python/agent/agent/providers/anthropic/tools/bash.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/bash.py rename to libs/python/agent/agent/providers/anthropic/tools/bash.py diff --git a/libs/agent/agent/providers/anthropic/tools/collection.py b/libs/python/agent/agent/providers/anthropic/tools/collection.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/collection.py rename to libs/python/agent/agent/providers/anthropic/tools/collection.py diff --git a/libs/agent/agent/providers/anthropic/tools/computer.py b/libs/python/agent/agent/providers/anthropic/tools/computer.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/computer.py rename to libs/python/agent/agent/providers/anthropic/tools/computer.py diff --git a/libs/agent/agent/providers/anthropic/tools/edit.py b/libs/python/agent/agent/providers/anthropic/tools/edit.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/edit.py rename to libs/python/agent/agent/providers/anthropic/tools/edit.py diff --git a/libs/agent/agent/providers/anthropic/tools/manager.py b/libs/python/agent/agent/providers/anthropic/tools/manager.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/manager.py rename to libs/python/agent/agent/providers/anthropic/tools/manager.py diff --git a/libs/agent/agent/providers/anthropic/tools/run.py b/libs/python/agent/agent/providers/anthropic/tools/run.py similarity index 100% rename from libs/agent/agent/providers/anthropic/tools/run.py rename to libs/python/agent/agent/providers/anthropic/tools/run.py diff --git a/libs/agent/agent/providers/anthropic/types.py b/libs/python/agent/agent/providers/anthropic/types.py similarity index 100% rename from libs/agent/agent/providers/anthropic/types.py rename to libs/python/agent/agent/providers/anthropic/types.py diff --git a/libs/agent/agent/providers/anthropic/utils.py b/libs/python/agent/agent/providers/anthropic/utils.py similarity index 100% rename from libs/agent/agent/providers/anthropic/utils.py rename to libs/python/agent/agent/providers/anthropic/utils.py diff --git a/libs/agent/agent/providers/omni/__init__.py b/libs/python/agent/agent/providers/omni/__init__.py similarity index 100% rename from libs/agent/agent/providers/omni/__init__.py rename to libs/python/agent/agent/providers/omni/__init__.py diff --git a/libs/agent/agent/providers/omni/api_handler.py b/libs/python/agent/agent/providers/omni/api_handler.py similarity index 100% rename from libs/agent/agent/providers/omni/api_handler.py rename to libs/python/agent/agent/providers/omni/api_handler.py diff --git a/libs/agent/agent/providers/omni/clients/anthropic.py b/libs/python/agent/agent/providers/omni/clients/anthropic.py similarity index 100% rename from libs/agent/agent/providers/omni/clients/anthropic.py rename to libs/python/agent/agent/providers/omni/clients/anthropic.py diff --git a/libs/agent/agent/providers/omni/clients/base.py b/libs/python/agent/agent/providers/omni/clients/base.py similarity index 100% rename from libs/agent/agent/providers/omni/clients/base.py rename to libs/python/agent/agent/providers/omni/clients/base.py diff --git a/libs/agent/agent/providers/omni/clients/oaicompat.py b/libs/python/agent/agent/providers/omni/clients/oaicompat.py similarity index 100% rename from libs/agent/agent/providers/omni/clients/oaicompat.py rename to libs/python/agent/agent/providers/omni/clients/oaicompat.py diff --git a/libs/agent/agent/providers/omni/clients/ollama.py b/libs/python/agent/agent/providers/omni/clients/ollama.py similarity index 100% rename from libs/agent/agent/providers/omni/clients/ollama.py rename to libs/python/agent/agent/providers/omni/clients/ollama.py diff --git a/libs/agent/agent/providers/omni/clients/openai.py b/libs/python/agent/agent/providers/omni/clients/openai.py similarity index 100% rename from libs/agent/agent/providers/omni/clients/openai.py rename to libs/python/agent/agent/providers/omni/clients/openai.py diff --git a/libs/agent/agent/providers/omni/clients/utils.py b/libs/python/agent/agent/providers/omni/clients/utils.py similarity index 100% rename from libs/agent/agent/providers/omni/clients/utils.py rename to libs/python/agent/agent/providers/omni/clients/utils.py diff --git a/libs/agent/agent/providers/omni/image_utils.py b/libs/python/agent/agent/providers/omni/image_utils.py similarity index 100% rename from libs/agent/agent/providers/omni/image_utils.py rename to libs/python/agent/agent/providers/omni/image_utils.py diff --git a/libs/agent/agent/providers/omni/loop.py b/libs/python/agent/agent/providers/omni/loop.py similarity index 100% rename from libs/agent/agent/providers/omni/loop.py rename to libs/python/agent/agent/providers/omni/loop.py diff --git a/libs/agent/agent/providers/omni/parser.py b/libs/python/agent/agent/providers/omni/parser.py similarity index 100% rename from libs/agent/agent/providers/omni/parser.py rename to libs/python/agent/agent/providers/omni/parser.py diff --git a/libs/agent/agent/providers/omni/prompts.py b/libs/python/agent/agent/providers/omni/prompts.py similarity index 100% rename from libs/agent/agent/providers/omni/prompts.py rename to libs/python/agent/agent/providers/omni/prompts.py diff --git a/libs/agent/agent/providers/omni/tools/__init__.py b/libs/python/agent/agent/providers/omni/tools/__init__.py similarity index 100% rename from libs/agent/agent/providers/omni/tools/__init__.py rename to libs/python/agent/agent/providers/omni/tools/__init__.py diff --git a/libs/agent/agent/providers/omni/tools/base.py b/libs/python/agent/agent/providers/omni/tools/base.py similarity index 100% rename from libs/agent/agent/providers/omni/tools/base.py rename to libs/python/agent/agent/providers/omni/tools/base.py diff --git a/libs/agent/agent/providers/omni/tools/bash.py b/libs/python/agent/agent/providers/omni/tools/bash.py similarity index 100% rename from libs/agent/agent/providers/omni/tools/bash.py rename to libs/python/agent/agent/providers/omni/tools/bash.py diff --git a/libs/agent/agent/providers/omni/tools/computer.py b/libs/python/agent/agent/providers/omni/tools/computer.py similarity index 100% rename from libs/agent/agent/providers/omni/tools/computer.py rename to libs/python/agent/agent/providers/omni/tools/computer.py diff --git a/libs/agent/agent/providers/omni/tools/manager.py b/libs/python/agent/agent/providers/omni/tools/manager.py similarity index 100% rename from libs/agent/agent/providers/omni/tools/manager.py rename to libs/python/agent/agent/providers/omni/tools/manager.py diff --git a/libs/agent/agent/providers/omni/utils.py b/libs/python/agent/agent/providers/omni/utils.py similarity index 100% rename from libs/agent/agent/providers/omni/utils.py rename to libs/python/agent/agent/providers/omni/utils.py diff --git a/libs/agent/agent/providers/openai/__init__.py b/libs/python/agent/agent/providers/openai/__init__.py similarity index 100% rename from libs/agent/agent/providers/openai/__init__.py rename to libs/python/agent/agent/providers/openai/__init__.py diff --git a/libs/agent/agent/providers/openai/api_handler.py b/libs/python/agent/agent/providers/openai/api_handler.py similarity index 100% rename from libs/agent/agent/providers/openai/api_handler.py rename to libs/python/agent/agent/providers/openai/api_handler.py diff --git a/libs/agent/agent/providers/openai/loop.py b/libs/python/agent/agent/providers/openai/loop.py similarity index 100% rename from libs/agent/agent/providers/openai/loop.py rename to libs/python/agent/agent/providers/openai/loop.py diff --git a/libs/agent/agent/providers/openai/response_handler.py b/libs/python/agent/agent/providers/openai/response_handler.py similarity index 100% rename from libs/agent/agent/providers/openai/response_handler.py rename to libs/python/agent/agent/providers/openai/response_handler.py diff --git a/libs/agent/agent/providers/openai/tools/__init__.py b/libs/python/agent/agent/providers/openai/tools/__init__.py similarity index 100% rename from libs/agent/agent/providers/openai/tools/__init__.py rename to libs/python/agent/agent/providers/openai/tools/__init__.py diff --git a/libs/agent/agent/providers/openai/tools/base.py b/libs/python/agent/agent/providers/openai/tools/base.py similarity index 100% rename from libs/agent/agent/providers/openai/tools/base.py rename to libs/python/agent/agent/providers/openai/tools/base.py diff --git a/libs/agent/agent/providers/openai/tools/computer.py b/libs/python/agent/agent/providers/openai/tools/computer.py similarity index 100% rename from libs/agent/agent/providers/openai/tools/computer.py rename to libs/python/agent/agent/providers/openai/tools/computer.py diff --git a/libs/agent/agent/providers/openai/tools/manager.py b/libs/python/agent/agent/providers/openai/tools/manager.py similarity index 100% rename from libs/agent/agent/providers/openai/tools/manager.py rename to libs/python/agent/agent/providers/openai/tools/manager.py diff --git a/libs/agent/agent/providers/openai/types.py b/libs/python/agent/agent/providers/openai/types.py similarity index 100% rename from libs/agent/agent/providers/openai/types.py rename to libs/python/agent/agent/providers/openai/types.py diff --git a/libs/agent/agent/providers/openai/utils.py b/libs/python/agent/agent/providers/openai/utils.py similarity index 100% rename from libs/agent/agent/providers/openai/utils.py rename to libs/python/agent/agent/providers/openai/utils.py diff --git a/libs/agent/agent/providers/uitars/__init__.py b/libs/python/agent/agent/providers/uitars/__init__.py similarity index 100% rename from libs/agent/agent/providers/uitars/__init__.py rename to libs/python/agent/agent/providers/uitars/__init__.py diff --git a/libs/agent/agent/providers/uitars/clients/base.py b/libs/python/agent/agent/providers/uitars/clients/base.py similarity index 100% rename from libs/agent/agent/providers/uitars/clients/base.py rename to libs/python/agent/agent/providers/uitars/clients/base.py diff --git a/libs/agent/agent/providers/uitars/clients/mlxvlm.py b/libs/python/agent/agent/providers/uitars/clients/mlxvlm.py similarity index 100% rename from libs/agent/agent/providers/uitars/clients/mlxvlm.py rename to libs/python/agent/agent/providers/uitars/clients/mlxvlm.py diff --git a/libs/agent/agent/providers/uitars/clients/oaicompat.py b/libs/python/agent/agent/providers/uitars/clients/oaicompat.py similarity index 100% rename from libs/agent/agent/providers/uitars/clients/oaicompat.py rename to libs/python/agent/agent/providers/uitars/clients/oaicompat.py diff --git a/libs/agent/agent/providers/uitars/loop.py b/libs/python/agent/agent/providers/uitars/loop.py similarity index 100% rename from libs/agent/agent/providers/uitars/loop.py rename to libs/python/agent/agent/providers/uitars/loop.py diff --git a/libs/agent/agent/providers/uitars/prompts.py b/libs/python/agent/agent/providers/uitars/prompts.py similarity index 100% rename from libs/agent/agent/providers/uitars/prompts.py rename to libs/python/agent/agent/providers/uitars/prompts.py diff --git a/libs/agent/agent/providers/uitars/tools/__init__.py b/libs/python/agent/agent/providers/uitars/tools/__init__.py similarity index 100% rename from libs/agent/agent/providers/uitars/tools/__init__.py rename to libs/python/agent/agent/providers/uitars/tools/__init__.py diff --git a/libs/agent/agent/providers/uitars/tools/computer.py b/libs/python/agent/agent/providers/uitars/tools/computer.py similarity index 100% rename from libs/agent/agent/providers/uitars/tools/computer.py rename to libs/python/agent/agent/providers/uitars/tools/computer.py diff --git a/libs/agent/agent/providers/uitars/tools/manager.py b/libs/python/agent/agent/providers/uitars/tools/manager.py similarity index 100% rename from libs/agent/agent/providers/uitars/tools/manager.py rename to libs/python/agent/agent/providers/uitars/tools/manager.py diff --git a/libs/agent/agent/providers/uitars/utils.py b/libs/python/agent/agent/providers/uitars/utils.py similarity index 100% rename from libs/agent/agent/providers/uitars/utils.py rename to libs/python/agent/agent/providers/uitars/utils.py diff --git a/libs/agent/agent/telemetry.py b/libs/python/agent/agent/telemetry.py similarity index 100% rename from libs/agent/agent/telemetry.py rename to libs/python/agent/agent/telemetry.py diff --git a/libs/agent/agent/ui/__init__.py b/libs/python/agent/agent/ui/__init__.py similarity index 100% rename from libs/agent/agent/ui/__init__.py rename to libs/python/agent/agent/ui/__init__.py diff --git a/libs/agent/agent/ui/__main__.py b/libs/python/agent/agent/ui/__main__.py similarity index 100% rename from libs/agent/agent/ui/__main__.py rename to libs/python/agent/agent/ui/__main__.py diff --git a/libs/agent/agent/ui/gradio/__init__.py b/libs/python/agent/agent/ui/gradio/__init__.py similarity index 100% rename from libs/agent/agent/ui/gradio/__init__.py rename to libs/python/agent/agent/ui/gradio/__init__.py diff --git a/libs/agent/agent/ui/gradio/app.py b/libs/python/agent/agent/ui/gradio/app.py similarity index 100% rename from libs/agent/agent/ui/gradio/app.py rename to libs/python/agent/agent/ui/gradio/app.py diff --git a/libs/agent/poetry.toml b/libs/python/agent/poetry.toml similarity index 100% rename from libs/agent/poetry.toml rename to libs/python/agent/poetry.toml diff --git a/libs/agent/pyproject.toml b/libs/python/agent/pyproject.toml similarity index 100% rename from libs/agent/pyproject.toml rename to libs/python/agent/pyproject.toml diff --git a/libs/computer-server/README.md b/libs/python/computer-server/README.md similarity index 100% rename from libs/computer-server/README.md rename to libs/python/computer-server/README.md diff --git a/libs/computer-server/computer_server/__init__.py b/libs/python/computer-server/computer_server/__init__.py similarity index 100% rename from libs/computer-server/computer_server/__init__.py rename to libs/python/computer-server/computer_server/__init__.py diff --git a/libs/computer-server/computer_server/__main__.py b/libs/python/computer-server/computer_server/__main__.py similarity index 100% rename from libs/computer-server/computer_server/__main__.py rename to libs/python/computer-server/computer_server/__main__.py diff --git a/libs/computer-server/computer_server/cli.py b/libs/python/computer-server/computer_server/cli.py similarity index 100% rename from libs/computer-server/computer_server/cli.py rename to libs/python/computer-server/computer_server/cli.py diff --git a/libs/computer-server/computer_server/diorama/__init__.py b/libs/python/computer-server/computer_server/diorama/__init__.py similarity index 100% rename from libs/computer-server/computer_server/diorama/__init__.py rename to libs/python/computer-server/computer_server/diorama/__init__.py diff --git a/libs/computer-server/computer_server/diorama/base.py b/libs/python/computer-server/computer_server/diorama/base.py similarity index 100% rename from libs/computer-server/computer_server/diorama/base.py rename to libs/python/computer-server/computer_server/diorama/base.py diff --git a/libs/computer-server/computer_server/diorama/diorama.py b/libs/python/computer-server/computer_server/diorama/diorama.py similarity index 100% rename from libs/computer-server/computer_server/diorama/diorama.py rename to libs/python/computer-server/computer_server/diorama/diorama.py diff --git a/libs/computer-server/computer_server/diorama/diorama_computer.py b/libs/python/computer-server/computer_server/diorama/diorama_computer.py similarity index 100% rename from libs/computer-server/computer_server/diorama/diorama_computer.py rename to libs/python/computer-server/computer_server/diorama/diorama_computer.py diff --git a/libs/computer-server/computer_server/diorama/draw.py b/libs/python/computer-server/computer_server/diorama/draw.py similarity index 100% rename from libs/computer-server/computer_server/diorama/draw.py rename to libs/python/computer-server/computer_server/diorama/draw.py diff --git a/libs/computer-server/computer_server/diorama/macos.py b/libs/python/computer-server/computer_server/diorama/macos.py similarity index 100% rename from libs/computer-server/computer_server/diorama/macos.py rename to libs/python/computer-server/computer_server/diorama/macos.py diff --git a/libs/computer-server/computer_server/diorama/safezone.py b/libs/python/computer-server/computer_server/diorama/safezone.py similarity index 100% rename from libs/computer-server/computer_server/diorama/safezone.py rename to libs/python/computer-server/computer_server/diorama/safezone.py diff --git a/libs/computer-server/computer_server/handlers/base.py b/libs/python/computer-server/computer_server/handlers/base.py similarity index 100% rename from libs/computer-server/computer_server/handlers/base.py rename to libs/python/computer-server/computer_server/handlers/base.py diff --git a/libs/computer-server/computer_server/handlers/factory.py b/libs/python/computer-server/computer_server/handlers/factory.py similarity index 100% rename from libs/computer-server/computer_server/handlers/factory.py rename to libs/python/computer-server/computer_server/handlers/factory.py diff --git a/libs/computer-server/computer_server/handlers/generic.py b/libs/python/computer-server/computer_server/handlers/generic.py similarity index 100% rename from libs/computer-server/computer_server/handlers/generic.py rename to libs/python/computer-server/computer_server/handlers/generic.py diff --git a/libs/computer-server/computer_server/handlers/linux.py b/libs/python/computer-server/computer_server/handlers/linux.py similarity index 100% rename from libs/computer-server/computer_server/handlers/linux.py rename to libs/python/computer-server/computer_server/handlers/linux.py diff --git a/libs/computer-server/computer_server/handlers/macos.py b/libs/python/computer-server/computer_server/handlers/macos.py similarity index 100% rename from libs/computer-server/computer_server/handlers/macos.py rename to libs/python/computer-server/computer_server/handlers/macos.py diff --git a/libs/computer-server/computer_server/handlers/windows.py b/libs/python/computer-server/computer_server/handlers/windows.py similarity index 100% rename from libs/computer-server/computer_server/handlers/windows.py rename to libs/python/computer-server/computer_server/handlers/windows.py diff --git a/libs/computer-server/computer_server/main.py b/libs/python/computer-server/computer_server/main.py similarity index 100% rename from libs/computer-server/computer_server/main.py rename to libs/python/computer-server/computer_server/main.py diff --git a/libs/computer-server/computer_server/server.py b/libs/python/computer-server/computer_server/server.py similarity index 100% rename from libs/computer-server/computer_server/server.py rename to libs/python/computer-server/computer_server/server.py diff --git a/libs/computer-server/examples/__init__.py b/libs/python/computer-server/examples/__init__.py similarity index 100% rename from libs/computer-server/examples/__init__.py rename to libs/python/computer-server/examples/__init__.py diff --git a/libs/computer-server/examples/usage_example.py b/libs/python/computer-server/examples/usage_example.py similarity index 100% rename from libs/computer-server/examples/usage_example.py rename to libs/python/computer-server/examples/usage_example.py diff --git a/libs/computer-server/pyproject.toml b/libs/python/computer-server/pyproject.toml similarity index 100% rename from libs/computer-server/pyproject.toml rename to libs/python/computer-server/pyproject.toml diff --git a/libs/computer-server/run_server.py b/libs/python/computer-server/run_server.py similarity index 100% rename from libs/computer-server/run_server.py rename to libs/python/computer-server/run_server.py diff --git a/libs/computer-server/test_connection.py b/libs/python/computer-server/test_connection.py similarity index 100% rename from libs/computer-server/test_connection.py rename to libs/python/computer-server/test_connection.py diff --git a/libs/computer/README.md b/libs/python/computer/README.md similarity index 100% rename from libs/computer/README.md rename to libs/python/computer/README.md diff --git a/libs/computer/computer/__init__.py b/libs/python/computer/computer/__init__.py similarity index 100% rename from libs/computer/computer/__init__.py rename to libs/python/computer/computer/__init__.py diff --git a/libs/computer/computer/computer.py b/libs/python/computer/computer/computer.py similarity index 100% rename from libs/computer/computer/computer.py rename to libs/python/computer/computer/computer.py diff --git a/libs/computer/computer/diorama_computer.py b/libs/python/computer/computer/diorama_computer.py similarity index 100% rename from libs/computer/computer/diorama_computer.py rename to libs/python/computer/computer/diorama_computer.py diff --git a/libs/computer/computer/helpers.py b/libs/python/computer/computer/helpers.py similarity index 100% rename from libs/computer/computer/helpers.py rename to libs/python/computer/computer/helpers.py diff --git a/libs/computer/computer/interface/__init__.py b/libs/python/computer/computer/interface/__init__.py similarity index 100% rename from libs/computer/computer/interface/__init__.py rename to libs/python/computer/computer/interface/__init__.py diff --git a/libs/computer/computer/interface/base.py b/libs/python/computer/computer/interface/base.py similarity index 100% rename from libs/computer/computer/interface/base.py rename to libs/python/computer/computer/interface/base.py diff --git a/libs/computer/computer/interface/factory.py b/libs/python/computer/computer/interface/factory.py similarity index 100% rename from libs/computer/computer/interface/factory.py rename to libs/python/computer/computer/interface/factory.py diff --git a/libs/computer/computer/interface/linux.py b/libs/python/computer/computer/interface/linux.py similarity index 100% rename from libs/computer/computer/interface/linux.py rename to libs/python/computer/computer/interface/linux.py diff --git a/libs/computer/computer/interface/macos.py b/libs/python/computer/computer/interface/macos.py similarity index 100% rename from libs/computer/computer/interface/macos.py rename to libs/python/computer/computer/interface/macos.py diff --git a/libs/computer/computer/interface/models.py b/libs/python/computer/computer/interface/models.py similarity index 100% rename from libs/computer/computer/interface/models.py rename to libs/python/computer/computer/interface/models.py diff --git a/libs/computer/computer/interface/windows.py b/libs/python/computer/computer/interface/windows.py similarity index 100% rename from libs/computer/computer/interface/windows.py rename to libs/python/computer/computer/interface/windows.py diff --git a/libs/computer/computer/logger.py b/libs/python/computer/computer/logger.py similarity index 100% rename from libs/computer/computer/logger.py rename to libs/python/computer/computer/logger.py diff --git a/libs/computer/computer/models.py b/libs/python/computer/computer/models.py similarity index 100% rename from libs/computer/computer/models.py rename to libs/python/computer/computer/models.py diff --git a/libs/computer/computer/providers/__init__.py b/libs/python/computer/computer/providers/__init__.py similarity index 100% rename from libs/computer/computer/providers/__init__.py rename to libs/python/computer/computer/providers/__init__.py diff --git a/libs/computer/computer/providers/base.py b/libs/python/computer/computer/providers/base.py similarity index 100% rename from libs/computer/computer/providers/base.py rename to libs/python/computer/computer/providers/base.py diff --git a/libs/computer/computer/providers/cloud/__init__.py b/libs/python/computer/computer/providers/cloud/__init__.py similarity index 100% rename from libs/computer/computer/providers/cloud/__init__.py rename to libs/python/computer/computer/providers/cloud/__init__.py diff --git a/libs/computer/computer/providers/cloud/provider.py b/libs/python/computer/computer/providers/cloud/provider.py similarity index 100% rename from libs/computer/computer/providers/cloud/provider.py rename to libs/python/computer/computer/providers/cloud/provider.py diff --git a/libs/computer/computer/providers/factory.py b/libs/python/computer/computer/providers/factory.py similarity index 100% rename from libs/computer/computer/providers/factory.py rename to libs/python/computer/computer/providers/factory.py diff --git a/libs/computer/computer/providers/lume/__init__.py b/libs/python/computer/computer/providers/lume/__init__.py similarity index 100% rename from libs/computer/computer/providers/lume/__init__.py rename to libs/python/computer/computer/providers/lume/__init__.py diff --git a/libs/computer/computer/providers/lume/provider.py b/libs/python/computer/computer/providers/lume/provider.py similarity index 100% rename from libs/computer/computer/providers/lume/provider.py rename to libs/python/computer/computer/providers/lume/provider.py diff --git a/libs/computer/computer/providers/lume_api.py b/libs/python/computer/computer/providers/lume_api.py similarity index 100% rename from libs/computer/computer/providers/lume_api.py rename to libs/python/computer/computer/providers/lume_api.py diff --git a/libs/computer/computer/providers/lumier/__init__.py b/libs/python/computer/computer/providers/lumier/__init__.py similarity index 100% rename from libs/computer/computer/providers/lumier/__init__.py rename to libs/python/computer/computer/providers/lumier/__init__.py diff --git a/libs/computer/computer/providers/lumier/provider.py b/libs/python/computer/computer/providers/lumier/provider.py similarity index 100% rename from libs/computer/computer/providers/lumier/provider.py rename to libs/python/computer/computer/providers/lumier/provider.py diff --git a/libs/computer/computer/providers/winsandbox/__init__.py b/libs/python/computer/computer/providers/winsandbox/__init__.py similarity index 100% rename from libs/computer/computer/providers/winsandbox/__init__.py rename to libs/python/computer/computer/providers/winsandbox/__init__.py diff --git a/libs/computer/computer/providers/winsandbox/provider.py b/libs/python/computer/computer/providers/winsandbox/provider.py similarity index 100% rename from libs/computer/computer/providers/winsandbox/provider.py rename to libs/python/computer/computer/providers/winsandbox/provider.py diff --git a/libs/computer/computer/providers/winsandbox/setup_script.ps1 b/libs/python/computer/computer/providers/winsandbox/setup_script.ps1 similarity index 100% rename from libs/computer/computer/providers/winsandbox/setup_script.ps1 rename to libs/python/computer/computer/providers/winsandbox/setup_script.ps1 diff --git a/libs/computer/computer/telemetry.py b/libs/python/computer/computer/telemetry.py similarity index 100% rename from libs/computer/computer/telemetry.py rename to libs/python/computer/computer/telemetry.py diff --git a/libs/computer/computer/ui/__init__.py b/libs/python/computer/computer/ui/__init__.py similarity index 100% rename from libs/computer/computer/ui/__init__.py rename to libs/python/computer/computer/ui/__init__.py diff --git a/libs/computer/computer/ui/__main__.py b/libs/python/computer/computer/ui/__main__.py similarity index 100% rename from libs/computer/computer/ui/__main__.py rename to libs/python/computer/computer/ui/__main__.py diff --git a/libs/computer/computer/ui/gradio/__init__.py b/libs/python/computer/computer/ui/gradio/__init__.py similarity index 100% rename from libs/computer/computer/ui/gradio/__init__.py rename to libs/python/computer/computer/ui/gradio/__init__.py diff --git a/libs/computer/computer/ui/gradio/app.py b/libs/python/computer/computer/ui/gradio/app.py similarity index 100% rename from libs/computer/computer/ui/gradio/app.py rename to libs/python/computer/computer/ui/gradio/app.py diff --git a/libs/computer/computer/utils.py b/libs/python/computer/computer/utils.py similarity index 100% rename from libs/computer/computer/utils.py rename to libs/python/computer/computer/utils.py diff --git a/libs/computer/poetry.toml b/libs/python/computer/poetry.toml similarity index 100% rename from libs/computer/poetry.toml rename to libs/python/computer/poetry.toml diff --git a/libs/computer/pyproject.toml b/libs/python/computer/pyproject.toml similarity index 100% rename from libs/computer/pyproject.toml rename to libs/python/computer/pyproject.toml diff --git a/libs/core/README.md b/libs/python/core/README.md similarity index 100% rename from libs/core/README.md rename to libs/python/core/README.md diff --git a/libs/core/core/__init__.py b/libs/python/core/core/__init__.py similarity index 100% rename from libs/core/core/__init__.py rename to libs/python/core/core/__init__.py diff --git a/libs/core/core/telemetry/__init__.py b/libs/python/core/core/telemetry/__init__.py similarity index 100% rename from libs/core/core/telemetry/__init__.py rename to libs/python/core/core/telemetry/__init__.py diff --git a/libs/core/core/telemetry/client.py b/libs/python/core/core/telemetry/client.py similarity index 100% rename from libs/core/core/telemetry/client.py rename to libs/python/core/core/telemetry/client.py diff --git a/libs/core/core/telemetry/models.py b/libs/python/core/core/telemetry/models.py similarity index 100% rename from libs/core/core/telemetry/models.py rename to libs/python/core/core/telemetry/models.py diff --git a/libs/core/core/telemetry/posthog_client.py b/libs/python/core/core/telemetry/posthog_client.py similarity index 100% rename from libs/core/core/telemetry/posthog_client.py rename to libs/python/core/core/telemetry/posthog_client.py diff --git a/libs/core/core/telemetry/sender.py b/libs/python/core/core/telemetry/sender.py similarity index 100% rename from libs/core/core/telemetry/sender.py rename to libs/python/core/core/telemetry/sender.py diff --git a/libs/core/core/telemetry/telemetry.py b/libs/python/core/core/telemetry/telemetry.py similarity index 100% rename from libs/core/core/telemetry/telemetry.py rename to libs/python/core/core/telemetry/telemetry.py diff --git a/libs/core/poetry.toml b/libs/python/core/poetry.toml similarity index 100% rename from libs/core/poetry.toml rename to libs/python/core/poetry.toml diff --git a/libs/core/pyproject.toml b/libs/python/core/pyproject.toml similarity index 100% rename from libs/core/pyproject.toml rename to libs/python/core/pyproject.toml diff --git a/libs/mcp-server/README.md b/libs/python/mcp-server/README.md similarity index 100% rename from libs/mcp-server/README.md rename to libs/python/mcp-server/README.md diff --git a/libs/mcp-server/mcp_server/__init__.py b/libs/python/mcp-server/mcp_server/__init__.py similarity index 100% rename from libs/mcp-server/mcp_server/__init__.py rename to libs/python/mcp-server/mcp_server/__init__.py diff --git a/libs/mcp-server/mcp_server/__main__.py b/libs/python/mcp-server/mcp_server/__main__.py similarity index 100% rename from libs/mcp-server/mcp_server/__main__.py rename to libs/python/mcp-server/mcp_server/__main__.py diff --git a/libs/mcp-server/mcp_server/server.py b/libs/python/mcp-server/mcp_server/server.py similarity index 100% rename from libs/mcp-server/mcp_server/server.py rename to libs/python/mcp-server/mcp_server/server.py diff --git a/libs/mcp-server/pyproject.toml b/libs/python/mcp-server/pyproject.toml similarity index 100% rename from libs/mcp-server/pyproject.toml rename to libs/python/mcp-server/pyproject.toml diff --git a/libs/mcp-server/scripts/install_mcp_server.sh b/libs/python/mcp-server/scripts/install_mcp_server.sh similarity index 100% rename from libs/mcp-server/scripts/install_mcp_server.sh rename to libs/python/mcp-server/scripts/install_mcp_server.sh diff --git a/libs/mcp-server/scripts/start_mcp_server.sh b/libs/python/mcp-server/scripts/start_mcp_server.sh similarity index 100% rename from libs/mcp-server/scripts/start_mcp_server.sh rename to libs/python/mcp-server/scripts/start_mcp_server.sh diff --git a/libs/pylume/README.md b/libs/python/pylume/README.md similarity index 100% rename from libs/pylume/README.md rename to libs/python/pylume/README.md diff --git a/libs/pylume/__init__.py b/libs/python/pylume/__init__.py similarity index 100% rename from libs/pylume/__init__.py rename to libs/python/pylume/__init__.py diff --git a/libs/pylume/pylume/__init__.py b/libs/python/pylume/pylume/__init__.py similarity index 100% rename from libs/pylume/pylume/__init__.py rename to libs/python/pylume/pylume/__init__.py diff --git a/libs/pylume/pylume/client.py b/libs/python/pylume/pylume/client.py similarity index 100% rename from libs/pylume/pylume/client.py rename to libs/python/pylume/pylume/client.py diff --git a/libs/pylume/pylume/exceptions.py b/libs/python/pylume/pylume/exceptions.py similarity index 100% rename from libs/pylume/pylume/exceptions.py rename to libs/python/pylume/pylume/exceptions.py diff --git a/libs/pylume/pylume/lume b/libs/python/pylume/pylume/lume similarity index 100% rename from libs/pylume/pylume/lume rename to libs/python/pylume/pylume/lume diff --git a/libs/pylume/pylume/models.py b/libs/python/pylume/pylume/models.py similarity index 100% rename from libs/pylume/pylume/models.py rename to libs/python/pylume/pylume/models.py diff --git a/libs/pylume/pylume/pylume.py b/libs/python/pylume/pylume/pylume.py similarity index 100% rename from libs/pylume/pylume/pylume.py rename to libs/python/pylume/pylume/pylume.py diff --git a/libs/pylume/pylume/server.py b/libs/python/pylume/pylume/server.py similarity index 100% rename from libs/pylume/pylume/server.py rename to libs/python/pylume/pylume/server.py diff --git a/libs/pylume/pyproject.toml b/libs/python/pylume/pyproject.toml similarity index 100% rename from libs/pylume/pyproject.toml rename to libs/python/pylume/pyproject.toml diff --git a/libs/som/README.md b/libs/python/som/README.md similarity index 100% rename from libs/som/README.md rename to libs/python/som/README.md diff --git a/libs/som/poetry.toml b/libs/python/som/poetry.toml similarity index 100% rename from libs/som/poetry.toml rename to libs/python/som/poetry.toml diff --git a/libs/som/pyproject.toml b/libs/python/som/pyproject.toml similarity index 100% rename from libs/som/pyproject.toml rename to libs/python/som/pyproject.toml diff --git a/libs/som/som/__init__.py b/libs/python/som/som/__init__.py similarity index 100% rename from libs/som/som/__init__.py rename to libs/python/som/som/__init__.py diff --git a/libs/som/som/detect.py b/libs/python/som/som/detect.py similarity index 100% rename from libs/som/som/detect.py rename to libs/python/som/som/detect.py diff --git a/libs/som/som/detection.py b/libs/python/som/som/detection.py similarity index 100% rename from libs/som/som/detection.py rename to libs/python/som/som/detection.py diff --git a/libs/som/som/models.py b/libs/python/som/som/models.py similarity index 100% rename from libs/som/som/models.py rename to libs/python/som/som/models.py diff --git a/libs/som/som/ocr.py b/libs/python/som/som/ocr.py similarity index 100% rename from libs/som/som/ocr.py rename to libs/python/som/som/ocr.py diff --git a/libs/som/som/util/utils.py b/libs/python/som/som/util/utils.py similarity index 100% rename from libs/som/som/util/utils.py rename to libs/python/som/som/util/utils.py diff --git a/libs/som/som/visualization.py b/libs/python/som/som/visualization.py similarity index 100% rename from libs/som/som/visualization.py rename to libs/python/som/som/visualization.py diff --git a/libs/som/tests/test_omniparser.py b/libs/python/som/tests/test_omniparser.py similarity index 100% rename from libs/som/tests/test_omniparser.py rename to libs/python/som/tests/test_omniparser.py diff --git a/libs/typescript/.gitignore b/libs/typescript/.gitignore new file mode 100644 index 00000000..77dae5e3 --- /dev/null +++ b/libs/typescript/.gitignore @@ -0,0 +1,5 @@ +node_modules + +*.log +.DS_Store +.eslintcache diff --git a/libs/typescript/.nvmrc b/libs/typescript/.nvmrc new file mode 100644 index 00000000..dc864a05 --- /dev/null +++ b/libs/typescript/.nvmrc @@ -0,0 +1 @@ +v24.2.0 diff --git a/libs/typescript/README.md b/libs/typescript/README.md new file mode 100644 index 00000000..78fda2e7 --- /dev/null +++ b/libs/typescript/README.md @@ -0,0 +1,119 @@ +# C/UA TypeScript Libraries + +This repository contains TypeScript implementations of the C/UA libraries: + +- `@trycua/core`: Core functionality including telemetry and logging +- `@trycua/computer`: Computer interaction SDK for VM management and control + +## Project Structure + +```text +libs/typescript/ +├── computer/ # Computer SDK package +├── core/ # Core functionality package +├── package.json # Root package configuration +└── pnpm-workspace.yaml # Workspace configuration +``` + +## Prerequisites + +- [Node.js](https://nodejs.org/) (v18 or later) +- [pnpm](https://pnpm.io/) (v10 or later) + +## Setup and Installation + +1. Install dependencies for all packages: + +```bash +pnpm install +``` + +1. Build all packages: + +```bash +pnpm build:all +``` + +## Development Workflow + +### Building Packages + +Build all packages in the correct dependency order: + +```bash +pnpm build:all +``` + +Build specific packages: + +```bash +# Build core package +pnpm --filter @trycua/core build + +# Build computer package +pnpm --filter @trycua/computer build +``` + +### Running Tests + +Run tests for all packages: + +```bash +pnpm test:all +``` + +Run tests for specific packages: + +```bash +# Test core package +pnpm --filter @trycua/core test + +# Test computer package +pnpm --filter @trycua/computer test +``` + +### Linting + +Lint all packages: + +```bash +pnpm lint:all +``` + +Fix linting issues: + +```bash +pnpm lint:fix:all +``` + +## Package Details + +### @trycua/core + +Core functionality for C/UA libraries including: + +- Telemetry with PostHog integration +- Common utilities and types + +### @trycua/computer + +Computer interaction SDK for managing and controlling virtual machines: + +- VM provider system (Cloud) +- Interface system for OS-specific interactions +- Screenshot, keyboard, and mouse control +- Command execution + +## Publishing + +Prepare packages for publishing: + +```bash +pnpm -r build +``` + +Publish packages: + +```bash +pnpm -r publish +``` diff --git a/libs/typescript/biome.json b/libs/typescript/biome.json new file mode 100644 index 00000000..a0394eff --- /dev/null +++ b/libs/typescript/biome.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "include": ["core/**/*.ts", "computer/**/*.ts"], + "ignore": ["dist", "node_modules"] + }, + "formatter": { + "enabled": true, + "useEditorconfig": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + "attributePosition": "auto", + "bracketSpacing": true + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useSelfClosingElements": "warn", + "noUnusedTemplateLiteral": "warn", + "noNonNullAssertion": "off" + }, + "a11y": { + "useMediaCaption": "off", + "useKeyWithClickEvents": "warn", + "useKeyWithMouseEvents": "warn", + "noSvgWithoutTitle": "off", + "useButtonType": "warn", + "noAutofocus": "off" + }, + "suspicious": { + "noArrayIndexKey": "off" + }, + "correctness": { + "noUnusedVariables": "warn", + "noUnusedFunctionParameters": "warn", + "noUnusedImports": "warn" + }, + "complexity": { + "useOptionalChain": "info" + }, + "nursery": { + "useSortedClasses": { + "level": "warn", + "fix": "safe", + "options": { + "attributes": ["className"], + "functions": ["cn"] + } + } + } + } + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + } + } +} diff --git a/libs/typescript/computer/.editorconfig b/libs/typescript/computer/.editorconfig new file mode 100644 index 00000000..7095e7fb --- /dev/null +++ b/libs/typescript/computer/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +indent_size = 2 +end_of_line = lf +insert_final_newline = true diff --git a/libs/typescript/computer/.gitattributes b/libs/typescript/computer/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/libs/typescript/computer/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/libs/typescript/computer/.gitignore b/libs/typescript/computer/.gitignore new file mode 100644 index 00000000..e79f2036 --- /dev/null +++ b/libs/typescript/computer/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist + +*.log +.DS_Store +.eslintcache diff --git a/libs/typescript/computer/LICENSE b/libs/typescript/computer/LICENSE new file mode 100644 index 00000000..7ff04379 --- /dev/null +++ b/libs/typescript/computer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2025 C/UA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/typescript/computer/README.md b/libs/typescript/computer/README.md new file mode 100644 index 00000000..a9d42740 --- /dev/null +++ b/libs/typescript/computer/README.md @@ -0,0 +1,90 @@ +# C/ua Computer TypeScript Library + +The TypeScript library for C/cua Computer - a powerful computer control and automation library. + +## Overview + +This library is a TypeScript port of the Python computer library, providing the same functionality for controlling virtual machines and computer interfaces. It enables programmatic control of virtual machines through various providers and offers a consistent interface for interacting with the VM's operating system. + +## Installation + +```bash +npm install @trycua/computer +# or +pnpm add @trycua/computer +``` + +## Usage + +```typescript +import { Computer } from '@trycua/computer'; + +// Create a new computer instance +const computer = new Computer({ + osType: OSType.LINUX, + name: 's-linux-vm_id' + apiKey: 'your-api-key' +}); + +// Start the computer +await computer.run(); + +// Get the computer interface for interaction +const interface = computer.interface; + +// Take a screenshot +const screenshot = await interface.getScreenshot(); + +// Click at coordinates +await interface.click(500, 300); + +// Type text +await interface.typeText('Hello, world!'); + +// Stop the computer +await computer.stop(); +``` + +## Architecture + +The library is organized into the following structure: + +### Core Components + +- **Computer Factory**: A factory object that creates appropriate computer instances +- **BaseComputer**: Abstract base class with shared functionality for all computer types +- **Types**: Type definitions for configuration options and shared interfaces + +### Provider Implementations + +- **Computer**: Implementation for cloud-based VMs + +## Development + +- Install dependencies: + +```bash +pnpm install +``` + +- Run the unit tests: + +```bash +pnpm test +``` + +- Build the library: + +```bash +pnpm build +``` + +- Type checking: + +```bash +pnpm typecheck +``` + +## License + +[MIT](./LICENSE) License 2025 [C/UA](https://github.com/trycua) diff --git a/libs/typescript/computer/package.json b/libs/typescript/computer/package.json new file mode 100644 index 00000000..797ecbc3 --- /dev/null +++ b/libs/typescript/computer/package.json @@ -0,0 +1,56 @@ +{ + "name": "@trycua/computer", + "version": "0.0.1", + "packageManager": "pnpm@10.11.0", + "description": "Typescript SDK for c/ua computer interaction", + "type": "module", + "license": "MIT", + "homepage": "https://github.com/trycua/cua/tree/feature/computer/typescript/libs/typescript/computer", + "bugs": { + "url": "https://github.com/trycua/cua/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/trycua/cua.git" + }, + "author": "c/ua", + "files": [ + "dist" + ], + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "lint": "biome lint .", + "lint:fix": "biome lint --fix .", + "build": "tsdown", + "dev": "tsdown --watch", + "test": "vitest", + "typecheck": "tsc --noEmit", + "release": "bumpp && pnpm publish", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@trycua/core": "link:../core", + "pino": "^9.7.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^22.15.17", + "@types/ws": "^8.18.1", + "bumpp": "^10.1.0", + "happy-dom": "^17.4.7", + "tsdown": "^0.11.9", + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "vitest": "^3.1.3" + } +} \ No newline at end of file diff --git a/libs/typescript/computer/src/computer/index.ts b/libs/typescript/computer/src/computer/index.ts new file mode 100644 index 00000000..d7411b63 --- /dev/null +++ b/libs/typescript/computer/src/computer/index.ts @@ -0,0 +1 @@ +export { BaseComputer, CloudComputer } from './providers'; diff --git a/libs/typescript/computer/src/computer/providers/base.ts b/libs/typescript/computer/src/computer/providers/base.ts new file mode 100644 index 00000000..ffd6da2d --- /dev/null +++ b/libs/typescript/computer/src/computer/providers/base.ts @@ -0,0 +1,117 @@ +import os from "node:os"; +import { Telemetry } from "@trycua/core"; +import pino from "pino"; +import type { OSType } from "../../types"; +import type { BaseComputerConfig, Display, VMProviderType } from "../types"; + +const logger = pino({ name: "computer.provider_base" }); + +/** + * Base Computer class with shared functionality + */ +export abstract class BaseComputer { + protected name: string; + protected osType: OSType; + protected vmProvider?: VMProviderType; + protected telemetry: Telemetry; + + constructor(config: BaseComputerConfig) { + this.name = config.name; + this.osType = config.osType; + this.telemetry = new Telemetry(); + this.telemetry.recordEvent("module_init", { + module: "computer", + version: process.env.npm_package_version, + node_version: process.version, + }); + + this.telemetry.recordEvent("computer_initialized", { + os: os.platform(), + os_version: os.version(), + node_version: process.version, + }); + } + + /** + * Get the name of the computer + */ + getName(): string { + return this.name; + } + + /** + * Get the OS type of the computer + */ + getOSType(): OSType { + return this.osType; + } + + /** + * Get the VM provider type + */ + getVMProviderType(): VMProviderType | undefined { + return this.vmProvider; + } + + /** + * Shared method available to all computer types + */ + async disconnect(): Promise { + logger.info(`Disconnecting from ${this.name}`); + // Implementation would go here + } + + /** + * Parse display string into Display object + * @param display Display string in format "WIDTHxHEIGHT" + * @returns Display object + */ + public static parseDisplayString(display: string): Display { + const match = display.match(/^(\d+)x(\d+)$/); + if (!match) { + throw new Error( + `Invalid display format: ${display}. Expected format: WIDTHxHEIGHT`, + ); + } + + return { + width: Number.parseInt(match[1], 10), + height: Number.parseInt(match[2], 10), + }; + } + + /** + * Parse memory string to MB integer. + * + * Examples: + * "8GB" -> 8192 + * "1024MB" -> 1024 + * "512" -> 512 + * + * @param memoryStr - Memory string to parse + * @returns Memory value in MB + */ + public static parseMemoryString(memoryStr: string): number { + if (!memoryStr) { + return 0; + } + + // Convert to uppercase for case-insensitive matching + const upperStr = memoryStr.toUpperCase().trim(); + + // Extract numeric value and unit + const match = upperStr.match(/^(\d+(?:\.\d+)?)\s*(GB|MB)?$/); + if (!match) { + throw new Error(`Invalid memory format: ${memoryStr}`); + } + + const value = Number.parseFloat(match[1]); + const unit = match[2] || "MB"; // Default to MB if no unit specified + + // Convert to MB + if (unit === "GB") { + return Math.round(value * 1024); + } + return Math.round(value); + } +} diff --git a/libs/typescript/computer/src/computer/providers/cloud.ts b/libs/typescript/computer/src/computer/providers/cloud.ts new file mode 100644 index 00000000..5614fb64 --- /dev/null +++ b/libs/typescript/computer/src/computer/providers/cloud.ts @@ -0,0 +1,94 @@ +import pino from 'pino'; +import { + type BaseComputerInterface, + InterfaceFactory, +} from '../../interface/index'; +import type { CloudComputerConfig, VMProviderType } from '../types'; +import { BaseComputer } from './base'; + +/** + * Cloud-specific computer implementation + */ +export class CloudComputer extends BaseComputer { + protected static vmProviderType: VMProviderType.CLOUD; + protected apiKey: string; + private iface?: BaseComputerInterface; + private initialized = false; + + protected logger = pino({ name: 'computer.provider_cloud' }); + + constructor(config: CloudComputerConfig) { + super(config); + this.apiKey = config.apiKey; + } + + get ip() { + return `${this.name}.containers.cloud.trycua.com`; + } + + /** + * Initialize the cloud VM and interface + */ + async run(): Promise { + if (this.initialized) { + this.logger.info('Computer already initialized, skipping initialization'); + return; + } + + try { + // For cloud provider, the VM is already running, we just need to connect + const ipAddress = this.ip; + this.logger.info(`Connecting to cloud VM at ${ipAddress}`); + + // Create the interface with API key authentication + this.iface = InterfaceFactory.createInterfaceForOS( + this.osType, + ipAddress, + this.apiKey, + this.name + ); + + // Wait for the interface to be ready + this.logger.info('Waiting for interface to be ready...'); + await this.iface.waitForReady(); + + this.initialized = true; + this.logger.info('Cloud computer ready'); + } catch (error) { + this.logger.error(`Failed to initialize cloud computer: ${error}`); + throw new Error(`Failed to initialize cloud computer: ${error}`); + } + } + + /** + * Stop the cloud computer (disconnect interface) + */ + async stop(): Promise { + this.logger.info('Disconnecting from cloud computer...'); + + if (this.iface) { + this.iface.disconnect(); + this.iface = undefined; + } + + this.initialized = false; + this.logger.info('Disconnected from cloud computer'); + } + + /** + * Get the computer interface + */ + get interface(): BaseComputerInterface { + if (!this.iface) { + throw new Error('Computer not initialized. Call run() first.'); + } + return this.iface; + } + + /** + * Disconnect from the cloud computer + */ + async disconnect(): Promise { + await this.stop(); + } +} diff --git a/libs/typescript/computer/src/computer/providers/index.ts b/libs/typescript/computer/src/computer/providers/index.ts new file mode 100644 index 00000000..27faf7d6 --- /dev/null +++ b/libs/typescript/computer/src/computer/providers/index.ts @@ -0,0 +1,2 @@ +export * from './base'; +export * from './cloud'; diff --git a/libs/typescript/computer/src/computer/types.ts b/libs/typescript/computer/src/computer/types.ts new file mode 100644 index 00000000..7bca918b --- /dev/null +++ b/libs/typescript/computer/src/computer/types.ts @@ -0,0 +1,36 @@ +import type { OSType, ScreenSize } from '../types'; + +/** + * Display configuration for the computer. + */ +export interface Display extends ScreenSize { + scale_factor?: number; +} + +/** + * Computer configuration model. + */ +export interface BaseComputerConfig { + /** + * The VM name + * @default "" + */ + name: string; + + /** + * The operating system type ('macos', 'windows', or 'linux') + * @default "macos" + */ + osType: OSType; +} + +export interface CloudComputerConfig extends BaseComputerConfig { + /** + * Optional API key for cloud providers + */ + apiKey: string; +} + +export enum VMProviderType { + CLOUD = 'cloud', +} diff --git a/libs/typescript/computer/src/index.ts b/libs/typescript/computer/src/index.ts new file mode 100644 index 00000000..44b515fe --- /dev/null +++ b/libs/typescript/computer/src/index.ts @@ -0,0 +1,6 @@ +// Export classes +export { CloudComputer as Computer } from './computer'; + +//todo: figure out what types to export and how to do that +// +export { OSType } from './types'; diff --git a/libs/typescript/computer/src/interface/base.ts b/libs/typescript/computer/src/interface/base.ts new file mode 100644 index 00000000..21b63389 --- /dev/null +++ b/libs/typescript/computer/src/interface/base.ts @@ -0,0 +1,363 @@ +/** + * Base interface for computer control. + */ + +import pino from 'pino'; +import WebSocket from 'ws'; +import type { ScreenSize } from '../types'; + +export type MouseButton = 'left' | 'middle' | 'right'; + +export interface CursorPosition { + x: number; + y: number; +} + +export interface AccessibilityNode { + role: string; + title?: string; + value?: string; + description?: string; + bounds?: { + x: number; + y: number; + width: number; + height: number; + }; + children?: AccessibilityNode[]; +} + +/** + * Base class for computer control interfaces. + */ +export abstract class BaseComputerInterface { + protected ipAddress: string; + protected username: string; + protected password: string; + protected closed = false; + protected commandLock: Promise = Promise.resolve(); + protected ws: WebSocket; + protected apiKey?: string; + protected vmName?: string; + + protected logger = pino({ name: 'computer.interface-base' }); + + constructor( + ipAddress: string, + username = 'lume', + password = 'lume', + apiKey?: string, + vmName?: string + ) { + this.ipAddress = ipAddress; + this.username = username; + this.password = password; + this.apiKey = apiKey; + this.vmName = vmName; + + // Initialize WebSocket with headers if needed + const headers: { [key: string]: string } = {}; + if (this.apiKey && this.vmName) { + headers['X-API-Key'] = this.apiKey; + headers['X-VM-Name'] = this.vmName; + } + + // Create the WebSocket instance + this.ws = new WebSocket(this.wsUri, { headers }); + } + + /** + * Get the WebSocket URI for connection. + * Subclasses can override this to customize the URI. + */ + protected get wsUri(): string { + const protocol = this.apiKey ? 'wss' : 'ws'; + + // Check if ipAddress already includes a port + if (this.ipAddress.includes(':')) { + return `${protocol}://${this.ipAddress}/ws`; + } + + // Otherwise, append the default port + const port = this.apiKey ? '8443' : '8000'; + return `${protocol}://${this.ipAddress}:${port}/ws`; + } + + /** + * Wait for interface to be ready. + * @param timeout Maximum time to wait in seconds + * @throws Error if interface is not ready within timeout + */ + async waitForReady(timeout = 60): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout * 1000) { + try { + await this.connect(); + return; + } catch (error) { + console.log(error); + // Wait a bit before retrying + this.logger.error( + `Error connecting to websocket: ${JSON.stringify(error)}` + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + throw new Error(`Interface not ready after ${timeout} seconds`); + } + + /** + * Authenticate with the WebSocket server. + * This should be called immediately after the WebSocket connection is established. + */ + private async authenticate(): Promise { + if (!this.apiKey || !this.vmName) { + // No authentication needed + return; + } + + this.logger.info('Performing authentication handshake...'); + const authMessage = { + command: 'authenticate', + params: { + api_key: this.apiKey, + container_name: this.vmName, + }, + }; + + return new Promise((resolve, reject) => { + const authHandler = (data: WebSocket.RawData) => { + try { + const authResult = JSON.parse(data.toString()); + if (!authResult.success) { + const errorMsg = authResult.error || 'Authentication failed'; + this.logger.error(`Authentication failed: ${errorMsg}`); + this.ws.close(); + reject(new Error(`Authentication failed: ${errorMsg}`)); + } else { + this.logger.info('Authentication successful'); + this.ws.off('message', authHandler); + resolve(); + } + } catch (error) { + this.ws.off('message', authHandler); + reject(error); + } + }; + + this.ws.on('message', authHandler); + this.ws.send(JSON.stringify(authMessage)); + }); + } + + /** + * Connect to the WebSocket server. + */ + public async connect(): Promise { + // If the WebSocket is already open, check if we need to authenticate + if (this.ws.readyState === WebSocket.OPEN) { + this.logger.info( + 'Websocket is open, ensuring authentication is complete.' + ); + return this.authenticate(); + } + + // If the WebSocket is closed or closing, reinitialize it + if ( + this.ws.readyState === WebSocket.CLOSED || + this.ws.readyState === WebSocket.CLOSING + ) { + this.logger.info('Websocket is closed. Reinitializing connection.'); + const headers: { [key: string]: string } = {}; + if (this.apiKey && this.vmName) { + headers['X-API-Key'] = this.apiKey; + headers['X-VM-Name'] = this.vmName; + } + this.ws = new WebSocket(this.wsUri, { headers }); + return this.authenticate(); + } + + // Connect and authenticate + return new Promise((resolve, reject) => { + const onOpen = async () => { + try { + // Always authenticate immediately after connection + await this.authenticate(); + resolve(); + } catch (error) { + reject(error); + } + }; + + // If already connecting, wait for it to complete then authenticate + if (this.ws.readyState === WebSocket.CONNECTING) { + this.ws.addEventListener('open', onOpen, { once: true }); + this.ws.addEventListener('error', (error) => reject(error), { + once: true, + }); + return; + } + + // Set up event handlers + this.ws.on('open', onOpen); + + this.ws.on('error', (error: Error) => { + reject(error); + }); + + this.ws.on('close', () => { + if (!this.closed) { + // Attempt to reconnect + setTimeout(() => this.connect(), 1000); + } + }); + }); + } + + /** + * Send a command to the WebSocket server. + */ + public async sendCommand( + command: string, + params: { [key: string]: unknown } = {} + ): Promise<{ [key: string]: unknown }> { + // Create a new promise for this specific command + const commandPromise = new Promise<{ [key: string]: unknown }>( + (resolve, reject) => { + // Chain it to the previous commands + const executeCommand = async (): Promise<{ + [key: string]: unknown; + }> => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + await this.connect(); + } + + return new Promise<{ [key: string]: unknown }>( + (innerResolve, innerReject) => { + const messageHandler = (data: WebSocket.RawData) => { + try { + const response = JSON.parse(data.toString()); + if (response.error) { + innerReject(new Error(response.error)); + } else { + innerResolve(response); + } + } catch (error) { + innerReject(error); + } + this.ws.off('message', messageHandler); + }; + + this.ws.on('message', messageHandler); + const wsCommand = { command, params }; + this.ws.send(JSON.stringify(wsCommand)); + } + ); + }; + + // Add this command to the lock chain + this.commandLock = this.commandLock.then(() => + executeCommand().then(resolve, reject) + ); + } + ); + + return commandPromise; + } + + /** + * Check if the WebSocket is connected. + */ + public isConnected(): boolean { + return this.ws && this.ws.readyState === WebSocket.OPEN; + } + + /** + * Close the interface connection. + */ + disconnect(): void { + this.closed = true; + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.close(); + } else if (this.ws && this.ws.readyState === WebSocket.CONNECTING) { + // If still connecting, terminate the connection attempt + this.ws.terminate(); + } + } + + /** + * Force close the interface connection. + * By default, this just calls close(), but subclasses can override + * to provide more forceful cleanup. + */ + forceClose(): void { + this.disconnect(); + } + + // Mouse Actions + abstract mouseDown( + x?: number, + y?: number, + button?: MouseButton + ): Promise; + abstract mouseUp(x?: number, y?: number, button?: MouseButton): Promise; + abstract leftClick(x?: number, y?: number): Promise; + abstract rightClick(x?: number, y?: number): Promise; + abstract doubleClick(x?: number, y?: number): Promise; + abstract moveCursor(x: number, y: number): Promise; + abstract dragTo( + x: number, + y: number, + button?: MouseButton, + duration?: number + ): Promise; + abstract drag( + path: Array<[number, number]>, + button?: MouseButton, + duration?: number + ): Promise; + + // Keyboard Actions + abstract keyDown(key: string): Promise; + abstract keyUp(key: string): Promise; + abstract typeText(text: string): Promise; + abstract pressKey(key: string): Promise; + abstract hotkey(...keys: string[]): Promise; + + // Scrolling Actions + abstract scroll(x: number, y: number): Promise; + abstract scrollDown(clicks?: number): Promise; + abstract scrollUp(clicks?: number): Promise; + + // Screen Actions + abstract screenshot(): Promise; + abstract getScreenSize(): Promise; + abstract getCursorPosition(): Promise; + + // Clipboard Actions + abstract copyToClipboard(): Promise; + abstract setClipboard(text: string): Promise; + + // File System Actions + abstract fileExists(path: string): Promise; + abstract directoryExists(path: string): Promise; + abstract listDir(path: string): Promise; + abstract readText(path: string): Promise; + abstract writeText(path: string, content: string): Promise; + abstract readBytes(path: string): Promise; + abstract writeBytes(path: string, content: Buffer): Promise; + abstract deleteFile(path: string): Promise; + abstract createDir(path: string): Promise; + abstract deleteDir(path: string): Promise; + abstract runCommand(command: string): Promise<[string, string]>; + + // Accessibility Actions + abstract getAccessibilityTree(): Promise; + abstract toScreenCoordinates(x: number, y: number): Promise<[number, number]>; + abstract toScreenshotCoordinates( + x: number, + y: number + ): Promise<[number, number]>; +} diff --git a/libs/typescript/computer/src/interface/factory.ts b/libs/typescript/computer/src/interface/factory.ts new file mode 100644 index 00000000..60864f0e --- /dev/null +++ b/libs/typescript/computer/src/interface/factory.ts @@ -0,0 +1,57 @@ +/** + * Factory for creating computer interfaces. + */ + +import type { OSType } from '../types'; +import type { BaseComputerInterface } from './base'; +import { LinuxComputerInterface } from './linux'; +import { MacOSComputerInterface } from './macos'; +import { WindowsComputerInterface } from './windows'; + +export const InterfaceFactory = { + /** + * Create an interface for the specified OS. + * + * @param os Operating system type ('macos', 'linux', or 'windows') + * @param ipAddress IP address of the computer to control + * @param apiKey Optional API key for cloud authentication + * @param vmName Optional VM name for cloud authentication + * @returns The appropriate interface for the OS + * @throws Error if the OS type is not supported + */ + createInterfaceForOS( + os: OSType, + ipAddress: string, + apiKey?: string, + vmName?: string + ): BaseComputerInterface { + switch (os) { + case 'macos': + return new MacOSComputerInterface( + ipAddress, + 'lume', + 'lume', + apiKey, + vmName + ); + case 'linux': + return new LinuxComputerInterface( + ipAddress, + 'lume', + 'lume', + apiKey, + vmName + ); + case 'windows': + return new WindowsComputerInterface( + ipAddress, + 'lume', + 'lume', + apiKey, + vmName + ); + default: + throw new Error(`Unsupported OS type: ${os}`); + } + }, +}; diff --git a/libs/typescript/computer/src/interface/index.ts b/libs/typescript/computer/src/interface/index.ts new file mode 100644 index 00000000..285f594b --- /dev/null +++ b/libs/typescript/computer/src/interface/index.ts @@ -0,0 +1,6 @@ +export { BaseComputerInterface } from './base'; +export type { MouseButton, CursorPosition, AccessibilityNode } from './base'; +export { InterfaceFactory } from './factory'; +export { MacOSComputerInterface } from './macos'; +export { LinuxComputerInterface } from './linux'; +export { WindowsComputerInterface } from './windows'; diff --git a/libs/typescript/computer/src/interface/linux.ts b/libs/typescript/computer/src/interface/linux.ts new file mode 100644 index 00000000..5fcf27c3 --- /dev/null +++ b/libs/typescript/computer/src/interface/linux.ts @@ -0,0 +1,14 @@ +/** + * Linux computer interface implementation. + */ + +import { MacOSComputerInterface } from './macos'; + +/** + * Linux interface implementation. + * Since the cloud provider uses the same WebSocket protocol for all OS types, + * we can reuse the macOS implementation. + */ +export class LinuxComputerInterface extends MacOSComputerInterface { + // Linux uses the same WebSocket interface as macOS for cloud provider +} diff --git a/libs/typescript/computer/src/interface/macos.ts b/libs/typescript/computer/src/interface/macos.ts new file mode 100644 index 00000000..3c41e03f --- /dev/null +++ b/libs/typescript/computer/src/interface/macos.ts @@ -0,0 +1,252 @@ +/** + * macOS computer interface implementation. + */ + +import type { ScreenSize } from '../types'; +import { BaseComputerInterface } from './base'; +import type { AccessibilityNode, CursorPosition, MouseButton } from './base'; + +export class MacOSComputerInterface extends BaseComputerInterface { + // Mouse Actions + async mouseDown( + x?: number, + y?: number, + button: MouseButton = 'left' + ): Promise { + await this.sendCommand('mouse_down', { x, y, button }); + } + + async mouseUp( + x?: number, + y?: number, + button: MouseButton = 'left' + ): Promise { + await this.sendCommand('mouse_up', { x, y, button }); + } + + async leftClick(x?: number, y?: number): Promise { + await this.sendCommand('left_click', { x, y }); + } + + async rightClick(x?: number, y?: number): Promise { + await this.sendCommand('right_click', { x, y }); + } + + async doubleClick(x?: number, y?: number): Promise { + await this.sendCommand('double_click', { x, y }); + } + + async moveCursor(x: number, y: number): Promise { + await this.sendCommand('move_cursor', { x, y }); + } + + async dragTo( + x: number, + y: number, + button: MouseButton = 'left', + duration = 0.5 + ): Promise { + await this.sendCommand('drag_to', { x, y, button, duration }); + } + + async drag( + path: Array<[number, number]>, + button: MouseButton = 'left', + duration = 0.5 + ): Promise { + await this.sendCommand('drag', { path, button, duration }); + } + + // Keyboard Actions + async keyDown(key: string): Promise { + await this.sendCommand('key_down', { key }); + } + + async keyUp(key: string): Promise { + await this.sendCommand('key_up', { key }); + } + + async typeText(text: string): Promise { + await this.sendCommand('type_text', { text }); + } + + async pressKey(key: string): Promise { + await this.sendCommand('press_key', { key }); + } + + async hotkey(...keys: string[]): Promise { + await this.sendCommand('hotkey', { keys }); + } + + // Scrolling Actions + async scroll(x: number, y: number): Promise { + await this.sendCommand('scroll', { x, y }); + } + + async scrollDown(clicks = 1): Promise { + await this.sendCommand('scroll_down', { clicks }); + } + + async scrollUp(clicks = 1): Promise { + await this.sendCommand('scroll_up', { clicks }); + } + + // Screen Actions + async screenshot(): Promise { + const response = await this.sendCommand('screenshot'); + if (!response.image_data) { + throw new Error('Failed to take screenshot'); + } + return Buffer.from(response.image_data as string, 'base64'); + } + + async getScreenSize(): Promise { + const response = await this.sendCommand('get_screen_size'); + if (!response.success || !response.size) { + throw new Error('Failed to get screen size'); + } + return response.size as ScreenSize; + } + + async getCursorPosition(): Promise { + const response = await this.sendCommand('get_cursor_position'); + if (!response.success || !response.position) { + throw new Error('Failed to get cursor position'); + } + return response.position as CursorPosition; + } + + // Clipboard Actions + async copyToClipboard(): Promise { + const response = await this.sendCommand('copy_to_clipboard'); + if (!response.success || !response.content) { + throw new Error('Failed to get clipboard content'); + } + return response.content as string; + } + + async setClipboard(text: string): Promise { + await this.sendCommand('set_clipboard', { text }); + } + + // File System Actions + async fileExists(path: string): Promise { + const response = await this.sendCommand('file_exists', { path }); + return (response.exists as boolean) || false; + } + + async directoryExists(path: string): Promise { + const response = await this.sendCommand('directory_exists', { path }); + return (response.exists as boolean) || false; + } + + async listDir(path: string): Promise { + const response = await this.sendCommand('list_dir', { path }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to list directory'); + } + return (response.files as string[]) || []; + } + + async readText(path: string): Promise { + const response = await this.sendCommand('read_text', { path }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to read file'); + } + return (response.content as string) || ''; + } + + async writeText(path: string, content: string): Promise { + const response = await this.sendCommand('write_text', { path, content }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to write file'); + } + } + + async readBytes(path: string): Promise { + const response = await this.sendCommand('read_bytes', { path }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to read file'); + } + return Buffer.from(response.content_b64 as string, 'base64'); + } + + async writeBytes(path: string, content: Buffer): Promise { + const response = await this.sendCommand('write_bytes', { + path, + content_b64: content.toString('base64'), + }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to write file'); + } + } + + async deleteFile(path: string): Promise { + const response = await this.sendCommand('delete_file', { path }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to delete file'); + } + } + + async createDir(path: string): Promise { + const response = await this.sendCommand('create_dir', { path }); + if (!response.success) { + throw new Error( + (response.error as string) || 'Failed to create directory' + ); + } + } + + async deleteDir(path: string): Promise { + const response = await this.sendCommand('delete_dir', { path }); + if (!response.success) { + throw new Error( + (response.error as string) || 'Failed to delete directory' + ); + } + } + + async runCommand(command: string): Promise<[string, string]> { + const response = await this.sendCommand('run_command', { command }); + if (!response.success) { + throw new Error((response.error as string) || 'Failed to run command'); + } + return [ + (response.stdout as string) || '', + (response.stderr as string) || '', + ]; + } + + // Accessibility Actions + async getAccessibilityTree(): Promise { + const response = await this.sendCommand('get_accessibility_tree'); + if (!response.success) { + throw new Error( + (response.error as string) || 'Failed to get accessibility tree' + ); + } + return response as unknown as AccessibilityNode; + } + + async toScreenCoordinates(x: number, y: number): Promise<[number, number]> { + const response = await this.sendCommand('to_screen_coordinates', { x, y }); + if (!response.success || !response.coordinates) { + throw new Error('Failed to convert to screen coordinates'); + } + return response.coordinates as [number, number]; + } + + async toScreenshotCoordinates( + x: number, + y: number + ): Promise<[number, number]> { + const response = await this.sendCommand('to_screenshot_coordinates', { + x, + y, + }); + if (!response.success || !response.coordinates) { + throw new Error('Failed to convert to screenshot coordinates'); + } + return response.coordinates as [number, number]; + } +} diff --git a/libs/typescript/computer/src/interface/windows.ts b/libs/typescript/computer/src/interface/windows.ts new file mode 100644 index 00000000..c9f138d1 --- /dev/null +++ b/libs/typescript/computer/src/interface/windows.ts @@ -0,0 +1,14 @@ +/** + * Windows computer interface implementation. + */ + +import { MacOSComputerInterface } from './macos'; + +/** + * Windows interface implementation. + * Since the cloud provider uses the same WebSocket protocol for all OS types, + * we can reuse the macOS implementation. + */ +export class WindowsComputerInterface extends MacOSComputerInterface { + // Windows uses the same WebSocket interface as macOS for cloud provider +} diff --git a/libs/typescript/computer/src/types.ts b/libs/typescript/computer/src/types.ts new file mode 100644 index 00000000..f8a175fe --- /dev/null +++ b/libs/typescript/computer/src/types.ts @@ -0,0 +1,10 @@ +export enum OSType { + MACOS = 'macos', + WINDOWS = 'windows', + LINUX = 'linux', +} + +export interface ScreenSize { + width: number; + height: number; +} diff --git a/libs/typescript/computer/tests/computer/cloud.test.ts b/libs/typescript/computer/tests/computer/cloud.test.ts new file mode 100644 index 00000000..9b927d57 --- /dev/null +++ b/libs/typescript/computer/tests/computer/cloud.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { Computer } from '../../src'; +import { OSType } from '../../src/types'; + +describe('Computer Cloud', () => { + it('Should create computer instance', () => { + const cloud = new Computer({ + apiKey: 'asdf', + name: 's-linux-1234', + osType: OSType.LINUX, + }); + expect(cloud).toBeInstanceOf(Computer); + }); +}); diff --git a/libs/typescript/computer/tests/interface/factory.test.ts b/libs/typescript/computer/tests/interface/factory.test.ts new file mode 100644 index 00000000..e5f3296a --- /dev/null +++ b/libs/typescript/computer/tests/interface/factory.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest'; +import { InterfaceFactory } from '../../src/interface/factory.ts'; +import { LinuxComputerInterface } from '../../src/interface/linux.ts'; +import { MacOSComputerInterface } from '../../src/interface/macos.ts'; +import { WindowsComputerInterface } from '../../src/interface/windows.ts'; +import { OSType } from '../../src/types.ts'; + +describe('InterfaceFactory', () => { + const testParams = { + ipAddress: '192.168.1.100', + username: 'testuser', + password: 'testpass', + apiKey: 'test-api-key', + vmName: 'test-vm', + }; + + describe('createInterfaceForOS', () => { + it('should create MacOSComputerInterface for macOS', () => { + const interface_ = InterfaceFactory.createInterfaceForOS( + OSType.MACOS, + testParams.ipAddress, + testParams.apiKey, + testParams.vmName + ); + + expect(interface_).toBeInstanceOf(MacOSComputerInterface); + }); + + it('should create LinuxComputerInterface for Linux', () => { + const interface_ = InterfaceFactory.createInterfaceForOS( + OSType.LINUX, + testParams.ipAddress, + testParams.apiKey, + testParams.vmName + ); + + expect(interface_).toBeInstanceOf(LinuxComputerInterface); + }); + + it('should create WindowsComputerInterface for Windows', () => { + const interface_ = InterfaceFactory.createInterfaceForOS( + OSType.WINDOWS, + testParams.ipAddress, + testParams.apiKey, + testParams.vmName + ); + + expect(interface_).toBeInstanceOf(WindowsComputerInterface); + }); + + it('should throw error for unsupported OS type', () => { + expect(() => { + InterfaceFactory.createInterfaceForOS( + 'unsupported' as OSType, + testParams.ipAddress, + testParams.apiKey, + testParams.vmName + ); + }).toThrow('Unsupported OS type: unsupported'); + }); + + it('should create interface without API key and VM name', () => { + const interface_ = InterfaceFactory.createInterfaceForOS( + OSType.MACOS, + testParams.ipAddress + ); + + expect(interface_).toBeInstanceOf(MacOSComputerInterface); + }); + }); +}); diff --git a/libs/typescript/computer/tests/interface/index.test.ts b/libs/typescript/computer/tests/interface/index.test.ts new file mode 100644 index 00000000..1a0cb8ef --- /dev/null +++ b/libs/typescript/computer/tests/interface/index.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import * as InterfaceExports from '../../src/interface/index.ts'; + +describe('Interface Module Exports', () => { + it('should export InterfaceFactory', () => { + expect(InterfaceExports.InterfaceFactory).toBeDefined(); + expect( + InterfaceExports.InterfaceFactory.createInterfaceForOS + ).toBeDefined(); + }); + + it('should export BaseComputerInterface', () => { + expect(InterfaceExports.BaseComputerInterface).toBeDefined(); + }); + + it('should export MacOSComputerInterface', () => { + expect(InterfaceExports.MacOSComputerInterface).toBeDefined(); + }); + + it('should export LinuxComputerInterface', () => { + expect(InterfaceExports.LinuxComputerInterface).toBeDefined(); + }); + + it('should export WindowsComputerInterface', () => { + expect(InterfaceExports.WindowsComputerInterface).toBeDefined(); + }); + + it('should export all expected interfaces', () => { + const expectedExports = [ + 'InterfaceFactory', + 'BaseComputerInterface', + 'MacOSComputerInterface', + 'LinuxComputerInterface', + 'WindowsComputerInterface', + ]; + + const actualExports = Object.keys(InterfaceExports); + for (const exportName of expectedExports) { + expect(actualExports).toContain(exportName); + } + }); +}); diff --git a/libs/typescript/computer/tests/interface/linux.test.ts b/libs/typescript/computer/tests/interface/linux.test.ts new file mode 100644 index 00000000..ae086fcf --- /dev/null +++ b/libs/typescript/computer/tests/interface/linux.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { LinuxComputerInterface } from '../../src/interface/linux.ts'; +import { MacOSComputerInterface } from '../../src/interface/macos.ts'; + +describe('LinuxComputerInterface', () => { + const testParams = { + ipAddress: 'test.cua.com', // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable + username: 'testuser', + password: 'testpass', + apiKey: 'test-api-key', + vmName: 'test-vm', + }; + + describe('Inheritance', () => { + it('should extend MacOSComputerInterface', () => { + const linuxInterface = new LinuxComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + testParams.apiKey, + testParams.vmName + ); + + expect(linuxInterface).toBeInstanceOf(MacOSComputerInterface); + expect(linuxInterface).toBeInstanceOf(LinuxComputerInterface); + }); + }); +}); diff --git a/libs/typescript/computer/tests/interface/macos.test.ts b/libs/typescript/computer/tests/interface/macos.test.ts new file mode 100644 index 00000000..e655254f --- /dev/null +++ b/libs/typescript/computer/tests/interface/macos.test.ts @@ -0,0 +1,938 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { WebSocket, WebSocketServer } from 'ws'; +import { MacOSComputerInterface } from '../../src/interface/macos.ts'; + +describe('MacOSComputerInterface', () => { + // Define test parameters + const testParams = { + ipAddress: 'localhost', + username: 'testuser', + password: 'testpass', + // apiKey: "test-api-key", No API Key for local testing + vmName: 'test-vm', + }; + + // WebSocket server mock + let wss: WebSocketServer; + let serverPort: number; + let connectedClients: WebSocket[] = []; + + // Track received messages for verification + interface ReceivedMessage { + action: string; + [key: string]: unknown; + } + let receivedMessages: ReceivedMessage[] = []; + + // Set up WebSocket server before all tests + beforeEach(async () => { + receivedMessages = []; + connectedClients = []; + + // Create WebSocket server on a random available port + wss = new WebSocketServer({ port: 0 }); + serverPort = (wss.address() as { port: number }).port; + + // Update test params with the actual server address + testParams.ipAddress = `localhost:${serverPort}`; + + // Handle WebSocket connections + wss.on('connection', (ws) => { + connectedClients.push(ws); + + // Handle incoming messages + ws.on('message', (data) => { + try { + const message = JSON.parse(data.toString()); + receivedMessages.push(message); + + // Send appropriate responses based on action + switch (message.command) { + case 'screenshot': + ws.send( + JSON.stringify({ + image_data: Buffer.from('fake-screenshot-data').toString( + 'base64' + ), + success: true, + }) + ); + break; + case 'get_screen_size': + ws.send( + JSON.stringify({ + size: { width: 1920, height: 1080 }, + success: true, + }) + ); + break; + case 'get_cursor_position': + ws.send( + JSON.stringify({ + position: { x: 100, y: 200 }, + success: true, + }) + ); + break; + case 'copy_to_clipboard': + ws.send( + JSON.stringify({ + content: 'clipboard content', + success: true, + }) + ); + break; + case 'file_exists': + ws.send( + JSON.stringify({ + exists: true, + success: true, + }) + ); + break; + case 'directory_exists': + ws.send( + JSON.stringify({ + exists: true, + success: true, + }) + ); + break; + case 'list_dir': + ws.send( + JSON.stringify({ + files: ['file1.txt', 'file2.txt'], + success: true, + }) + ); + break; + case 'read_text': + ws.send( + JSON.stringify({ + content: 'file content', + success: true, + }) + ); + break; + case 'read_bytes': + ws.send( + JSON.stringify({ + content_b64: Buffer.from('binary content').toString('base64'), + success: true, + }) + ); + break; + case 'run_command': + ws.send( + JSON.stringify({ + stdout: 'command output', + stderr: '', + success: true, + }) + ); + break; + case 'get_accessibility_tree': + ws.send( + JSON.stringify({ + role: 'window', + title: 'Test Window', + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + children: [], + success: true, + }) + ); + break; + case 'to_screen_coordinates': + case 'to_screenshot_coordinates': + ws.send( + JSON.stringify({ + coordinates: [message.params?.x || 0, message.params?.y || 0], + success: true, + }) + ); + break; + default: + // For all other actions, just send success + ws.send(JSON.stringify({ success: true })); + break; + } + } catch (error) { + ws.send(JSON.stringify({ error: (error as Error).message })); + } + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); + }); + }); + + // Clean up WebSocket server after each test + afterEach(async () => { + // Close all connected clients + for (const client of connectedClients) { + if (client.readyState === WebSocket.OPEN) { + client.close(); + } + } + + // Close the server + await new Promise((resolve) => { + wss.close(() => resolve()); + }); + }); + + describe('Connection Management', () => { + it('should connect with proper authentication headers', async () => { + const macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + + await macosInterface.connect(); + + // Verify the interface is connected + expect(macosInterface.isConnected()).toBe(true); + expect(connectedClients.length).toBe(1); + + await macosInterface.disconnect(); + }); + + it('should handle connection without API key', async () => { + // Create a separate server that doesn't check auth + const noAuthWss = new WebSocketServer({ port: 0 }); + const noAuthPort = (noAuthWss.address() as { port: number }).port; + + noAuthWss.on('connection', (ws) => { + ws.on('message', () => { + ws.send(JSON.stringify({ success: true })); + }); + }); + + const macosInterface = new MacOSComputerInterface( + `localhost:${noAuthPort}`, + testParams.username, + testParams.password, + undefined, + undefined + ); + + await macosInterface.connect(); + expect(macosInterface.isConnected()).toBe(true); + + await macosInterface.disconnect(); + await new Promise((resolve) => { + noAuthWss.close(() => resolve()); + }); + }); + }); + + describe('Mouse Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should send mouse_down command', async () => { + await macosInterface.mouseDown(100, 200, 'left'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'mouse_down', + params: { + x: 100, + y: 200, + button: 'left', + }, + }); + }); + + it('should send mouse_up command', async () => { + await macosInterface.mouseUp(100, 200, 'right'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'mouse_up', + params: { + x: 100, + y: 200, + button: 'right', + }, + }); + }); + + it('should send left_click command', async () => { + await macosInterface.leftClick(150, 250); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'left_click', + params: { + x: 150, + y: 250, + }, + }); + }); + + it('should send right_click command', async () => { + await macosInterface.rightClick(200, 300); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'right_click', + params: { + x: 200, + y: 300, + }, + }); + }); + + it('should send double_click command', async () => { + await macosInterface.doubleClick(250, 350); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'double_click', + params: { + x: 250, + y: 350, + }, + }); + }); + + it('should send move_cursor command', async () => { + await macosInterface.moveCursor(300, 400); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'move_cursor', + params: { + x: 300, + y: 400, + }, + }); + }); + + it('should send drag_to command', async () => { + await macosInterface.dragTo(400, 500, 'left', 1.5); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'drag_to', + params: { + x: 400, + y: 500, + button: 'left', + duration: 1.5, + }, + }); + }); + + it('should send drag command with path', async () => { + const path: Array<[number, number]> = [ + [100, 100], + [200, 200], + [300, 300], + ]; + await macosInterface.drag(path, 'middle', 2.0); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'drag', + params: { + path: path, + button: 'middle', + duration: 2.0, + }, + }); + }); + }); + + describe('Keyboard Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should send key_down command', async () => { + await macosInterface.keyDown('a'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'key_down', + params: { + key: 'a', + }, + }); + }); + + it('should send key_up command', async () => { + await macosInterface.keyUp('b'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'key_up', + params: { + key: 'b', + }, + }); + }); + + it('should send type_text command', async () => { + await macosInterface.typeText('Hello, World!'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'type_text', + params: { + text: 'Hello, World!', + }, + }); + }); + + it('should send press_key command', async () => { + await macosInterface.pressKey('enter'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'press_key', + params: { + key: 'enter', + }, + }); + }); + + it('should send hotkey command', async () => { + await macosInterface.hotkey('cmd', 'c'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'hotkey', + params: { + keys: ['cmd', 'c'], + }, + }); + }); + }); + + describe('Scrolling Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should send scroll command', async () => { + await macosInterface.scroll(10, -5); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'scroll', + params: { + x: 10, + y: -5, + }, + }); + }); + + it('should send scroll_down command', async () => { + await macosInterface.scrollDown(3); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'scroll_down', + params: { + clicks: 3, + }, + }); + }); + + it('should send scroll_up command', async () => { + await macosInterface.scrollUp(2); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'scroll_up', + params: { + clicks: 2, + }, + }); + }); + }); + + describe('Screen Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should get screenshot', async () => { + const screenshot = await macosInterface.screenshot(); + + expect(screenshot).toBeInstanceOf(Buffer); + expect(screenshot.toString()).toBe('fake-screenshot-data'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'screenshot', + params: {}, + }); + }); + + it('should get screen size', async () => { + const size = await macosInterface.getScreenSize(); + + expect(size).toEqual({ width: 1920, height: 1080 }); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'get_screen_size', + params: {}, + }); + }); + + it('should get cursor position', async () => { + const position = await macosInterface.getCursorPosition(); + + expect(position).toEqual({ x: 100, y: 200 }); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'get_cursor_position', + params: {}, + }); + }); + }); + + describe('Clipboard Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should copy to clipboard', async () => { + const text = await macosInterface.copyToClipboard(); + + expect(text).toBe('clipboard content'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'copy_to_clipboard', + params: {}, + }); + }); + + it('should set clipboard', async () => { + await macosInterface.setClipboard('new clipboard text'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'set_clipboard', + params: { + text: 'new clipboard text', + }, + }); + }); + }); + + describe('File System Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should check file exists', async () => { + const exists = await macosInterface.fileExists('/path/to/file'); + + expect(exists).toBe(true); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'file_exists', + params: { + path: '/path/to/file', + }, + }); + }); + + it('should check directory exists', async () => { + const exists = await macosInterface.directoryExists('/path/to/dir'); + + expect(exists).toBe(true); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'directory_exists', + params: { + path: '/path/to/dir', + }, + }); + }); + + it('should list directory', async () => { + const files = await macosInterface.listDir('/path/to/dir'); + + expect(files).toEqual(['file1.txt', 'file2.txt']); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'list_dir', + params: { + path: '/path/to/dir', + }, + }); + }); + + it('should read text file', async () => { + const content = await macosInterface.readText('/path/to/file.txt'); + + expect(content).toBe('file content'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'read_text', + params: { + path: '/path/to/file.txt', + }, + }); + }); + + it('should write text file', async () => { + await macosInterface.writeText('/path/to/file.txt', 'new content'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'write_text', + params: { + path: '/path/to/file.txt', + content: 'new content', + }, + }); + }); + + it('should read binary file', async () => { + const content = await macosInterface.readBytes('/path/to/file.bin'); + + expect(content).toBeInstanceOf(Buffer); + expect(content.toString()).toBe('binary content'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'read_bytes', + params: { + path: '/path/to/file.bin', + }, + }); + }); + + it('should write binary file', async () => { + const buffer = Buffer.from('binary data'); + await macosInterface.writeBytes('/path/to/file.bin', buffer); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'write_bytes', + params: { + path: '/path/to/file.bin', + content_b64: buffer.toString('base64'), + }, + }); + }); + + it('should delete file', async () => { + await macosInterface.deleteFile('/path/to/file'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'delete_file', + params: { + path: '/path/to/file', + }, + }); + }); + + it('should create directory', async () => { + await macosInterface.createDir('/path/to/new/dir'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'create_dir', + params: { + path: '/path/to/new/dir', + }, + }); + }); + + it('should delete directory', async () => { + await macosInterface.deleteDir('/path/to/dir'); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'delete_dir', + params: { + path: '/path/to/dir', + }, + }); + }); + + it('should run command', async () => { + const [stdout, stderr] = await macosInterface.runCommand('ls -la'); + + expect(stdout).toBe('command output'); + expect(stderr).toBe(''); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'run_command', + params: { + command: 'ls -la', + }, + }); + }); + }); + + describe('Accessibility Actions', () => { + let macosInterface: MacOSComputerInterface; + + beforeEach(async () => { + macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + await macosInterface.connect(); + }); + + afterEach(async () => { + if (macosInterface) { + await macosInterface.disconnect(); + } + }); + + it('should get accessibility tree', async () => { + const tree = await macosInterface.getAccessibilityTree(); + + expect(tree).toEqual({ + role: 'window', + title: 'Test Window', + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + children: [], + success: true, + }); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'get_accessibility_tree', + params: {}, + }); + }); + + it('should convert to screen coordinates', async () => { + const [x, y] = await macosInterface.toScreenCoordinates(100, 200); + + expect(x).toBe(100); + expect(y).toBe(200); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'to_screen_coordinates', + params: { + x: 100, + y: 200, + }, + }); + }); + + it('should convert to screenshot coordinates', async () => { + const [x, y] = await macosInterface.toScreenshotCoordinates(300, 400); + + expect(x).toBe(300); + expect(y).toBe(400); + + const lastMessage = receivedMessages[receivedMessages.length - 1]; + expect(lastMessage).toEqual({ + command: 'to_screenshot_coordinates', + params: { + x: 300, + y: 400, + }, + }); + }); + }); + + describe('Error Handling', () => { + it('should handle WebSocket connection errors', async () => { + // Use a valid but unreachable IP to avoid DNS errors + const macosInterface = new MacOSComputerInterface( + 'localhost:9999', + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + + // Connection should fail + await expect(macosInterface.connect()).rejects.toThrow(); + }); + + it('should handle command errors', async () => { + // Create a server that returns errors + const errorWss = new WebSocketServer({ port: 0 }); + const errorPort = (errorWss.address() as { port: number }).port; + + errorWss.on('connection', (ws) => { + ws.on('message', () => { + ws.send(JSON.stringify({ error: 'Command failed', success: false })); + }); + }); + + const macosInterface = new MacOSComputerInterface( + `localhost:${errorPort}`, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + + await macosInterface.connect(); + + // Command should throw error + await expect(macosInterface.leftClick(100, 100)).rejects.toThrow( + 'Command failed' + ); + + await macosInterface.disconnect(); + await new Promise((resolve) => { + errorWss.close(() => resolve()); + }); + }); + + it('should handle disconnection gracefully', async () => { + const macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + + await macosInterface.connect(); + expect(macosInterface.isConnected()).toBe(true); + + // Disconnect + macosInterface.disconnect(); + expect(macosInterface.isConnected()).toBe(false); + + // Should reconnect automatically on next command + await macosInterface.leftClick(100, 100); + expect(macosInterface.isConnected()).toBe(true); + + await macosInterface.disconnect(); + }); + + it('should handle force close', async () => { + const macosInterface = new MacOSComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + undefined, + testParams.vmName + ); + + await macosInterface.connect(); + expect(macosInterface.isConnected()).toBe(true); + + // Force close + macosInterface.forceClose(); + expect(macosInterface.isConnected()).toBe(false); + }); + }); +}); diff --git a/libs/typescript/computer/tests/interface/windows.test.ts b/libs/typescript/computer/tests/interface/windows.test.ts new file mode 100644 index 00000000..68a7490d --- /dev/null +++ b/libs/typescript/computer/tests/interface/windows.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { MacOSComputerInterface } from '../../src/interface/macos.ts'; +import { WindowsComputerInterface } from '../../src/interface/windows.ts'; + +describe('WindowsComputerInterface', () => { + const testParams = { + ipAddress: '192.0.2.1', // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable + username: 'testuser', + password: 'testpass', + apiKey: 'test-api-key', + vmName: 'test-vm', + }; + + describe('Inheritance', () => { + it('should extend MacOSComputerInterface', () => { + const windowsInterface = new WindowsComputerInterface( + testParams.ipAddress, + testParams.username, + testParams.password, + testParams.apiKey, + testParams.vmName + ); + + expect(windowsInterface).toBeInstanceOf(MacOSComputerInterface); + expect(windowsInterface).toBeInstanceOf(WindowsComputerInterface); + }); + }); +}); diff --git a/libs/typescript/computer/tests/setup.ts b/libs/typescript/computer/tests/setup.ts new file mode 100644 index 00000000..9425b20f --- /dev/null +++ b/libs/typescript/computer/tests/setup.ts @@ -0,0 +1,7 @@ +import { afterAll, afterEach, beforeAll } from 'vitest'; + +beforeAll(() => {}); + +afterAll(() => {}); + +afterEach(() => {}); diff --git a/libs/typescript/computer/tsconfig.json b/libs/typescript/computer/tsconfig.json new file mode 100644 index 00000000..cdcd74de --- /dev/null +++ b/libs/typescript/computer/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "preserve", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "types": ["node"], + "allowSyntheticDefaultImports": true, + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/libs/typescript/computer/tsdown.config.ts b/libs/typescript/computer/tsdown.config.ts new file mode 100644 index 00000000..b3c70ea9 --- /dev/null +++ b/libs/typescript/computer/tsdown.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig([ + { + entry: ['./src/index.ts'], + platform: 'node', + dts: true, + external: ['child_process', 'util'], + }, +]); diff --git a/libs/typescript/computer/vitest.config.ts b/libs/typescript/computer/vitest.config.ts new file mode 100644 index 00000000..53ab43d4 --- /dev/null +++ b/libs/typescript/computer/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + setupFiles: ['./tests/setup.ts'], + environment: 'node', + globals: true, + }, +}); diff --git a/libs/typescript/core/.editorconfig b/libs/typescript/core/.editorconfig new file mode 100644 index 00000000..7095e7fb --- /dev/null +++ b/libs/typescript/core/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +indent_size = 2 +end_of_line = lf +insert_final_newline = true diff --git a/libs/typescript/core/.gitattributes b/libs/typescript/core/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/libs/typescript/core/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/libs/typescript/core/.gitignore b/libs/typescript/core/.gitignore new file mode 100644 index 00000000..e79f2036 --- /dev/null +++ b/libs/typescript/core/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist + +*.log +.DS_Store +.eslintcache diff --git a/libs/typescript/core/LICENSE b/libs/typescript/core/LICENSE new file mode 100644 index 00000000..74987166 --- /dev/null +++ b/libs/typescript/core/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2025 C/ua + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/typescript/core/README.md b/libs/typescript/core/README.md new file mode 100644 index 00000000..a6b568ff --- /dev/null +++ b/libs/typescript/core/README.md @@ -0,0 +1,27 @@ +# tsdown-starter + +A starter for creating a tsdown package. + +## Development + +- Install dependencies: + +```bash +pnpm install +``` + +- Run the unit tests: + +```bash +pnpm test +``` + +- Build the library: + +```bash +pnpm build +``` + +## License + +[MIT](./LICENSE) License 2025 [C/UA](https://github.com/trycua) diff --git a/libs/typescript/core/package.json b/libs/typescript/core/package.json new file mode 100644 index 00000000..2a03920c --- /dev/null +++ b/libs/typescript/core/package.json @@ -0,0 +1,57 @@ +{ + "name": "@trycua/core", + "version": "0.0.1", + "packageManager": "pnpm@10.11.0", + "description": "Typescript SDK for c/ua core", + "type": "module", + "license": "MIT", + "homepage": "https://github.com/trycua/cua/tree/feature/computer/typescript/libs/typescript/computer", + "bugs": { + "url": "https://github.com/trycua/cua/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/trycua/cua.git" + }, + "author": "c/ua", + "files": [ + "dist" + ], + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "lint": "biome lint .", + "lint:fix": "biome lint --fix .", + "build": "tsdown", + "dev": "tsdown --watch", + "test": "vitest", + "typecheck": "tsc --noEmit", + "release": "bumpp && pnpm publish", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@types/uuid": "^10.0.0", + "pino": "^9.7.0", + "posthog-node": "^5.1.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^22.15.17", + "@types/ws": "^8.18.1", + "bumpp": "^10.1.0", + "happy-dom": "^17.4.7", + "tsdown": "^0.11.9", + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "vitest": "^3.1.3" + } +} \ No newline at end of file diff --git a/libs/typescript/core/src/index.ts b/libs/typescript/core/src/index.ts new file mode 100644 index 00000000..d986c9d6 --- /dev/null +++ b/libs/typescript/core/src/index.ts @@ -0,0 +1,7 @@ +/** + * This module provides the core telemetry functionality for CUA libraries. + * + * It provides a low-overhead way to collect anonymous usage data. + */ + +export * from './telemetry'; diff --git a/libs/typescript/core/src/telemetry/clients/index.ts b/libs/typescript/core/src/telemetry/clients/index.ts new file mode 100644 index 00000000..450fcfbb --- /dev/null +++ b/libs/typescript/core/src/telemetry/clients/index.ts @@ -0,0 +1 @@ +export * from './posthog'; diff --git a/libs/typescript/core/src/telemetry/clients/posthog.ts b/libs/typescript/core/src/telemetry/clients/posthog.ts new file mode 100644 index 00000000..e42bc449 --- /dev/null +++ b/libs/typescript/core/src/telemetry/clients/posthog.ts @@ -0,0 +1,312 @@ +/** + * Telemetry client using PostHog for collecting anonymous usage data. + */ + +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { pino } from 'pino'; +import { PostHog } from 'posthog-node'; +import { v4 as uuidv4 } from 'uuid'; + +// Controls how frequently telemetry will be sent (percentage) +export const TELEMETRY_SAMPLE_RATE = 100; // 100% sampling rate + +// Public PostHog config for anonymous telemetry +// These values are intentionally public and meant for anonymous telemetry only +// https://posthog.com/docs/product-analytics/troubleshooting#is-it-ok-for-my-api-key-to-be-exposed-and-public +export const PUBLIC_POSTHOG_API_KEY = + 'phc_eSkLnbLxsnYFaXksif1ksbrNzYlJShr35miFLDppF14'; +export const PUBLIC_POSTHOG_HOST = 'https://eu.i.posthog.com'; + +export class PostHogTelemetryClient { + private config: { + enabled: boolean; + sampleRate: number; + posthog: { apiKey: string; host: string }; + }; + private installationId: string; + private initialized = false; + private queuedEvents: { + name: string; + properties: Record; + timestamp: number; + }[] = []; + private startTime: number; // seconds + private posthogClient?: PostHog; + private counters: Record = {}; + + private logger = pino({ name: 'core.telemetry' }); + + constructor() { + // set up config + this.config = { + enabled: true, + sampleRate: TELEMETRY_SAMPLE_RATE, + posthog: { apiKey: PUBLIC_POSTHOG_API_KEY, host: PUBLIC_POSTHOG_HOST }, + }; + // Check for multiple environment variables that can disable telemetry: + // CUA_TELEMETRY=off to disable telemetry (legacy way) + // CUA_TELEMETRY_DISABLED=1 to disable telemetry (new, more explicit way) + const telemetryDisabled = + process.env.CUA_TELEMETRY?.toLowerCase() === 'off' || + ['1', 'true', 'yes', 'on'].includes( + process.env.CUA_TELEMETRY_DISABLED?.toLowerCase() || '' + ); + + this.config.enabled = !telemetryDisabled; + this.config.sampleRate = Number.parseFloat( + process.env.CUA_TELEMETRY_SAMPLE_RATE || String(TELEMETRY_SAMPLE_RATE) + ); + // init client + this.installationId = this._getOrCreateInstallationId(); + this.startTime = Date.now() / 1000; // Convert to seconds + + // Log telemetry status on startup + if (this.config.enabled) { + this.logger.info( + `Telemetry enabled (sampling at ${this.config.sampleRate}%)` + ); + // Initialize PostHog client if config is available + this._initializePosthog(); + } else { + this.logger.info('Telemetry disabled'); + } + } + + /** + * Get or create a random installation ID. + * This ID is not tied to any personal information. + */ + private _getOrCreateInstallationId(): string { + const homeDir = os.homedir(); + const idFile = path.join(homeDir, '.cua', 'installation_id'); + + try { + if (fs.existsSync(idFile)) { + return fs.readFileSync(idFile, 'utf-8').trim(); + } + } catch (error) { + this.logger.debug(`Failed to read installation ID: ${error}`); + } + + // Create new ID if not exists + const newId = uuidv4(); + try { + const dir = path.dirname(idFile); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(idFile, newId); + return newId; + } catch (error) { + this.logger.debug(`Failed to write installation ID: ${error}`); + } + + // Fallback to in-memory ID if file operations fail + return newId; + } + + /** + * Initialize the PostHog client with configuration. + */ + private _initializePosthog(): boolean { + if (this.initialized) { + return true; + } + + try { + this.posthogClient = new PostHog(this.config.posthog.apiKey, { + host: this.config.posthog.host, + flushAt: 20, // Number of events to batch before sending + flushInterval: 30000, // Send events every 30 seconds + }); + this.initialized = true; + this.logger.debug('PostHog client initialized successfully'); + + // Process any queued events + this._processQueuedEvents(); + return true; + } catch (error) { + this.logger.error(`Failed to initialize PostHog client: ${error}`); + return false; + } + } + + /** + * Process any events that were queued before initialization. + */ + private _processQueuedEvents(): void { + if (!this.posthogClient || this.queuedEvents.length === 0) { + return; + } + + for (const event of this.queuedEvents) { + this._captureEvent(event.name, event.properties); + } + this.queuedEvents = []; + } + + /** + * Capture an event with PostHog. + */ + private _captureEvent( + eventName: string, + properties?: Record + ): void { + if (!this.posthogClient) { + return; + } + + try { + // Add standard properties + const eventProperties = { + ...properties, + version: process.env.npm_package_version || 'unknown', + platform: process.platform, + node_version: process.version, + is_ci: this._isCI, + }; + + this.posthogClient.capture({ + distinctId: this.installationId, + event: eventName, + properties: eventProperties, + }); + } catch (error) { + this.logger.debug(`Failed to capture event: ${error}`); + } + } + + private get _isCI(): boolean { + /** + * Detect if running in CI environment. + */ + return !!( + process.env.CI || + process.env.CONTINUOUS_INTEGRATION || + process.env.GITHUB_ACTIONS || + process.env.GITLAB_CI || + process.env.CIRCLECI || + process.env.TRAVIS || + process.env.JENKINS_URL + ); + } + + increment(counterName: string, value = 1) { + /** + * Increment a named counter. + */ + if (!this.config.enabled) { + return; + } + + if (!(counterName in this.counters)) { + this.counters[counterName] = 0; + } + this.counters[counterName] += value; + } + + recordEvent(eventName: string, properties?: Record): void { + /** + * Record an event with optional properties. + */ + if (!this.config.enabled) { + return; + } + + // Increment counter for this event type + const counterKey = `event:${eventName}`; + this.increment(counterKey); + + // Apply sampling + if (Math.random() * 100 > this.config.sampleRate) { + return; + } + + const event = { + name: eventName, + properties: properties || {}, + timestamp: Date.now() / 1000, + }; + + if (this.initialized && this.posthogClient) { + this._captureEvent(eventName, properties); + } else { + // Queue event if not initialized + this.queuedEvents.push(event); + // Try to initialize again + if (this.config.enabled && !this.initialized) { + this._initializePosthog(); + } + } + } + + /** + * Flush any pending events to PostHog. + */ + async flush(): Promise { + if (!this.config.enabled || !this.posthogClient) { + return false; + } + + try { + // Send counter data as a single event + if (Object.keys(this.counters).length > 0) { + this._captureEvent('telemetry_counters', { + counters: { ...this.counters }, + duration: Date.now() / 1000 - this.startTime, + }); + } + + await this.posthogClient.flush(); + this.logger.debug('Telemetry flushed successfully'); + + // Clear counters after sending + this.counters = {}; + return true; + } catch (error) { + this.logger.debug(`Failed to flush telemetry: ${error}`); + return false; + } + } + + enable(): void { + /** + * Enable telemetry collection. + */ + this.config.enabled = true; + this.logger.info('Telemetry enabled'); + if (!this.initialized) { + this._initializePosthog(); + } + } + + async disable(): Promise { + /** + * Disable telemetry collection. + */ + this.config.enabled = false; + await this.posthogClient?.disable(); + this.logger.info('Telemetry disabled'); + } + + get enabled(): boolean { + /** + * Check if telemetry is enabled. + */ + return this.config.enabled; + } + + async shutdown(): Promise { + /** + * Shutdown the telemetry client and flush any pending events. + */ + if (this.posthogClient) { + await this.flush(); + await this.posthogClient.shutdown(); + this.initialized = false; + this.posthogClient = undefined; + } + } +} diff --git a/libs/typescript/core/src/telemetry/index.ts b/libs/typescript/core/src/telemetry/index.ts new file mode 100644 index 00000000..44dd951a --- /dev/null +++ b/libs/typescript/core/src/telemetry/index.ts @@ -0,0 +1,7 @@ +/** + * This module provides the core telemetry functionality for CUA libraries. + * + * It provides a low-overhead way to collect anonymous usage data. + */ + +export { PostHogTelemetryClient as Telemetry } from './clients'; diff --git a/libs/typescript/core/tests/telemetry.test.ts b/libs/typescript/core/tests/telemetry.test.ts new file mode 100644 index 00000000..4c4d47f6 --- /dev/null +++ b/libs/typescript/core/tests/telemetry.test.ts @@ -0,0 +1,30 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { Telemetry } from '../src/'; + +describe('Telemetry', () => { + let telemetry: Telemetry; + beforeEach(() => { + process.env.CUA_TELEMETRY = ''; + process.env.CUA_TELEMETRY_DISABLED = ''; + telemetry = new Telemetry(); + }); + describe('telemetry.enabled', () => { + it('should return false when CUA_TELEMETRY is off', () => { + process.env.CUA_TELEMETRY = 'off'; + telemetry = new Telemetry(); + expect(telemetry.enabled).toBe(false); + }); + + it('should return true when CUA_TELEMETRY is not set', () => { + process.env.CUA_TELEMETRY = ''; + telemetry = new Telemetry(); + expect(telemetry.enabled).toBe(true); + }); + + it('should return false if CUA_TELEMETRY_DISABLED is 1', () => { + process.env.CUA_TELEMETRY_DISABLED = '1'; + telemetry = new Telemetry(); + expect(telemetry.enabled).toBe(false); + }); + }); +}); diff --git a/libs/typescript/core/tsconfig.json b/libs/typescript/core/tsconfig.json new file mode 100644 index 00000000..77b838a8 --- /dev/null +++ b/libs/typescript/core/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/libs/typescript/core/tsdown.config.ts b/libs/typescript/core/tsdown.config.ts new file mode 100644 index 00000000..21d62816 --- /dev/null +++ b/libs/typescript/core/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig([ + { + entry: ['./src/index.ts'], + platform: 'neutral', + dts: true, + }, +]); diff --git a/libs/typescript/core/vitest.config.ts b/libs/typescript/core/vitest.config.ts new file mode 100644 index 00000000..94ede10e --- /dev/null +++ b/libs/typescript/core/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}); diff --git a/libs/typescript/package.json b/libs/typescript/package.json new file mode 100644 index 00000000..0e0d6ce1 --- /dev/null +++ b/libs/typescript/package.json @@ -0,0 +1,29 @@ +{ + "name": "cua-ts", + "version": "1.0.0", + "description": "The c/ua typescript libs.", + "keywords": [], + "author": "c/ua", + "license": "MIT", + "scripts": { + "lint": "biome check", + "lint:fix": "biome check --fix", + "build:core": "pnpm --filter @trycua/core build", + "build:computer": "pnpm --filter @trycua/computer build", + "build": "pnpm build:core && pnpm build:computer", + "test:core": "pnpm --filter @trycua/core test", + "test:computer": "pnpm --filter @trycua/computer test", + "test": "pnpm -r test", + "typecheck": "pnpm -r typecheck" + }, + "packageManager": "pnpm@10.12.3", + "devDependencies": { + "@biomejs/biome": "^1.9.4" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@biomejs/biome", + "esbuild" + ] + } +} \ No newline at end of file diff --git a/libs/typescript/pnpm-lock.yaml b/libs/typescript/pnpm-lock.yaml new file mode 100644 index 00000000..0050b9b6 --- /dev/null +++ b/libs/typescript/pnpm-lock.yaml @@ -0,0 +1,1900 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + + computer: + dependencies: + '@trycua/core': + specifier: link:../core + version: link:../core + pino: + specifier: ^9.7.0 + version: 9.7.0 + ws: + specifier: ^8.18.0 + version: 8.18.3 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@types/node': + specifier: ^22.15.17 + version: 22.15.34 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + bumpp: + specifier: ^10.1.0 + version: 10.2.0 + happy-dom: + specifier: ^17.4.7 + version: 17.6.3 + tsdown: + specifier: ^0.11.9 + version: 0.11.13(typescript@5.8.3) + tsx: + specifier: ^4.19.4 + version: 4.20.3 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.1.3 + version: 3.2.4(@types/node@22.15.34)(happy-dom@17.6.3)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0) + + core: + dependencies: + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + pino: + specifier: ^9.7.0 + version: 9.7.0 + posthog-node: + specifier: ^5.1.1 + version: 5.1.1 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@types/node': + specifier: ^22.15.17 + version: 22.15.34 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + bumpp: + specifier: ^10.1.0 + version: 10.2.0 + happy-dom: + specifier: ^17.4.7 + version: 17.6.3 + tsdown: + specifier: ^0.11.9 + version: 0.11.13(typescript@5.8.3) + tsx: + specifier: ^4.19.4 + version: 4.20.3 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^3.1.3 + version: 3.2.4(@types/node@22.15.34)(happy-dom@17.6.3)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0) + +packages: + + '@babel/generator@7.27.5': + resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.7': + resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.27.7': + resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.10': + resolution: {integrity: sha512-HM2F4B9N4cA0RH2KQiIZOHAZqtP4xGS4IZ+SFe1SIbO4dyjf9MTY2Bo3vHYnm0hglWfXqBrzUBSa+cJfl3Xvrg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.2': + resolution: {integrity: sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg==} + + '@jridgewell/trace-mapping@0.3.27': + resolution: {integrity: sha512-VO95AxtSFMelbg3ouljAYnfvTEwSWVt/2YLf+U5Ejd8iT5mXE2Sa/1LGyvySMne2CGsepGLI7KpF3EzE3Aq9Mg==} + + '@napi-rs/wasm-runtime@0.2.11': + resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + + '@oxc-project/types@0.70.0': + resolution: {integrity: sha512-ngyLUpUjO3dpqygSRQDx7nMx8+BmXbWOU4oIwTJFV2MVIDG7knIZwgdwXlQWLg3C3oxg1lS7ppMtPKqKFb7wzw==} + + '@quansync/fs@0.1.3': + resolution: {integrity: sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==} + engines: {node: '>=20.0.0'} + + '@rolldown/binding-darwin-arm64@1.0.0-beta.9': + resolution: {integrity: sha512-geUG/FUpm+membLC0NQBb39vVyOfguYZ2oyXc7emr6UjH6TeEECT4b0CPZXKFnELareTiU/Jfl70/eEgNxyQeA==} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.9': + resolution: {integrity: sha512-7wPXDwcOtv2I+pWTL2UNpNAxMAGukgBT90Jz4DCfwaYdGvQncF7J0S7IWrRVsRFhBavxM+65RcueE3VXw5UIbg==} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.9': + resolution: {integrity: sha512-agO5mONTNKVrcIt4SRxw5Ni0FOVV3gaH8dIiNp1A4JeU91b9kw7x+JRuNJAQuM2X3pYqVvA6qh13UTNOsaqM/Q==} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9': + resolution: {integrity: sha512-dDNDV9p/8WYDriS9HCcbH6y6+JP38o3enj/pMkdkmkxEnZ0ZoHIfQ9RGYWeRYU56NKBCrya4qZBJx49Jk9LRug==} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9': + resolution: {integrity: sha512-kZKegmHG1ZvfsFIwYU6DeFSxSIcIliXzeznsJHUo9D9/dlVSDi/PUvsRKcuJkQjZoejM6pk8MHN/UfgGdIhPHw==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.9': + resolution: {integrity: sha512-f+VL8mO31pyMJiJPr2aA1ryYONkP2UqgbwK7fKtKHZIeDd/AoUGn3+ujPqDhuy2NxgcJ5H8NaSvDpG1tJMHh+g==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.9': + resolution: {integrity: sha512-GiUEZ0WPjX5LouDoC3O8aJa4h6BLCpIvaAboNw5JoRour/3dC6rbtZZ/B5FC3/ySsN3/dFOhAH97ylQxoZJi7A==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.9': + resolution: {integrity: sha512-AMb0dicw+QHh6RxvWo4BRcuTMgS0cwUejJRMpSyIcHYnKTbj6nUW4HbWNQuDfZiF27l6F5gEwBS+YLUdVzL9vg==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.9': + resolution: {integrity: sha512-+pdaiTx7L8bWKvsAuCE0HAxP1ze1WOLoWGCawcrZbMSY10dMh2i82lJiH6tXGXbfYYwsNWhWE2NyG4peFZvRfQ==} + engines: {node: '>=14.21.3'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9': + resolution: {integrity: sha512-A7kN248viWvb8eZMzQu024TBKGoyoVYBsDG2DtoP8u2pzwoh5yDqUL291u01o4f8uzpUHq8mfwQJmcGChFu8KQ==} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9': + resolution: {integrity: sha512-DzKN7iEYjAP8AK8F2G2aCej3fk43Y/EQrVrR3gF0XREes56chjQ7bXIhw819jv74BbxGdnpPcslhet/cgt7WRA==} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.9': + resolution: {integrity: sha512-GMWgTvvbZ8TfBsAiJpoz4SRq3IN3aUMn0rYm8q4I8dcEk4J1uISyfb6ZMzvqW+cvScTWVKWZNqnrmYOKLLUt4w==} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.9': + resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + + '@rollup/rollup-android-arm-eabi@4.44.1': + resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.44.1': + resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.44.1': + resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.44.1': + resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.44.1': + resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.44.1': + resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.44.1': + resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.44.1': + resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.44.1': + resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.44.1': + resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.44.1': + resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.44.1': + resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.44.1': + resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.44.1': + resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.44.1': + resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} + cpu: [x64] + os: [win32] + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.15.34': + resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + + args-tokenizer@0.3.0: + resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@2.1.0: + resolution: {integrity: sha512-ROM2LlXbZBZVk97crfw8PGDOBzzsJvN2uJCmwswvPUNyfH14eg90mSN3xNqsri1JS1G9cz0VzeDUhxJkTrr4Ew==} + engines: {node: '>=20.18.0'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + birpc@2.4.0: + resolution: {integrity: sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==} + + bumpp@10.2.0: + resolution: {integrity: sha512-1EJ2NG3M3WYJj4m+GtcxNH6Y7zMQ8q68USMoUGKjM6qFTVXSXCnTxcQSUDV7j4KjLVbk2uK6345Z+6RKOv0w5A==} + engines: {node: '>=18'} + hasBin: true + + c12@3.0.4: + resolution: {integrity: sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dts-resolver@2.1.1: + resolution: {integrity: sha512-3BiGFhB6mj5Kv+W2vdJseQUYW+SKVzAFJL6YNP6ursbrwy1fXHRotfHi3xLNxe4wZl/K8qbAFeCDjZLjzqxxRw==} + engines: {node: '>=20.18.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + empathic@1.1.0: + resolution: {integrity: sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA==} + engines: {node: '>=14'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + happy-dom@17.6.3: + resolution: {integrity: sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==} + engines: {node: '>=20.0.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + nypm@0.6.0: + resolution: {integrity: sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + + pkg-types@2.1.1: + resolution: {integrity: sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + posthog-node@5.1.1: + resolution: {integrity: sha512-6VISkNdxO24ehXiDA4dugyCSIV7lpGVaEu5kn/dlAj+SJ1lgcDru9PQ8p/+GSXsXVxohd1t7kHL2JKc9NoGb0w==} + engines: {node: '>=20'} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.13.13: + resolution: {integrity: sha512-Nchx9nQoa4IpfQ/BJzodKMvtJ3H3dT322siAJSp3uvQJ+Pi1qgEjOp7hSQwGSQRhaC5gC+9hparbWEH5oiAL9Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.9 + typescript: ^5.0.0 + vue-tsc: ~2.2.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-beta.9: + resolution: {integrity: sha512-ZgZky52n6iF0UainGKjptKGrOG4Con2S5sdc4C4y2Oj25D5PHAY8Y8E5f3M2TSd/zlhQs574JlMeTe3vREczSg==} + hasBin: true + peerDependencies: + '@oxc-project/runtime': 0.70.0 + peerDependenciesMeta: + '@oxc-project/runtime': + optional: true + + rollup@4.44.1: + resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + + tsdown@0.11.13: + resolution: {integrity: sha512-VSfoNm8MJXFdg7PJ4p2javgjMRiQQHpkP9N3iBBTrmCixcT6YZ9ZtqYMW3NDHczqR0C0Qnur1HMQr1ZfZcmrng==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + unconfig@7.3.2: + resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.0.0: + resolution: {integrity: sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@babel/generator@7.27.5': + dependencies: + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 + '@jridgewell/gen-mapping': 0.3.10 + '@jridgewell/trace-mapping': 0.3.27 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.27.7': + dependencies: + '@babel/types': 7.27.7 + + '@babel/types@7.27.7': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@jridgewell/gen-mapping@0.3.10': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.2 + '@jridgewell/trace-mapping': 0.3.27 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.2': {} + + '@jridgewell/trace-mapping@0.3.27': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.2 + + '@napi-rs/wasm-runtime@0.2.11': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@oxc-project/types@0.70.0': {} + + '@quansync/fs@0.1.3': + dependencies: + quansync: 0.2.10 + + '@rolldown/binding-darwin-arm64@1.0.0-beta.9': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.9': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.9': + dependencies: + '@napi-rs/wasm-runtime': 0.2.11 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.9': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.9': {} + + '@rollup/rollup-android-arm-eabi@4.44.1': + optional: true + + '@rollup/rollup-android-arm64@4.44.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.44.1': + optional: true + + '@rollup/rollup-darwin-x64@4.44.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.44.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.44.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.44.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.44.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.44.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.44.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.44.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.44.1': + optional: true + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@22.15.34': + dependencies: + undici-types: 6.21.0 + + '@types/uuid@10.0.0': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.15.34 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.0(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.0(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + + ansis@4.1.0: {} + + args-tokenizer@0.3.0: {} + + assertion-error@2.0.1: {} + + ast-kit@2.1.0: + dependencies: + '@babel/parser': 7.27.7 + pathe: 2.0.3 + + atomic-sleep@1.0.0: {} + + birpc@2.4.0: {} + + bumpp@10.2.0: + dependencies: + ansis: 4.1.0 + args-tokenizer: 0.3.0 + c12: 3.0.4 + cac: 6.7.14 + escalade: 3.2.0 + jsonc-parser: 3.3.1 + package-manager-detector: 1.3.0 + semver: 7.7.2 + tinyexec: 1.0.1 + tinyglobby: 0.2.14 + yaml: 2.8.0 + transitivePeerDependencies: + - magicast + + c12@3.0.4: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.4.2 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.1.1 + rc9: 2.1.2 + + cac@6.7.14: {} + + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + + check-error@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + confbox@0.2.2: {} + + consola@3.4.2: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + defu@6.1.4: {} + + destr@2.0.5: {} + + diff@8.0.2: {} + + dotenv@16.6.1: {} + + dts-resolver@2.1.1: {} + + empathic@1.1.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.2.1: {} + + exsolve@1.0.7: {} + + fast-redact@3.5.0: {} + + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.6 + nypm: 0.6.0 + pathe: 2.0.3 + + happy-dom@17.6.3: + dependencies: + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + hookable@5.5.3: {} + + jiti@2.4.2: {} + + js-tokens@9.0.1: {} + + jsesc@3.1.0: {} + + jsonc-parser@3.3.1: {} + + loupe@3.1.4: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.2 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-fetch-native@1.6.6: {} + + nypm@0.6.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.1.1 + tinyexec: 0.3.2 + + ohash@2.0.11: {} + + on-exit-leak-free@2.1.2: {} + + package-manager-detector@1.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.2: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.7.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + pkg-types@2.1.1: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-node@5.1.1: {} + + process-warning@5.0.0: {} + + quansync@0.2.10: {} + + quick-format-unescaped@4.0.4: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + + readdirp@4.1.2: {} + + real-require@0.2.0: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.13.13(rolldown@1.0.0-beta.9)(typescript@5.8.3): + dependencies: + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 + ast-kit: 2.1.0 + birpc: 2.4.0 + debug: 4.4.1 + dts-resolver: 2.1.1 + get-tsconfig: 4.10.1 + rolldown: 1.0.0-beta.9 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - oxc-resolver + - supports-color + + rolldown@1.0.0-beta.9: + dependencies: + '@oxc-project/types': 0.70.0 + '@rolldown/pluginutils': 1.0.0-beta.9 + ansis: 4.1.0 + optionalDependencies: + '@rolldown/binding-darwin-arm64': 1.0.0-beta.9 + '@rolldown/binding-darwin-x64': 1.0.0-beta.9 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.9 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.9 + + rollup@4.44.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.44.1 + '@rollup/rollup-android-arm64': 4.44.1 + '@rollup/rollup-darwin-arm64': 4.44.1 + '@rollup/rollup-darwin-x64': 4.44.1 + '@rollup/rollup-freebsd-arm64': 4.44.1 + '@rollup/rollup-freebsd-x64': 4.44.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 + '@rollup/rollup-linux-arm-musleabihf': 4.44.1 + '@rollup/rollup-linux-arm64-gnu': 4.44.1 + '@rollup/rollup-linux-arm64-musl': 4.44.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-musl': 4.44.1 + '@rollup/rollup-linux-s390x-gnu': 4.44.1 + '@rollup/rollup-linux-x64-gnu': 4.44.1 + '@rollup/rollup-linux-x64-musl': 4.44.1 + '@rollup/rollup-win32-arm64-msvc': 4.44.1 + '@rollup/rollup-win32-ia32-msvc': 4.44.1 + '@rollup/rollup-win32-x64-msvc': 4.44.1 + fsevents: 2.3.3 + + safe-stable-stringify@2.5.0: {} + + semver@7.7.2: {} + + siginfo@2.0.0: {} + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + + tsdown@0.11.13(typescript@5.8.3): + dependencies: + ansis: 4.1.0 + cac: 6.7.14 + chokidar: 4.0.3 + debug: 4.4.1 + diff: 8.0.2 + empathic: 1.1.0 + hookable: 5.5.3 + rolldown: 1.0.0-beta.9 + rolldown-plugin-dts: 0.13.13(rolldown@1.0.0-beta.9)(typescript@5.8.3) + semver: 7.7.2 + tinyexec: 1.0.1 + tinyglobby: 0.2.14 + unconfig: 7.3.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@oxc-project/runtime' + - '@typescript/native-preview' + - oxc-resolver + - supports-color + - vue-tsc + + tslib@2.8.1: + optional: true + + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.8.3: {} + + unconfig@7.3.2: + dependencies: + '@quansync/fs': 0.1.3 + defu: 6.1.4 + jiti: 2.4.2 + quansync: 0.2.10 + + undici-types@6.21.0: {} + + uuid@11.1.0: {} + + vite-node@3.2.4(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.0(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.0.0(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.15.34 + fsevents: 2.3.3 + jiti: 2.4.2 + tsx: 4.20.3 + yaml: 2.8.0 + + vitest@3.2.4(@types/node@22.15.34)(happy-dom@17.6.3)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.0(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.0(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@22.15.34)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.8.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.34 + happy-dom: 17.6.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.18.3: {} + + yaml@2.8.0: {} diff --git a/libs/typescript/pnpm-workspace.yaml b/libs/typescript/pnpm-workspace.yaml new file mode 100644 index 00000000..aa33c1bc --- /dev/null +++ b/libs/typescript/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "computer" + - "core"